Files
minsungandClaude Opus 5.5 aa27945d61 feat(viewer2d): 2.5D view, viewport fill transform and Z-aware fit box
- 2.5D view: a toolbar toggle (plus an options-panel checkbox) re-aims the
  orthographic camera at an SE-isometric angle and unlocks orbit, so the
  CAD elevation already in the vertex buffers (contours, TIN, 3D
  polylines) becomes visible. Nothing is re-tessellated; switching back
  restores the locked top view. A Z exaggeration slider (persisted in the
  options store) makes kilometre-wide sheets with metres of relief
  legible; picking and the X/Y readout intersect the Z=0 plane.
- Viewport fills: SOLID/HATCH fills and dimension arrowheads drawn
  through a paper-space VIEWPORT now get the model-to-paper transform
  (_fillXf / _bakeFillXf), as lines already did via pushSeg.
- The trimmed content box keeps the drawing's real Z range instead of
  collapsing to z=0, so the 2.5D fit and clip slab cover the relief.
- src/topProgress.ts: driver for the existing #progress loading bar. It
  is not imported from main.ts yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 19:48:19 +09:00

378 lines
18 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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); }
/* 켜져 있는 토글 버튼 (2.5D 보기 등) */
#toolbar > button.on { background: #1f6feb; border-color: #58a6ff; color: #fff; }
.opt-row.disabled { opacity: .45; pointer-events: none; }
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="view25Btn" type="button" title="2.5D 보기 — Top 뷰를 풀고 기울여서 등고선 등의 높낮이(Z)를 본다. 왼쪽 드래그=회전, 가운데/오른쪽 드래그=이동">2.5D 보기</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 class="opt-sec">
<h4>2.5D 보기</h4>
<div class="opt-row">
<label for="opt25D">기울여 보기 (Top 해제)</label>
<input id="opt25D" type="checkbox" />
</div>
<div id="optZRow" class="opt-row disabled">
<label for="optZScale">높이 과장 (Z)</label>
<input id="optZScale" type="range" min="1" max="50" step="0.5" value="1" />
<input id="optZScaleNum" type="number" min="1" max="200" step="0.5" value="1" />
<span class="opt-unit">×</span>
</div>
<p class="opt-help">
도면은 이미 CAD 표고(Z)를 그대로 갖고 있다. 2.5D 는 같은 직교 카메라를
남동 등각(SE Isometric)으로 돌려 그 높낮이를 보여준다 — 재파싱 없음.
부지가 넓고 기복이 작아 평평하게 보이면 Z 과장을 올린다.
<b>왼쪽 드래그 = 회전, 가운데·오른쪽 드래그 = 이동.</b>
</p>
</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>