feat(viewer2d): make the lineweight display scale adjustable from the toolbar
The px-per-mm mapping was a hard-coded guess twice over, and the right value depends on the monitor - AutoCAD has the same knob as the Adjust Display Scale slider. Expose it: a slider plus a number box next to the lineweight toggle, with a readout of what the drawing's own weights come out to (0.35mm -> 7px, 0.50mm -> 10px at the 20 px/mm default). The value persists in localStorage. Retuning applies to the live materials instead of rebuilding the scene. The hairline cut-off is a millimetre value, so changing the scale moves every weight by the same factor without changing which bucket a segment belongs to - each fat batch now remembers its weight in mm, and only material.linewidth is rewritten. That keeps a drag on the slider instant even on the 37 MB road drawing. Measured headless on its 0.35mm gutter line: 2-3px at scale 8, 7px at 20, 14px at 40 - matching the readout in each case. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
+23
-1
@@ -66,6 +66,20 @@
|
|||||||
button.primary { background: #238636; border-color: #2ea043; }
|
button.primary { background: #238636; border-color: #2ea043; }
|
||||||
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
||||||
label.btn input { display: none; }
|
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);
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
font: 12px var(--mono); text-align: right;
|
||||||
|
}
|
||||||
|
#lwScaleBox .unit { color: var(--muted); }
|
||||||
|
#lwScaleOut { color: var(--accent); white-space: nowrap; }
|
||||||
#status {
|
#status {
|
||||||
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
||||||
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
||||||
@@ -198,7 +212,7 @@
|
|||||||
}
|
}
|
||||||
.layer-sub-btn:hover { color: var(--ink); border-color: var(--accent); background: rgba(63,185,80,.1); }
|
.layer-sub-btn:hover { color: var(--ink); border-color: var(--accent); background: rgba(63,185,80,.1); }
|
||||||
</style>
|
</style>
|
||||||
<script type="module" crossorigin src="/assets/index-BsOJ4upQ.js"></script>
|
<script type="module" crossorigin src="/assets/index-CycdONYE.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
@@ -215,6 +229,14 @@
|
|||||||
<button id="fit" type="button">Fit (F)</button>
|
<button id="fit" type="button">Fit (F)</button>
|
||||||
<button id="theme" type="button">Theme</button>
|
<button id="theme" type="button">Theme</button>
|
||||||
<button id="lwt" type="button" title="선가중치 표시 (AutoCAD LWDISPLAY)">선가중치</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="layersBtn" type="button">Layers</button>
|
<button id="layersBtn" type="button">Layers</button>
|
||||||
<button id="propsBtn" type="button">Props (속성)</button>
|
<button id="propsBtn" type="button">Props (속성)</button>
|
||||||
<div id="status">ready</div>
|
<div id="status">ready</div>
|
||||||
|
|||||||
+22
@@ -66,6 +66,20 @@
|
|||||||
button.primary { background: #238636; border-color: #2ea043; }
|
button.primary { background: #238636; border-color: #2ea043; }
|
||||||
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
button.wasm { border-color: var(--wasm); color: var(--wasm); }
|
||||||
label.btn input { display: none; }
|
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);
|
||||||
|
}
|
||||||
|
#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;
|
||||||
|
font: 12px var(--mono); text-align: right;
|
||||||
|
}
|
||||||
|
#lwScaleBox .unit { color: var(--muted); }
|
||||||
|
#lwScaleOut { color: var(--accent); white-space: nowrap; }
|
||||||
#status {
|
#status {
|
||||||
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
margin-left: auto; font: 12px var(--mono); color: var(--muted);
|
||||||
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
background: rgba(22,27,34,.85); border: 1px solid var(--line);
|
||||||
@@ -214,6 +228,14 @@
|
|||||||
<button id="fit" type="button">Fit (F)</button>
|
<button id="fit" type="button">Fit (F)</button>
|
||||||
<button id="theme" type="button">Theme</button>
|
<button id="theme" type="button">Theme</button>
|
||||||
<button id="lwt" type="button" title="선가중치 표시 (AutoCAD LWDISPLAY)">선가중치</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="layersBtn" type="button">Layers</button>
|
<button id="layersBtn" type="button">Layers</button>
|
||||||
<button id="propsBtn" type="button">Props (속성)</button>
|
<button id="propsBtn" type="button">Props (속성)</button>
|
||||||
<div id="status">ready</div>
|
<div id="status">ready</div>
|
||||||
|
|||||||
+33
@@ -163,6 +163,7 @@ async function loadAndRender(buf: ArrayBuffer, name: string): Promise<CadParseRe
|
|||||||
viewer.load(result, { spaceHandle: def?.handleHex ?? null });
|
viewer.load(result, { spaceHandle: def?.handleHex ?? null });
|
||||||
viewer.resize();
|
viewer.resize();
|
||||||
syncLwtBtn();
|
syncLwtBtn();
|
||||||
|
syncLwScale();
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +241,39 @@ const syncLwtBtn = () => {
|
|||||||
lwtBtn.addEventListener('click', () => {
|
lwtBtn.addEventListener('click', () => {
|
||||||
viewer.setLineweightEnabled(!viewer.getLineweightEnabled());
|
viewer.setLineweightEnabled(!viewer.getLineweightEnabled());
|
||||||
syncLwtBtn();
|
syncLwtBtn();
|
||||||
|
syncLwScale();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 선가중치 표시 배율 (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() {
|
||||||
|
const s = viewer.getLineweightScale();
|
||||||
|
lwScaleRange.value = String(s);
|
||||||
|
lwScaleNum.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`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyLwScale(v: number) {
|
||||||
|
if (!Number.isFinite(v) || v <= 0) return;
|
||||||
|
viewer.setLineweightScale(v);
|
||||||
|
localStorage.setItem(LW_SCALE_KEY, String(v));
|
||||||
|
syncLwScale();
|
||||||
|
}
|
||||||
|
lwScaleRange.addEventListener('input', () => applyLwScale(parseFloat(lwScaleRange.value)));
|
||||||
|
lwScaleNum.addEventListener('input', () => applyLwScale(parseFloat(lwScaleNum.value)));
|
||||||
|
|
||||||
|
const savedLwScale = parseFloat(localStorage.getItem(LW_SCALE_KEY) || '');
|
||||||
|
if (Number.isFinite(savedLwScale) && savedLwScale > 0) viewer.setLineweightScale(savedLwScale);
|
||||||
|
syncLwScale();
|
||||||
document.getElementById('layersBtn')!.addEventListener('click', () => {
|
document.getElementById('layersBtn')!.addEventListener('click', () => {
|
||||||
layersPanel.style.display = layersPanel.style.display === 'flex' ? 'none' : 'flex';
|
layersPanel.style.display = layersPanel.style.display === 'flex' ? 'none' : 'flex';
|
||||||
});
|
});
|
||||||
|
|||||||
Vendored
+2
@@ -22,6 +22,8 @@ export class Viewer2D {
|
|||||||
/** Lineweight display scale in px per mm (AutoCAD's display-scale slider). Default 20. */
|
/** Lineweight display scale in px per mm (AutoCAD's display-scale slider). Default 20. */
|
||||||
setLineweightScale(pxPerMm: number | null): void;
|
setLineweightScale(pxPerMm: number | null): void;
|
||||||
getLineweightScale(): number;
|
getLineweightScale(): number;
|
||||||
|
/** Distinct lineweights (mm) present in the loaded drawing, ascending. */
|
||||||
|
getLineweightClasses(): number[];
|
||||||
getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[];
|
getLayerInfo(): { name: string; colorHex: string; count: number; visible: boolean }[];
|
||||||
setHiddenLayers(nameSet: Set<string>): void;
|
setHiddenLayers(nameSet: Set<string>): void;
|
||||||
/** Model / Paper (Layout) spaces for the loaded drawing. */
|
/** Model / Paper (Layout) spaces for the loaded drawing. */
|
||||||
|
|||||||
@@ -469,7 +469,11 @@ export class Viewer2D {
|
|||||||
const key = `${curDash ? curDash.key : ''}#${lwPx}`;
|
const key = `${curDash ? curDash.key : ''}#${lwPx}`;
|
||||||
let bk = segBuckets.get(key);
|
let bk = segBuckets.get(key);
|
||||||
if (!bk) {
|
if (!bk) {
|
||||||
bk = { dash: curDash?.dash ?? 0, gap: curDash?.gap ?? 0, lwPx, verts: [], colors: [], segLayer: [], entIdx: -2 };
|
// lwMm is kept so setLineweightScale() can retune the material width
|
||||||
|
// without rebuilding the scene (the hairline cut-off is in mm, so
|
||||||
|
// bucket membership does not depend on the scale).
|
||||||
|
bk = { dash: curDash?.dash ?? 0, gap: curDash?.gap ?? 0, lwPx, lwMm: this._curLwMm || 0,
|
||||||
|
verts: [], colors: [], segLayer: [], entIdx: -2 };
|
||||||
segBuckets.set(key, bk);
|
segBuckets.set(key, bk);
|
||||||
}
|
}
|
||||||
// Selection highlight recolors an entity's own vertices. Its colors no
|
// Selection highlight recolors an entity's own vertices. Its colors no
|
||||||
@@ -1208,7 +1212,7 @@ export class Viewer2D {
|
|||||||
this._sizeLineMaterial(fmat);
|
this._sizeLineMaterial(fmat);
|
||||||
const fline = new LineSegments2(fgeom, fmat);
|
const fline = new LineSegments2(fgeom, fmat);
|
||||||
if (bk.dash > 0) fline.computeLineDistances();
|
if (bk.dash > 0) fline.computeLineDistances();
|
||||||
this._registerFat(fline, bk.segLayer);
|
this._registerFat(fline, bk.segLayer, bk.lwMm);
|
||||||
bk.rt = this._fatBufs[this._fatBufs.length - 1];
|
bk.rt = this._fatBufs[this._fatBufs.length - 1];
|
||||||
this._group.add(fline);
|
this._group.add(fline);
|
||||||
} else {
|
} else {
|
||||||
@@ -1343,7 +1347,7 @@ export class Viewer2D {
|
|||||||
* into the front of the instance buffers and drops `instanceCount`.
|
* into the front of the instance buffers and drops `instanceCount`.
|
||||||
* The untouched originals are kept so any later mask starts from clean data.
|
* The untouched originals are kept so any later mask starts from clean data.
|
||||||
*/
|
*/
|
||||||
_registerFat(mesh, segLayerArr) {
|
_registerFat(mesh, segLayerArr, lwMm = 0) {
|
||||||
const geo = mesh.geometry;
|
const geo = mesh.geometry;
|
||||||
const posBuf = geo.attributes.instanceStart?.data;
|
const posBuf = geo.attributes.instanceStart?.data;
|
||||||
const colBuf = geo.attributes.instanceColorStart?.data;
|
const colBuf = geo.attributes.instanceColorStart?.data;
|
||||||
@@ -1351,7 +1355,7 @@ export class Viewer2D {
|
|||||||
posBuf.setUsage(THREE.DynamicDrawUsage);
|
posBuf.setUsage(THREE.DynamicDrawUsage);
|
||||||
colBuf?.setUsage(THREE.DynamicDrawUsage);
|
colBuf?.setUsage(THREE.DynamicDrawUsage);
|
||||||
this._fatBufs.push({
|
this._fatBufs.push({
|
||||||
mesh,
|
mesh, lwMm,
|
||||||
segLayer: Int32Array.from(segLayerArr),
|
segLayer: Int32Array.from(segLayerArr),
|
||||||
posBuf, colBuf,
|
posBuf, colBuf,
|
||||||
posSrc: Float32Array.from(posBuf.array),
|
posSrc: Float32Array.from(posBuf.array),
|
||||||
@@ -1383,18 +1387,31 @@ export class Viewer2D {
|
|||||||
/**
|
/**
|
||||||
* Display scale in screen pixels per mm of lineweight — AutoCAD's
|
* Display scale in screen pixels per mm of lineweight — AutoCAD's
|
||||||
* "Adjust Display Scale" slider. Default 20 (1 px per 0.05 mm).
|
* "Adjust Display Scale" slider. Default 20 (1 px per 0.05 mm).
|
||||||
|
*
|
||||||
|
* Applied to the live materials, not by rebuilding: the hairline cut-off is
|
||||||
|
* a millimetre value, so changing the scale moves every weight by the same
|
||||||
|
* factor without changing which bucket a segment belongs to. Retuning is
|
||||||
|
* therefore instant even on a 37 MB drawing.
|
||||||
|
*
|
||||||
* @param {number|null} pxPerMm null restores the default.
|
* @param {number|null} pxPerMm null restores the default.
|
||||||
*/
|
*/
|
||||||
setLineweightScale(pxPerMm) {
|
setLineweightScale(pxPerMm) {
|
||||||
this._lwScale = (pxPerMm > 0) ? pxPerMm : null;
|
this._lwScale = (pxPerMm > 0) ? pxPerMm : null;
|
||||||
if (this._lastResult && !this._isLoading) {
|
const scale = this.getLineweightScale();
|
||||||
this.load(this._lastResult, { keepView: true, keepTheme: true, spaceHandle: this._activeSpaceHex });
|
for (const buf of this._fatBufs) {
|
||||||
|
if (!(buf.lwMm > 0)) continue;
|
||||||
|
buf.mesh.material.linewidth = Math.min(LW_MAX_PX, Math.round(buf.lwMm * scale * 10) / 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Current lineweight display scale (px per mm). */
|
/** Current lineweight display scale (px per mm). */
|
||||||
getLineweightScale() { return this._lwScale || LW_PX_PER_MM; }
|
getLineweightScale() { return this._lwScale || LW_PX_PER_MM; }
|
||||||
|
|
||||||
|
/** Lineweights present in the current drawing, in mm (ascending). */
|
||||||
|
getLineweightClasses() {
|
||||||
|
return [...new Set(this._fatBufs.map((b) => b.lwMm).filter((m) => m > 0))].sort((a, b) => a - b);
|
||||||
|
}
|
||||||
|
|
||||||
/** Track an entity-owned object so layer toggles can flip its visibility. */
|
/** Track an entity-owned object so layer toggles can flip its visibility. */
|
||||||
_addObj(obj, layer) {
|
_addObj(obj, layer) {
|
||||||
const key = layer ?? this._curLayer;
|
const key = layer ?? this._curLayer;
|
||||||
@@ -2339,10 +2356,15 @@ export class Viewer2D {
|
|||||||
* assigned a weight render exactly as before.
|
* assigned a weight render exactly as before.
|
||||||
*/
|
*/
|
||||||
_lwPx(entity, inheritedPx = 0) {
|
_lwPx(entity, inheritedPx = 0) {
|
||||||
|
this._curLwMm = 0;
|
||||||
if (!this._lwDisplay) return 0;
|
if (!this._lwDisplay) return 0;
|
||||||
let raw = entity?.entityHeader?.lineWeight ?? entity?.lineWeight ?? entity?.lineweight;
|
let raw = entity?.entityHeader?.lineWeight ?? entity?.lineWeight ?? entity?.lineweight;
|
||||||
if (typeof raw !== 'number') return 0;
|
if (typeof raw !== 'number') return 0;
|
||||||
if (raw === -2) return inheritedPx; // ByBlock
|
if (raw === -2) { // ByBlock
|
||||||
|
const scale = this.getLineweightScale();
|
||||||
|
this._curLwMm = inheritedPx > 0 ? inheritedPx / scale : 0;
|
||||||
|
return inheritedPx;
|
||||||
|
}
|
||||||
if (raw === -1) { // ByLayer
|
if (raw === -1) { // ByLayer
|
||||||
const lh = entity.layerHandle?.value ?? entity.layerHandle;
|
const lh = entity.layerHandle?.value ?? entity.layerHandle;
|
||||||
const byHandle = lh != null ? this._layerLwByHandle.get(String(lh)) : undefined;
|
const byHandle = lh != null ? this._layerLwByHandle.get(String(lh)) : undefined;
|
||||||
@@ -2352,7 +2374,8 @@ export class Viewer2D {
|
|||||||
}
|
}
|
||||||
const mm = raw === -3 ? LW_DEFAULT_MM : raw / 100;
|
const mm = raw === -3 ? LW_DEFAULT_MM : raw / 100;
|
||||||
if (!(mm > LW_HAIRLINE_MM)) return 0;
|
if (!(mm > LW_HAIRLINE_MM)) return 0;
|
||||||
const scale = this._lwScale || LW_PX_PER_MM;
|
this._curLwMm = mm;
|
||||||
|
const scale = this.getLineweightScale();
|
||||||
return Math.min(LW_MAX_PX, Math.round(mm * scale * 10) / 10);
|
return Math.min(LW_MAX_PX, Math.round(mm * scale * 10) / 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user