perf(viewer2d): toggle layers by visibility instead of reloading the drawing
setHiddenLayers() called load(result, {keepView:true}), so every layer
on/off re-parsed and re-tessellated the whole drawing — arc sampling,
hatch triangulation, complex linetypes and the Slug text batch all rebuilt
from scratch. On an 18 MB DWG a single eye-icon click took 5 s+.
Build every layer once, then switch geometry on and off:
- Merged LineSegments buffers get an index buffer plus a per-segment layer
id (_registerIndexed). A toggle rewrites the index and setDrawRange;
positions, colors and lineDistances are untouched, so draw order, dash
phase and the selection-highlight offsets in meta.colStart stay valid.
- Per-entity meshes (hatch fills, SOLID quads, arrowheads, OLE images,
sprites) are tagged by layer via _addObj and toggled with .visible.
- Slug text builds one merged mesh per CAD layer instead of one per
draw order, so text hides with a visible flag. The shader source is
identical across batches, so three still shares one WebGLProgram.
- Hidden geometry stays in the pick buffers, so _onClick now skips it via
the _metaHidden mask.
- Fit keeps framing only what is drawn: per-layer Box3 + fit samples are
recorded at build and unioned over the visible layers.
- getLayerInfo() caches the O(entities) name/color/count pass per load;
only the visible flag is recomputed.
Layer panel: one delegated click listener instead of re-binding a handler
per row, and a toggle patches the affected row's opacity/icon in place
rather than regenerating the whole list's innerHTML.
Trade-off: hidden layers are now built and kept in memory, so loading a
file with layers already off costs what a full load costs.
Rebuilds dist2.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+271
-61
@@ -18,6 +18,18 @@ const ARC_SEGS = 64;
|
||||
const ELLIPSE_SEGS = 72;
|
||||
const CLICK_THRESHOLD_PX = 14;
|
||||
|
||||
/** Bucket pending texts by their owning CAD layer (insertion order preserved). */
|
||||
function groupByLayer(list) {
|
||||
const m = new Map();
|
||||
for (const t of list) {
|
||||
const k = t.layer ?? '0';
|
||||
let arr = m.get(k);
|
||||
if (!arr) m.set(k, arr = []);
|
||||
arr.push(t);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
function stripMText(s) {
|
||||
if (!s) return '';
|
||||
return s
|
||||
@@ -46,6 +58,23 @@ export class Viewer2D {
|
||||
this._measurePtA = null;
|
||||
this._measureMarkers = [];
|
||||
this._textGen = 0; // invalidation token for the async Slug text flush
|
||||
// ── Layer visibility index (see _applyLayerVisibility) ───────────────────
|
||||
// Layer on/off used to re-run load() = full re-tessellation (5 s+ on an
|
||||
// 18 MB DWG). Instead we build every layer once and toggle visibility:
|
||||
// · _layerObjs layer name → Object3D[] (hatch/solid/text/sprite meshes)
|
||||
// · _indexedBufs merged LineSegments whose index buffer is rebuilt from a
|
||||
// per-segment layer id — keeps the original draw order.
|
||||
// · _layerBoxes / _layerSamples per-layer AABB + fit samples so Fit still
|
||||
// frames only what is visible.
|
||||
this._curLayer = null;
|
||||
this._curLayerId = -1;
|
||||
this._layerObjs = new Map();
|
||||
this._layerBoxes = new Map();
|
||||
this._layerSamples = new Map();
|
||||
this._indexedBufs = [];
|
||||
this._metaHidden = null;
|
||||
this._fullContentBox = null;
|
||||
this._hiddenLayers = new Set();
|
||||
SlugTextEngine.shared().catch(() => {}); // warm the font load; sprite fallback covers failure
|
||||
|
||||
this._scene = new THREE.Scene();
|
||||
@@ -228,7 +257,7 @@ export class Viewer2D {
|
||||
const box = new THREE.Box3();
|
||||
const samples = [];
|
||||
this._group.traverse((obj) => {
|
||||
if (!obj.geometry) return;
|
||||
if (!obj.geometry || obj.visible === false) return;
|
||||
box.expandByObject(obj);
|
||||
const pos = obj.geometry.attributes?.position;
|
||||
if (!pos?.array) return;
|
||||
@@ -240,7 +269,9 @@ export class Viewer2D {
|
||||
});
|
||||
if (box.isEmpty()) return;
|
||||
const use = this._trimmedContentBox(samples, box) || box;
|
||||
this._contentBox = use.clone();
|
||||
// Don't cache while layers are off — the merged line buffer still holds the
|
||||
// hidden segments, so this box is wider than what's on screen.
|
||||
if (!this._hiddenLayers.size) this._contentBox = use.clone();
|
||||
this._fit(use);
|
||||
}
|
||||
|
||||
@@ -289,6 +320,16 @@ export class Viewer2D {
|
||||
const entities = result?.entities || [];
|
||||
const lineVerts = [];
|
||||
const lineColors = [];
|
||||
// Layer id per emitted segment (2 verts). Drives the index rebuild that
|
||||
// hides/shows layers without touching positions or colors.
|
||||
const lineSegLayer = [];
|
||||
const layerIds = new Map();
|
||||
const layerNames = [];
|
||||
const layerIdOf = (name) => {
|
||||
let id = layerIds.get(name);
|
||||
if (id === undefined) { id = layerNames.length; layerNames.push(name); layerIds.set(name, id); }
|
||||
return id;
|
||||
};
|
||||
// Dashed-linetype segments go into separate buckets keyed by dash|gap size;
|
||||
// each becomes its own LineDashedMaterial LineSegments at assembly. Solid
|
||||
// (Continuous / ByLayer→Continuous) segments stay in lineVerts/lineColors.
|
||||
@@ -354,11 +395,29 @@ export class Viewer2D {
|
||||
// Samples feed percentile trim so Fit matches CAD extents (ignore 1–2 junk verts).
|
||||
const fitSamples = [];
|
||||
let fitSampleN = 0;
|
||||
// Per-layer AABB + fit samples: Fit must frame only the VISIBLE layers, and
|
||||
// toggling no longer re-runs load(), so the split has to happen at build.
|
||||
let curLayerBox = null;
|
||||
let curLayerSamples = null;
|
||||
const setCurLayer = (name) => {
|
||||
const key = name || '0';
|
||||
if (key === this._curLayer) return;
|
||||
this._curLayer = key;
|
||||
this._curLayerId = layerIdOf(key);
|
||||
curLayerBox = this._layerBoxes.get(key);
|
||||
if (!curLayerBox) this._layerBoxes.set(key, curLayerBox = new THREE.Box3());
|
||||
curLayerSamples = this._layerSamples.get(key);
|
||||
if (!curLayerSamples) this._layerSamples.set(key, curLayerSamples = []);
|
||||
};
|
||||
const expand = (x, y, z = 0) => {
|
||||
if (!Number.isFinite(x) || !Number.isFinite(y)) return;
|
||||
box.expandByPoint(_tmp.set(x, y, z));
|
||||
if (curLayerBox) curLayerBox.expandByPoint(_tmp);
|
||||
fitSampleN++;
|
||||
if ((fitSampleN & 7) === 0 && fitSamples.length < 100000) fitSamples.push(x, y);
|
||||
if ((fitSampleN & 7) === 0 && fitSamples.length < 100000) {
|
||||
fitSamples.push(x, y);
|
||||
if (curLayerSamples) curLayerSamples.push(x, y);
|
||||
}
|
||||
};
|
||||
const pushSeg = (ax, ay, bx, by, z, color) => {
|
||||
const r = ((color >> 16) & 0xFF) / 255;
|
||||
@@ -366,12 +425,14 @@ export class Viewer2D {
|
||||
const b = (color & 0xFF) / 255;
|
||||
if (curDash) {
|
||||
let bk = dashBuckets.get(curDash.key);
|
||||
if (!bk) { bk = { dash: curDash.dash, gap: curDash.gap, verts: [], colors: [] }; dashBuckets.set(curDash.key, bk); }
|
||||
if (!bk) { bk = { dash: curDash.dash, gap: curDash.gap, verts: [], colors: [], segLayer: [] }; dashBuckets.set(curDash.key, bk); }
|
||||
bk.verts.push(ax, ay, z, bx, by, z);
|
||||
bk.colors.push(r, g, b, r, g, b);
|
||||
bk.segLayer.push(this._curLayerId);
|
||||
} else {
|
||||
lineVerts.push(ax, ay, z, bx, by, z);
|
||||
lineColors.push(r, g, b, r, g, b);
|
||||
lineSegLayer.push(this._curLayerId);
|
||||
}
|
||||
if (this._pickCurIdx >= 0) { this._pickSegs.push(ax, ay, bx, by); this._pickSegMeta.push(this._pickCurIdx); }
|
||||
expand(ax, ay, z); expand(bx, by, z);
|
||||
@@ -413,6 +474,7 @@ export class Viewer2D {
|
||||
pushSeg: vpPush,
|
||||
expand: vpExpand,
|
||||
pendingTexts,
|
||||
setCurLayer,
|
||||
box,
|
||||
_tmp,
|
||||
underlay: true, // tag pending texts drawn under paper labels
|
||||
@@ -430,20 +492,20 @@ export class Viewer2D {
|
||||
// Model/Paper filter: only entities belonging to the active space (and
|
||||
// their INSERT children such as ATTRIB). Other space geometry is hidden.
|
||||
if (allowedOwners && ownerHex && !allowedOwners.has(ownerHex)) continue;
|
||||
if (this._hiddenLayers?.size) {
|
||||
const lh = e.layerHandle?.value ?? e.layerHandle;
|
||||
const lname = (lh != null && this._layerNameByHandle.get(String(lh))) ?? e.layer ?? e.layerName;
|
||||
if (lname && this._hiddenLayers.has(lname)) continue;
|
||||
}
|
||||
// Hidden layers are NOT skipped here: geometry for every layer is built
|
||||
// once and switched on/off by _applyLayerVisibility, so a toggle costs an
|
||||
// index rebuild instead of a full reload.
|
||||
setCurLayer(this._layerNameOf(e));
|
||||
const d = e.data || e;
|
||||
const type = (e.type || e.typeName || '').toUpperCase();
|
||||
const color = this._entityColor(e);
|
||||
const complexLt = this._resolveComplexLinetype(e);
|
||||
if (complexLt) curDash = null;
|
||||
else curDash = this._resolveDash(e);
|
||||
const meta = { entity: e, type, bounds: null, colStart: lineColors.length };
|
||||
const meta = { entity: e, type, bounds: null, layer: this._curLayer, colStart: lineColors.length };
|
||||
this._entityMeta.push(meta);
|
||||
this._pickCurIdx = this._entityMeta.length - 1; // owner for pick geometry emitted below
|
||||
const textStart = pendingTexts.length;
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
@@ -943,7 +1005,7 @@ export class Viewer2D {
|
||||
});
|
||||
const mesh = new THREE.Mesh(planeGeom, planeMat);
|
||||
mesh.position.set(minX + width / 2, minY + Math.abs(height) / 2, z + 10);
|
||||
this._group.add(mesh);
|
||||
this._addObj(mesh);
|
||||
this._renderer.render(this._scene, this._camera);
|
||||
} catch (err) {
|
||||
console.error('Failed rendering CanvasTexture for OLE image:', err);
|
||||
@@ -983,6 +1045,11 @@ export class Viewer2D {
|
||||
}
|
||||
} catch { /* skip malformed entity */ }
|
||||
meta.colEnd = lineColors.length;
|
||||
// Deferred texts are flushed after the loop (async) — stamp the owning
|
||||
// layer now so _drawTexts can batch per layer.
|
||||
for (let ti = textStart; ti < pendingTexts.length; ti++) {
|
||||
if (pendingTexts[ti].layer === undefined) pendingTexts[ti].layer = this._curLayer;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Render deferred text (underlay / paper stack) ─────────────────────
|
||||
@@ -1020,13 +1087,16 @@ export class Viewer2D {
|
||||
void this._drawTexts(renderTexts, minTextH);
|
||||
}
|
||||
|
||||
this._layerNames = layerNames;
|
||||
this._lineColorAttr = null; this._origColors = null;
|
||||
if (lineVerts.length) {
|
||||
const geom = new THREE.BufferGeometry();
|
||||
geom.setAttribute('position', new THREE.Float32BufferAttribute(lineVerts, 3));
|
||||
const colorAttr = new THREE.Float32BufferAttribute(lineColors, 3);
|
||||
geom.setAttribute('color', colorAttr);
|
||||
this._group.add(new THREE.LineSegments(geom, new THREE.LineBasicMaterial({ vertexColors: true })));
|
||||
const lmesh = new THREE.LineSegments(geom, new THREE.LineBasicMaterial({ vertexColors: true }));
|
||||
this._registerIndexed(lmesh, lineSegLayer);
|
||||
this._group.add(lmesh);
|
||||
this._lineColorAttr = colorAttr;
|
||||
this._origColors = Float32Array.from(colorAttr.array);
|
||||
}
|
||||
@@ -1043,7 +1113,8 @@ export class Viewer2D {
|
||||
const dline = new THREE.LineSegments(dgeom, new THREE.LineDashedMaterial({
|
||||
vertexColors: true, dashSize: bk.dash, gapSize: bk.gap,
|
||||
}));
|
||||
dline.computeLineDistances();
|
||||
dline.computeLineDistances(); // must run BEFORE setIndex (three skips indexed geometry)
|
||||
this._registerIndexed(dline, bk.segLayer);
|
||||
this._group.add(dline);
|
||||
}
|
||||
|
||||
@@ -1052,8 +1123,13 @@ export class Viewer2D {
|
||||
|
||||
// Visible-content AABB for Fit — trim extreme outliers (matches CAD zoom extents better).
|
||||
const fitBox = box.isEmpty() ? null : (this._trimmedContentBox(fitSamples, box) || box);
|
||||
this._fullContentBox = fitBox ? fitBox.clone() : null;
|
||||
this._contentBox = fitBox ? fitBox.clone() : null;
|
||||
if (!opts.keepView && fitBox && !fitBox.isEmpty()) this._fit(fitBox);
|
||||
// Geometry for every layer is in the scene now — switch off the hidden ones
|
||||
// (also refreshes _contentBox to the visible subset).
|
||||
this._applyLayerVisibility();
|
||||
const useBox = this._contentBox;
|
||||
if (!opts.keepView && useBox && !useBox.isEmpty()) this._fit(useBox);
|
||||
} finally {
|
||||
this._isLoading = false;
|
||||
}
|
||||
@@ -1117,12 +1193,118 @@ export class Viewer2D {
|
||||
/** Subscribe to pan/zoom changes (for live zoom readout). */
|
||||
onViewChange(cb) { this._controls?.addEventListener('change', cb); }
|
||||
|
||||
/** Hide entities on the given layer names (Set<string>); re-renders keeping the view. */
|
||||
/**
|
||||
* Hide entities on the given layer names (Set<string>).
|
||||
* O(scene objects + segments) — no re-parse, no re-tessellation, no reload.
|
||||
* Falls back to a full load() only if the scene predates the layer index.
|
||||
*/
|
||||
setHiddenLayers(nameSet) {
|
||||
this._hiddenLayers = nameSet || new Set();
|
||||
// Copy: the Layers panel keeps mutating the Set it handed us.
|
||||
this._hiddenLayers = new Set(nameSet || []);
|
||||
if (this._applyLayerVisibility()) return;
|
||||
if (this._lastResult) this.load(this._lastResult, { keepView: true });
|
||||
}
|
||||
|
||||
/** Layer name of an entity ('0' when the file gives none). */
|
||||
_layerNameOf(e) {
|
||||
const lh = e.layerHandle?.value ?? e.layerHandle;
|
||||
return (lh != null && this._layerNameByHandle?.get(String(lh))) ?? e.layer ?? e.layerName ?? '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Give a merged LineSegments an index buffer so layers can be masked by
|
||||
* rewriting indices (positions/colors/lineDistances stay put, which keeps
|
||||
* draw order and the selection-highlight offsets in meta.colStart valid).
|
||||
*/
|
||||
_registerIndexed(mesh, segLayerArr) {
|
||||
const vcount = mesh.geometry.attributes.position.count;
|
||||
if (!vcount) return;
|
||||
const IdxArr = vcount > 65535 ? Uint32Array : Uint16Array;
|
||||
const index = new IdxArr(vcount);
|
||||
for (let i = 0; i < vcount; i++) index[i] = i;
|
||||
const attr = new THREE.BufferAttribute(index, 1);
|
||||
attr.setUsage(THREE.DynamicDrawUsage);
|
||||
mesh.geometry.setIndex(attr);
|
||||
this._indexedBufs.push({ mesh, segLayer: Int32Array.from(segLayerArr), index, attr });
|
||||
}
|
||||
|
||||
/** Track an entity-owned object so layer toggles can flip its visibility. */
|
||||
_addObj(obj, layer) {
|
||||
const key = layer ?? this._curLayer;
|
||||
if (key != null) {
|
||||
obj.userData.cadLayer = key;
|
||||
let arr = this._layerObjs.get(key);
|
||||
if (!arr) this._layerObjs.set(key, arr = []);
|
||||
arr.push(obj);
|
||||
if (this._hiddenLayers.has(key)) obj.visible = false;
|
||||
}
|
||||
this._group.add(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply _hiddenLayers to the already-built scene.
|
||||
* @returns {boolean} false when there is nothing built yet (caller reloads).
|
||||
*/
|
||||
_applyLayerVisibility() {
|
||||
if (!this._layerNames && !this._layerObjs.size) return false;
|
||||
const hidden = this._hiddenLayers;
|
||||
|
||||
// 1) Whole objects (hatch fills, SOLID quads, arrows, images, text meshes).
|
||||
for (const [name, objs] of this._layerObjs) {
|
||||
const vis = !hidden.has(name);
|
||||
for (const o of objs) o.visible = vis;
|
||||
}
|
||||
|
||||
// 2) Merged line buffers → rewrite the index to skip hidden segments.
|
||||
const names = this._layerNames || [];
|
||||
const hiddenId = new Uint8Array(names.length);
|
||||
for (let i = 0; i < names.length; i++) if (hidden.has(names[i])) hiddenId[i] = 1;
|
||||
for (const buf of this._indexedBufs) {
|
||||
const seg = buf.segLayer, idx = buf.index;
|
||||
let w = 0;
|
||||
for (let s = 0; s < seg.length; s++) {
|
||||
const id = seg[s];
|
||||
if (id >= 0 && hiddenId[id]) continue;
|
||||
const v = s * 2;
|
||||
idx[w++] = v; idx[w++] = v + 1;
|
||||
}
|
||||
buf.attr.needsUpdate = true;
|
||||
buf.mesh.geometry.setDrawRange(0, w);
|
||||
}
|
||||
|
||||
// 3) Picking mask — _onClick skips segments/fills owned by hidden layers.
|
||||
const metas = this._entityMeta;
|
||||
const mh = new Uint8Array(metas.length);
|
||||
if (hidden.size) {
|
||||
for (let i = 0; i < metas.length; i++) if (hidden.has(metas[i].layer)) mh[i] = 1;
|
||||
}
|
||||
this._metaHidden = mh;
|
||||
if (this._selMeta && hidden.has(this._selMeta.layer)) {
|
||||
this._highlight(null);
|
||||
this._onSelectCb?.(null);
|
||||
}
|
||||
|
||||
// 4) Fit box for the visible subset.
|
||||
this._contentBox = this._visibleContentBox();
|
||||
return true;
|
||||
}
|
||||
|
||||
/** AABB (outlier-trimmed) of the layers currently switched on. */
|
||||
_visibleContentBox() {
|
||||
const hidden = this._hiddenLayers;
|
||||
if (!hidden.size) return this._fullContentBox ? this._fullContentBox.clone() : null;
|
||||
const box = new THREE.Box3();
|
||||
const samples = [];
|
||||
for (const [name, b] of this._layerBoxes) {
|
||||
if (hidden.has(name) || b.isEmpty()) continue;
|
||||
box.union(b);
|
||||
const s = this._layerSamples.get(name);
|
||||
if (s) for (let i = 0; i < s.length; i++) samples.push(s[i]);
|
||||
}
|
||||
if (box.isEmpty()) return null;
|
||||
return (this._trimmedContentBox(samples, box) || box).clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Model / Paper (Layout) spaces present in the last loaded result.
|
||||
* @returns {import('./cadSpaces').CadSpace[]}
|
||||
@@ -1271,7 +1453,7 @@ export class Viewer2D {
|
||||
_emitModelThroughViewport(vp, ctx) {
|
||||
const {
|
||||
entities, modelAllowed, blockDefHandles, entsByOwner, blockBaseByHex,
|
||||
pushSeg: basePush, expand: baseExpand, pendingTexts,
|
||||
pushSeg: basePush, expand: baseExpand, pendingTexts, setCurLayer,
|
||||
underlay = false,
|
||||
} = ctx;
|
||||
const xf = this._viewportModelToPaper(vp);
|
||||
@@ -1300,11 +1482,8 @@ export class Viewer2D {
|
||||
const ownerHex = e.ownerHandle?.value?.toString(16);
|
||||
if (!ownerHex || !modelAllowed.has(ownerHex)) continue;
|
||||
if (blockDefHandles.has(ownerHex)) continue;
|
||||
if (this._hiddenLayers?.size) {
|
||||
const lh = e.layerHandle?.value ?? e.layerHandle;
|
||||
const lname = (lh != null && this._layerNameByHandle.get(String(lh))) ?? e.layer ?? e.layerName;
|
||||
if (lname && this._hiddenLayers.has(lname)) continue;
|
||||
}
|
||||
// Layer visibility is applied post-build (see _applyLayerVisibility).
|
||||
setCurLayer?.(this._layerNameOf(e));
|
||||
if (frozen.size) {
|
||||
const lh = e.layerHandle?.value;
|
||||
if (lh != null && frozen.has(String(lh))) continue;
|
||||
@@ -1516,6 +1695,9 @@ export class Viewer2D {
|
||||
}
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
for (let ti = vpTextStart; ti < pendingTexts.length; ti++) {
|
||||
if (pendingTexts[ti].layer === undefined) pendingTexts[ti].layer = this._curLayer;
|
||||
}
|
||||
}
|
||||
|
||||
// Transform any pending texts that are still in model space (from INSERT helpers).
|
||||
@@ -1540,33 +1722,39 @@ export class Viewer2D {
|
||||
});
|
||||
}
|
||||
|
||||
/** Layer list for the Layers panel: [{ name, colorHex, count, visible }]. */
|
||||
/**
|
||||
* Layer list for the Layers panel: [{ name, colorHex, count, visible }].
|
||||
* The name/color/count part is O(entities) — cached per load so the panel can
|
||||
* call this on every toggle without re-walking a 500k-entity drawing.
|
||||
*/
|
||||
getLayerInfo() {
|
||||
const result = this._lastResult;
|
||||
if (!result) return [];
|
||||
const counts = new Map();
|
||||
const allowed = this._activeSpaceHex
|
||||
? buildAllowedOwnerHexes(result.entities || [], this._activeSpaceHex)
|
||||
: null;
|
||||
for (const e of (result.entities || [])) {
|
||||
const ownerHex = e.ownerHandle?.value?.toString(16);
|
||||
if (allowed && ownerHex && !allowed.has(ownerHex)) continue;
|
||||
const lh = e.layerHandle?.value ?? e.layerHandle;
|
||||
const name = (lh != null && this._layerNameByHandle?.get(String(lh)))
|
||||
?? e.layer ?? e.layerName ?? '0';
|
||||
counts.set(name, (counts.get(name) || 0) + 1);
|
||||
let base = this._layerInfoCache;
|
||||
if (!base) {
|
||||
const counts = new Map();
|
||||
const allowed = this._activeSpaceHex
|
||||
? buildAllowedOwnerHexes(result.entities || [], this._activeSpaceHex)
|
||||
: null;
|
||||
for (const e of (result.entities || [])) {
|
||||
const ownerHex = e.ownerHandle?.value?.toString(16);
|
||||
if (allowed && ownerHex && !allowed.has(ownerHex)) continue;
|
||||
const name = this._layerNameOf(e);
|
||||
counts.set(name, (counts.get(name) || 0) + 1);
|
||||
}
|
||||
base = (result.tables?.layers || []).map(l => {
|
||||
const name = l.name ?? l.layerName ?? '0';
|
||||
const aci = Math.abs(l.colorIndex ?? l.color ?? l.colorNumber ?? 7);
|
||||
return {
|
||||
name,
|
||||
colorHex: '#' + ((aciToHex(aci, this._isDark) ?? DEFAULT_COLOR).toString(16).padStart(6, '0')),
|
||||
count: counts.get(name) || 0,
|
||||
};
|
||||
}).filter(l => l.count > 0 || !l.name.startsWith('*'));
|
||||
this._layerInfoCache = base;
|
||||
}
|
||||
const hidden = this._hiddenLayers || new Set();
|
||||
return (result.tables?.layers || []).map(l => {
|
||||
const name = l.name ?? l.layerName ?? '0';
|
||||
const aci = Math.abs(l.colorIndex ?? l.color ?? l.colorNumber ?? 7);
|
||||
return {
|
||||
name,
|
||||
colorHex: '#' + ((aciToHex(aci, this._isDark) ?? DEFAULT_COLOR).toString(16).padStart(6, '0')),
|
||||
count: counts.get(name) || 0,
|
||||
visible: !hidden.has(name),
|
||||
};
|
||||
}).filter(l => l.count > 0 || !l.name.startsWith('*'));
|
||||
return base.map(l => ({ ...l, visible: !hidden.has(l.name) }));
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────────
|
||||
@@ -2050,7 +2238,7 @@ export class Viewer2D {
|
||||
const mesh = new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide }));
|
||||
mesh.position.set(pt.x, pt.y, (pt.z || 0) + 0.5);
|
||||
mesh.rotation.z = rotation;
|
||||
this._group.add(mesh);
|
||||
this._addObj(mesh);
|
||||
}
|
||||
|
||||
// Default geometry for AutoCAD standard arrowhead blocks that ship no geometry
|
||||
@@ -2097,7 +2285,7 @@ export class Viewer2D {
|
||||
const geom = new THREE.BufferGeometry();
|
||||
geom.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
|
||||
geom.setAttribute('color', new THREE.Float32BufferAttribute(cols, 3));
|
||||
this._group.add(new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide })));
|
||||
this._addObj(new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide })));
|
||||
}
|
||||
|
||||
// Dot radius (block-local) from an arrow block's geometry, measured from its centroid.
|
||||
@@ -2279,7 +2467,7 @@ export class Viewer2D {
|
||||
side: THREE.DoubleSide,
|
||||
depthWrite: false,
|
||||
});
|
||||
this._group.add(new THREE.Mesh(geo, mat));
|
||||
this._addObj(new THREE.Mesh(geo, mat));
|
||||
// Pick as edge segments only — do NOT register the poly as a fill loop.
|
||||
// A closed outer frame (도곽 cw=2) used as _pickFills would mark the whole
|
||||
// interior as "covered" and (with a weak size check) hide every inner line.
|
||||
@@ -2350,7 +2538,7 @@ export class Viewer2D {
|
||||
const geom = new THREE.BufferGeometry();
|
||||
geom.setAttribute('position', new THREE.Float32BufferAttribute(verts, 3));
|
||||
geom.setAttribute('color', new THREE.Float32BufferAttribute(cols, 3));
|
||||
this._group.add(new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide })));
|
||||
this._addObj(new THREE.Mesh(geom, new THREE.MeshBasicMaterial({ vertexColors: true, side: THREE.DoubleSide })));
|
||||
// Filled quad/tri → pickable region (DWG SOLID vertex order is 0,1,3,2).
|
||||
if (this._pickCurIdx >= 0 && corners.length >= 3) {
|
||||
const loop = corners.length >= 4 ? [corners[0], corners[1], corners[3], corners[2]] : corners.slice(0, 3);
|
||||
@@ -2538,7 +2726,7 @@ export class Viewer2D {
|
||||
shape.moveTo(poly[0].x, poly[0].y);
|
||||
for (let k = 1; k < poly.length; k++) shape.lineTo(poly[k].x, poly[k].y);
|
||||
shape.closePath();
|
||||
this._group.add(new THREE.Mesh(new THREE.ShapeGeometry(shape), mat));
|
||||
this._addObj(new THREE.Mesh(new THREE.ShapeGeometry(shape), mat));
|
||||
} catch { /* degenerate */ }
|
||||
}
|
||||
}
|
||||
@@ -2803,7 +2991,7 @@ export class Viewer2D {
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.position.set((minX + maxX) / 2, (minY + maxY) / 2, 0);
|
||||
// PlaneGeometry is XY; our 2D world is already XY. No rotation needed.
|
||||
this._group.add(mesh);
|
||||
this._addObj(mesh);
|
||||
}
|
||||
|
||||
// Multi-line MTEXT: one canvas with stacked lines, aligned as a block.
|
||||
@@ -2854,7 +3042,7 @@ export class Viewer2D {
|
||||
sprite.scale.set(vw, vh, 1);
|
||||
sprite.position.set(pos.x + ox, pos.y + oy, 1);
|
||||
sprite.renderOrder = renderOrder;
|
||||
this._group.add(sprite);
|
||||
this._addObj(sprite);
|
||||
}
|
||||
|
||||
// MTEXT background mask (opaque fill behind text). Flags (DXF 90):
|
||||
@@ -2930,7 +3118,7 @@ export class Viewer2D {
|
||||
const mesh = new THREE.Mesh(geo, mat);
|
||||
mesh.position.set(cx, cy, 0.4); // behind text (z≈1)
|
||||
mesh.rotation.z = rot;
|
||||
this._group.add(mesh);
|
||||
this._addObj(mesh);
|
||||
}
|
||||
if (useFrame && !useFill) {
|
||||
// Outline only — four edges as hairline segs (rare path)
|
||||
@@ -2971,21 +3159,27 @@ export class Viewer2D {
|
||||
for (const { list, order } of layers) {
|
||||
if (!list.length) continue;
|
||||
for (const t of list) {
|
||||
if ((t.bgFillFlags | 0) !== 0) this._drawMTextBackground(t, engine, minTextH);
|
||||
if ((t.bgFillFlags | 0) !== 0) {
|
||||
this._curLayer = t.layer ?? '0'; // _drawMTextBackground emits via _addObj
|
||||
this._drawMTextBackground(t, engine, minTextH);
|
||||
}
|
||||
}
|
||||
const batch = new SlugTextBatch(engine);
|
||||
for (const t of list) {
|
||||
batch.add(t.text, t.pos, Math.max(t.height, minTextH), t.rotation || 0, t.color, t.alignH ?? 0, t.alignV ?? 0);
|
||||
}
|
||||
const mesh = batch.build();
|
||||
if (mesh) {
|
||||
// One merged mesh per CAD layer (not one per order): a layer toggle then
|
||||
// just flips mesh.visible instead of rebuilding the whole text batch.
|
||||
for (const [lname, items] of groupByLayer(list)) {
|
||||
const batch = new SlugTextBatch(engine);
|
||||
for (const t of items) {
|
||||
batch.add(t.text, t.pos, Math.max(t.height, minTextH), t.rotation || 0, t.color, t.alignH ?? 0, t.alignV ?? 0);
|
||||
}
|
||||
const mesh = batch.build();
|
||||
if (!mesh) continue;
|
||||
mesh.renderOrder = order;
|
||||
// Transparent text: draw later order on top regardless of z
|
||||
if (mesh.material) {
|
||||
mesh.material.depthWrite = false;
|
||||
mesh.material.transparent = true;
|
||||
}
|
||||
this._group.add(mesh);
|
||||
this._addObj(mesh, lname);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -2993,6 +3187,7 @@ export class Viewer2D {
|
||||
if (gen !== this._textGen) return;
|
||||
for (const { list, order } of layers) {
|
||||
for (const t of list) {
|
||||
this._curLayer = t.layer ?? '0';
|
||||
if ((t.bgFillFlags | 0) !== 0) this._drawMTextBackground(t, null, minTextH);
|
||||
this._textSprite(t.text, t.pos, Math.max(t.height, minTextH), t.rotation, t.color, t.alignH ?? 0, t.alignV ?? 0, order);
|
||||
}
|
||||
@@ -3067,7 +3262,7 @@ export class Viewer2D {
|
||||
sprite.scale.set(vw, vh, 1);
|
||||
sprite.position.set(pos.x + ox, pos.y + oy, 1);
|
||||
sprite.renderOrder = renderOrder;
|
||||
this._group.add(sprite);
|
||||
this._addObj(sprite);
|
||||
}
|
||||
|
||||
// Render block definition entities transformed by INSERT params
|
||||
@@ -3416,7 +3611,11 @@ export class Viewer2D {
|
||||
// circles, polylines, block wires, dimension leaders/arrows).
|
||||
let bestIdx = -1, bestDist = thresh;
|
||||
const S = this._pickSegs, SM = this._pickSegMeta;
|
||||
// Hidden layers stay in the buffers (only masked out of the draw index), so
|
||||
// picking has to skip them explicitly.
|
||||
const MH = this._hiddenLayers.size ? this._metaHidden : null;
|
||||
for (let i = 0, j = 0; i < S.length; i += 4, j++) {
|
||||
if (MH && MH[SM[j]]) continue;
|
||||
const d = this._segDist(wx, wy, S[i], S[i + 1], S[i + 2], S[i + 3]);
|
||||
if (d < bestDist) { bestDist = d; bestIdx = SM[j]; }
|
||||
}
|
||||
@@ -3425,6 +3624,7 @@ export class Viewer2D {
|
||||
if (bestIdx < 0) {
|
||||
let bestArea = Infinity;
|
||||
for (const f of this._pickFills) {
|
||||
if (MH && MH[f.metaIdx]) continue;
|
||||
if (this._fillHit(f.loops, wx, wy)) {
|
||||
const a = this._loopArea(f.loops[0] || []);
|
||||
if (a < bestArea) { bestArea = a; bestIdx = f.metaIdx; }
|
||||
@@ -3705,6 +3905,16 @@ export class Viewer2D {
|
||||
this._texCache?.clear();
|
||||
this._textGen++; // invalidate any in-flight async text flush
|
||||
this._contentBox = null;
|
||||
this._fullContentBox = null;
|
||||
this._layerObjs.clear();
|
||||
this._layerBoxes.clear();
|
||||
this._layerSamples.clear();
|
||||
this._indexedBufs = [];
|
||||
this._layerNames = null;
|
||||
this._metaHidden = null;
|
||||
this._curLayer = null;
|
||||
this._curLayerId = -1;
|
||||
this._layerInfoCache = null;
|
||||
}
|
||||
|
||||
_onResize() {
|
||||
|
||||
Reference in New Issue
Block a user