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>
This commit is contained in:
minsung
2026-08-05 09:00:37 +09:00
co-authored by Claude Opus 5
parent a924b1ec34
commit 1f80442f6c
8 changed files with 6310 additions and 102 deletions
File diff suppressed because one or more lines are too long
+64 -21
View File
@@ -66,20 +66,25 @@
button.primary { background: #238636; border-color: #2ea043; }
button.wasm { border-color: var(--wasm); color: var(--wasm); }
label.btn input { display: none; }
/* 선가중치 표시 배율 조절 (슬라이더 + 직접 입력 + 환산 결과) */
#lwScaleBox {
display: flex; align-items: center; gap: 6px;
background: var(--panel); border: 1px solid var(--line);
border-radius: 8px; padding: 5px 10px; font: 12px var(--mono); color: var(--muted);
/* 옵션 패널 — 값은 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);
}
#lwScaleBox input[type=range] { width: 110px; accent-color: var(--accent); }
#lwScaleBox input[type=number] {
width: 52px; background: #0d1117; color: var(--ink);
border: 1px solid var(--line); border-radius: 6px; padding: 3px 5px;
.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;
}
#lwScaleBox .unit { color: var(--muted); }
#lwScaleOut { color: var(--accent); white-space: nowrap; }
.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);
@@ -183,6 +188,7 @@
#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; }
@@ -212,7 +218,7 @@
}
.layer-sub-btn:hover { color: var(--ink); border-color: var(--accent); background: rgba(63,185,80,.1); }
</style>
<script type="module" crossorigin src="/assets/index-CycdONYE.js"></script>
<script type="module" crossorigin src="/assets/index-VgpJrp_J.js"></script>
</head>
<body>
<div id="app"></div>
@@ -228,15 +234,7 @@
<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="lwt" type="button" title="선가중치 표시 (AutoCAD LWDISPLAY)">선가중치</button>
<!-- 표시 배율: AutoCAD "선가중치 설정 → 표시 배율" 슬라이더에 해당.
리로드 없이 즉시 반영된다. -->
<div id="lwScaleBox" title="선가중치 표시 배율 (px / mm)">
<input id="lwScale" type="range" min="2" max="60" step="1" value="20" />
<input id="lwScaleNum" type="number" min="1" max="80" step="1" value="20" />
<span class="unit">px/mm</span>
<span id="lwScaleOut"></span>
</div>
<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>
@@ -272,6 +270,51 @@
</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">
+49 -21
View File
@@ -96,17 +96,21 @@ lineweight 는 **화면 픽셀 폭**이다. 줌해도 굵기가 변하지 않는
따라서 월드 단위가 아니라 스크린 스페이스로 그려야 한다.
```
px = mm × 20 (LW_PX_PER_MM, 최대 48px)
0.35mm = 7px · 0.50mm = 10px · 2.11mm = 42px ← AutoCAD 기본 표시 배율(0.05mm당 1px)
px = mm × 11 (LW_PX_PER_MM 기본값, 최대 48px)
0.35mm = 3.9px · 0.50mm = 5.5px · 2.11mm = 23px
```
배율은 `setLineweightScale(pxPerMm)` 로 조정 가능하다(AutoCAD "표시 배율" 슬라이더 대응,
`null` 이면 기본값 복귀).
**이 배율은 상수로 확정할 수 있는 값이 아니다.** AutoCAD 도 모델 공간에서는 선가중치를
줌과 무관한 픽셀로만 그리고, 그 환산은 "선가중치 설정 → 표시 배율" 슬라이더가 정한다.
즉 화면 DPI·모니터에 따라 맞는 값이 달라진다.
> **처음에는 8px/mm 로 넣었다가 손테스트에서 "얇다"고 반려됐다.** 정답지(AutoCAD
> 캡처)의 `도로 가장자리`(0.50mm) 선이 약 10px 인데 8px/mm 면 4px 밖에 안 된다.
> lineweight 는 줌 무관 화면 픽셀이므로 두 캡처의 픽셀 두께를 직접 비교하면 되고,
> 그 결과가 20px/mm 다. 자세한 대조는 5-5 절.
- 코드 기본값: `LW_PX_PER_MM = 11` (사용자가 실제 화면에서 AutoCAD 와 나란히 놓고 맞춘 값)
- 런타임 조정: **옵션 패널 → 선가중치 → 표시 배율**, 또는 `setLineweightScale(pxPerMm)`
- 조정은 **재파싱 없이 즉시 반영**된다 (3-6 절)
> 시행착오 기록: 처음 8px/mm(96dpi 어림) → 손테스트 "얇다" 반려 → 정답지 캡처에서
> 역산해 20px/mm → 실제 화면 대조 후 최종 11px/mm. 상수 추정이 두 번 빗나갔고,
> 그래서 값을 코드에서 빼 옵션으로 노출했다.
### 3-2. 0.25mm 이하는 헤어라인 유지 — **회귀 방지 장치**
@@ -137,6 +141,17 @@ px = mm × 20 (LW_PX_PER_MM, 최대 48px)
- **레이어 토글** — 인스턴스 지오메트리는 인덱스 재작성이 불가
- **선택 하이라이트** — 색상 배열이 여러 버퍼로 쪼개짐
### 3-6. 배율 조정은 리빌드 없이
헤어라인 컷오프가 **mm 값**(0.25mm)이라 배율이 바뀌어도 어떤 세그먼트가 어느 버킷에
들어가는지는 변하지 않는다. 모든 굵기가 같은 배수로 움직일 뿐이다.
→ 각 fat 배치가 자기 굵기를 **mm 로 기억**(`_fatBufs[].lwMm`)하고,
`setLineweightScale()``material.linewidth` 만 다시 쓴다. 37MB 도면에서도 슬라이더를
끄는 동안 지연이 없다.
반면 **표시 on/off** 는 굵기가 0이 되어 버킷 구성이 바뀌므로 재빌드가 필요하다.
### 3-5. 표시 on/off — AutoCAD LWDISPLAY 와 동일하게
굵기 표시는 CAD 에서 원래 켜고 끌 수 있는 옵션이다. 도면의 헤더 값을 기본으로 따르되
@@ -188,9 +203,21 @@ entity lineWeight = { -3:15176, -1:2613, 13:8794, 35:37944, 40:119, 50:849 }
### 4-3. UI
- `index.html` · `src/main.ts` — 툴바 **선가중치** 버튼 (상태에 따라 강조, 툴팁에 ON/OFF)
- `src/propertyInspector.ts``선가중치 (Lineweight)` 행 추가.
`-1``BYLAYER`, `-2``BYBLOCK`, `-3``DEFAULT (0.25mm)`, 그 외 `0.35mm` 형식
툴바 **옵션** 버튼 → `옵션 (Options)` 패널 (`index.html` · `src/main.ts`).
| 항목 | 동작 |
|------|------|
| 표시 (LWDISPLAY) | on/off. 체크 해제 시 전부 헤어라인 (재빌드) |
| 표시 배율 | 슬라이더 + 숫자 입력 (px/mm). 즉시 반영 |
| 환산 표시 | 그 도면이 실제로 쓰는 굵기만 `0.35mm → 3.9px` 식으로 표기 |
| ↺ 기본값 | 배율·표시·그리드·줌속도 초기화 |
설정은 **IndexedDB** (`hmw-viewer` DB → `options` 스토어)에 저장한다
([`../src/optionsStore.ts`](../src/optionsStore.ts)). 키: `lineweightScale`,
`lineweightOn`, `grid`, `zoomSpeed`. 스토리지가 막혀 있어도 조용히 기본값으로 동작한다.
속성 패널에는 `선가중치 (Lineweight)` 행이 있다 (`src/propertyInspector.ts`).
`-1``BYLAYER`, `-2``BYBLOCK`, `-3``DEFAULT (0.25mm)`, 그 외 `0.35mm` 형식.
### 4-4. 배포
@@ -263,14 +290,16 @@ cyan 픽셀 수가 감소(11502→11486) → fat 배치의 정점이 강조색
lineweight 는 줌과 무관한 화면 픽셀이므로, 두 캡처의 **절대 픽셀 두께**를 그대로 비교한다.
| | `도로 가장자리` 0.50mm | 헤어라인(MESH, Default) |
| 배율 | `도로 가장자리` 0.50mm | `노면측구` 0.35mm (헤드리스 실측) |
|--|--|--|
| AutoCAD 정답지 | **≈10 px** | 1 px |
| 뷰어 (8px/mm, 반려) | ≈4 px | 1 px |
| 뷰어 (20px/mm, 현재) | **10 px** | 1 px |
| 8 px/mm (최초, 반려) | 4 px | 23 px |
| 20 px/mm (캡처 역산) | 10 px | 7 px |
| **11 px/mm (현재 기본값)** | **5.5 px** | **34 px** |
| 25 px/mm (참고) | 12.5 px | 89 px |
헤드리스 재측정으로 교차 확인: 시안색 `노면측구`(0.35mm)가 화면에서 **≈7 px**
(= 0.35 × 20). 두 클래스가 같은 배율로 맞는다.
캡처 이미지에서 역산한 20 은 실제 화면에서 과했다 — 캡처 DPI 와 사용자 모니터가 달라서다.
최종값은 사용자가 뷰어와 AutoCAD 를 나란히 놓고 맞춘 **11**. 이래서 상수로 못 박지 않고
옵션으로 뺐다.
### 5-6. 회귀 없음
@@ -282,10 +311,9 @@ lineweight 는 줌과 무관한 화면 픽셀이므로, 두 캡처의 **절대
## 6. 남은 과제
1. **표시 배율 UI 부재** — 값은 `setLineweightScale(pxPerMm)` 로 조절되지만 툴바에
on/off 버튼만 있다. 슬라이더가 필요하면 이 API 에 붙이면 된다.
0.35mm 가 7px 라 `비탈면방향`(34,758개)이 있는 구간은 화면이 상당히 무거워 보인다 —
도면이 지정한 수치대로이긴 하나, 배율을 낮추고 싶으면 이 API 를 쓴다.
1. **배율 기본값은 환경 의존** — 11 은 특정 모니터에서 맞춘 값이다. 다른 화면에서
옵션 패널로 다시 맞춰야 한다. `devicePixelRatio` 로 자동 보정하는 것은 검토만 하고
넣지 않았다(캡처·원격 데스크톱 등에서 오히려 어긋난다).
2. **ByBlock 근사** — 블록 자식의 `-2` 는 INSERT 굵기를 상속하지만, 중첩 블록에서
중간 INSERT 가 `-2` 인 경우까지는 추적하지 않는다(점선 처리와 동일한 기존 정책).
3. **DXF 레이어 굵기 없음**`dxf-parser` 의 LAYER 핸들러가 코드 370 을 무시한다.
+63 -20
View File
@@ -66,20 +66,25 @@
button.primary { background: #238636; border-color: #2ea043; }
button.wasm { border-color: var(--wasm); color: var(--wasm); }
label.btn input { display: none; }
/* 선가중치 표시 배율 조절 (슬라이더 + 직접 입력 + 환산 결과) */
#lwScaleBox {
display: flex; align-items: center; gap: 6px;
background: var(--panel); border: 1px solid var(--line);
border-radius: 8px; padding: 5px 10px; font: 12px var(--mono); color: var(--muted);
/* 옵션 패널 — 값은 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);
}
#lwScaleBox input[type=range] { width: 110px; accent-color: var(--accent); }
#lwScaleBox input[type=number] {
width: 52px; background: #0d1117; color: var(--ink);
border: 1px solid var(--line); border-radius: 6px; padding: 3px 5px;
.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;
}
#lwScaleBox .unit { color: var(--muted); }
#lwScaleOut { color: var(--accent); white-space: nowrap; }
.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);
@@ -183,6 +188,7 @@
#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; }
@@ -227,15 +233,7 @@
<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="lwt" type="button" title="선가중치 표시 (AutoCAD LWDISPLAY)">선가중치</button>
<!-- 표시 배율: AutoCAD "선가중치 설정 → 표시 배율" 슬라이더에 해당.
리로드 없이 즉시 반영된다. -->
<div id="lwScaleBox" title="선가중치 표시 배율 (px / mm)">
<input id="lwScale" type="range" min="2" max="60" step="1" value="20" />
<input id="lwScaleNum" type="number" min="1" max="80" step="1" value="20" />
<span class="unit">px/mm</span>
<span id="lwScaleOut"></span>
</div>
<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>
@@ -271,6 +269,51 @@
</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">
+77 -35
View File
@@ -29,6 +29,7 @@ import { setDefaultFontUrl } from './viewer2d/slugText';
import { renderPropertyInspector } from './propertyInspector';
import { LayerPanelManager } from './layerPanel';
import { makePanelDraggableAndCollapsible } from './panelUtils';
import { getAllOptions, setOption } from './optionsStore';
// parseDxfBuffer 내부:
// dxf-parser → HatchHandler → dxfAdapter
@@ -162,8 +163,7 @@ async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseRe
const def = pickDefaultSpace(spaces);
viewer.load(result, { spaceHandle: def?.handleHex ?? null });
viewer.resize();
syncLwtBtn();
syncLwScale();
syncOptions();
return result;
}
@@ -231,49 +231,91 @@ document.getElementById('theme')!.addEventListener('click', () => {
viewer.setTheme(dark);
fillLayers();
});
// 선가중치(LWT): 처음에는 도면의 LWDISPLAY 값을 따르고, 누르면 강제 on/off.
const lwtBtn = document.getElementById('lwt') as HTMLButtonElement;
const syncLwtBtn = () => {
const on = viewer.getLineweightEnabled();
lwtBtn.classList.toggle('primary', on);
lwtBtn.title = `선가중치 표시 ${on ? 'ON' : 'OFF'} (AutoCAD LWDISPLAY)`;
};
lwtBtn.addEventListener('click', () => {
viewer.setLineweightEnabled(!viewer.getLineweightEnabled());
syncLwtBtn();
syncLwScale();
// ── 옵션 패널 ────────────────────────────────────────────────────
// 값은 IndexedDB(hmw-viewer/options)에 남는다. 선가중치 배율은 재파싱 없이
// 즉시 반영되므로 드래그하면서 화면을 보고 맞출 수 있다.
const optionsPanel = document.getElementById('options')!;
const optLwOn = document.getElementById('optLwOn') as HTMLInputElement;
const optLwScale = document.getElementById('optLwScale') as HTMLInputElement;
const optLwScaleNum = document.getElementById('optLwScaleNum') as HTMLInputElement;
const optLwOut = document.getElementById('optLwOut')!;
const optGrid = document.getElementById('optGrid') as HTMLInputElement;
const optZoomSpeed = document.getElementById('optZoomSpeed') as HTMLInputElement;
const optZoomSpeedNum = document.getElementById('optZoomSpeedNum') as HTMLInputElement;
makePanelDraggableAndCollapsible(optionsPanel, {
collapseBtnEl: document.getElementById('collapseOptBtn')!,
});
document.getElementById('optionsBtn')!.addEventListener('click', () => {
optionsPanel.style.display = optionsPanel.style.display === 'flex' ? 'none' : 'flex';
});
document.getElementById('closeOptBtn')!.addEventListener('click', () => {
optionsPanel.style.display = 'none';
});
// 선가중치 표시 배율 (px per mm) — AutoCAD "표시 배율" 슬라이더에 해당.
// 리로드 없이 즉시 반영되고, 값은 localStorage 에 남는다.
const LW_SCALE_KEY = 'hmw:lw-scale';
const lwScaleRange = document.getElementById('lwScale') as HTMLInputElement;
const lwScaleNum = document.getElementById('lwScaleNum') as HTMLInputElement;
const lwScaleOut = document.getElementById('lwScaleOut') as HTMLSpanElement;
/** 현재 배율에서 도면이 실제로 쓰는 굵기들이 몇 px 로 그려지는지 표기. */
function syncLwScale() {
/** 컨트롤 값을 뷰어의 현재 상태로 되돌린다(도면을 새로 열었을 때도 호출). */
function syncOptions() {
const s = viewer.getLineweightScale();
lwScaleRange.value = String(s);
lwScaleNum.value = String(s);
optLwOn.checked = viewer.getLineweightEnabled();
optLwScale.value = String(s);
optLwScaleNum.value = String(s);
const classes = viewer.getLineweightClasses();
lwScaleOut.textContent = classes.length
? classes.map((mm) => `${mm.toFixed(2)}mm${(Math.round(mm * s * 10) / 10)}px`).join(' ')
: `0.50mm${Math.round(0.5 * s * 10) / 10}px`;
optLwOut.textContent = classes.length
? classes.map((mm) => `${mm.toFixed(2)}mm${Math.round(mm * s * 10) / 10}px`).join(' ')
: `이 도면에는 굵은 선이 없다 (0.50mm 였다면 ${Math.round(0.5 * s * 10) / 10}px)`;
}
function applyLwScale(v: number) {
function applyLwScale(v: number, persist = true) {
if (!Number.isFinite(v) || v <= 0) return;
viewer.setLineweightScale(v);
localStorage.setItem(LW_SCALE_KEY, String(v));
syncLwScale();
if (persist) void setOption('lineweightScale', v);
syncOptions();
}
lwScaleRange.addEventListener('input', () => applyLwScale(parseFloat(lwScaleRange.value)));
lwScaleNum.addEventListener('input', () => applyLwScale(parseFloat(lwScaleNum.value)));
optLwScale.addEventListener('input', () => applyLwScale(parseFloat(optLwScale.value)));
optLwScaleNum.addEventListener('input', () => applyLwScale(parseFloat(optLwScaleNum.value)));
const savedLwScale = parseFloat(localStorage.getItem(LW_SCALE_KEY) || '');
if (Number.isFinite(savedLwScale) && savedLwScale > 0) viewer.setLineweightScale(savedLwScale);
syncLwScale();
optLwOn.addEventListener('change', () => {
viewer.setLineweightEnabled(optLwOn.checked);
void setOption('lineweightOn', optLwOn.checked);
syncOptions();
});
optGrid.addEventListener('change', () => {
viewer.setGrid(optGrid.checked);
void setOption('grid', optGrid.checked);
});
function applyZoomSpeed(v: number, persist = true) {
if (!Number.isFinite(v) || v <= 0) return;
viewer.setZoomSpeed(v);
optZoomSpeed.value = String(v);
optZoomSpeedNum.value = String(v);
if (persist) void setOption('zoomSpeed', v);
}
optZoomSpeed.addEventListener('input', () => applyZoomSpeed(parseFloat(optZoomSpeed.value)));
optZoomSpeedNum.addEventListener('input', () => applyZoomSpeed(parseFloat(optZoomSpeedNum.value)));
document.getElementById('optReset')!.addEventListener('click', () => {
viewer.setLineweightScale(null); // 뷰어 기본 배율
viewer.setLineweightEnabled(null); // 도면의 LWDISPLAY 를 다시 따름
viewer.setGrid(false);
applyZoomSpeed(1);
optGrid.checked = false;
void setOption('lineweightScale', null);
void setOption('lineweightOn', null);
void setOption('grid', false);
syncOptions();
});
// 저장된 옵션 복원 (IndexedDB 는 비동기라 첫 도면 로드와 경쟁하지 않도록 즉시 적용)
void getAllOptions().then((o) => {
if (typeof o.lineweightScale === 'number') applyLwScale(o.lineweightScale, false);
if (typeof o.lineweightOn === 'boolean') viewer.setLineweightEnabled(o.lineweightOn);
if (o.grid === true) { viewer.setGrid(true); optGrid.checked = true; }
if (typeof o.zoomSpeed === 'number') applyZoomSpeed(o.zoomSpeed, false);
syncOptions();
});
syncOptions();
document.getElementById('layersBtn')!.addEventListener('click', () => {
layersPanel.style.display = layersPanel.style.display === 'flex' ? 'none' : 'flex';
});
+75
View File
@@ -0,0 +1,75 @@
/**
* 뷰어 옵션 영구 저장소 (IndexedDB).
*
* localStorage 대신 IndexedDB 를 쓰는 이유: 값이 문자열로 강제되지 않고(숫자/불린/
* 객체 그대로), 용량 제한이 사실상 없으며, 나중에 도면별 설정처럼 구조가 커져도
* 같은 저장소를 그대로 쓸 수 있다.
*
* DB 하나(`hmw-viewer`) 안에 key-value 스토어(`options`) 하나만 둔다.
* 실패(사생활 보호 모드, 스토리지 차단 등)해도 앱이 죽지 않도록 모든 API 는
* 조용히 기본값으로 떨어진다.
*/
const DB_NAME = 'hmw-viewer';
const DB_VERSION = 1;
const STORE = 'options';
let dbPromise: Promise<IDBDatabase | null> | null = null;
function openDb(): Promise<IDBDatabase | null> {
if (dbPromise) return dbPromise;
dbPromise = new Promise((resolve) => {
if (typeof indexedDB === 'undefined') return resolve(null);
let req: IDBOpenDBRequest;
try {
req = indexedDB.open(DB_NAME, DB_VERSION);
} catch {
return resolve(null);
}
req.onupgradeneeded = () => {
const db = req.result;
if (!db.objectStoreNames.contains(STORE)) db.createObjectStore(STORE);
};
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve(null);
req.onblocked = () => resolve(null);
});
return dbPromise;
}
function tx<T>(mode: IDBTransactionMode, run: (store: IDBObjectStore) => IDBRequest): Promise<T | null> {
return openDb().then(
(db) =>
new Promise<T | null>((resolve) => {
if (!db) return resolve(null);
try {
const t = db.transaction(STORE, mode);
const req = run(t.objectStore(STORE));
req.onsuccess = () => resolve(req.result as T);
req.onerror = () => resolve(null);
} catch {
resolve(null);
}
}),
);
}
/** 저장된 옵션 하나. 없으면 `fallback`. */
export async function getOption<T>(key: string, fallback: T): Promise<T> {
const v = await tx<T>('readonly', (s) => s.get(key));
return v === null || v === undefined ? fallback : v;
}
/** 옵션 하나 저장. 저장이 막혀 있어도 예외를 던지지 않는다. */
export async function setOption(key: string, value: unknown): Promise<void> {
await tx('readwrite', (s) => s.put(value, key));
}
/** 전체 옵션을 한 번에 읽는다 (기동 시 1회 호출용). */
export async function getAllOptions(): Promise<Record<string, unknown>> {
const keys = (await tx<IDBValidKey[]>('readonly', (s) => s.getAllKeys())) ?? [];
const vals = (await tx<unknown[]>('readonly', (s) => s.getAll())) ?? [];
const out: Record<string, unknown> = {};
keys.forEach((k, i) => { out[String(k)] = vals[i]; });
return out;
}
+2
View File
@@ -16,6 +16,8 @@ export class Viewer2D {
setAccent(hex: string): void;
setTheme(dark: boolean): void;
setGrid(visible: boolean): void;
setZoomSpeed(speed: number): void;
getZoomSpeed(): number;
/** Lineweight display (AutoCAD LWDISPLAY); null follows the drawing's own flag. */
setLineweightEnabled(on: boolean | null): void;
getLineweightEnabled(): boolean;
+5 -5
View File
@@ -25,11 +25,11 @@ const CLICK_THRESHOLD_PX = 14;
// ── Lineweight (AutoCAD LWDISPLAY) ─────────────────────────────────────────
// DWG stores lineweight as an i16 in 1/100 mm (-1 ByLayer, -2 ByBlock,
// -3 Default). AutoCAD shows it in *screen* pixels — the width does not grow
// when you zoom in — at one pixel per 0.05 mm on the default display-scale
// slider (0.50 mm = 10 px, 2.11 mm = 42 px). Measured against an AutoCAD
// capture of the 노면 drawing: its 0.50 mm road edge is ~10 px, so 20 px/mm.
// Host apps can retune this with setLineweightScale().
const LW_PX_PER_MM = 20;
// when you zoom in. The px-per-mm factor is AutoCAD's "Adjust Display Scale"
// slider and depends on the monitor, so it is only a default: 11 px/mm is the
// value that matched an AutoCAD reference side by side (0.35 mm ≈ 4 px,
// 0.50 mm ≈ 5.5 px). Retune per host/monitor with setLineweightScale().
const LW_PX_PER_MM = 11;
// LWDEFAULT: what -3 (and a layer that never set one) resolves to.
const LW_DEFAULT_MM = 0.25;
// At or below the CAD default the line stays in the 1-px hairline batch: a