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:
minsungandClaude Opus 5 committed 2026-08-05 09:00:37 +09:00
1 parent a924b1ec34
commit 1f80442f6c
8 files changed
+6310 -102

No files matched your search

+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';
});