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:
+45
-26
@@ -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, '&')
|
||||
|
||||
Reference in New Issue
Block a user