/** * ═══════════════════════════════════════════════════════════════════ * DWG/DXF viewer sample — 모듈 조합 레시피 * (docs/modules-dwg-dxf.html 에 정리된 모듈을 그대로 조립) * * 이 프로젝트는 hmwebviewer 와 형제 폴더로 분리된 독립 샘플입니다. * ═══════════════════════════════════════════════════════════════════ * * ┌─ 모듈 지도 카드 ──────────────┬─ 이 파일에서의 역할 ──────────┐ * │ acadrust-dwg (WASM) │ DWG 바이트 → parseResult │ * │ dxf-parser + dxfHatchHandler │ DXF 텍스트 → parseResult │ * │ dxfAdapter │ dxf-parser 출력을 공통 스키마로 │ * │ Viewer2D │ parseResult → three 씬 렌더 │ * │ three (peer) │ WebGLRenderer / OrbitControls │ * │ slugText + NanumGothic.ttf │ TEXT/MTEXT 한글·라틴 표시 │ * │ ext2d │ .dwg / .dxf 확장자 판별 │ * └───────────────────────────────┴───────────────────────────────┘ * * 흐름: File/URL → (확장자 분기) → 파서 → CadParseResult → Viewer2D.load() */ // ── ① 모듈 지도의 각 카드 (src/viewer2d/*) ──────────────────────── import { Viewer2D } from './viewer2d/Viewer2D.js'; import { parseDwgBuffer, type CadParseResult } from './viewer2d/dwgParser'; import { parseDxfBuffer } from './viewer2d/parseDxf'; import { is2D, isDxf, isDwg } from './viewer2d/ext2d'; import { listCadSpaces, pickDefaultSpace } from './viewer2d/cadSpaces'; import { setDefaultFontUrl } from './viewer2d/slugText'; // parseDxfBuffer 내부: // dxf-parser → HatchHandler → dxfAdapter // parseDwgBuffer 내부: // acadrustParser → acadrust-dwg WASM // ── ② 호스트 정적 자산 ────────────────────────────────────────── setDefaultFontUrl('/fonts/NanumGothic-Regular.ttf'); // ── ③ Viewer2D 마운트 ─────────────────────────────────────────── const app = document.getElementById('app')!; const statusEl = document.getElementById('status')!; const layerList = document.getElementById('layerList')!; const layersPanel = document.getElementById('layers')!; const spacesBar = document.getElementById('spaces')!; function setStatus(msg: string, err = false) { statusEl.textContent = msg; statusEl.classList.toggle('err', err); } const viewer = new Viewer2D(app); viewer.onSelect((entity: unknown) => { if (!entity) { setStatus('deselected'); return; } const e = entity as { type?: string; layer?: string; handle?: { value?: string } }; setStatus(`select ${e.type ?? '?'} · layer=${e.layer ?? '-'} · h=${e.handle?.value ?? '-'}`); }); // CAD coordinates under the cursor (Model: UCS like AutoCAD; Layout: paper units) const coordXEl = document.getElementById('coordX')!; const coordYEl = document.getElementById('coordY')!; function formatCadCoord(v: number): string { const a = Math.abs(v); if (a >= 1e6) return v.toFixed(1); if (a >= 1e3) return v.toFixed(2); if (a >= 1) return v.toFixed(3); return v.toFixed(4); } viewer.onPointerWorld((p) => { if (!p) { coordXEl.textContent = '—'; coordYEl.textContent = '—'; return; } coordXEl.textContent = formatCadCoord(p.x); coordYEl.textContent = formatCadCoord(p.y); }); /** Model / Layout tab strip after each load or space switch. */ function fillSpaces() { const spaces = viewer.getSpaces(); const active = viewer.getActiveSpace(); spacesBar.innerHTML = ''; if (spaces.length < 2 && !(spaces[0]?.entityCount)) { spacesBar.style.display = 'none'; return; } spacesBar.style.display = 'flex'; for (const s of spaces) { const btn = document.createElement('button'); btn.type = 'button'; btn.textContent = `${s.label}${s.entityCount ? ` (${s.entityCount})` : ''}`; btn.title = `${s.name} · handle ${s.handleHex}`; if (s.handleHex === active) btn.classList.add('active'); btn.addEventListener('click', () => { if (s.handleHex === viewer.getActiveSpace()) return; viewer.setSpace(s.handleHex); fillSpaces(); fillLayers(); viewer.fit(); setStatus(`space ${s.label} · ${s.entityCount} entities`); }); spacesBar.append(btn); } } // ── ④ 파서 조합 ───────────────────────────────────────────────── async function parseCad(buf: ArrayBuffer, name: string): Promise { if (isDxf(name)) return parseDxfBuffer(buf); if (isDwg(name)) return parseDwgBuffer(new Uint8Array(buf)); const head = new TextDecoder('latin1').decode( new Uint8Array(buf, 0, Math.min(6, buf.byteLength)), ); if (head.startsWith('AC')) return parseDwgBuffer(new Uint8Array(buf)); return parseDxfBuffer(buf); } async function loadAndRender(buf: ArrayBuffer, name: string): Promise { const result = await parseCad(buf, name); const spaces = listCadSpaces(result); const def = pickDefaultSpace(spaces); viewer.load(result, { spaceHandle: def?.handleHex ?? null }); viewer.resize(); return result; } async function loadUrl(url: string, nameHint?: string) { const name = nameHint ?? url.split('?')[0]!.split('/').pop() ?? url; const res = await fetch(url); if (!res.ok) throw new Error(`HTTP ${res.status}`); return loadAndRender(await res.arrayBuffer(), name); } async function loadFile(file: File) { if (!is2D(file.name)) throw new Error(`not .dwg/.dxf: ${file.name}`); return loadAndRender(await file.arrayBuffer(), file.name); } // ── ⑤ 호스트 UI 글루 ──────────────────────────────────────────── function fillLayers() { layerList.innerHTML = ''; for (const L of viewer.getLayerInfo()) { const label = document.createElement('label'); label.dataset.name = L.name; const sw = document.createElement('span'); sw.className = 'swatch'; sw.style.background = L.colorHex || '#888'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = L.visible; cb.addEventListener('change', () => { const next = new Set(); layerList.querySelectorAll('label').forEach((row) => { const input = row.querySelector('input') as HTMLInputElement; if (!input.checked) next.add(row.dataset.name!); }); viewer.setHiddenLayers(next); }); const text = document.createElement('span'); text.textContent = `${L.name} (${L.count})`; label.append(sw, cb, text); layerList.append(label); } } async function afterLoad(name: string, result: CadParseResult) { // Force cache invalidation for OLE image rendering const spaces = listCadSpaces(result); const active = spaces.find((s) => s.handleHex === viewer.getActiveSpace()); const spaceNote = active ? ` · ${active.label}` : ''; setStatus(`✓ ${name} · ${result.entities?.length ?? 0} entities${spaceNote}`); fillSpaces(); fillLayers(); viewer.fit(); } async function run(label: string, job: () => Promise) { setStatus(`loading ${label}…`); try { await afterLoad(label, await job()); } catch (err) { setStatus(`${label}: ${(err as Error).message}`, true); console.error(err); } } const civilFile = '/samples/civil.dwg'; document.getElementById('sample')!.addEventListener('click', () => { void run('BasicSample.dwg', () => loadUrl('/samples/BasicSample.dwg', 'BasicSample.dwg')); }); document.getElementById('sampleCivil')?.addEventListener('click', () => { void run('C0060202-001-평면및종단면도.dwg', () => loadUrl(civilFile, 'C0060202-001-평면및종단면도.dwg')); }); document.getElementById('sampleDxf')!.addEventListener('click', () => { void run('simple.dxf', () => loadUrl('/samples/simple.dxf', 'simple.dxf')); }); document.getElementById('file')!.addEventListener('change', (ev) => { const file = (ev.target as HTMLInputElement).files?.[0]; if (!file) return; void run(file.name, () => loadFile(file)); (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(); const file = e.dataTransfer?.files?.[0]; if (file) void run(file.name, () => loadFile(file)); }); const model = new URLSearchParams(location.search).get('model'); if (model) { void run(model, () => loadUrl(model)); } else { setStatus('ready'); } window.addEventListener('resize', () => viewer.resize());