Files
dwg-dxf-viewer-sample/index.html
minsungandClaude Opus 5 1f80442f6c feat(ui): options panel with an IndexedDB-backed lineweight display scale
Set the default lineweight scale to 11 px/mm: the value the user matched
against AutoCAD on the actual screen. The 20 px/mm derived from their
screenshot was too heavy in practice - the capture and the monitor do
not share a DPI - so the constant is now only a starting point.

Move the lineweight controls off the toolbar into an Options panel:
display toggle, scale slider plus number box, a readout of what the
drawing's own weights come out to, grid toggle, zoom speed, and a reset.
Settings persist in IndexedDB (hmw-viewer/options) rather than
localStorage, so values keep their type and the store has room to grow
per-drawing later; every call degrades quietly if storage is blocked.

Scale changes still apply to the live materials - each fat batch now
remembers its weight in mm - so dragging the slider retunes a 37MB
drawing instantly.

Verified headless: 0.35mm renders 3-4px at scale 11 and 8-9px at 25,
matching the readout, and the value survives a reload via IndexedDB.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-05 09:00:37 +09:00

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 (with our local patch) can be obtained. -->
<p style="margin:6px 0 0">
오픈소스 고지: <a href="/licenses/THIRD-PARTY-NOTICES.md" target="_blank">THIRD-PARTY-NOTICES</a>
(acadrust · MPL-2.0 · <a href="/licenses/acadrust-0.4.1-read_mesh.patch" target="_blank">local patch</a>)
</p>
</aside>
<script type="module" src="/src/main.ts"></script>
</body>
</html>