/** * Layer Panel manager for dwg-dxf-viewer-sample. * Renders layer items, color swatches, entity counts, search filters, and visibility toggles. */ import type { Viewer2D } from './viewer2d/Viewer2D'; export interface LayerItem { name: string; colorHex: string; count: number; visible: boolean; } export class LayerPanelManager { private viewer: Viewer2D; private listEl: HTMLElement; private countEl: HTMLElement | null; private searchInputEl: HTMLInputElement | null; private hiddenLayers: Set = new Set(); private searchQuery: string = ''; constructor( viewer: Viewer2D, listEl: HTMLElement, options?: { countEl?: HTMLElement | null; searchInputEl?: HTMLInputElement | null; showAllBtnEl?: HTMLElement | null; hideAllBtnEl?: HTMLElement | null; } ) { this.viewer = viewer; this.listEl = listEl; this.countEl = options?.countEl || null; this.searchInputEl = options?.searchInputEl || null; if (this.searchInputEl) { this.searchInputEl.addEventListener('input', (e) => { this.searchQuery = (e.target as HTMLInputElement).value.trim().toLowerCase(); this.render(); }); } if (options?.showAllBtnEl) { options.showAllBtnEl.addEventListener('click', () => this.showAllLayers()); } 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 */ public update(): void { this.hiddenLayers.clear(); const layers = this.viewer.getLayerInfo() || []; for (const l of layers) { if (!l.visible) { this.hiddenLayers.add(l.name); } } this.render(); } public showAllLayers(): void { this.hiddenLayers.clear(); this.viewer.setHiddenLayers(this.hiddenLayers); this.syncRows(); } public hideAllLayers(): void { const layers = this.viewer.getLayerInfo() || []; for (const l of layers) { this.hiddenLayers.add(l.name); } this.viewer.setHiddenLayers(this.hiddenLayers); this.syncRows(); } public toggleLayer(name: string): void { if (this.hiddenLayers.has(name)) { this.hiddenLayers.delete(name); } else { this.hiddenLayers.add(name); } this.viewer.setHiddenLayers(this.hiddenLayers); 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( `.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('.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('.eye-btn'); if (btn) { btn.textContent = visible ? '👁️' : '🕶️'; btn.title = visible ? '숨기기' : '표시하기'; } } public render(): void { if (!this.listEl) return; const layers: LayerItem[] = this.viewer.getLayerInfo() || []; if (this.countEl) { this.countEl.textContent = `${layers.length}개`; } if (!layers.length) { this.listEl.innerHTML = `
도면에 레이어 정보가 없습니다
`; return; } const filtered = layers.filter((l) => l.name.toLowerCase().includes(this.searchQuery) ); if (!filtered.length) { this.listEl.innerHTML = `
검색 결과가 없습니다 ("${escapeHtml(this.searchQuery)}")
`; return; } this.listEl.innerHTML = filtered .map((l) => { const isVisible = !this.hiddenLayers.has(l.name); const opacity = isVisible ? '1' : '0.4'; const eyeIcon = isVisible ? '👁️' : '🕶️'; const eyeTitle = isVisible ? '숨기기' : '표시하기'; return `
${escapeHtml(l.name)} ${l.count}
`; }) .join(''); // 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, '&') .replace(//g, '>') .replace(/"/g, '"'); }