diff --git a/src/main.ts b/src/main.ts index 8f95c50..1b11adb 100644 --- a/src/main.ts +++ b/src/main.ts @@ -203,14 +203,24 @@ document.getElementById('file')!.addEventListener('change', (ev) => { (ev.target as HTMLInputElement).value = ''; }); let dark = true; +document.getElementById('fit')?.addEventListener('click', () => { + viewer.fit(); +}); document.getElementById('theme')!.addEventListener('click', () => { dark = !dark; viewer.setTheme(dark); + fillLayers(); }); document.getElementById('layersBtn')!.addEventListener('click', () => { layersPanel.style.display = layersPanel.style.display === 'block' ? 'none' : 'block'; }); +window.addEventListener('keydown', (e) => { + if ((e.key === 'f' || e.key === 'F') && (e.target as HTMLElement)?.tagName !== 'INPUT') { + viewer.fit(); + } +}); + window.addEventListener('dragover', (e) => e.preventDefault()); window.addEventListener('drop', (e) => { e.preventDefault(); @@ -219,7 +229,11 @@ window.addEventListener('drop', (e) => { }); const model = new URLSearchParams(location.search).get('model'); -if (model) void run(model, () => loadUrl(model)); -else void run('C0060202-001-평면및종단면도.dwg', () => loadUrl(civilFile, 'C0060202-001-평면및종단면도.dwg')); +if (model) { + void run(model, () => loadUrl(model)); +} else { + setStatus('ready'); +} window.addEventListener('resize', () => viewer.resize()); + diff --git a/src/viewer2d/Viewer2D.d.ts b/src/viewer2d/Viewer2D.d.ts index a8a60a0..4565721 100644 --- a/src/viewer2d/Viewer2D.d.ts +++ b/src/viewer2d/Viewer2D.d.ts @@ -5,7 +5,7 @@ import type { CadSpace } from './cadSpaces'; export class Viewer2D { constructor(container: HTMLElement); /** Render a DWG/DXF parseResult. `keepView` preserves pan/zoom on re-render. */ - load(result: unknown, opts?: { keepView?: boolean; spaceHandle?: string | number | null }): void; + load(result: unknown, opts?: { keepView?: boolean; keepTheme?: boolean; spaceHandle?: string | number | null }): void; /** Zoom-extents to the loaded drawing. */ fit(): void; /** Resync renderer/camera to the container size (call after un-hiding). */ diff --git a/src/viewer2d/Viewer2D.js b/src/viewer2d/Viewer2D.js index b2f503b..c34705c 100644 --- a/src/viewer2d/Viewer2D.js +++ b/src/viewer2d/Viewer2D.js @@ -50,6 +50,8 @@ export class Viewer2D { this._scene = new THREE.Scene(); this._scene.background = new THREE.Color(0x0d1117); + this._isDark = true; + this._isLoading = false; const w = container.clientWidth || 800; const h = container.clientHeight || 600; @@ -243,43 +245,46 @@ export class Viewer2D { } load(result, opts = {}) { - this._lastResult = result; - this._selMeta = null; - this._clear(); - this._entityMeta = []; - this._buildLayerMap(result?.tables?.layers); - this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles); - this._buildLinetypeMap(result); - // Sheet orientation: the active model-space viewport's VIEWTWIST rotates the - // view so a drawing stored tilted in WCS (rotated survey sheets — header UCS - // stays identity) displays with its title border upright. Applied as a - // camera-up rotation in _fit (geometry untouched → picking stays exact). - this._viewTwist = this._readViewTwist(result); - // Geometric pick index — the ACTUAL rendered geometry, not a bounds proxy. - // _pickSegs: flat [ax,ay,bx,by,…] of every line segment (lines, arcs, circles, - // polylines, block wires, dimension leaders/arrows) with a parallel owner-meta - // index. _pickFills: filled regions (hatch loops, SOLID quads, text quads) - // tested by even-odd point-in-polygon. _onClick raycasts these directly. - this._pickSegs = []; - this._pickSegMeta = []; - this._pickFills = []; - this._pickCurIdx = -1; + this._isLoading = true; + try { + this._lastResult = result; + this._selMeta = null; + this._clear(); + this._entityMeta = []; - // Model vs Paper (Layout): never draw both spaces at once. - // - opts.spaceHandle provided (including null) → set active space - // - omitted → keep previous _activeSpaceHex if still valid for this file - if (Object.prototype.hasOwnProperty.call(opts, 'spaceHandle')) { - const sh = opts.spaceHandle; - if (sh == null || sh === '') this._activeSpaceHex = null; - else this._activeSpaceHex = typeof sh === 'number' ? sh.toString(16) : String(sh).toLowerCase(); - } + // Model vs Paper (Layout): never draw both spaces at once. + // - opts.spaceHandle provided (including null) → set active space + // - omitted → keep previous _activeSpaceHex if still valid for this file + if (Object.prototype.hasOwnProperty.call(opts, 'spaceHandle')) { + const sh = opts.spaceHandle; + if (sh == null || sh === '') this._activeSpaceHex = null; + else this._activeSpaceHex = typeof sh === 'number' ? sh.toString(16) : String(sh).toLowerCase(); + } - if (!opts.keepTheme) { - const spacesList = listCadSpaces(result); - const activeSp = spacesList.find((s) => s.handleHex === this._activeSpaceHex); - const isPaper = activeSp?.kind === 'paper'; - this.setTheme(!isPaper); - } + if (!opts.keepTheme) { + const spacesList = listCadSpaces(result); + const activeSp = spacesList.find((s) => s.handleHex === this._activeSpaceHex); + const isPaper = activeSp?.kind === 'paper'; + this.setTheme(!isPaper); + } + + this._buildLayerMap(result?.tables?.layers); + this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles); + this._buildLinetypeMap(result); + // Sheet orientation: the active model-space viewport's VIEWTWIST rotates the + // view so a drawing stored tilted in WCS (rotated survey sheets — header UCS + // stays identity) displays with its title border upright. Applied as a + // camera-up rotation in _fit (geometry untouched → picking stays exact). + this._viewTwist = this._readViewTwist(result); + // Geometric pick index — the ACTUAL rendered geometry, not a bounds proxy. + // _pickSegs: flat [ax,ay,bx,by,…] of every line segment (lines, arcs, circles, + // polylines, block wires, dimension leaders/arrows) with a parallel owner-meta + // index. _pickFills: filled regions (hatch loops, SOLID quads, text quads) + // tested by even-odd point-in-polygon. _onClick raycasts these directly. + this._pickSegs = []; + this._pickSegMeta = []; + this._pickFills = []; + this._pickCurIdx = -1; const entities = result?.entities || []; const lineVerts = []; @@ -1049,14 +1054,24 @@ export class Viewer2D { const fitBox = box.isEmpty() ? null : (this._trimmedContentBox(fitSamples, box) || box); this._contentBox = fitBox ? fitBox.clone() : null; if (!opts.keepView && fitBox && !fitBox.isEmpty()) this._fit(fitBox); + } finally { + this._isLoading = false; + } } // ── UI integration ───────────────────────────────────────────────────────── /** Swap canvas background for theme. dark=true → near-black (Model), false → white (Layout). */ setTheme(dark) { - this._scene.background = new THREE.Color(dark ? 0x0a0b0d : 0xffffff); + const isDarkBool = !!dark; + const changed = (this._isDark !== isDarkBool); + this._isDark = isDarkBool; + this._scene.background = new THREE.Color(this._isDark ? 0x0a0b0d : 0xffffff); if (this._gridVisible) this._rebuildGrid(); + + if (changed && this._lastResult && !this._isLoading) { + this.load(this._lastResult, { keepView: true, keepTheme: true, spaceHandle: this._activeSpaceHex }); + } } setGrid(visible) { @@ -1547,7 +1562,7 @@ export class Viewer2D { const aci = Math.abs(l.colorIndex ?? l.color ?? l.colorNumber ?? 7); return { name, - colorHex: '#' + ((aciToHex(aci) ?? DEFAULT_COLOR).toString(16).padStart(6, '0')), + colorHex: '#' + ((aciToHex(aci, this._isDark) ?? DEFAULT_COLOR).toString(16).padStart(6, '0')), count: counts.get(name) || 0, visible: !hidden.has(name), }; @@ -1914,7 +1929,7 @@ export class Viewer2D { const handle = l.handle?.value ?? l.handle; const name = l.name ?? l.layerName; const aci = Math.abs(l.colorIndex ?? l.color ?? l.colorNumber ?? 7); - const hex = aciToHex(aci) ?? DEFAULT_COLOR; + const hex = aciToHex(aci, this._isDark) ?? DEFAULT_COLOR; if (handle != null) this._layerByHandle.set(String(handle), hex); if (name) this._layerByName.set(name, hex); if (handle != null && name) this._layerNameByHandle.set(String(handle), name); @@ -2006,7 +2021,7 @@ export class Viewer2D { const aci = entity.entityHeader?.colorIndex ?? entity.color ?? entity.colorIndex ?? entity.colorNumber ?? 256; if (aci != null) { - const hex = aciToHex(aci); + const hex = aciToHex(aci, this._isDark); if (hex !== null) return hex; } const lh = entity.layerHandle?.value ?? entity.layerHandle; @@ -2900,7 +2915,7 @@ export class Viewer2D { } else if (t.bgColorRgb && (t.bgColorRgb.r != null)) { bgColor = ((t.bgColorRgb.r & 255) << 16) | ((t.bgColorRgb.g & 255) << 8) | (t.bgColorRgb.b & 255); } else if (t.bgColorIndex != null) { - const hex = aciToHex(t.bgColorIndex); + const hex = aciToHex(t.bgColorIndex, this._isDark); if (hex != null) bgColor = hex; } diff --git a/src/viewer2d/aciColors.js b/src/viewer2d/aciColors.js index 59bceba..cb01809 100644 --- a/src/viewer2d/aciColors.js +++ b/src/viewer2d/aciColors.js @@ -47,8 +47,9 @@ const VARIANTS_SL = [ [0.00, 0.30], // 9: dark gray ]; -export function aciToHex(aci) { +export function aciToHex(aci, isDark = true) { if (aci == null || aci === 0 || aci === 256) return null; + if (aci === 7) return isDark ? 0xFFFFFF : 0x000000; if (aci <= 9) return FIXED[aci] ?? null; if (aci >= 250 && aci <= 255) { const v = Math.round(40 + (aci - 250) * 43);