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:
minsung
2026-08-04 14:08:59 +09:00
co-authored by Claude Opus 5
parent fb8f7ca4eb
commit 3480945b5a
4 changed files with 5900 additions and 88 deletions
+45 -26
View File
@@ -49,6 +49,15 @@ export class LayerPanelManager {
if (options?.hideAllBtnEl) {
options.hideAllBtnEl.addEventListener('click', () => this.hideAllLayers());
}
// Delegated click handling: bound once, so render() never has to re-attach
// a listener per row (that was O(layers) DOM work on every single toggle).
this.listEl.addEventListener('click', (e) => {
const row = (e.target as HTMLElement)?.closest?.('.layer-row') as HTMLElement | null;
if (!row) return;
const layerName = row.dataset.name;
if (layerName) this.toggleLayer(layerName);
});
}
/** Refresh and re-render layer list from Viewer2D state */
@@ -66,7 +75,7 @@ export class LayerPanelManager {
public showAllLayers(): void {
this.hiddenLayers.clear();
this.viewer.setHiddenLayers(this.hiddenLayers);
this.render();
this.syncRows();
}
public hideAllLayers(): void {
@@ -75,7 +84,7 @@ export class LayerPanelManager {
this.hiddenLayers.add(l.name);
}
this.viewer.setHiddenLayers(this.hiddenLayers);
this.render();
this.syncRows();
}
public toggleLayer(name: string): void {
@@ -85,7 +94,34 @@ export class LayerPanelManager {
this.hiddenLayers.add(name);
}
this.viewer.setHiddenLayers(this.hiddenLayers);
this.render();
this.syncRow(name);
}
/** Patch one row in place — a full render() would rebuild the whole list. */
private syncRow(name: string): void {
const row = this.listEl?.querySelector<HTMLElement>(
`.layer-row[data-name="${cssEscape(name)}"]`
);
if (!row) return;
this.paintRow(row, !this.hiddenLayers.has(name));
}
/** Patch every row (show-all / hide-all) without touching the DOM structure. */
private syncRows(): void {
if (!this.listEl) return;
this.listEl.querySelectorAll<HTMLElement>('.layer-row').forEach((row) => {
const name = row.dataset.name;
if (name != null) this.paintRow(row, !this.hiddenLayers.has(name));
});
}
private paintRow(row: HTMLElement, visible: boolean): void {
row.style.opacity = visible ? '1' : '0.4';
const btn = row.querySelector<HTMLElement>('.eye-btn');
if (btn) {
btn.textContent = visible ? '👁️' : '🕶️';
btn.title = visible ? '숨기기' : '표시하기';
}
}
public render(): void {
@@ -141,32 +177,15 @@ export class LayerPanelManager {
</div>`;
})
.join('');
// Bind eye button click events
this.listEl.querySelectorAll('.eye-btn').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const layerName = (btn as HTMLElement).dataset.toggle;
if (layerName) {
this.toggleLayer(layerName);
}
});
});
// Also toggle when clicking row
this.listEl.querySelectorAll('.layer-row').forEach((row) => {
row.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
if (target.closest('.eye-btn')) return;
const layerName = (row as HTMLElement).dataset.name;
if (layerName) {
this.toggleLayer(layerName);
}
});
});
// Clicks are handled by the delegated listener bound in the constructor.
}
}
/** Escape a layer name for use inside a double-quoted CSS attribute selector. */
function cssEscape(str: string): string {
return str.replace(/["\\]/g, '\\$&');
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')