feat: Entity 선택기능, Property Inspector 패널, Layer 관리 패널, 드래그/접기/크기조절 UI 구현
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
/**
|
||||
* 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<string> = 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());
|
||||
}
|
||||
}
|
||||
|
||||
/** 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.render();
|
||||
}
|
||||
|
||||
public hideAllLayers(): void {
|
||||
const layers = this.viewer.getLayerInfo() || [];
|
||||
for (const l of layers) {
|
||||
this.hiddenLayers.add(l.name);
|
||||
}
|
||||
this.viewer.setHiddenLayers(this.hiddenLayers);
|
||||
this.render();
|
||||
}
|
||||
|
||||
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.render();
|
||||
}
|
||||
|
||||
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 = `
|
||||
<div style="padding:30px 16px;text-align:center;color:var(--muted);font-size:12px">
|
||||
도면에 레이어 정보가 없습니다
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = layers.filter((l) =>
|
||||
l.name.toLowerCase().includes(this.searchQuery)
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
this.listEl.innerHTML = `
|
||||
<div style="padding:24px 16px;text-align:center;color:var(--muted);font-size:12px">
|
||||
검색 결과가 없습니다 ("${escapeHtml(this.searchQuery)}")
|
||||
</div>`;
|
||||
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 `
|
||||
<div class="layer-row" data-name="${escapeHtml(l.name)}"
|
||||
style="display:flex;align-items:center;gap:10px;padding:7px 10px;border-bottom:1px solid var(--line);opacity:${opacity};transition:opacity .15s;user-select:none">
|
||||
<span style="width:10px;height:10px;border-radius:3px;background:${l.colorHex || '#888'};flex-shrink:0;box-shadow:0 0 0 1px rgba(255,255,255,.15)"></span>
|
||||
<span class="mono" style="flex:1;font-size:12px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis" title="${escapeHtml(l.name)}">
|
||||
${escapeHtml(l.name)}
|
||||
</span>
|
||||
<span class="mono" style="font-size:11px;color:var(--muted);background:rgba(255,255,255,.06);padding:2px 6px;border-radius:4px;flex-shrink:0">
|
||||
${l.count}
|
||||
</span>
|
||||
<button type="button" class="eye-btn" data-toggle="${escapeHtml(l.name)}" title="${eyeTitle}"
|
||||
style="background:none;border:none;cursor:pointer;font-size:13px;padding:2px 4px;border-radius:4px;color:var(--ink);transition:transform .1s">
|
||||
${eyeIcon}
|
||||
</button>
|
||||
</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);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str: string): string {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
Reference in New Issue
Block a user