fix(parser): dynamically extract OLE2FRAME 4-corner coordinates from binary OLE stream header; remove hardcoded coordinates

This commit is contained in:
minsung
2026-07-30 16:10:43 +09:00
parent 963a90319e
commit 76f3e20582
12 changed files with 4865 additions and 17 deletions
+361
View File
@@ -0,0 +1,361 @@
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DWG/DXF · 모듈 조합 레시피 & 샘플</title>
<style>
:root {
--bg: #eef1f4; --panel: #fff; --ink: #1a2230; --ink-soft: #4a5568; --ink-faint: #8592a3;
--line: #d3dae2; --line-strong: #b7c1cd; --accent: #0e7c86; --accent-soft: #d3ecef;
--wasm: #b45309; --wasm-soft: #fbe8cf; --js: #0e7c86;
--grid: rgba(14,124,134,.06);
--shadow: 0 1px 2px rgba(26,34,48,.06), 0 8px 24px rgba(26,34,48,.06);
--mono: ui-monospace, "SF Mono", "Cascadia Code", Consolas, monospace;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", sans-serif;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1219; --panel: #141b25; --ink: #e6ecf3; --ink-soft: #a3b0c0; --ink-faint: #6b7a8d;
--line: #243040; --line-strong: #33445a; --accent: #3fd0dc; --accent-soft: #123038;
--wasm: #f0a03c; --wasm-soft: #33260f; --js: #3fd0dc; --grid: rgba(63,208,220,.05);
--shadow: 0 1px 2px rgba(0,0,0,.4), 0 8px 28px rgba(0,0,0,.35);
}
}
* { box-sizing: border-box; }
body {
margin: 0;
background:
linear-gradient(var(--grid) 1px, transparent 1px) 0 0 / 26px 26px,
linear-gradient(90deg, var(--grid) 1px, transparent 1px) 0 0 / 26px 26px,
var(--bg);
color: var(--ink); font-family: var(--sans); line-height: 1.6;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 48px 24px 96px; }
header { border-bottom: 1px solid var(--line); padding-bottom: 28px; margin-bottom: 36px; }
.eyebrow {
font-family: var(--mono); font-size: 12px; letter-spacing: .14em; text-transform: uppercase;
color: var(--accent); display: flex; align-items: center; gap: 10px; margin-bottom: 14px;
}
.eyebrow::before { content: ""; width: 28px; height: 1px; background: var(--accent); }
h1 { font-size: clamp(26px, 4vw, 38px); line-height: 1.15; margin: 0 0 12px; letter-spacing: -.01em; }
h1 .dim { color: var(--ink-faint); font-weight: 400; }
.lede { font-size: 16px; color: var(--ink-soft); max-width: 64ch; margin: 0; }
.cta {
display: inline-flex; align-items: center; gap: 10px; margin-top: 20px;
font-family: var(--mono); font-size: 13px; font-weight: 600; text-decoration: none;
color: var(--accent); background: var(--panel); border: 1px solid var(--line);
border-left: 3px solid var(--accent); border-radius: 8px; padding: 10px 14px;
box-shadow: var(--shadow);
}
h2 {
font-size: 12px; font-family: var(--mono); letter-spacing: .12em; text-transform: uppercase;
color: var(--ink-faint); margin: 48px 0 16px; display: flex; align-items: center; gap: 12px;
}
h2 .num { color: var(--accent); }
h2::after { content: ""; flex: 1; height: 1px; background: var(--line); }
.legend { display: flex; flex-wrap: wrap; gap: 16px; font-size: 13px; color: var(--ink-soft); margin: -4px 0 18px; }
.legend span { display: inline-flex; align-items: center; gap: 7px; }
.dot { width: 11px; height: 11px; border-radius: 3px; }
.dot.wasm { background: var(--wasm); }
.dot.js { background: var(--js); }
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(210px, 1fr)); gap: 12px; }
.card {
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
padding: 14px; box-shadow: var(--shadow); position: relative; overflow: hidden;
}
.card::before { content: ""; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; background: var(--tag); }
.card.w { --tag: var(--wasm); }
.card.j { --tag: var(--js); }
.card .k {
font-family: var(--mono); font-size: 13px; font-weight: 600; margin-bottom: 6px;
display: flex; justify-content: space-between; gap: 8px; align-items: baseline;
}
.badge {
font-family: var(--mono); font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
padding: 2px 6px; border-radius: 4px; font-weight: 600;
}
.badge.w { background: var(--wasm-soft); color: var(--wasm); }
.badge.j { background: var(--accent-soft); color: var(--js); }
.card .d { font-size: 13px; color: var(--ink-soft); line-height: 1.45; }
.card .path { font-family: var(--mono); font-size: 11px; color: var(--ink-faint); margin-top: 8px; word-break: break-all; }
.flow {
background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
padding: 22px 16px; box-shadow: var(--shadow); overflow-x: auto;
}
.pipe { display: flex; align-items: stretch; min-width: 720px; }
.node {
flex: 1; background: var(--bg); border: 1px solid var(--line-strong); border-radius: 9px;
padding: 12px; display: flex; flex-direction: column; gap: 4px; min-width: 0;
}
.node.hl { border-color: var(--accent); background: var(--accent-soft); }
.node.w { border-color: var(--wasm); }
.node .tier { font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--ink-faint); }
.node.hl .tier { color: var(--accent); }
.node.w .tier { color: var(--wasm); }
.node .name { font-family: var(--mono); font-size: 12.5px; font-weight: 600; word-break: break-all; }
.node .sub { font-size: 12px; color: var(--ink-soft); }
.arrow { flex: 0 0 32px; display: flex; align-items: center; justify-content: center; color: var(--accent); font-size: 18px; }
.flow-note { font-size: 12.5px; color: var(--ink-faint); margin-top: 14px; font-family: var(--mono); }
.flow-note b { color: var(--accent); }
pre {
background: var(--panel); border: 1px solid var(--line); border-radius: 10px; padding: 16px 18px;
overflow-x: auto; font-family: var(--mono); font-size: 12.5px; color: var(--ink-soft); line-height: 1.55;
box-shadow: var(--shadow); margin: 0;
}
pre .c { color: var(--accent); }
pre .w { color: var(--wasm); }
pre .cm { color: var(--ink-faint); }
.steps { display: grid; gap: 12px; }
.step {
background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
padding: 16px 18px; box-shadow: var(--shadow); display: grid; grid-template-columns: 36px 1fr; gap: 12px;
}
.step .n {
font-family: var(--mono); font-weight: 700; font-size: 18px; color: var(--accent);
width: 36px; height: 36px; border-radius: 8px; background: var(--accent-soft);
display: flex; align-items: center; justify-content: center;
}
.step h3 { margin: 0 0 4px; font-size: 15px; }
.step p { margin: 0; font-size: 13.5px; color: var(--ink-soft); }
.step code { font-family: var(--mono); font-size: .9em; background: var(--bg); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
code { font-family: var(--mono); font-size: .9em; background: var(--bg); padding: 1px 5px; border-radius: 4px; border: 1px solid var(--line); }
footer {
margin-top: 56px; padding-top: 18px; border-top: 1px solid var(--line);
font-family: var(--mono); font-size: 12px; color: var(--ink-faint);
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 8px;
}
a { color: var(--accent); }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">compose · dont invent</div>
<h1>DWG · DXF 모듈 조합 <span class="dim">레시피</span></h1>
<p class="lede">
전체 제품이 아니라, 지도에 있는 <b>재사용 모듈만</b> 골라 조립하면 2D CAD 뷰어가 됩니다.
아래 카드 → 파이프라인 → 샘플 코드 순으로 옮기면 다른 프로젝트에서도 동일하게 동작합니다.
</p>
<a class="cta" href="/">
▶ 실행 샘플 · 이 프로젝트 루트 (npm run dev)
</a>
<p style="margin:10px 0 0; font-family:var(--mono); font-size:12px; color:var(--ink-faint)">
cd dwg-dxf-viewer-sample && npm i && npm run dev → http://localhost:5173
</p>
</header>
<!-- 01 modules -->
<h2><span class="num">01</span> 조합에 쓰는 모듈 (지도 카드)</h2>
<div class="legend">
<span><i class="dot wasm"></i> WASM 파서</span>
<span><i class="dot js"></i> JS 모듈 · 렌더러</span>
</div>
<div class="grid">
<div class="card w">
<div class="k">acadrust-dwg <span class="badge w">wasm</span></div>
<div class="d">DWG 바이너리 → JSON parseResult. 외부 네트워크 없음.</div>
<div class="path">src/viewer2d/acadrust-dwg/*.wasm (이 프로젝트)</div>
</div>
<div class="card j">
<div class="k">dwgParser / acadrustParser <span class="badge j">wrap</span></div>
<div class="d">WASM 1회 init + <code>parseDwgBuffer(bytes)</code>.</div>
<div class="path">dwgParser.ts · acadrustParser.ts</div>
</div>
<div class="card j">
<div class="k">dxf-parser <span class="badge j">npm</span></div>
<div class="d">DXF 텍스트 파싱 (MIT). HATCH 기본 미지원.</div>
<div class="path">npm: dxf-parser</div>
</div>
<div class="card j">
<div class="k">dxfHatchHandler <span class="badge j">patch</span></div>
<div class="d"><code>registerEntityHandler</code>로 HATCH 보강.</div>
<div class="path">dxfHatchHandler.js</div>
</div>
<div class="card j">
<div class="k">dxfAdapter <span class="badge j">adapt</span></div>
<div class="d">dxf-parser 출력 → Viewer2D 공통 스키마.</div>
<div class="path">dxfAdapter.js</div>
</div>
<div class="card j">
<div class="k">Viewer2D <span class="badge j">render</span></div>
<div class="d">Orthographic three 씬. LINE/ARC/HATCH/TEXT/INSERT…</div>
<div class="path">Viewer2D.js</div>
</div>
<div class="card j">
<div class="k">three <span class="badge j">peer</span></div>
<div class="d">WebGLRenderer + OrbitControls (pan/zoom).</div>
<div class="path">npm: three ≥0.160</div>
</div>
<div class="card j">
<div class="k">slugText + font <span class="badge j">text</span></div>
<div class="d">opentype.js → GPU 텍스트. TTF를 호스트가 서빙.</div>
<div class="path">slugText.ts · /fonts/*.ttf</div>
</div>
<div class="card j">
<div class="k">ext2d <span class="badge j">util</span></div>
<div class="d"><code>isDwg</code> / <code>isDxf</code> / <code>is2D</code> — 의존 zero.</div>
<div class="path">ext2d.ts</div>
</div>
</div>
<!-- 02 pipeline -->
<h2><span class="num">02</span> 조립 파이프라인</h2>
<div class="flow">
<div class="pipe">
<div class="node">
<div class="tier">Host</div>
<div class="name">File · URL · Drop</div>
<div class="sub">.dwg / .dxf</div>
</div>
<div class="arrow"></div>
<div class="node hl">
<div class="tier">ext2d</div>
<div class="name">isDwg / isDxf</div>
<div class="sub">분기</div>
</div>
<div class="arrow"></div>
<div class="node w">
<div class="tier">Parser A or B</div>
<div class="name">acadrust · dxf-parser</div>
<div class="sub">(+ hatch · adapter)</div>
</div>
<div class="arrow"></div>
<div class="node hl">
<div class="tier">Contract</div>
<div class="name">CadParseResult</div>
<div class="sub">공통 JSON</div>
</div>
<div class="arrow"></div>
<div class="node">
<div class="tier">Viewer2D</div>
<div class="name">.load(result)</div>
<div class="sub">three 씬</div>
</div>
</div>
<div class="flow-note">
<b>핵심 계약</b>: 파서는 파일을 안 열고, 렌더러는 바이트를 안 연다.
중간은 항상 <code>CadParseResult</code> 하나.
</div>
</div>
<!-- 03 recipe steps -->
<h2><span class="num">03</span> 다른 프로젝트에 옮기는 순서</h2>
<div class="steps">
<div class="step">
<div class="n">1</div>
<div>
<h3>모듈 소스 복사</h3>
<p>
이 샘플의 <code>src/viewer2d/</code> 전체를 복사.
npm: <code>three</code>, <code>dxf-parser</code>, <code>opentype.js</code>.
(원본 소스는 형제 폴더 <code>hmwebviewer/src/viewer2d</code>.)
</p>
</div>
</div>
<div class="step">
<div class="n">2</div>
<div>
<h3>정적 자산</h3>
<p>
<code>public/fonts/NanumGothic-Regular.ttf</code> (한글 TEXT).
WASM은 소스가 <code>?url</code>로 import — Vite면 <code>assetsInclude: ['**/*.wasm']</code>.
</p>
</div>
</div>
<div class="step">
<div class="n">3</div>
<div>
<h3>아래 스니펫처럼 조합</h3>
<p>
래퍼 클래스 없이 모듈을 직접 import 해도 됩니다.
실행 가능한 전체 예: 이 프로젝트 <code>src/main.ts</code>.
</p>
</div>
</div>
<div class="step">
<div class="n">4</div>
<div>
<h3>호스트 UI만 붙이기</h3>
<p>
드롭존 · 툴바 · 레이어 패널은 모듈이 아님.
샘플의 ⑤ 영역만 교체하면 디자인 시스템에 맞게 붙일 수 있음.
</p>
</div>
</div>
</div>
<!-- 04 code -->
<h2><span class="num">04</span> 최소 조합 코드 (샘플과 동일 패턴)</h2>
<pre><span class="cm">// ① 모듈 지도 카드 → import</span>
<span class="c">import</span> { Viewer2D } <span class="c">from</span> './viewer2d/Viewer2D.js';
<span class="c">import</span> { parseDwgBuffer } <span class="c">from</span> './viewer2d/dwgParser'; <span class="cm">// acadrust-dwg</span>
<span class="c">import</span> { parseDxfBuffer } <span class="c">from</span> './viewer2d/parseDxf'; <span class="cm">// dxf-parser+hatch+adapter</span>
<span class="c">import</span> { isDwg, isDxf } <span class="c">from</span> './viewer2d/ext2d';
<span class="c">import</span> { setDefaultFontUrl } <span class="c">from</span> './viewer2d/slugText';
setDefaultFontUrl('/fonts/NanumGothic-Regular.ttf');
<span class="cm">// ② 렌더러</span>
<span class="c">const</span> viewer = <span class="c">new</span> Viewer2D(document.getElementById('app')!);
<span class="cm">// ③ 파서 조합</span>
<span class="c">async function</span> parseCad(buf: ArrayBuffer, name: string) {
<span class="c">if</span> (isDxf(name)) <span class="c">return</span> parseDxfBuffer(buf); <span class="cm">// dxf-parser 경로</span>
<span class="c">if</span> (isDwg(name)) <span class="c">return</span> parseDwgBuffer(<span class="c">new</span> Uint8Array(buf)); <span class="cm">// WASM 경로</span>
<span class="c">throw new</span> Error('need .dwg or .dxf name');
}
<span class="cm">// ④ 계약: parseResult → load</span>
<span class="c">async function</span> open(file: File) {
<span class="c">const</span> result = <span class="c">await</span> parseCad(<span class="c">await</span> file.arrayBuffer(), file.name);
viewer.load(result);
viewer.resize();
viewer.fit();
}</pre>
<h2><span class="num">05</span> DWG 경로 vs DXF 경로</h2>
<div class="grid">
<div class="card w">
<div class="k">.dwg 경로 <span class="badge w">wasm</span></div>
<div class="d">
<code>parseDwgBuffer</code><br>
→ acadrustParser.init<br>
<span style="color:var(--wasm)">parse_dwg(bytes)</span><br>
→ CadParseResult
</div>
<div class="path">Sample DWG 버튼으로 검증</div>
</div>
<div class="card j">
<div class="k">.dxf 경로 <span class="badge j">js</span></div>
<div class="d">
<code>parseDxfBuffer</code><br>
→ decodeDxf (UTF-8 / CP949…)<br>
→ DxfParser + HatchHandler<br>
→ dxfToParseResult
</div>
<div class="path">Sample DXF 버튼으로 검증</div>
</div>
</div>
<h2><span class="num">06</span> 샘플 위치</h2>
<pre style="white-space:pre-wrap"><span class="c">dwg-dxf-viewer-sample/</span> ← hmwebviewer 형제 폴더
src/main.ts ← 모듈을 직접 import 해서 조립 (핵심)
src/viewer2d/ ← acadrust-dwg · Viewer2D · dxf-*
index.html ← 툴바 + Module recipe 패널
public/fonts/ ← NanumGothic (slugText)
public/samples/ ← BasicSample.dwg · simple.dxf
docs/modules-dwg-dxf.html
<span class="c">실행</span>
cd ../dwg-dxf-viewer-sample # (hmwebviewer 기준 형제)
npm install
npm run dev → http://localhost:5173</pre>
<footer>
<span>DWG/DXF only · sibling of hmwebviewer</span>
<span>no 3D / SHP / R2</span>
</footer>
</div>
</body>
</html>