forked from kimminsung/dwg-dxf-viewer-sample
The shipped wasm was built from a vendored acadrust 0.4.1 carrying local changes in two files (read_mesh array bounds, OLE2FRAME corner decoding), and the published read_mesh patch did not even apply to the pristine crate, so the notice could not reproduce the source compiled into the wasm (MPL-2.0 3.2). acadrust 0.6.5 already does both on its own: the road-surface MESH and the C0060203 OLE2FRAME come out identical to the patched build, so the parser now uses the crate exactly as crates.io serves it. The notices name 0.6.5, its upstream commit and the crate SHA-256 (equal to the Cargo.lock checksum the build used), the stale patch file is removed, and the page links to the crate source instead. Verified on 22 samples, loading each with the old and the new wasm in Chrome: all load, 17 render pixel-identical, and the 5 that differ are 0.4.1 misreads that 0.6.5 corrects. The wrapper changes this needed live in hmwebviewer rust/dwg-wasm (branch chore/acadrust-0.6.5). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
354 lines
16 KiB
HTML
354 lines
16 KiB
HTML
<!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: #0d1117; --panel: #161b22; --ink: #e6edf3; --muted: #8b949e;
|
|
--accent: #3fb950; --wasm: #f0a03c; --line: #30363d; --danger: #f85149;
|
|
--mono: ui-monospace, "Cascadia Code", Consolas, monospace;
|
|
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "Malgun Gothic", sans-serif;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; background: var(--bg); color: var(--ink); font-family: var(--sans); }
|
|
#app { position: fixed; inset: 0; }
|
|
|
|
/* ── 상단 로딩 프로그레스 바 (src/topProgress.ts 가 구동) ───────────────
|
|
.bar : 실제 진행률. width 트랜지션 = 메인 스레드 의존.
|
|
.sweep : WASM 파싱 / Viewer2D.load() 같은 동기 블로킹 구간용. transform
|
|
키프레임은 컴포지터 스레드에서 돌기 때문에 메인 스레드가 멈춰
|
|
있어도 계속 흐른다 — 그 구간에 줄 수 있는 유일한 피드백. */
|
|
#progress {
|
|
position: fixed; top: 0; left: 0; right: 0; height: 3px; z-index: 100;
|
|
pointer-events: none; overflow: hidden;
|
|
opacity: 0; transition: opacity .3s ease .15s;
|
|
}
|
|
#progress.on { opacity: 1; transition-delay: 0s; }
|
|
#progress .bar {
|
|
position: absolute; top: 0; bottom: 0; left: 0; width: 0;
|
|
background: linear-gradient(90deg, var(--accent), #58a6ff 55%, var(--wasm));
|
|
box-shadow: 0 0 10px rgba(63,185,80,.65), 0 0 4px rgba(88,166,255,.5);
|
|
transition: width .28s cubic-bezier(.25,.8,.25,1);
|
|
}
|
|
#progress.err .bar {
|
|
background: var(--danger);
|
|
box-shadow: 0 0 10px rgba(248,81,73,.7);
|
|
}
|
|
#progress .sweep {
|
|
position: absolute; top: 0; bottom: 0; left: 0; width: 34%;
|
|
background: linear-gradient(90deg, transparent, rgba(230,237,243,.9), transparent);
|
|
transform: translateX(-105%); will-change: transform; opacity: 0;
|
|
}
|
|
#progress.busy .sweep {
|
|
opacity: 1;
|
|
animation: pr-sweep 1.15s cubic-bezier(.4,0,.2,1) infinite;
|
|
}
|
|
@keyframes pr-sweep {
|
|
from { transform: translateX(-105%); }
|
|
to { transform: translateX(400%); }
|
|
}
|
|
@media (prefers-reduced-motion: reduce) {
|
|
#progress .sweep { animation: none; }
|
|
#progress .bar { transition: none; }
|
|
}
|
|
#toolbar {
|
|
position: fixed; top: 12px; left: 12px; right: 12px; z-index: 10;
|
|
display: flex; flex-wrap: wrap; gap: 8px; align-items: center; pointer-events: none;
|
|
}
|
|
#toolbar > * { pointer-events: auto; }
|
|
button, label.btn {
|
|
background: var(--panel); color: var(--ink); border: 1px solid var(--line);
|
|
border-radius: 8px; padding: 8px 12px; font: 13px var(--sans); cursor: pointer;
|
|
}
|
|
button:hover, label.btn:hover { border-color: var(--accent); }
|
|
button.primary { background: #238636; border-color: #2ea043; }
|
|
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
|
label.btn input { display: none; }
|
|
/* 옵션 패널 — 값은 IndexedDB(hmw-viewer/options)에 저장된다 */
|
|
#optBody { padding: 12px 14px; overflow: auto; font-size: 12px; color: var(--ink); }
|
|
.opt-sec { margin-bottom: 16px; }
|
|
.opt-sec > h4 {
|
|
margin: 0 0 8px; font-size: 11px; font-weight: 700; letter-spacing: .05em;
|
|
text-transform: uppercase; color: var(--muted);
|
|
}
|
|
.opt-row { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
|
|
.opt-row > label { flex: 1; color: var(--ink); }
|
|
.opt-row input[type=range] { flex: 1; min-width: 90px; accent-color: var(--accent); }
|
|
.opt-row input[type=number] {
|
|
width: 58px; background: #0d1117; color: var(--ink);
|
|
border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px;
|
|
font: 12px var(--mono); text-align: right;
|
|
}
|
|
.opt-row input[type=checkbox] { width: 15px; height: 15px; accent-color: var(--accent); }
|
|
.opt-unit { font: 11px var(--mono); color: var(--muted); }
|
|
.opt-note { font: 11px var(--mono); color: var(--accent); line-height: 1.6; word-break: keep-all; }
|
|
.opt-help { font-size: 11px; color: var(--muted); line-height: 1.5; margin: 4px 0 0; }
|
|
#status {
|
|
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
|
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
|
border-radius: 8px; padding: 8px 12px; max-width: min(48vw, 480px);
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
}
|
|
#status.err { color: var(--danger); }
|
|
#recipe {
|
|
position: fixed; bottom: 14px; left: 14px; z-index: 10; max-width: 340px;
|
|
background: rgba(22,27,34,.94); border: 1px solid var(--line); border-radius: 10px;
|
|
padding: 12px 14px; font-size: 12px; line-height: 1.45; color: var(--muted);
|
|
box-shadow: 0 8px 24px rgba(0,0,0,.35);
|
|
}
|
|
#recipe h3 {
|
|
margin: 0 0 8px; font: 600 11px var(--mono); letter-spacing: .1em;
|
|
text-transform: uppercase; color: var(--accent);
|
|
}
|
|
#recipe ol { margin: 0; padding-left: 18px; }
|
|
#recipe li { margin-bottom: 4px; }
|
|
#recipe code { font-family: var(--mono); font-size: 11px; color: var(--ink); }
|
|
#recipe .mod-w { color: var(--wasm); }
|
|
#recipe .mod-j { color: var(--accent); }
|
|
#recipe a { color: var(--accent); font-family: var(--mono); font-size: 11px; }
|
|
#layers {
|
|
position: fixed; top: 56px; right: 12px; width: 200px; max-height: 50vh;
|
|
overflow: auto; z-index: 10; background: rgba(22,27,34,.95);
|
|
border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px;
|
|
font-size: 12px; display: none;
|
|
}
|
|
#layers h3 { margin: 0 0 8px; font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); }
|
|
#layers label { display: flex; gap: 8px; align-items: center; margin: 4px 0; cursor: pointer; }
|
|
#layers .swatch { width: 10px; height: 10px; border-radius: 2px; flex-shrink: 0; }
|
|
#spaces {
|
|
display: none; align-items: center; gap: 0;
|
|
background: var(--panel); border: 1px solid var(--line); border-radius: 8px;
|
|
overflow: hidden;
|
|
}
|
|
#spaces button {
|
|
border: none; border-radius: 0; border-right: 1px solid var(--line);
|
|
background: transparent; padding: 8px 12px; margin: 0;
|
|
}
|
|
#spaces button:last-child { border-right: none; }
|
|
#spaces button.active {
|
|
background: #238636; border-color: #2ea043; color: #fff;
|
|
}
|
|
#spaces button:hover:not(.active) { border-color: transparent; color: var(--accent); }
|
|
/* CAD 좌표 — 항상 우측 하단 (툴바 status 와 별개) */
|
|
#coords {
|
|
position: fixed;
|
|
right: 12px;
|
|
bottom: 12px;
|
|
left: auto;
|
|
top: auto;
|
|
z-index: 20;
|
|
font: 13px/1.35 var(--mono);
|
|
color: var(--ink);
|
|
background: rgba(13,17,23,.95);
|
|
border: 1px solid var(--line);
|
|
border-radius: 8px;
|
|
padding: 10px 14px;
|
|
min-width: 240px;
|
|
text-align: left;
|
|
pointer-events: none;
|
|
letter-spacing: 0.02em;
|
|
box-shadow: 0 8px 28px rgba(0,0,0,.45);
|
|
user-select: none;
|
|
}
|
|
#coords .lbl { color: var(--accent); margin-right: 6px; font-weight: 600; }
|
|
#coords .sep { color: var(--line); margin: 0 12px; }
|
|
/* 공통 패널 윈도우 스타일 (Properties & Layers - Draggable & Resizable) */
|
|
.cad-panel {
|
|
position: fixed; top: 56px; width: 320px; height: 480px;
|
|
min-width: 250px; min-height: 140px; max-width: 90vw; max-height: 90vh;
|
|
z-index: 15; background: rgba(22,27,34,.96); backdrop-filter: blur(12px);
|
|
border: 1px solid var(--line); border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.5);
|
|
display: flex; flex-direction: column; overflow: hidden; resize: both;
|
|
transition: border-color .15s;
|
|
}
|
|
.cad-panel.collapsed {
|
|
height: 42px !important;
|
|
min-height: 42px !important;
|
|
max-height: 42px !important;
|
|
overflow: hidden !important;
|
|
resize: none !important;
|
|
}
|
|
.cad-panel .hdr {
|
|
padding: 10px 14px; border-bottom: 1px solid var(--line);
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
background: rgba(13,17,23,.7); position: sticky; top: 0; z-index: 5;
|
|
cursor: grab; user-select: none; flex-shrink: 0;
|
|
}
|
|
.cad-panel .hdr:active { cursor: grabbing; }
|
|
.cad-panel .hdr h3 { margin: 0; font-size: 13px; font-weight: 700; color: var(--ink); display: flex; align-items: center; gap: 6px; }
|
|
.cad-panel .hdr-actions { display: flex; gap: 4px; align-items: center; }
|
|
.cad-panel .hdr-btn {
|
|
background: none; border: none; color: var(--muted); font-size: 13px;
|
|
cursor: pointer; padding: 2px 6px; border-radius: 4px; display: inline-flex;
|
|
align-items: center; justify-content: center; transition: all .15s;
|
|
}
|
|
.cad-panel .hdr-btn:hover { color: var(--ink); background: rgba(255,255,255,.1); }
|
|
|
|
#properties { right: 12px; }
|
|
#layers { right: 344px; display: none; }
|
|
#options { right: 676px; width: 340px; height: auto; max-height: 70vh; display: none; }
|
|
|
|
#propBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
#layerBody { flex: 1; overflow-y: auto; display: flex; flex-direction: column; }
|
|
#layerList { flex: 1; overflow-y: auto; }
|
|
|
|
#zoomSelBtn {
|
|
font: 11px var(--sans); padding: 3px 8px; border-radius: 6px;
|
|
border: 1px solid var(--accent); background: rgba(63,185,80,.15); color: var(--accent);
|
|
cursor: pointer; display: none; align-items: center; gap: 4px; transition: all .15s;
|
|
}
|
|
#zoomSelBtn:hover { background: var(--accent); color: #fff; }
|
|
|
|
.layer-search-wrap { padding: 8px 10px; border-bottom: 1px solid var(--line); background: rgba(13,17,23,.3); }
|
|
.layer-search-input {
|
|
width: 100%; background: var(--bg); border: 1px solid var(--line);
|
|
border-radius: 6px; padding: 6px 10px; color: var(--ink); font-size: 12px;
|
|
outline: none; transition: border-color .15s;
|
|
}
|
|
.layer-search-input:focus { border-color: var(--accent); }
|
|
.layer-sub-actions {
|
|
display: flex; gap: 6px; padding: 6px 10px; border-bottom: 1px solid var(--line);
|
|
background: rgba(22,27,34,.5);
|
|
}
|
|
.layer-sub-btn {
|
|
font-size: 11px; padding: 2px 8px; border-radius: 4px; border: 1px solid var(--line);
|
|
background: transparent; color: var(--muted); cursor: pointer; transition: all .15s;
|
|
}
|
|
.layer-sub-btn:hover { color: var(--ink); border-color: var(--accent); background: rgba(63,185,80,.1); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div id="app"></div>
|
|
|
|
<div id="toolbar">
|
|
<label class="btn">
|
|
Open file
|
|
<input id="file" type="file" accept=".dwg,.dxf" />
|
|
</label>
|
|
<button id="sample" class="primary wasm" type="button" title="acadrust-dwg WASM">Sample DWG</button>
|
|
<button id="sampleCivil" class="primary wasm" type="button" title="Civil Plan & Profile DWG">평면및종단면도.dwg</button>
|
|
<button id="sampleDxf" class="primary" type="button" title="dxf-parser">Sample DXF</button>
|
|
<div id="spaces" title="Model Space / Paper Space (Layout)"></div>
|
|
<button id="fit" type="button">Fit (F)</button>
|
|
<button id="theme" type="button">Theme</button>
|
|
<button id="optionsBtn" type="button" title="표시 옵션 (선가중치 · 그리드 · 줌)">옵션</button>
|
|
<button id="layersBtn" type="button">Layers</button>
|
|
<button id="propsBtn" type="button">Props (속성)</button>
|
|
<div id="status">ready</div>
|
|
</div>
|
|
|
|
<div id="coords" title="마우스 위치 CAD 좌표 (Model=UCS, Layout=Paper 도면단위)">
|
|
<span class="lbl">X</span><span id="coordX">—</span>
|
|
<span class="sep">|</span>
|
|
<span class="lbl">Y</span><span id="coordY">—</span>
|
|
</div>
|
|
|
|
<!-- 레이어 (Layers) 관리 패널 -->
|
|
<div id="layers" class="cad-panel">
|
|
<div class="hdr">
|
|
<h3>
|
|
<span>🎨</span> 레이어 (Layers)
|
|
<span id="layerCount" style="font-family:var(--mono);font-size:11px;color:var(--muted);background:rgba(255,255,255,.08);padding:1px 6px;border-radius:4px;font-weight:400"></span>
|
|
</h3>
|
|
<div class="hdr-actions">
|
|
<button id="collapseLayersBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
|
<button id="closeLayersBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
|
</div>
|
|
</div>
|
|
<div id="layerBody">
|
|
<div class="layer-search-wrap">
|
|
<input id="layerSearch" class="layer-search-input" type="text" placeholder="🔍 레이어 검색..." />
|
|
</div>
|
|
<div class="layer-sub-actions">
|
|
<button id="layerShowAll" class="layer-sub-btn" type="button">전체 켜기</button>
|
|
<button id="layerHideAll" class="layer-sub-btn" type="button">전체 끄기</button>
|
|
</div>
|
|
<div id="layerList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 옵션 (Options) 패널 — 설정은 IndexedDB 에 저장 -->
|
|
<div id="options" class="cad-panel">
|
|
<div class="hdr">
|
|
<h3><span>⚙️</span> 옵션 (Options)</h3>
|
|
<div class="hdr-actions">
|
|
<button id="optReset" type="button" title="기본값으로">↺ 기본값</button>
|
|
<button id="collapseOptBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
|
<button id="closeOptBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
|
</div>
|
|
</div>
|
|
<div id="optBody">
|
|
<div class="opt-sec">
|
|
<h4>선가중치 (Lineweight)</h4>
|
|
<div class="opt-row">
|
|
<label for="optLwOn">표시 (LWDISPLAY)</label>
|
|
<input id="optLwOn" type="checkbox" />
|
|
</div>
|
|
<div class="opt-row">
|
|
<label for="optLwScale">표시 배율</label>
|
|
<input id="optLwScale" type="range" min="2" max="60" step="0.5" value="11" />
|
|
<input id="optLwScaleNum" type="number" min="1" max="80" step="0.5" value="11" />
|
|
<span class="opt-unit">px/mm</span>
|
|
</div>
|
|
<div id="optLwOut" class="opt-note">—</div>
|
|
<p class="opt-help">
|
|
AutoCAD "선가중치 설정 → 표시 배율"과 같다. 모델 공간에서 선가중치는 줌과
|
|
무관한 화면 픽셀로 그려지므로, 화면 DPI에 맞춰 이 값을 조정한다.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="opt-sec">
|
|
<h4>화면 (Display)</h4>
|
|
<div class="opt-row">
|
|
<label for="optGrid">그리드 표시</label>
|
|
<input id="optGrid" type="checkbox" />
|
|
</div>
|
|
<div class="opt-row">
|
|
<label for="optZoomSpeed">줌 속도</label>
|
|
<input id="optZoomSpeed" type="range" min="0.2" max="3" step="0.1" value="1" />
|
|
<input id="optZoomSpeedNum" type="number" min="0.1" max="5" step="0.1" value="1" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 속성 (Properties) 패널 -->
|
|
<div id="properties" class="cad-panel">
|
|
<div class="hdr">
|
|
<h3><span>📋</span> 속성 (Properties)</h3>
|
|
<div class="hdr-actions">
|
|
<button id="zoomSelBtn" type="button" title="선택 요소 위치로 확대">🔍 선택 확대</button>
|
|
<button id="collapsePropsBtn" class="hdr-btn collapse-btn" type="button" title="접기">▾</button>
|
|
<button id="closePropsBtn" class="hdr-btn" type="button" title="닫기">✕</button>
|
|
</div>
|
|
</div>
|
|
<div id="propBody"></div>
|
|
</div>
|
|
|
|
<aside id="recipe">
|
|
<h3>Module recipe</h3>
|
|
<ol>
|
|
<li><code class="mod-j">ext2d</code> — .dwg / .dxf 분기</li>
|
|
<li><code class="mod-w">acadrust-dwg</code> 또는 <code class="mod-j">dxf-parser</code></li>
|
|
<li><code class="mod-j">dxfAdapter</code> — DXF → 공통 스키마</li>
|
|
<li><code class="mod-j">Viewer2D</code> + <code class="mod-j">three</code> 렌더</li>
|
|
<li><code class="mod-j">slugText</code> + TTF 폰트</li>
|
|
</ol>
|
|
<p style="margin:10px 0 0">
|
|
코드: <code>src/main.ts</code> ·
|
|
지도: <a href="/docs/modules-dwg-dxf.html" target="_blank">docs/modules-dwg-dxf.html</a>
|
|
</p>
|
|
<!-- MPL-2.0 §3.2(b): recipients of the wasm must be told where the
|
|
acadrust source can be obtained (unmodified 0.6.5, see the notice). -->
|
|
<p style="margin:6px 0 0">
|
|
오픈소스 고지: <a href="/licenses/THIRD-PARTY-NOTICES.md" target="_blank">THIRD-PARTY-NOTICES</a>
|
|
(acadrust 0.6.5 · MPL-2.0 · <a href="https://crates.io/crates/acadrust/0.6.5" target="_blank" rel="noopener">source</a>)
|
|
</p>
|
|
</aside>
|
|
|
|
<script type="module" src="/src/main.ts"></script>
|
|
</body>
|
|
</html>
|