feat(viewer2d): render DWG lineweight in screen pixels
Every line was drawn 1px regardless of its CAD lineweight. Two causes: the dwg-wasm parseResult never carried the field (acadrust reads EntityCommon::line_weight but it was dropped in serialization), and the renderer merged all segments into one LineSegments whose LineBasicMaterial.linewidth WebGL ignores. Resolve ByLayer/ByBlock/Default per entity and bucket segments by dash|lineweight. Weighted buckets get their own LineSegments2 + LineMaterial with worldUnits:false, so width stays constant in pixels while zooming - AutoCAD LWDISPLAY semantics - at 8 px/mm (0.25mm = 2px). Anything at or below the 0.25mm default stays in the hairline batch so drawings that never assigned a weight render exactly as before. Fat batches are instanced, so layer masking compacts the instance buffer and lowers instanceCount instead of rewriting an index; slotOf tracks where each segment moved so the selection highlight still finds its vertices. Per-entity color spans (meta.spans) let the highlight repaint across the hairline batch and every bucket it touched. Buckets past 400k segments fall back to hairline to bound GPU/heap cost. Display follows the drawing's LWDISPLAY and can be forced from the toolbar; the property panel now shows the entity lineweight. Verified headless at a fixed camera on the 65k-entity road drawing: cyan road edge 2px -> 5px, layer hide/restore returns the exact baseline pixel counts, and a 0.35mm LWPOLYLINE selects and highlights. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+341
-33
@@ -4,10 +4,14 @@
|
||||
* 2D+3D merge. Renders CAD entities as LineSegments / Mesh / CanvasTexture sprites.
|
||||
* Supported: LINE, CIRCLE, ARC, LWPOLYLINE, POLYLINE, POINT, ELLIPSE, SOLID,
|
||||
* TEXT, MTEXT, INSERT (ownerHandle block expansion), HATCH (solid+outline+bulge),
|
||||
* MESH (AcDbSubDMesh, base-mesh wireframe),
|
||||
* DIMENSION_LINEAR/ALIGNED/RADIUS/DIAMETER/ANG_3PT/ANG_2LN/ORDINATE
|
||||
*/
|
||||
import * as THREE from 'three';
|
||||
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { LineSegments2 } from 'three/examples/jsm/lines/LineSegments2.js';
|
||||
import { LineSegmentsGeometry } from 'three/examples/jsm/lines/LineSegmentsGeometry.js';
|
||||
import { LineMaterial } from 'three/examples/jsm/lines/LineMaterial.js';
|
||||
import { aciToHex } from './aciColors.js';
|
||||
import { buildAllowedOwnerHexes, listCadSpaces } from './cadSpaces';
|
||||
import { SlugTextEngine, SlugTextBatch } from './slugText';
|
||||
@@ -18,6 +22,23 @@ const ARC_SEGS = 64;
|
||||
const ELLIPSE_SEGS = 72;
|
||||
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 roughly 8 px per mm on the default display-scale
|
||||
// slider (0.25 mm ≈ 2 px, 2.11 mm ≈ 17 px), so that is the mapping used here.
|
||||
const LW_PX_PER_MM = 8;
|
||||
// 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
|
||||
// drawing that never assigned weights must look exactly as it did before.
|
||||
const LW_HAIRLINE_MM = 0.25;
|
||||
const LW_MAX_PX = 24;
|
||||
// Fat lines cost 12 floats + 8 verts per segment (instanced quads). Past this
|
||||
// many segments in one weight bucket, fall back to hairline instead of risking
|
||||
// a GPU/heap stall on huge survey drawings.
|
||||
const LW_MAX_FAT_SEGS = 400000;
|
||||
|
||||
/** Bucket pending texts by their owning CAD layer (insertion order preserved). */
|
||||
function groupByLayer(list) {
|
||||
const m = new Map();
|
||||
@@ -72,9 +93,18 @@ export class Viewer2D {
|
||||
this._layerBoxes = new Map();
|
||||
this._layerSamples = new Map();
|
||||
this._indexedBufs = [];
|
||||
// · _fatBufs LineSegments2 batches (one per lineweight/dash bucket).
|
||||
// Instanced, so layer masking compacts the instance buffer
|
||||
// instead of rewriting an index (see _applyLayerVisibility).
|
||||
this._fatBufs = [];
|
||||
this._metaHidden = null;
|
||||
this._fullContentBox = null;
|
||||
this._hiddenLayers = new Set();
|
||||
// Lineweight display: null = follow the drawing's LWDISPLAY header var,
|
||||
// true/false = forced by the host UI (setLineweightEnabled).
|
||||
this._lwForced = null;
|
||||
this._lwDisplay = false;
|
||||
this._curLwPx = 0;
|
||||
SlugTextEngine.shared().catch(() => {}); // warm the font load; sprite fallback covers failure
|
||||
|
||||
this._scene = new THREE.Scene();
|
||||
@@ -302,6 +332,11 @@ export class Viewer2D {
|
||||
this._buildLayerMap(result?.tables?.layers);
|
||||
this._buildStyleMap(result?.tables?.styles || result?.tables?.textStyles);
|
||||
this._buildLinetypeMap(result);
|
||||
// LWDISPLAY decides whether weights show at all — same switch as AutoCAD's
|
||||
// status-bar "Show/Hide Lineweight". The host UI can override it.
|
||||
// DXF spells the header var $LWDISPLAY; dwg-wasm emits vars.lwdisplay.
|
||||
this._lwDisplay = this._lwForced
|
||||
?? !!(result?.vars?.lwdisplay ?? result?.vars?.$LWDISPLAY ?? result?.vars?.LWDISPLAY);
|
||||
// Sheet orientation: the active model-space viewport's VIEWTWIST rotates the
|
||||
// view so a drawing stored tilted in WCS (rotated survey sheets — header UCS
|
||||
// stays identity) displays with its title border upright. Applied as a
|
||||
@@ -330,11 +365,14 @@ export class Viewer2D {
|
||||
if (id === undefined) { id = layerNames.length; layerNames.push(name); layerIds.set(name, id); }
|
||||
return id;
|
||||
};
|
||||
// Dashed-linetype segments go into separate buckets keyed by dash|gap size;
|
||||
// each becomes its own LineDashedMaterial LineSegments at assembly. Solid
|
||||
// (Continuous / ByLayer→Continuous) segments stay in lineVerts/lineColors.
|
||||
const dashBuckets = new Map();
|
||||
// Segments that can't join the one merged hairline batch go into buckets
|
||||
// keyed by dash|gap size and lineweight; each becomes its own mesh at
|
||||
// assembly (LineDashedMaterial for thin dashes, LineSegments2 for anything
|
||||
// with a real lineweight). Solid hairlines stay in lineVerts/lineColors.
|
||||
const segBuckets = new Map();
|
||||
let curDash = null; // {key,dash,gap} for the entity currently being emitted
|
||||
let entIdx = -1; // _entityMeta index of that entity (-1 before the loop)
|
||||
let curSpans = []; // buckets it has written to so far
|
||||
const box = new THREE.Box3();
|
||||
const _tmp = new THREE.Vector3();
|
||||
const pendingTexts = [];
|
||||
@@ -423,9 +461,18 @@ export class Viewer2D {
|
||||
const r = ((color >> 16) & 0xFF) / 255;
|
||||
const g = ((color >> 8) & 0xFF) / 255;
|
||||
const b = (color & 0xFF) / 255;
|
||||
if (curDash) {
|
||||
let bk = dashBuckets.get(curDash.key);
|
||||
if (!bk) { bk = { dash: curDash.dash, gap: curDash.gap, verts: [], colors: [], segLayer: [] }; dashBuckets.set(curDash.key, bk); }
|
||||
const lwPx = this._curLwPx;
|
||||
if (curDash || lwPx > 0) {
|
||||
const key = `${curDash ? curDash.key : ''}#${lwPx}`;
|
||||
let bk = segBuckets.get(key);
|
||||
if (!bk) {
|
||||
bk = { dash: curDash?.dash ?? 0, gap: curDash?.gap ?? 0, lwPx, verts: [], colors: [], segLayer: [], entIdx: -2 };
|
||||
segBuckets.set(key, bk);
|
||||
}
|
||||
// Selection highlight recolors an entity's own vertices. Its colors no
|
||||
// longer live in one array, so remember where this entity's run starts
|
||||
// in every bucket it touches (see meta.spans).
|
||||
if (bk.entIdx !== entIdx) { bk.entIdx = entIdx; bk.entStart = bk.colors.length; curSpans.push(bk); }
|
||||
bk.verts.push(ax, ay, z, bx, by, z);
|
||||
bk.colors.push(r, g, b, r, g, b);
|
||||
bk.segLayer.push(this._curLayerId);
|
||||
@@ -502,7 +549,10 @@ export class Viewer2D {
|
||||
const complexLt = this._resolveComplexLinetype(e);
|
||||
if (complexLt) curDash = null;
|
||||
else curDash = this._resolveDash(e);
|
||||
this._curLwPx = this._lwPx(e);
|
||||
const meta = { entity: e, type, bounds: null, layer: this._curLayer, colStart: lineColors.length };
|
||||
entIdx = this._entityMeta.length;
|
||||
curSpans = [];
|
||||
this._entityMeta.push(meta);
|
||||
this._pickCurIdx = this._entityMeta.length - 1; // owner for pick geometry emitted below
|
||||
const textStart = pendingTexts.length;
|
||||
@@ -709,6 +759,16 @@ export class Viewer2D {
|
||||
break;
|
||||
}
|
||||
|
||||
// ── MESH (AcDbSubDMesh) ───────────────────────────────────────────
|
||||
// Drawn as its face wireframe, matching AutoCAD's 2D wireframe view.
|
||||
case 'MESH': {
|
||||
if (d.vertices?.length >= 3) {
|
||||
this._meshSegs(d, color, pushSeg);
|
||||
meta.bounds = { type:'point', cx:d.vertices[0], cy:d.vertices[1] };
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ── Text ──────────────────────────────────────────────────────────
|
||||
case 'TEXT':
|
||||
case 'MTEXT': {
|
||||
@@ -1045,6 +1105,10 @@ export class Viewer2D {
|
||||
}
|
||||
} catch { /* skip malformed entity */ }
|
||||
meta.colEnd = lineColors.length;
|
||||
if (curSpans.length) {
|
||||
meta.spans = curSpans.map((bk) => ({ bk, start: bk.entStart, end: bk.colors.length }));
|
||||
curSpans = [];
|
||||
}
|
||||
// Deferred texts are flushed after the loop (async) — stamp the owning
|
||||
// layer now so _drawTexts can batch per layer.
|
||||
for (let ti = textStart; ti < pendingTexts.length; ti++) {
|
||||
@@ -1088,7 +1152,26 @@ export class Viewer2D {
|
||||
}
|
||||
|
||||
this._layerNames = layerNames;
|
||||
this._lineColorAttr = null; this._origColors = null;
|
||||
this._lineColorAttr = null; this._origColors = null; this._hairRt = null;
|
||||
|
||||
// A weighted bucket only earns its own fat-line mesh while it stays inside
|
||||
// the segment cap; past that it is folded back into the hairline batch
|
||||
// (solid) or drawn as a thin dash bucket, which is what the viewer did
|
||||
// before lineweight existed.
|
||||
for (const bk of segBuckets.values()) {
|
||||
bk.fat = bk.lwPx > 0 && bk.verts.length / 6 <= LW_MAX_FAT_SEGS;
|
||||
if (bk.lwPx > 0 && !bk.fat) {
|
||||
console.warn(`선가중치 ${bk.lwPx}px 세그먼트 ${bk.verts.length / 6}개 → 헤어라인으로 대체`);
|
||||
}
|
||||
if (!bk.fat && bk.dash <= 0) {
|
||||
bk.mergeOffset = lineColors.length;
|
||||
for (let i = 0; i < bk.verts.length; i++) lineVerts.push(bk.verts[i]);
|
||||
for (let i = 0; i < bk.colors.length; i++) lineColors.push(bk.colors[i]);
|
||||
for (let i = 0; i < bk.segLayer.length; i++) lineSegLayer.push(bk.segLayer[i]);
|
||||
bk.merged = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (lineVerts.length) {
|
||||
const geom = new THREE.BufferGeometry();
|
||||
geom.setAttribute('position', new THREE.Float32BufferAttribute(lineVerts, 3));
|
||||
@@ -1099,23 +1182,45 @@ export class Viewer2D {
|
||||
this._group.add(lmesh);
|
||||
this._lineColorAttr = colorAttr;
|
||||
this._origColors = Float32Array.from(colorAttr.array);
|
||||
this._hairRt = { attr: colorAttr, orig: this._origColors };
|
||||
}
|
||||
|
||||
// Dashed linetypes (HIDDEN / CENTER / …) — one LineSegments per dash|gap
|
||||
// bucket with a LineDashedMaterial. computeLineDistances() is REQUIRED for
|
||||
// the dash pattern to appear; on LineSegments each 2-vertex pair dashes
|
||||
// independently from its own start (correct for CAD segments).
|
||||
for (const bk of dashBuckets.values()) {
|
||||
if (!bk.verts.length) continue;
|
||||
const dgeom = new THREE.BufferGeometry();
|
||||
dgeom.setAttribute('position', new THREE.Float32BufferAttribute(bk.verts, 3));
|
||||
dgeom.setAttribute('color', new THREE.Float32BufferAttribute(bk.colors, 3));
|
||||
const dline = new THREE.LineSegments(dgeom, new THREE.LineDashedMaterial({
|
||||
vertexColors: true, dashSize: bk.dash, gapSize: bk.gap,
|
||||
}));
|
||||
dline.computeLineDistances(); // must run BEFORE setIndex (three skips indexed geometry)
|
||||
this._registerIndexed(dline, bk.segLayer);
|
||||
this._group.add(dline);
|
||||
// One mesh per remaining bucket.
|
||||
// · lineweight > 0 → LineSegments2: screen-space quads, so the width stays
|
||||
// constant in pixels while zooming (AutoCAD LWDISPLAY semantics).
|
||||
// · otherwise → LineSegments + LineDashedMaterial for the dash
|
||||
// linetypes (HIDDEN / CENTER / …). computeLineDistances() is REQUIRED
|
||||
// for the pattern to appear; on LineSegments each 2-vertex pair dashes
|
||||
// independently from its own start (correct for CAD segments).
|
||||
for (const bk of segBuckets.values()) {
|
||||
if (!bk.verts.length || bk.merged) continue;
|
||||
if (bk.fat) {
|
||||
const fgeom = new LineSegmentsGeometry();
|
||||
fgeom.setPositions(bk.verts);
|
||||
fgeom.setColors(bk.colors);
|
||||
const fmat = new LineMaterial({
|
||||
vertexColors: true, linewidth: bk.lwPx, worldUnits: false,
|
||||
dashed: bk.dash > 0, dashSize: bk.dash, gapSize: bk.gap,
|
||||
});
|
||||
this._sizeLineMaterial(fmat);
|
||||
const fline = new LineSegments2(fgeom, fmat);
|
||||
if (bk.dash > 0) fline.computeLineDistances();
|
||||
this._registerFat(fline, bk.segLayer);
|
||||
bk.rt = this._fatBufs[this._fatBufs.length - 1];
|
||||
this._group.add(fline);
|
||||
} else {
|
||||
const dgeom = new THREE.BufferGeometry();
|
||||
dgeom.setAttribute('position', new THREE.Float32BufferAttribute(bk.verts, 3));
|
||||
const dcol = new THREE.Float32BufferAttribute(bk.colors, 3);
|
||||
dgeom.setAttribute('color', dcol);
|
||||
const dline = new THREE.LineSegments(dgeom, new THREE.LineDashedMaterial({
|
||||
vertexColors: true, dashSize: bk.dash, gapSize: bk.gap,
|
||||
}));
|
||||
dline.computeLineDistances(); // must run BEFORE setIndex (three skips indexed geometry)
|
||||
this._registerIndexed(dline, bk.segLayer);
|
||||
bk.rt = { attr: dcol, orig: Float32Array.from(dcol.array) };
|
||||
this._group.add(dline);
|
||||
}
|
||||
}
|
||||
|
||||
// Coordinate readout frame (Model UCS vs paper identity).
|
||||
@@ -1228,6 +1333,50 @@ export class Viewer2D {
|
||||
this._indexedBufs.push({ mesh, segLayer: Int32Array.from(segLayerArr), index, attr });
|
||||
}
|
||||
|
||||
/**
|
||||
* Track a LineSegments2 batch for layer masking. Its geometry is instanced
|
||||
* (one instance per segment, positions/colors interleaved 6 floats each), so
|
||||
* there is no index to rewrite: hiding a layer compacts the visible segments
|
||||
* into the front of the instance buffers and drops `instanceCount`.
|
||||
* The untouched originals are kept so any later mask starts from clean data.
|
||||
*/
|
||||
_registerFat(mesh, segLayerArr) {
|
||||
const geo = mesh.geometry;
|
||||
const posBuf = geo.attributes.instanceStart?.data;
|
||||
const colBuf = geo.attributes.instanceColorStart?.data;
|
||||
if (!posBuf) return;
|
||||
posBuf.setUsage(THREE.DynamicDrawUsage);
|
||||
colBuf?.setUsage(THREE.DynamicDrawUsage);
|
||||
this._fatBufs.push({
|
||||
mesh,
|
||||
segLayer: Int32Array.from(segLayerArr),
|
||||
posBuf, colBuf,
|
||||
posSrc: Float32Array.from(posBuf.array),
|
||||
colSrc: colBuf ? Float32Array.from(colBuf.array) : null,
|
||||
});
|
||||
}
|
||||
|
||||
/** Fat-line materials need the canvas size in px to convert linewidth. */
|
||||
_sizeLineMaterial(mat) {
|
||||
const w = this._container?.clientWidth || this._renderer?.domElement?.clientWidth || 1;
|
||||
const h = this._container?.clientHeight || this._renderer?.domElement?.clientHeight || 1;
|
||||
mat.resolution.set(w, h);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lineweight display switch (AutoCAD LWDISPLAY).
|
||||
* @param {boolean|null} on true/false forces it, null follows the drawing.
|
||||
*/
|
||||
setLineweightEnabled(on) {
|
||||
this._lwForced = (on == null) ? null : !!on;
|
||||
if (this._lastResult && !this._isLoading) {
|
||||
this.load(this._lastResult, { keepView: true, keepTheme: true, spaceHandle: this._activeSpaceHex });
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether lineweights are currently being drawn. */
|
||||
getLineweightEnabled() { return this._lwDisplay; }
|
||||
|
||||
/** Track an entity-owned object so layer toggles can flip its visibility. */
|
||||
_addObj(obj, layer) {
|
||||
const key = layer ?? this._curLayer;
|
||||
@@ -1272,6 +1421,28 @@ export class Viewer2D {
|
||||
buf.mesh.geometry.setDrawRange(0, w);
|
||||
}
|
||||
|
||||
// 2b) Fat-line batches → compact the instance buffers, then cap instanceCount.
|
||||
// slotOf records where each source segment ended up so the selection
|
||||
// highlight can still find its vertices (-1 = this segment is hidden).
|
||||
for (const buf of this._fatBufs) {
|
||||
const seg = buf.segLayer;
|
||||
if (!buf.slotOf) buf.slotOf = new Int32Array(seg.length);
|
||||
const slotOf = buf.slotOf;
|
||||
let w = 0;
|
||||
for (let s = 0; s < seg.length; s++) {
|
||||
const id = seg[s];
|
||||
if (id >= 0 && hiddenId[id]) { slotOf[s] = -1; continue; }
|
||||
buf.posBuf.array.set(buf.posSrc.subarray(s * 6, s * 6 + 6), w * 6);
|
||||
if (buf.colBuf) buf.colBuf.array.set(buf.colSrc.subarray(s * 6, s * 6 + 6), w * 6);
|
||||
slotOf[s] = w;
|
||||
w++;
|
||||
}
|
||||
buf.posBuf.needsUpdate = true;
|
||||
if (buf.colBuf) buf.colBuf.needsUpdate = true;
|
||||
buf.mesh.geometry.instanceCount = w;
|
||||
buf.mesh.visible = w > 0;
|
||||
}
|
||||
|
||||
// 3) Picking mask — _onClick skips segments/fills owned by hidden layers.
|
||||
const metas = this._entityMeta;
|
||||
const mh = new Uint8Array(metas.length);
|
||||
@@ -1282,6 +1453,10 @@ export class Viewer2D {
|
||||
if (this._selMeta && hidden.has(this._selMeta.layer)) {
|
||||
this._highlight(null);
|
||||
this._onSelectCb?.(null);
|
||||
} else if (this._selMeta && this._fatBufs.length) {
|
||||
// Compaction above rewrote the fat instance colors from the pristine
|
||||
// source — repaint the selection on its new slots.
|
||||
this._paintMeta(this._selMeta, this._selColor);
|
||||
}
|
||||
|
||||
// 4) Fit box for the visible subset.
|
||||
@@ -1586,6 +1761,10 @@ export class Viewer2D {
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'MESH':
|
||||
// pushSeg already maps model → paper and clips, so no xf here.
|
||||
if (d.vertices?.length >= 3) this._meshSegs(d, color, pushSeg);
|
||||
break;
|
||||
case 'HATCH': {
|
||||
const paths = d.paths || e.paths || [];
|
||||
for (const path of paths) {
|
||||
@@ -2111,6 +2290,8 @@ export class Viewer2D {
|
||||
this._layerByHandle.clear();
|
||||
this._layerByName.clear();
|
||||
this._layerNameByHandle = new Map();
|
||||
this._layerLwByHandle = new Map();
|
||||
this._layerLwByName = new Map();
|
||||
this._layer0Handle = null;
|
||||
if (!layers) return;
|
||||
for (const l of layers) {
|
||||
@@ -2122,9 +2303,40 @@ export class Viewer2D {
|
||||
if (name) this._layerByName.set(name, hex);
|
||||
if (handle != null && name) this._layerNameByHandle.set(String(handle), name);
|
||||
if (name === '0' && handle != null) this._layer0Handle = String(handle);
|
||||
// Raw i16 lineweight (1/100 mm, or -1/-2/-3). DXF spells it 370.
|
||||
const lw = l.lineWeight ?? l.lineweight ?? l.lineWeightRaw;
|
||||
if (typeof lw === 'number') {
|
||||
if (handle != null) this._layerLwByHandle.set(String(handle), lw);
|
||||
if (name) this._layerLwByName.set(name, lw); // DXF layers carry no handle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Effective lineweight of an entity, in screen pixels (0 = hairline).
|
||||
*
|
||||
* ByLayer (-1) resolves against the LAYER table, ByBlock (-2) against the
|
||||
* weight inherited from the owning INSERT, Default (-3) against LWDEFAULT.
|
||||
* Anything at or below the CAD default stays hairline so drawings that never
|
||||
* assigned a weight render exactly as before.
|
||||
*/
|
||||
_lwPx(entity, inheritedPx = 0) {
|
||||
if (!this._lwDisplay) return 0;
|
||||
let raw = entity?.entityHeader?.lineWeight ?? entity?.lineWeight ?? entity?.lineweight;
|
||||
if (typeof raw !== 'number') return 0;
|
||||
if (raw === -2) return inheritedPx; // ByBlock
|
||||
if (raw === -1) { // ByLayer
|
||||
const lh = entity.layerHandle?.value ?? entity.layerHandle;
|
||||
const byHandle = lh != null ? this._layerLwByHandle.get(String(lh)) : undefined;
|
||||
const ln = entity.layer ?? entity.layerName;
|
||||
raw = byHandle ?? (ln != null ? this._layerLwByName.get(ln) : undefined) ?? -3;
|
||||
if (raw === -1 || raw === -2) raw = -3; // layer can't be ByLayer/ByBlock
|
||||
}
|
||||
const mm = raw === -3 ? LW_DEFAULT_MM : raw / 100;
|
||||
if (!(mm > LW_HAIRLINE_MM)) return 0;
|
||||
return Math.min(LW_MAX_PX, Math.round(mm * LW_PX_PER_MM * 10) / 10);
|
||||
}
|
||||
|
||||
// Active-viewport VIEWTWIST (radians). The sheet is un-twisted by rotating the
|
||||
// camera up by -viewTwist (verified against samples/11.dwg: title border upright).
|
||||
_readViewTwist(result) {
|
||||
@@ -2370,6 +2582,58 @@ export class Viewer2D {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* MESH (AcDbSubDMesh) → wireframe segments.
|
||||
*
|
||||
* dwg-wasm emits the mesh as flat number arrays (see its `EntityType::Mesh`
|
||||
* arm) because a civil road-surface mesh runs to ~10^5 vertices:
|
||||
* vertices [x, y, z, …]
|
||||
* edges deduplicated vertex-index pairs [a, b, …]
|
||||
* faceList DXF group-93 layout — [n, i0 … i(n-1)] repeated, n-gons included
|
||||
*
|
||||
* The edge list is preferred: it is already deduplicated, so an edge shared by
|
||||
* two triangles is drawn once instead of twice (~2× fewer segments on a TIN).
|
||||
* faceList is the fallback for a mesh whose edge list is empty, and it is what
|
||||
* a future filled pass would triangulate.
|
||||
*
|
||||
* Only the base (control) mesh is drawn. `subdivisionLevel > 0` means AutoCAD
|
||||
* displays a Catmull-Clark refinement of these vertices; refining is not
|
||||
* implemented, so such a mesh renders slightly more angular than in AutoCAD.
|
||||
*
|
||||
* `xf(x, y) -> [x, y]` optionally maps the coordinates (block-local INSERT,
|
||||
* or model space seen through a paper-space viewport).
|
||||
*/
|
||||
_meshSegs(d, color, pushSeg, xf = null) {
|
||||
const v = d.vertices;
|
||||
if (!v?.length) return;
|
||||
const n = (v.length / 3) | 0;
|
||||
const seg = (a, b) => {
|
||||
if (a === b || !(a >= 0 && a < n) || !(b >= 0 && b < n)) return;
|
||||
let ax = v[a*3], ay = v[a*3+1];
|
||||
let bx = v[b*3], by = v[b*3+1];
|
||||
// pushSeg carries one z per segment; a sloped edge uses its midpoint
|
||||
// elevation, which is what a top view needs for depth ordering.
|
||||
const z = ((v[a*3+2] || 0) + (v[b*3+2] || 0)) / 2;
|
||||
if (xf) { [ax, ay] = xf(ax, ay); [bx, by] = xf(bx, by); }
|
||||
pushSeg(ax, ay, bx, by, z, color);
|
||||
};
|
||||
|
||||
const edges = d.edges;
|
||||
if (edges?.length >= 2) {
|
||||
for (let i = 0; i + 1 < edges.length; i += 2) seg(edges[i], edges[i+1]);
|
||||
return;
|
||||
}
|
||||
const fl = d.faceList;
|
||||
if (!fl?.length) return;
|
||||
for (let i = 0; i < fl.length; ) {
|
||||
const cnt = fl[i++];
|
||||
// A corrupt count would run the cursor off the end — stop instead.
|
||||
if (!(cnt >= 3) || i + cnt > fl.length) break;
|
||||
for (let k = 0; k < cnt; k++) seg(fl[i + k], fl[i + ((k + 1) % cnt)]);
|
||||
i += cnt;
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve start/end width for segment i→i+1 (DXF 40/41, fallback 43 constant).
|
||||
_segWidths(ent, i, scale = 1) {
|
||||
const d = ent?.data || ent || {};
|
||||
@@ -3295,17 +3559,23 @@ export class Viewer2D {
|
||||
const scaleAcc = parentScale * Math.max(Math.abs(sx), Math.abs(sy));
|
||||
const rotAcc = parentRot + rot;
|
||||
|
||||
// Weight the INSERT itself carries — what a ByBlock child resolves to.
|
||||
const insertLwPx = this._curLwPx;
|
||||
for (const be of entities) {
|
||||
const bd = be.data || be;
|
||||
const bt = (be.type || be.typeName || '').toUpperCase();
|
||||
if (bt === 'ATTDEF') continue; // skip attribute definitions
|
||||
const ecol = this._resolveBlockColor(be, color);
|
||||
this._curLwPx = this._lwPx(be, insertLwPx);
|
||||
try {
|
||||
if (bt === 'LINE' && bd.start && bd.end) {
|
||||
const [ax, ay] = xf(bd.start.x, bd.start.y);
|
||||
const [bx, by] = xf(bd.end.x, bd.end.y);
|
||||
pushSeg(ax, ay, bx, by, 0, ecol);
|
||||
|
||||
} else if (bt === 'MESH' && bd.vertices?.length >= 3) {
|
||||
this._meshSegs(bd, ecol, pushSeg, xf);
|
||||
|
||||
} else if ((bt === 'CIRCLE' || bt === 'ARC') && bd.center && bd.radius != null) {
|
||||
const [cx, cy] = xf(bd.center.x, bd.center.y);
|
||||
const r = bd.radius * scaleAcc;
|
||||
@@ -3484,6 +3754,7 @@ export class Viewer2D {
|
||||
}
|
||||
} catch { /* skip */ }
|
||||
}
|
||||
this._curLwPx = insertLwPx; // hand the INSERT's own weight back to the caller
|
||||
}
|
||||
|
||||
// Fallback dimension rendering from entity properties when no block geometry available
|
||||
@@ -3710,19 +3981,53 @@ export class Viewer2D {
|
||||
|
||||
/** Recolor the selected entity's line geometry to the accent color (deselect previous). */
|
||||
_highlight(meta) {
|
||||
const attr = this._lineColorAttr, orig = this._origColors;
|
||||
if (this._selMeta && attr && orig) {
|
||||
const a = this._selMeta;
|
||||
for (let i = a.colStart; i < a.colEnd && i < orig.length; i++) attr.array[i] = orig[i];
|
||||
}
|
||||
if (this._selMeta) this._paintMeta(this._selMeta, null);
|
||||
this._selMeta = meta || null;
|
||||
if (meta && attr && meta.colEnd > meta.colStart) {
|
||||
const c = this._selColor;
|
||||
for (let i = meta.colStart; i < meta.colEnd; i += 3) {
|
||||
attr.array[i] = c.r; attr.array[i + 1] = c.g; attr.array[i + 2] = c.b;
|
||||
if (meta) this._paintMeta(meta, this._selColor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Paint every vertex an entity owns. Its segments are spread over the
|
||||
* hairline batch plus one run per dash/lineweight bucket it touched
|
||||
* (meta.spans), so all of them get recolored — or restored when rgb is null.
|
||||
*/
|
||||
_paintMeta(meta, rgb) {
|
||||
const hair = this._hairRt;
|
||||
if (hair && meta.colEnd > meta.colStart) this._paintSpan(hair, meta.colStart, meta.colEnd, rgb);
|
||||
if (!meta.spans) return;
|
||||
for (const s of meta.spans) {
|
||||
const bk = s.bk;
|
||||
if (bk.merged) {
|
||||
if (hair) this._paintSpan(hair, s.start + bk.mergeOffset, s.end + bk.mergeOffset, rgb);
|
||||
} else if (bk.rt) {
|
||||
this._paintSpan(bk.rt, s.start, s.end, rgb);
|
||||
}
|
||||
}
|
||||
if (attr) attr.needsUpdate = true;
|
||||
}
|
||||
|
||||
/** Write one color run: `rgb` to select, null to restore the original color. */
|
||||
_paintSpan(rt, start, end, rgb) {
|
||||
if (rt.attr) { // plain vertex-color batch (hairline / thin dashes)
|
||||
const arr = rt.attr.array, orig = rt.orig;
|
||||
for (let i = start; i < end && i < arr.length; i += 3) {
|
||||
if (rgb) { arr[i] = rgb.r; arr[i + 1] = rgb.g; arr[i + 2] = rgb.b; }
|
||||
else { arr[i] = orig[i]; arr[i + 1] = orig[i + 1]; arr[i + 2] = orig[i + 2]; }
|
||||
}
|
||||
rt.attr.needsUpdate = true;
|
||||
return;
|
||||
}
|
||||
// Fat batch: hiding a layer compacts the instance buffer, so segment s of
|
||||
// the source colors may now live at a different slot (slotOf, -1 = hidden).
|
||||
const arr = rt.colBuf?.array, orig = rt.colSrc, slotOf = rt.slotOf;
|
||||
if (!arr || !orig) return;
|
||||
for (let i = start; i < end && i < orig.length; i += 3) {
|
||||
const slot = slotOf ? slotOf[(i / 6) | 0] : (i / 6) | 0;
|
||||
if (slot < 0) continue;
|
||||
const t = slot * 6 + (i % 6);
|
||||
if (rgb) { arr[t] = rgb.r; arr[t + 1] = rgb.g; arr[t + 2] = rgb.b; }
|
||||
else { arr[t] = orig[i]; arr[t + 1] = orig[i + 1]; arr[t + 2] = orig[i + 2]; }
|
||||
}
|
||||
rt.colBuf.needsUpdate = true;
|
||||
}
|
||||
|
||||
/** Selection highlight color (keeps in sync with the UI accent). */
|
||||
@@ -3910,6 +4215,7 @@ export class Viewer2D {
|
||||
this._layerBoxes.clear();
|
||||
this._layerSamples.clear();
|
||||
this._indexedBufs = [];
|
||||
this._fatBufs = [];
|
||||
this._layerNames = null;
|
||||
this._metaHidden = null;
|
||||
this._curLayer = null;
|
||||
@@ -3928,6 +4234,8 @@ export class Viewer2D {
|
||||
this._camera.left = -halfW; this._camera.right = halfW;
|
||||
this._camera.top = halfH; this._camera.bottom = -halfH;
|
||||
this._camera.updateProjectionMatrix();
|
||||
// Fat lines size themselves against the canvas resolution — restate it.
|
||||
for (const buf of this._fatBufs) this._sizeLineMaterial(buf.mesh.material);
|
||||
}
|
||||
|
||||
_animate() {
|
||||
|
||||
Reference in New Issue
Block a user