feat(viewer2d): add complex linetype rendering & LIN pattern parser support
This commit is contained in:
+395
-15
@@ -11,6 +11,7 @@ import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
||||
import { aciToHex } from './aciColors.js';
|
||||
import { buildAllowedOwnerHexes, listCadSpaces } from './cadSpaces';
|
||||
import { SlugTextEngine, SlugTextBatch } from './slugText';
|
||||
import { getLinPattern } from './linParser';
|
||||
|
||||
const DEFAULT_COLOR = 0xc9d1d9;
|
||||
const ARC_SEGS = 64;
|
||||
@@ -424,7 +425,9 @@ export class Viewer2D {
|
||||
const d = e.data || e;
|
||||
const type = (e.type || e.typeName || '').toUpperCase();
|
||||
const color = this._entityColor(e);
|
||||
curDash = this._resolveDash(e);
|
||||
const complexLt = this._resolveComplexLinetype(e);
|
||||
if (complexLt) curDash = null;
|
||||
else curDash = this._resolveDash(e);
|
||||
const meta = { entity: e, type, bounds: null, colStart: lineColors.length };
|
||||
this._entityMeta.push(meta);
|
||||
this._pickCurIdx = this._entityMeta.length - 1; // owner for pick geometry emitted below
|
||||
@@ -435,21 +438,35 @@ export class Viewer2D {
|
||||
// ── Basic geometry ────────────────────────────────────────────────
|
||||
case 'LINE':
|
||||
if (d.start && d.end) {
|
||||
pushSeg(d.start.x, d.start.y, d.end.x, d.end.y, d.start.z || 0, color);
|
||||
if (complexLt) {
|
||||
this._drawComplexPath([d.start, d.end], false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
} else {
|
||||
pushSeg(d.start.x, d.start.y, d.end.x, d.end.y, d.start.z || 0, color);
|
||||
}
|
||||
meta.bounds = { type:'line', x1:d.start.x, y1:d.start.y, x2:d.end.x, y2:d.end.y };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'CIRCLE':
|
||||
if (d.center && d.radius != null) {
|
||||
this._arcSegs(d.center, d.radius, 0, Math.PI*2, d.center.z||0, color, pushSeg);
|
||||
if (complexLt) {
|
||||
const pts = this._sampleArcPoints(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0);
|
||||
this._drawComplexPath(pts, true, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
} else {
|
||||
this._arcSegs(d.center, d.radius, 0, Math.PI*2, d.center.z||0, color, pushSeg);
|
||||
}
|
||||
meta.bounds = { type:'circle', cx:d.center.x, cy:d.center.y, r:d.radius };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ARC':
|
||||
if (d.center && d.radius != null) {
|
||||
this._arcSegs(d.center, d.radius, d.startAngle??0, d.endAngle??Math.PI*2, d.center.z||0, color, pushSeg);
|
||||
if (complexLt) {
|
||||
const pts = this._sampleArcPoints(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0);
|
||||
this._drawComplexPath(pts, false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
} else {
|
||||
this._arcSegs(d.center, d.radius, d.startAngle??0, d.endAngle??Math.PI*2, d.center.z||0, color, pushSeg);
|
||||
}
|
||||
meta.bounds = { type:'circle', cx:d.center.x, cy:d.center.y, r:d.radius };
|
||||
}
|
||||
break;
|
||||
@@ -461,6 +478,10 @@ export class Viewer2D {
|
||||
const nSeg = closed ? d.points.length : d.points.length - 1;
|
||||
if (this._polyHasWidth(e, nSeg, 1)) {
|
||||
this._widePolyMesh(d.points, d.bulges, closed, e, color, 1, expand);
|
||||
} else if (complexLt) {
|
||||
const pts = this._samplePolylinePoints(d.points, d.bulges, closed, d.elevation || 0);
|
||||
this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
pts.forEach(p => expand(p.x, p.y, p.z));
|
||||
} else if (hasBulge) {
|
||||
this._bulgePolySegs(d.points, d.bulges, closed, color, pushSeg, expand);
|
||||
} else {
|
||||
@@ -474,7 +495,14 @@ export class Viewer2D {
|
||||
break;
|
||||
|
||||
case 'POLYLINE':
|
||||
this._polylineSegs(d.vertices||d.points, d.closed||(d.flags&1), 0, color, pushSeg);
|
||||
if (complexLt && (d.vertices || d.points)) {
|
||||
const closed = !!(d.closed || (d.flags & 1));
|
||||
const pts = d.vertices || d.points;
|
||||
this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
pts.forEach(p => expand(p.x, p.y, p.z || 0));
|
||||
} else {
|
||||
this._polylineSegs(d.vertices||d.points, d.closed||(d.flags&1), 0, color, pushSeg);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'POLYLINE_2D': {
|
||||
@@ -487,7 +515,11 @@ export class Viewer2D {
|
||||
const bulges = kids.map(v => v.bulge || 0);
|
||||
const closed = (d.flags & 1) === 1;
|
||||
const elev = d.elevation || 0;
|
||||
if (bulges.some(b => Math.abs(b) >= 1e-6)) {
|
||||
if (complexLt) {
|
||||
const sampled = this._samplePolylinePoints(pts, bulges, closed, elev);
|
||||
this._drawComplexPath(sampled, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
sampled.forEach(p => expand(p.x, p.y, p.z));
|
||||
} else if (bulges.some(b => Math.abs(b) >= 1e-6)) {
|
||||
this._bulgePolySegs(pts, bulges, closed, color, pushSeg, expand);
|
||||
} else {
|
||||
this._polylineSegs(pts, closed, elev, color, pushSeg);
|
||||
@@ -511,7 +543,13 @@ export class Viewer2D {
|
||||
|
||||
case 'ELLIPSE':
|
||||
if (d.center) {
|
||||
this._ellipseSegs(d, color, pushSeg);
|
||||
if (complexLt) {
|
||||
const pts = this._sampleEllipsePoints(d);
|
||||
this._drawComplexPath(pts, false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
pts.forEach(p => expand(p.x, p.y, p.z || 0));
|
||||
} else {
|
||||
this._ellipseSegs(d, color, pushSeg);
|
||||
}
|
||||
meta.bounds = { type:'point', cx:d.center.x, cy:d.center.y };
|
||||
}
|
||||
break;
|
||||
@@ -530,8 +568,13 @@ export class Viewer2D {
|
||||
return result;
|
||||
})()
|
||||
: cps;
|
||||
this._polylineSegs(pts, false, 0, color, pushSeg);
|
||||
pts.forEach(p => expand(p.x, p.y, p.z || 0));
|
||||
if (complexLt) {
|
||||
this._drawComplexPath(pts, !!d.closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
pts.forEach(p => expand(p.x, p.y, p.z || 0));
|
||||
} else {
|
||||
this._polylineSegs(pts, false, 0, color, pushSeg);
|
||||
pts.forEach(p => expand(p.x, p.y, p.z || 0));
|
||||
}
|
||||
meta.bounds = { type:'point', cx:pts[0].x, cy:pts[0].y };
|
||||
}
|
||||
break;
|
||||
@@ -1247,29 +1290,50 @@ export class Viewer2D {
|
||||
const d = e.data || e;
|
||||
const type = (e.type || e.typeName || '').toUpperCase();
|
||||
const color = this._entityColor(e);
|
||||
const complexLt = this._resolveComplexLinetype(e);
|
||||
const vpTextStart = pendingTexts.length;
|
||||
|
||||
try {
|
||||
switch (type) {
|
||||
case 'LINE':
|
||||
if (d.start && d.end) {
|
||||
pushSeg(d.start.x, d.start.y, d.end.x, d.end.y, d.start.z || 0, color);
|
||||
if (complexLt) {
|
||||
this._drawComplexPath([d.start, d.end], false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
} else {
|
||||
pushSeg(d.start.x, d.start.y, d.end.x, d.end.y, d.start.z || 0, color);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'CIRCLE':
|
||||
if (d.center && d.radius != null) {
|
||||
// pushSeg maps model endpoints → paper; keep radius in model units
|
||||
this._arcSegs(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0, color, pushSeg);
|
||||
if (complexLt) {
|
||||
const pts = this._sampleArcPoints(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0);
|
||||
this._drawComplexPath(pts, true, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
} else {
|
||||
// pushSeg maps model endpoints → paper; keep radius in model units
|
||||
this._arcSegs(d.center, d.radius, 0, Math.PI * 2, d.center.z || 0, color, pushSeg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'ARC':
|
||||
if (d.center && d.radius != null) {
|
||||
this._arcSegs(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0, color, pushSeg);
|
||||
if (complexLt) {
|
||||
const pts = this._sampleArcPoints(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0);
|
||||
this._drawComplexPath(pts, false, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
} else {
|
||||
this._arcSegs(d.center, d.radius, d.startAngle ?? 0, d.endAngle ?? Math.PI * 2, d.center.z || 0, color, pushSeg);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'LWPOLYLINE':
|
||||
if (d.points?.length) {
|
||||
const hasBulge = d.bulges?.some((b) => Math.abs(b) >= 1e-6);
|
||||
const closed = !!(d.closed || (d.flags & 1));
|
||||
if (hasBulge) this._bulgePolySegs(d.points, d.bulges, closed, color, pushSeg, expand);
|
||||
if (complexLt) {
|
||||
const pts = this._samplePolylinePoints(d.points, d.bulges, closed, d.elevation || 0);
|
||||
this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
pts.forEach((p) => expand(p.x, p.y, p.z));
|
||||
} else if (hasBulge) this._bulgePolySegs(d.points, d.bulges, closed, color, pushSeg, expand);
|
||||
else {
|
||||
this._polylineSegs(d.points, closed, d.elevation || 0, color, pushSeg);
|
||||
d.points.forEach((p) => expand(p.x, p.y, d.elevation || 0));
|
||||
@@ -1277,7 +1341,14 @@ export class Viewer2D {
|
||||
}
|
||||
break;
|
||||
case 'POLYLINE':
|
||||
this._polylineSegs(d.vertices || d.points, d.closed || (d.flags & 1), 0, color, pushSeg);
|
||||
if (complexLt && (d.vertices || d.points)) {
|
||||
const closed = !!(d.closed || (d.flags & 1));
|
||||
const pts = d.vertices || d.points;
|
||||
this._drawComplexPath(pts, closed, complexLt.pattern, complexLt.ltscale, color, pushSeg, pendingTexts);
|
||||
pts.forEach((p) => expand(p.x, p.y, p.z || 0));
|
||||
} else {
|
||||
this._polylineSegs(d.vertices || d.points, d.closed || (d.flags & 1), 0, color, pushSeg);
|
||||
}
|
||||
break;
|
||||
case 'ELLIPSE':
|
||||
if (d.center) {
|
||||
@@ -1410,6 +1481,17 @@ export class Viewer2D {
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (complexLt && pendingTexts.length > vpTextStart) {
|
||||
for (let ti = vpTextStart; ti < pendingTexts.length; ti++) {
|
||||
const t = pendingTexts[ti];
|
||||
if (!t?.pos) continue;
|
||||
const q = xf(t.pos.x, t.pos.y);
|
||||
t.pos = q;
|
||||
t.height = (t.height || 2.5) * scale;
|
||||
t.rotation = (t.rotation || 0) + (xf.twist || 0);
|
||||
t.underlay = !!underlay;
|
||||
}
|
||||
}
|
||||
} catch { /* skip malformed */ }
|
||||
}
|
||||
|
||||
@@ -1466,6 +1548,304 @@ export class Viewer2D {
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────────
|
||||
|
||||
_resolveComplexLinetype(e) {
|
||||
let ltName = e.lineType ?? e.linetype;
|
||||
if (!ltName || ltName === 'ByLayer' || ltName === 'BYLAYER' ||
|
||||
ltName === 'ByBlock' || ltName === 'BYBLOCK') {
|
||||
const lh = e.layerHandle?.value ?? e.layerHandle;
|
||||
ltName = (lh != null && this._layerLtByHandle?.get(String(lh)))
|
||||
|| this._layerLtByName?.get(e.layer ?? e.layerName) || null;
|
||||
}
|
||||
if (!ltName || ltName === 'Continuous' || ltName === 'ByLayer' || ltName === 'ByBlock') return null;
|
||||
|
||||
const pattern = getLinPattern(ltName);
|
||||
if (!pattern) return null;
|
||||
|
||||
const entScale = (e.entityHeader?.linetypeScale > 0 ? e.entityHeader.linetypeScale : 1);
|
||||
const ltscale = (this._globalLtscale || 1) * entScale;
|
||||
|
||||
return { pattern, ltscale };
|
||||
}
|
||||
|
||||
_sampleArcPoints(center, radius, startAngle, endAngle, elevation = 0) {
|
||||
let sweep = endAngle - startAngle;
|
||||
if (sweep <= 0) sweep += Math.PI * 2;
|
||||
const steps = Math.max(16, Math.ceil((sweep / (Math.PI * 2)) * ARC_SEGS));
|
||||
const pts = [];
|
||||
const z = center.z || elevation || 0;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const a = startAngle + (sweep * i) / steps;
|
||||
pts.push({
|
||||
x: center.x + radius * Math.cos(a),
|
||||
y: center.y + radius * Math.sin(a),
|
||||
z,
|
||||
});
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
_samplePolylinePoints(points, bulges, closed, elevation = 0) {
|
||||
if (!points || points.length < 2) return points || [];
|
||||
const res = [];
|
||||
const n = closed ? points.length : points.length - 1;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const u = points[i];
|
||||
const v = points[(i + 1) % points.length];
|
||||
const b = bulges?.[i] || 0;
|
||||
const z = u.z || elevation || 0;
|
||||
if (Math.abs(b) < 1e-6) {
|
||||
if (res.length === 0) res.push({ x: u.x, y: u.y, z });
|
||||
res.push({ x: v.x, y: v.y, z });
|
||||
} else {
|
||||
const dx = v.x - u.x;
|
||||
const dy = v.y - u.y;
|
||||
const dist = Math.hypot(dx, dy);
|
||||
if (dist > 1e-9) {
|
||||
const theta = 4 * Math.atan(b);
|
||||
const halfTheta = Math.abs(theta) / 2;
|
||||
const R = dist / 2 / Math.sin(halfTheta);
|
||||
const dCenter = R * Math.cos(halfTheta);
|
||||
const mx = (u.x + v.x) / 2;
|
||||
const my = (u.y + v.y) / 2;
|
||||
const nx = -dy / dist;
|
||||
const ny = dx / dist;
|
||||
const dir = b > 0 ? 1 : -1;
|
||||
const cx = mx + dir * nx * dCenter;
|
||||
const cy = my + dir * ny * dCenter;
|
||||
const a1 = Math.atan2(u.y - cy, u.x - cx);
|
||||
const steps = Math.max(4, Math.ceil(Math.abs(theta) / (Math.PI / 16)));
|
||||
for (let k = (res.length === 0 ? 0 : 1); k <= steps; k++) {
|
||||
const a = a1 + (theta * k) / steps;
|
||||
res.push({
|
||||
x: cx + R * Math.cos(a),
|
||||
y: cy + R * Math.sin(a),
|
||||
z,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
_sampleEllipsePoints(d) {
|
||||
const major = d.majorAxis ?? d.smAxis ?? { x: d.radius || 1, y: 0 };
|
||||
const center = d.center || { x: 0, y: 0 };
|
||||
const ratio = d.ratio ?? d.axisRatio ?? d.minorAxisRatio ?? 0.5;
|
||||
const startParam = d.startParam ?? d.startAngle ?? 0;
|
||||
const endParam = d.endParam ?? d.endAngle ?? Math.PI * 2;
|
||||
const aLen = Math.hypot(major.x ?? 1, major.y ?? 0);
|
||||
const bLen = aLen * ratio;
|
||||
const rot = Math.atan2(major.y ?? 0, major.x ?? 1);
|
||||
let sweep = endParam - startParam;
|
||||
if (sweep <= 0) sweep += Math.PI * 2;
|
||||
const steps = ELLIPSE_SEGS;
|
||||
const pts = [];
|
||||
const z = center.z || 0;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = startParam + (sweep * i) / steps;
|
||||
const ex = aLen * Math.cos(t);
|
||||
const ey = bLen * Math.sin(t);
|
||||
const x = center.x + ex * Math.cos(rot) - ey * Math.sin(rot);
|
||||
const y = center.y + ex * Math.sin(rot) + ey * Math.cos(rot);
|
||||
pts.push({ x, y, z });
|
||||
}
|
||||
return pts;
|
||||
}
|
||||
|
||||
_drawComplexPath(points, closed, linPattern, ltscale, color, pushSeg, pendingTexts) {
|
||||
if (!points || points.length < 2) return;
|
||||
const pts = points.slice();
|
||||
if (closed && (Math.hypot(pts[pts.length - 1].x - pts[0].x, pts[pts.length - 1].y - pts[0].y) > 1e-6)) {
|
||||
pts.push(pts[0]);
|
||||
}
|
||||
|
||||
const segLens = [];
|
||||
let totalLen = 0;
|
||||
for (let i = 0; i < pts.length - 1; i++) {
|
||||
const dx = pts[i + 1].x - pts[i].x;
|
||||
const dy = pts[i + 1].y - pts[i].y;
|
||||
const len = Math.hypot(dx, dy);
|
||||
segLens.push(len);
|
||||
totalLen += len;
|
||||
}
|
||||
if (totalLen < 1e-6) return;
|
||||
|
||||
const getPathState = (dist) => {
|
||||
let s = Math.max(0, Math.min(dist, totalLen));
|
||||
for (let i = 0; i < segLens.length; i++) {
|
||||
const len = segLens[i];
|
||||
if (s <= len || i === segLens.length - 1) {
|
||||
const t = len > 1e-9 ? s / len : 0;
|
||||
const p0 = pts[i];
|
||||
const p1 = pts[i + 1];
|
||||
const x = p0.x + (p1.x - p0.x) * t;
|
||||
const y = p0.y + (p1.y - p0.y) * t;
|
||||
const z = (p0.z || 0) + ((p1.z || 0) - (p0.z || 0)) * t;
|
||||
const angle = Math.atan2(p1.y - p0.y, p1.x - p0.x);
|
||||
return { x, y, z, angle };
|
||||
}
|
||||
s -= len;
|
||||
}
|
||||
const last = pts[pts.length - 1];
|
||||
const prev = pts[pts.length - 2];
|
||||
return {
|
||||
x: last.x,
|
||||
y: last.y,
|
||||
z: last.z || 0,
|
||||
angle: Math.atan2(last.y - prev.y, last.x - prev.x),
|
||||
};
|
||||
};
|
||||
|
||||
const patternLen = linPattern.patternLength * ltscale;
|
||||
if (patternLen < 1e-6) return;
|
||||
|
||||
let s = 0;
|
||||
let elemIdx = 0;
|
||||
const elements = linPattern.elements;
|
||||
|
||||
while (s < totalLen) {
|
||||
const elem = elements[elemIdx % elements.length];
|
||||
elemIdx++;
|
||||
|
||||
if (elem.type === 'dash') {
|
||||
const dashLen = Math.max((elem.val || 0.05) * ltscale, 0.01 * ltscale);
|
||||
const startDist = s;
|
||||
const endDist = Math.min(s + dashLen, totalLen);
|
||||
|
||||
if (endDist > startDist) {
|
||||
const pStart = getPathState(startDist);
|
||||
const pEnd = getPathState(endDist);
|
||||
pushSeg(pStart.x, pStart.y, pEnd.x, pEnd.y, pStart.z, color);
|
||||
}
|
||||
s += dashLen;
|
||||
} else if (elem.type === 'gap') {
|
||||
const gapLen = Math.abs(elem.val || 0.1) * ltscale;
|
||||
s += gapLen;
|
||||
} else if (elem.type === 'shape') {
|
||||
const pState = getPathState(s);
|
||||
this._renderShapeSymbol(elem, pState, ltscale, color, pushSeg);
|
||||
} else if (elem.type === 'text') {
|
||||
const pState = getPathState(s);
|
||||
if (elem.text && pendingTexts) {
|
||||
const h = (elem.scale || 0.2) * ltscale;
|
||||
let angle = pState.angle + ((elem.rotation || 0) * Math.PI) / 180;
|
||||
if (elem.isAbsoluteAngle) {
|
||||
angle = ((elem.rotation || 0) * Math.PI) / 180;
|
||||
}
|
||||
|
||||
// CAD Standard Upright Rule: text must always read from bottom or right (-90deg to +90deg)
|
||||
let normA = Math.atan2(Math.sin(angle), Math.cos(angle));
|
||||
if (normA > Math.PI / 2) normA -= Math.PI;
|
||||
else if (normA < -Math.PI / 2) normA += Math.PI;
|
||||
angle = normA;
|
||||
|
||||
const cosT = Math.cos(angle);
|
||||
const sinT = Math.sin(angle);
|
||||
|
||||
const xo = (elem.xOffset || 0) * ltscale;
|
||||
const rawYo = (elem.yOffset != null && Math.abs(elem.yOffset) > 1e-4) ? Math.abs(elem.yOffset) : 0.02;
|
||||
const yo = rawYo * ltscale;
|
||||
|
||||
const tx = pState.x + (xo * cosT - yo * sinT);
|
||||
const ty = pState.y + (xo * sinT + yo * cosT);
|
||||
|
||||
pendingTexts.push({
|
||||
text: elem.text,
|
||||
pos: { x: tx, y: ty, z: pState.z },
|
||||
height: Math.max(h, 0.01),
|
||||
rotation: angle,
|
||||
color,
|
||||
alignH: 1,
|
||||
alignV: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_renderShapeSymbol(elem, pState, ltscale, color, pushSeg) {
|
||||
const name = (elem.shapeName || '').toUpperCase();
|
||||
const sc = (elem.scale || 0.1) * ltscale;
|
||||
const angle = elem.isAbsoluteAngle
|
||||
? ((elem.rotation || 0) * Math.PI) / 180
|
||||
: pState.angle + ((elem.rotation || 0) * Math.PI) / 180;
|
||||
|
||||
const xo = (elem.xOffset || 0) * ltscale;
|
||||
const yo = (elem.yOffset || 0) * ltscale;
|
||||
|
||||
const cosA = Math.cos(pState.angle);
|
||||
const sinA = Math.sin(pState.angle);
|
||||
const cx = pState.x + (xo * cosA - yo * sinA);
|
||||
const cy = pState.y + (xo * sinA + yo * cosA);
|
||||
const cz = pState.z || 0;
|
||||
|
||||
const cosR = Math.cos(angle);
|
||||
const sinR = Math.sin(angle);
|
||||
|
||||
const transform = (lx, ly) => ({
|
||||
x: cx + (lx * cosR - ly * sinR) * sc,
|
||||
y: cy + (lx * sinR + ly * cosR) * sc,
|
||||
});
|
||||
|
||||
if (name.includes('KSC35') || name.includes('KSC36') || name.includes('KSC19') || name.includes('ARROW')) {
|
||||
// Solid hatched arrowhead (matching AutoCAD KSC35 standard symbol)
|
||||
const steps = 12;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const t = i / steps;
|
||||
const lx = -0.5 + t;
|
||||
const hy = 0.25 * (1 - t);
|
||||
const top = transform(lx, hy);
|
||||
const bot = transform(lx, -hy);
|
||||
pushSeg(top.x, top.y, bot.x, bot.y, cz, color);
|
||||
}
|
||||
const p1 = transform(0.5, 0);
|
||||
const p2 = transform(-0.5, 0.25);
|
||||
const p3 = transform(-0.5, -0.25);
|
||||
pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color);
|
||||
pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color);
|
||||
pushSeg(p3.x, p3.y, p1.x, p1.y, cz, color);
|
||||
} else if (name.includes('CIRC') || name.includes('KSC11') || name.includes('KSC01')) {
|
||||
const segs = 12;
|
||||
let prev = transform(0.3, 0);
|
||||
for (let i = 1; i <= segs; i++) {
|
||||
const a = (i / segs) * Math.PI * 2;
|
||||
const curr = transform(0.3 * Math.cos(a), 0.3 * Math.sin(a));
|
||||
pushSeg(prev.x, prev.y, curr.x, curr.y, cz, color);
|
||||
prev = curr;
|
||||
}
|
||||
} else if (name.includes('BOX') || name.includes('KSC02') || name.includes('KSC05')) {
|
||||
const p1 = transform(-0.3, -0.3);
|
||||
const p2 = transform(0.3, -0.3);
|
||||
const p3 = transform(0.3, 0.3);
|
||||
const p4 = transform(-0.3, 0.3);
|
||||
pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color);
|
||||
pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color);
|
||||
pushSeg(p3.x, p3.y, p4.x, p4.y, cz, color);
|
||||
pushSeg(p4.x, p4.y, p1.x, p1.y, cz, color);
|
||||
} else if (name.includes('TRACK1')) {
|
||||
const p1 = transform(0, -0.4);
|
||||
const p2 = transform(0, 0.4);
|
||||
pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color);
|
||||
} else if (name.includes('BAT') || name.includes('DIAMOND')) {
|
||||
const p1 = transform(0, 0.4);
|
||||
const p2 = transform(0.4, 0);
|
||||
const p3 = transform(0, -0.4);
|
||||
const p4 = transform(-0.4, 0);
|
||||
pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color);
|
||||
pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color);
|
||||
pushSeg(p3.x, p3.y, p4.x, p4.y, cz, color);
|
||||
pushSeg(p4.x, p4.y, p1.x, p1.y, cz, color);
|
||||
} else {
|
||||
const p1 = transform(0.3, 0);
|
||||
const p2 = transform(-0.3, 0.2);
|
||||
const p3 = transform(-0.3, -0.2);
|
||||
pushSeg(p1.x, p1.y, p2.x, p2.y, cz, color);
|
||||
pushSeg(p2.x, p2.y, p3.x, p3.y, cz, color);
|
||||
pushSeg(p3.x, p3.y, p1.x, p1.y, cz, color);
|
||||
}
|
||||
}
|
||||
|
||||
_buildLayerMap(layers) {
|
||||
this._layerByHandle.clear();
|
||||
this._layerByName.clear();
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* LIN File Parser & Complex Linetype Registry
|
||||
* Parses AutoCAD LIN file definitions (including text and shape descriptors).
|
||||
*/
|
||||
|
||||
import { ICAD_LIN, KOSDIC_LIN } from './linData';
|
||||
|
||||
export interface LinElement {
|
||||
type: 'dash' | 'gap' | 'shape' | 'text';
|
||||
val?: number; // dash length (+) or gap length (-)
|
||||
shapeName?: string; // e.g. KSC35, CIRC1, BOX, TRACK1, BAT, ZIG
|
||||
shxFile?: string; // e.g. kosdic.shx, ltypeshp.shx
|
||||
text?: string; // e.g. "L2", "GAS", "HW"
|
||||
style?: string; // e.g. Standard
|
||||
scale: number; // s=... (default 1.0)
|
||||
rotation: number; // r=... (angle in degrees)
|
||||
isAbsoluteAngle: boolean;// a=...
|
||||
isUprightAngle: boolean; // u=...
|
||||
xOffset: number; // x=...
|
||||
yOffset: number; // y=...
|
||||
}
|
||||
|
||||
export interface LinPattern {
|
||||
name: string;
|
||||
description: string;
|
||||
elements: LinElement[];
|
||||
patternLength: number; // total length sum of dashes + abs(gaps)
|
||||
}
|
||||
|
||||
function parseBracketElement(inner: string): LinElement {
|
||||
const parts: string[] = [];
|
||||
let cur = '';
|
||||
let inQuote = false;
|
||||
for (let i = 0; i < inner.length; i++) {
|
||||
const c = inner[i];
|
||||
if (c === '"') inQuote = !inQuote;
|
||||
if (c === ',' && !inQuote) {
|
||||
parts.push(cur.trim());
|
||||
cur = '';
|
||||
} else {
|
||||
cur += c;
|
||||
}
|
||||
}
|
||||
if (cur.trim()) parts.push(cur.trim());
|
||||
|
||||
let type: 'shape' | 'text' = 'shape';
|
||||
let shapeName: string | undefined;
|
||||
let shxFile: string | undefined;
|
||||
let text: string | undefined;
|
||||
let style: string | undefined;
|
||||
|
||||
let scale = 1.0;
|
||||
let rotation = 0;
|
||||
let isAbsoluteAngle = false;
|
||||
let isUprightAngle = false;
|
||||
let xOffset = 0;
|
||||
let yOffset = 0;
|
||||
|
||||
if (parts.length > 0) {
|
||||
const first = parts[0];
|
||||
if (first.startsWith('"') && first.endsWith('"')) {
|
||||
type = 'text';
|
||||
text = first.slice(1, -1);
|
||||
if (parts.length > 1 && !parts[1].includes('=')) style = parts[1];
|
||||
} else {
|
||||
type = 'shape';
|
||||
shapeName = first;
|
||||
if (parts.length > 1 && !parts[1].includes('=')) shxFile = parts[1];
|
||||
}
|
||||
|
||||
for (let i = 1; i < parts.length; i++) {
|
||||
const part = parts[i];
|
||||
const eqIdx = part.indexOf('=');
|
||||
if (eqIdx > 0) {
|
||||
const key = part.substring(0, eqIdx).trim().toLowerCase();
|
||||
const valStr = part.substring(eqIdx + 1).trim();
|
||||
const val = parseFloat(valStr);
|
||||
if (!isNaN(val)) {
|
||||
if (key === 's') scale = val;
|
||||
else if (key === 'r') { rotation = val; }
|
||||
else if (key === 'a') { rotation = val; isAbsoluteAngle = true; }
|
||||
else if (key === 'u') { rotation = val; isUprightAngle = true; }
|
||||
else if (key === 'x') xOffset = val;
|
||||
else if (key === 'y') yOffset = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
type,
|
||||
shapeName,
|
||||
shxFile,
|
||||
text,
|
||||
style,
|
||||
scale,
|
||||
rotation,
|
||||
isAbsoluteAngle,
|
||||
isUprightAngle,
|
||||
xOffset,
|
||||
yOffset,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenizeLinPattern(patternStr: string): string[] {
|
||||
const s = patternStr.replace(/^A\s*,\s*/i, '').trim();
|
||||
const tokens: string[] = [];
|
||||
let current = '';
|
||||
let inBracket = false;
|
||||
let inQuote = false;
|
||||
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
const ch = s[i];
|
||||
if (ch === '"') inQuote = !inQuote;
|
||||
if (ch === '[' && !inQuote) inBracket = true;
|
||||
if (ch === ']' && !inQuote) inBracket = false;
|
||||
|
||||
if (ch === ',' && !inBracket && !inQuote) {
|
||||
if (current.trim()) tokens.push(current.trim());
|
||||
current = '';
|
||||
} else {
|
||||
current += ch;
|
||||
}
|
||||
}
|
||||
if (current.trim()) tokens.push(current.trim());
|
||||
return tokens;
|
||||
}
|
||||
|
||||
export function parseLinContent(content: string): Map<string, LinPattern> {
|
||||
const result = new Map<string, LinPattern>();
|
||||
const lines = content.split(/\r?\n/);
|
||||
let currentName: string | null = null;
|
||||
let currentDesc = '';
|
||||
|
||||
for (let line of lines) {
|
||||
line = line.trim();
|
||||
if (!line || line.startsWith(';')) continue;
|
||||
if (line.startsWith('*')) {
|
||||
const commaIdx = line.indexOf(',');
|
||||
if (commaIdx > 0) {
|
||||
currentName = line.substring(1, commaIdx).trim();
|
||||
currentDesc = line.substring(commaIdx + 1).trim();
|
||||
} else {
|
||||
currentName = line.substring(1).trim();
|
||||
currentDesc = '';
|
||||
}
|
||||
} else if (currentName && line.toUpperCase().startsWith('A,')) {
|
||||
const tokens = tokenizeLinPattern(line);
|
||||
const elements: LinElement[] = [];
|
||||
let totalLength = 0;
|
||||
|
||||
for (const tok of tokens) {
|
||||
if (tok.startsWith('[') && tok.endsWith(']')) {
|
||||
const elem = parseBracketElement(tok.slice(1, -1));
|
||||
elements.push(elem);
|
||||
} else {
|
||||
const val = parseFloat(tok);
|
||||
if (!isNaN(val)) {
|
||||
if (val > 1e-6) {
|
||||
elements.push({
|
||||
type: 'dash',
|
||||
val,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
isAbsoluteAngle: false,
|
||||
isUprightAngle: false,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
});
|
||||
totalLength += val;
|
||||
} else if (val < -1e-6) {
|
||||
const absVal = Math.abs(val);
|
||||
elements.push({
|
||||
type: 'gap',
|
||||
val: -absVal,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
isAbsoluteAngle: false,
|
||||
isUprightAngle: false,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
});
|
||||
totalLength += absVal;
|
||||
} else {
|
||||
// Dot (val === 0)
|
||||
elements.push({
|
||||
type: 'dash',
|
||||
val: 0.05,
|
||||
scale: 1,
|
||||
rotation: 0,
|
||||
isAbsoluteAngle: false,
|
||||
isUprightAngle: false,
|
||||
xOffset: 0,
|
||||
yOffset: 0,
|
||||
});
|
||||
totalLength += 0.05;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (elements.length > 0) {
|
||||
result.set(currentName.toUpperCase(), {
|
||||
name: currentName,
|
||||
description: currentDesc,
|
||||
elements,
|
||||
patternLength: Math.max(totalLength, 0.1),
|
||||
});
|
||||
}
|
||||
currentName = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// Global Linetype Registry initialized with Icad.lin and KOSDIC.lin
|
||||
const globalRegistry = new Map<string, LinPattern>();
|
||||
|
||||
export function registerLinContent(content: string) {
|
||||
const parsed = parseLinContent(content);
|
||||
for (const [key, pattern] of parsed.entries()) {
|
||||
globalRegistry.set(key, pattern);
|
||||
}
|
||||
}
|
||||
|
||||
// Warm up registry with bundled LIN files
|
||||
registerLinContent(ICAD_LIN);
|
||||
registerLinContent(KOSDIC_LIN);
|
||||
|
||||
const CIVIL_LT_TEXT_MAP: Record<string, string> = {
|
||||
'H-DICHL02': 'L2-2/0',
|
||||
'H-DICHL02R': 'L2-2',
|
||||
'H-DICHL01': 'L2-3/0',
|
||||
'H-DICHL01R': 'L2-3',
|
||||
'H-DICHL06': 'L5-1',
|
||||
'H-DICHL06R': 'L5-1',
|
||||
'H-DICHL05': 'L5',
|
||||
'H-DICHL05R': 'L5',
|
||||
'H-DICHU05': 'U5',
|
||||
'H-DICHU05R': 'U5',
|
||||
'H-DICHB01': 'B1',
|
||||
'H-DICHB01R': 'B1',
|
||||
'H-DICHM03': 'M3',
|
||||
'H-DICHM03R': 'M3',
|
||||
'H-DICHV01': 'V1',
|
||||
'H-DICHV01R': 'V1',
|
||||
'H-DICHO01': 'O1',
|
||||
'H-DICHO01R': 'O1',
|
||||
'H-DICHL03': 'L3',
|
||||
'H-DICHL03R': 'L3',
|
||||
'H-DICHL04': 'L4',
|
||||
'H-DICHL04R': 'L4',
|
||||
'H-DICHL08': 'L8',
|
||||
'H-DICHL08R': 'L8',
|
||||
};
|
||||
|
||||
const NO_SHAPE_LINETYPES = new Set([
|
||||
'H-DICHL05',
|
||||
'H-DICHL05R',
|
||||
'H-DICHU05',
|
||||
'H-DICHU05R',
|
||||
'H-DICHB01',
|
||||
'H-DICHB01R',
|
||||
'H-DICHM03',
|
||||
'H-DICHM03R',
|
||||
'H-DICHV01',
|
||||
'H-DICHV01R',
|
||||
'H-DICHO01',
|
||||
'H-DICHO01R',
|
||||
]);
|
||||
|
||||
export function getLinPattern(name: string): LinPattern | undefined {
|
||||
if (!name) return undefined;
|
||||
const key = name.toUpperCase().trim();
|
||||
const pattern = globalRegistry.get(key);
|
||||
if (!pattern) return undefined;
|
||||
|
||||
let elements = pattern.elements;
|
||||
|
||||
// Filter out shape/arrowhead elements for L5, U5, B1, etc. as specified in official CAD spec
|
||||
if (NO_SHAPE_LINETYPES.has(key)) {
|
||||
elements = elements.filter((elem) => elem.type !== 'shape');
|
||||
}
|
||||
|
||||
const overrideText = CIVIL_LT_TEXT_MAP[key];
|
||||
if (overrideText) {
|
||||
elements = elements.map((elem) =>
|
||||
elem.type === 'text' ? { ...elem, text: overrideText } : elem
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...pattern,
|
||||
elements,
|
||||
};
|
||||
}
|
||||
|
||||
export function getAllRegisteredPatterns(): Map<string, LinPattern> {
|
||||
return globalRegistry;
|
||||
}
|
||||
Reference in New Issue
Block a user