DXF-Import: SPLINE (De-Boor-B-Spline) + INSERT (Block-Expansion mit Transform/Array/Verschachtelung)
This commit is contained in:
@@ -119,3 +119,170 @@ describe("parseDxf — gemischt", () => {
|
||||
expect(res.contours[0].contours).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ── SPLINE ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/** LINE-Entity (10/20/30 Start, 11/21/31 Ende). */
|
||||
function line(x1: number, y1: number, x2: number, y2: number): string[] {
|
||||
return ["0", "LINE", "8", "0", "10", `${x1}`, "20", `${y1}`, "30", "0", "11", `${x2}`, "21", `${y2}`, "31", "0"];
|
||||
}
|
||||
|
||||
/** SPLINE mit Kontrollpunkten + Knoten (71 Grad, 40 Knoten, 10/20/30 CPs). */
|
||||
function spline(degree: number, knots: number[], cps: Array<[number, number]>): string[] {
|
||||
const g = ["0", "SPLINE", "8", "0", "71", `${degree}`];
|
||||
for (const k of knots) g.push("40", `${k}`);
|
||||
for (const [x, y] of cps) g.push("10", `${x}`, "20", `${y}`, "30", "0");
|
||||
return g;
|
||||
}
|
||||
|
||||
/** SPLINE nur mit Stützpunkten (11/21/31 fitPoints), ohne gültige Knoten. */
|
||||
function splineFit(fps: Array<[number, number]>): string[] {
|
||||
const g = ["0", "SPLINE", "8", "0", "71", "3"];
|
||||
for (const [x, y] of fps) g.push("11", `${x}`, "21", `${y}`, "31", "0");
|
||||
return g;
|
||||
}
|
||||
|
||||
describe("parseDxf — SPLINE", () => {
|
||||
it("Grad-1-Spline zeichnet exakt das Kontrollpolygon nach", () => {
|
||||
// Clamped-Knoten für 2 CPs, Grad 1: |U| = 2+1+1 = 4, Domain [0,1].
|
||||
const c = onlyContour(dxf(spline(1, [0, 0, 1, 1], [[0, 0], [10, 0]])));
|
||||
expect(c.closed).toBe(false);
|
||||
expect(c.pts[0].x).toBeCloseTo(0, 9);
|
||||
expect(c.pts[0].y).toBeCloseTo(0, 9);
|
||||
const last = c.pts[c.pts.length - 1];
|
||||
expect(last.x).toBeCloseTo(10, 9);
|
||||
expect(last.y).toBeCloseTo(0, 9);
|
||||
// Linear: alle Punkte auf y=0, x monoton steigend.
|
||||
for (let i = 1; i < c.pts.length; i++) {
|
||||
expect(c.pts[i].y).toBeCloseTo(0, 9);
|
||||
expect(c.pts[i].x).toBeGreaterThanOrEqual(c.pts[i - 1].x - 1e-9);
|
||||
}
|
||||
});
|
||||
|
||||
it("Grad-2-Spline bleibt in der konvexen Hülle und endet auf den Rand-CPs", () => {
|
||||
// 3 CPs, Grad 2, clamped: |U| = 3+2+1 = 6 → [0,0,0,1,1,1], Domain [0,1].
|
||||
const c = onlyContour(
|
||||
dxf(spline(2, [0, 0, 0, 1, 1, 1], [[0, 0], [5, 10], [10, 0]])),
|
||||
);
|
||||
// Endpunkte = erster/letzter Kontrollpunkt (clamped).
|
||||
expect(c.pts[0].x).toBeCloseTo(0, 9);
|
||||
expect(c.pts[0].y).toBeCloseTo(0, 9);
|
||||
const last = c.pts[c.pts.length - 1];
|
||||
expect(last.x).toBeCloseTo(10, 9);
|
||||
expect(last.y).toBeCloseTo(0, 9);
|
||||
// Konvexe Hülle: y nie über 10, x in [0,10]; Scheitel bei x≈5 unter 10.
|
||||
for (const p of c.pts) {
|
||||
expect(p.y).toBeLessThanOrEqual(10 + 1e-9);
|
||||
expect(p.y).toBeGreaterThanOrEqual(-1e-9);
|
||||
expect(p.x).toBeGreaterThanOrEqual(-1e-9);
|
||||
expect(p.x).toBeLessThanOrEqual(10 + 1e-9);
|
||||
}
|
||||
// Mittelpunkt (t=0.5) einer quadratischen Bézier: 0.25·P0+0.5·P1+0.25·P2 = (5,5).
|
||||
const mid = c.pts[Math.floor(c.pts.length / 2)];
|
||||
expect(mid.x).toBeCloseTo(5, 6);
|
||||
expect(mid.y).toBeCloseTo(5, 6);
|
||||
});
|
||||
|
||||
it("fällt ohne gültige Knoten auf die Stützpunkte zurück", () => {
|
||||
const c = onlyContour(dxf(splineFit([[0, 0], [1, 2], [3, 4]])));
|
||||
expect(c.pts).toHaveLength(3);
|
||||
expect(c.pts[1].x).toBeCloseTo(1, 9);
|
||||
expect(c.pts[1].y).toBeCloseTo(2, 9);
|
||||
});
|
||||
});
|
||||
|
||||
// ── INSERT (Block-Referenzen) ─────────────────────────────────────────────────
|
||||
|
||||
/** BLOCK-Definition: Name, Basispunkt, enthaltene Entities. */
|
||||
function block(name: string, bx: number, by: number, ...ents: string[][]): string[] {
|
||||
const g = ["0", "BLOCK", "8", "0", "2", name, "10", `${bx}`, "20", `${by}`, "30", "0", "70", "0"];
|
||||
for (const e of ents) g.push(...e);
|
||||
g.push("0", "ENDBLK");
|
||||
return g;
|
||||
}
|
||||
|
||||
interface InsOpts {
|
||||
rot?: number; sx?: number; sy?: number; nc?: number; nr?: number; dc?: number; dr?: number;
|
||||
}
|
||||
/** INSERT-Referenz auf einen Block. */
|
||||
function insert(name: string, x: number, y: number, o: InsOpts = {}): string[] {
|
||||
const g = ["0", "INSERT", "2", name, "10", `${x}`, "20", `${y}`, "30", "0"];
|
||||
if (o.sx !== undefined) g.push("41", `${o.sx}`);
|
||||
if (o.sy !== undefined) g.push("42", `${o.sy}`);
|
||||
if (o.rot !== undefined) g.push("50", `${o.rot}`);
|
||||
if (o.nc !== undefined) g.push("70", `${o.nc}`);
|
||||
if (o.nr !== undefined) g.push("71", `${o.nr}`);
|
||||
if (o.dc !== undefined) g.push("44", `${o.dc}`);
|
||||
if (o.dr !== undefined) g.push("45", `${o.dr}`);
|
||||
return g;
|
||||
}
|
||||
|
||||
/** DXF mit BLOCKS- und ENTITIES-Sektion. */
|
||||
function dxfFull(blocks: string[][], entities: string[][]): string {
|
||||
const lines: string[] = [];
|
||||
if (blocks.length) {
|
||||
lines.push("0", "SECTION", "2", "BLOCKS");
|
||||
for (const b of blocks) lines.push(...b);
|
||||
lines.push("0", "ENDSEC");
|
||||
}
|
||||
lines.push("0", "SECTION", "2", "ENTITIES");
|
||||
for (const e of entities) lines.push(...e);
|
||||
lines.push("0", "ENDSEC", "0", "EOF");
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
describe("parseDxf — INSERT", () => {
|
||||
it("verschiebt eine Block-Linie an den Einfügepunkt", () => {
|
||||
const c = onlyContour(
|
||||
dxfFull([block("seg", 0, 0, line(0, 0, 1, 0))], [insert("seg", 5, 5)]),
|
||||
);
|
||||
expect(c.pts[0].x).toBeCloseTo(5, 9);
|
||||
expect(c.pts[0].y).toBeCloseTo(5, 9);
|
||||
expect(c.pts[1].x).toBeCloseTo(6, 9);
|
||||
expect(c.pts[1].y).toBeCloseTo(5, 9);
|
||||
});
|
||||
|
||||
it("rotiert um 90° und skaliert", () => {
|
||||
const rot = onlyContour(
|
||||
dxfFull([block("seg", 0, 0, line(0, 0, 1, 0))], [insert("seg", 0, 0, { rot: 90 })]),
|
||||
);
|
||||
// (1,0) um 90° CCW → (0,1).
|
||||
expect(rot.pts[1].x).toBeCloseTo(0, 6);
|
||||
expect(rot.pts[1].y).toBeCloseTo(1, 6);
|
||||
|
||||
const scl = onlyContour(
|
||||
dxfFull([block("seg", 0, 0, line(0, 0, 1, 0))], [insert("seg", 0, 0, { sx: 2, sy: 3 })]),
|
||||
);
|
||||
expect(scl.pts[1].x).toBeCloseTo(2, 9);
|
||||
expect(scl.pts[1].y).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
it("expandiert ein 2×1-Array zu zwei Konturen", () => {
|
||||
const res = parseDxf(
|
||||
dxfFull([block("seg", 0, 0, line(0, 0, 1, 0))], [insert("seg", 0, 0, { nc: 2, dc: 10 })]),
|
||||
);
|
||||
const cs = res.contours[0].contours;
|
||||
expect(cs).toHaveLength(2);
|
||||
// Zweite Spalte um columnSpacing 10 versetzt.
|
||||
const xs = cs.map((c) => c.pts[0].x).sort((a, b) => a - b);
|
||||
expect(xs[0]).toBeCloseTo(0, 9);
|
||||
expect(xs[1]).toBeCloseTo(10, 9);
|
||||
});
|
||||
|
||||
it("löst verschachtelte Blockreferenzen auf (Transform-Komposition)", () => {
|
||||
const c = onlyContour(
|
||||
dxfFull(
|
||||
[
|
||||
block("inner", 0, 0, line(0, 0, 1, 0)),
|
||||
block("outer", 0, 0, insert("inner", 2, 0)),
|
||||
],
|
||||
[insert("outer", 0, 3)],
|
||||
),
|
||||
);
|
||||
// inner (0,0)-(1,0) → +(2,0) durch outer → +(0,3) durch top = (2,3)-(3,3).
|
||||
expect(c.pts[0].x).toBeCloseTo(2, 9);
|
||||
expect(c.pts[0].y).toBeCloseTo(3, 9);
|
||||
expect(c.pts[1].x).toBeCloseTo(3, 9);
|
||||
expect(c.pts[1].y).toBeCloseTo(3, 9);
|
||||
});
|
||||
});
|
||||
|
||||
+264
-44
@@ -20,6 +20,8 @@
|
||||
// • ARC → Kontur (offener Bogen, tesselliert).
|
||||
// • CIRCLE → Kontur (geschlossener Kreis, tesselliert).
|
||||
// • ELLIPSE → Kontur (Ellipsenbogen/-umlauf, tesselliert).
|
||||
// • SPLINE → Kontur (B-Spline via De Boor; Fallback fitPoints).
|
||||
// • INSERT → Block-Konturen, transformiert (Scale/Rot/Array, verschachtelt).
|
||||
|
||||
import DxfParser from "dxf-parser";
|
||||
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
|
||||
@@ -74,10 +76,34 @@ interface DxfEntity {
|
||||
majorAxisEndPoint?: DxfVertex;
|
||||
/** ELLIPSE: Verhältnis Neben-/Hauptachse (b/a). */
|
||||
axisRatio?: number;
|
||||
// SPLINE-Felder. Winkel/Parameter roh; Knoten/Grad definieren die B-Spline.
|
||||
controlPoints?: DxfVertex[];
|
||||
fitPoints?: DxfVertex[];
|
||||
knotValues?: number[];
|
||||
degreeOfSplineCurve?: number;
|
||||
closed?: boolean;
|
||||
// INSERT-Felder (Block-Referenz). `position` (Einfügepunkt) wird tolerant
|
||||
// gelesen (das Feld ist oben als MESH-Vertexliste getippt) — daher hier NICHT
|
||||
// erneut deklariert; `rotation` ist in GRAD (anders als ARC/CIRCLE).
|
||||
name?: string;
|
||||
xScale?: number;
|
||||
yScale?: number;
|
||||
rotation?: number;
|
||||
columnCount?: number;
|
||||
rowCount?: number;
|
||||
columnSpacing?: number;
|
||||
rowSpacing?: number;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
/** Ein Block (BLOCKS-Tabelle): Basispunkt `position` + eigene Entity-Liste. */
|
||||
interface DxfBlock {
|
||||
name?: string;
|
||||
position?: DxfVertex;
|
||||
entities?: DxfEntity[];
|
||||
}
|
||||
interface DxfDocument {
|
||||
entities?: DxfEntity[];
|
||||
blocks?: Record<string, DxfBlock>;
|
||||
}
|
||||
|
||||
let importSeq = 0;
|
||||
@@ -94,6 +120,7 @@ export function parseDxf(text: string): DxfImportResult {
|
||||
// parseSync wirft bei strukturell kaputtem DXF; das soll nach oben.
|
||||
const doc = parser.parseSync(text) as unknown as DxfDocument;
|
||||
const entities = doc?.entities ?? [];
|
||||
const blocks = doc?.blocks ?? {};
|
||||
|
||||
const meshTriangles: number[] = []; // gesammelte Mesh-Positions (x,y,z…)
|
||||
const meshIndices: number[] = [];
|
||||
@@ -101,51 +128,22 @@ export function parseDxf(text: string): DxfImportResult {
|
||||
|
||||
for (const e of entities) {
|
||||
const type = (e.type ?? "").toUpperCase();
|
||||
switch (type) {
|
||||
case "3DFACE":
|
||||
addFace(meshTriangles, meshIndices, e);
|
||||
break;
|
||||
case "MESH":
|
||||
addMesh(meshTriangles, meshIndices, e);
|
||||
break;
|
||||
case "POLYLINE":
|
||||
// POLYLINE ist mehrdeutig: Polyface/PolygonMesh → Mesh; sonst → Kontur.
|
||||
if (isMeshPolyline(e)) {
|
||||
addPolyfaceMesh(meshTriangles, meshIndices, e);
|
||||
} else {
|
||||
const ct = polylineContour(e);
|
||||
if (ct) contours.push(ct);
|
||||
}
|
||||
break;
|
||||
case "LWPOLYLINE": {
|
||||
const ct = polylineContour(e);
|
||||
if (ct) contours.push(ct);
|
||||
break;
|
||||
}
|
||||
case "LINE": {
|
||||
const ct = lineContour(e);
|
||||
if (ct) contours.push(ct);
|
||||
break;
|
||||
}
|
||||
case "ARC": {
|
||||
const ct = arcContour(e);
|
||||
if (ct) contours.push(ct);
|
||||
break;
|
||||
}
|
||||
case "CIRCLE": {
|
||||
const ct = circleContour(e);
|
||||
if (ct) contours.push(ct);
|
||||
break;
|
||||
}
|
||||
case "ELLIPSE": {
|
||||
const ct = ellipseContour(e);
|
||||
if (ct) contours.push(ct);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Unbekannte/irrelevante Entity → ignorieren (tolerant).
|
||||
break;
|
||||
// Mesh-Entities in die Dreiecks-Puffer; POLYLINE nur als Mesh-Variante.
|
||||
if (type === "3DFACE") {
|
||||
addFace(meshTriangles, meshIndices, e);
|
||||
continue;
|
||||
}
|
||||
if (type === "MESH") {
|
||||
addMesh(meshTriangles, meshIndices, e);
|
||||
continue;
|
||||
}
|
||||
if (type === "POLYLINE" && isMeshPolyline(e)) {
|
||||
addPolyfaceMesh(meshTriangles, meshIndices, e);
|
||||
continue;
|
||||
}
|
||||
// Alle übrigen Kontur-Entities (inkl. SPLINE + INSERT-Block-Expansion) über
|
||||
// den gemeinsamen Sammler — dieselbe Logik nutzt die INSERT-Rekursion.
|
||||
collectContours(e, blocks, contours, 0);
|
||||
}
|
||||
|
||||
const meshes: ImportedMesh[] = [];
|
||||
@@ -456,3 +454,225 @@ function ellipseContour(e: DxfEntity): Contour | null {
|
||||
}
|
||||
return { z, pts, closed: full, layer: e.layer };
|
||||
}
|
||||
|
||||
// ── SPLINE (B-Spline via De Boor) ────────────────────────────────────────────
|
||||
|
||||
/** DxfVertex-Liste → finite Vec2-Stützpunkte. */
|
||||
function toVec2s(vs: DxfVertex[] | undefined): Vec2[] {
|
||||
const out: Vec2[] = [];
|
||||
for (const v of vs ?? []) {
|
||||
if (Number.isFinite(v.x) && Number.isFinite(v.y)) {
|
||||
out.push({ x: v.x ?? 0, y: v.y ?? 0 });
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Erster finiter Z-Wert einer DxfVertex-Liste (sonst 0). */
|
||||
function firstZ(vs: DxfVertex[] | undefined): number {
|
||||
for (const v of vs ?? []) if (Number.isFinite(v.z)) return v.z as number;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Knoten-Span-Index (The NURBS Book, A2.1): grösstes k mit U[k] ≤ t < U[k+1],
|
||||
* geklemmt auf [p, n]. `n` = letzter Kontrollpunkt-Index.
|
||||
*/
|
||||
function findSpan(n: number, p: number, t: number, U: number[]): number {
|
||||
if (t >= U[n + 1]) return n;
|
||||
if (t <= U[p]) return p;
|
||||
let low = p;
|
||||
let high = n + 1;
|
||||
let mid = Math.floor((low + high) / 2);
|
||||
while (t < U[mid] || t >= U[mid + 1]) {
|
||||
if (t < U[mid]) high = mid;
|
||||
else low = mid;
|
||||
mid = Math.floor((low + high) / 2);
|
||||
}
|
||||
return mid;
|
||||
}
|
||||
|
||||
/** Punkt einer (nicht-rationalen) B-Spline bei Parameter t (De Boor, A2.4). */
|
||||
function deBoor(cps: Vec2[], U: number[], p: number, t: number): Vec2 {
|
||||
const n = cps.length - 1;
|
||||
const span = findSpan(n, p, t, U);
|
||||
const d: Vec2[] = [];
|
||||
for (let j = 0; j <= p; j++) d[j] = { ...cps[span - p + j] };
|
||||
for (let r = 1; r <= p; r++) {
|
||||
for (let j = p; j >= r; j--) {
|
||||
const i = span - p + j;
|
||||
const denom = U[i + p - r + 1] - U[i];
|
||||
const a = denom === 0 ? 0 : (t - U[i]) / denom;
|
||||
d[j] = {
|
||||
x: (1 - a) * d[j - 1].x + a * d[j].x,
|
||||
y: (1 - a) * d[j - 1].y + a * d[j].y,
|
||||
};
|
||||
}
|
||||
}
|
||||
return d[p];
|
||||
}
|
||||
|
||||
/**
|
||||
* SPLINE → Linienzug. Primär echte B-Spline-Auswertung (Kontrollpunkte + Knoten
|
||||
* + Grad, wenn der Knotenvektor konsistent ist: |U| = |P| + Grad + 1). Sonst
|
||||
* Rückfall auf Stützpunkte (fitPoints) bzw. — grob — das Kontrollpolygon.
|
||||
* Rationale Gewichte werden ignoriert (selten; dxf-parser liefert sie nicht).
|
||||
*/
|
||||
function splineContour(e: DxfEntity): Contour | null {
|
||||
const cps = toVec2s(e.controlPoints);
|
||||
const fps = toVec2s(e.fitPoints);
|
||||
const knots = (e.knotValues ?? []).filter((k) => Number.isFinite(k));
|
||||
const degree = Number.isFinite(e.degreeOfSplineCurve)
|
||||
? (e.degreeOfSplineCurve as number)
|
||||
: 3;
|
||||
const closed = e.closed === true;
|
||||
const z = cps.length > 0 ? firstZ(e.controlPoints) : firstZ(e.fitPoints);
|
||||
|
||||
if (cps.length >= 2 && degree >= 1 && knots.length === cps.length + degree + 1) {
|
||||
// ~8 Abtastpunkte je Kontrollpunkt, gedeckelt.
|
||||
const samples = Math.max(16, Math.min(CURVE_MAX_SEG, cps.length * 8));
|
||||
const n = cps.length - 1;
|
||||
const t0 = knots[degree];
|
||||
const t1 = knots[n + 1];
|
||||
const pts: Vec2[] = [];
|
||||
for (let i = 0; i <= samples; i++) {
|
||||
const t = i === samples ? t1 : t0 + ((t1 - t0) * i) / samples;
|
||||
pts.push(deBoor(cps, knots, degree, t));
|
||||
}
|
||||
return { z, pts, closed, layer: e.layer };
|
||||
}
|
||||
if (fps.length >= 2) return { z, pts: fps, closed, layer: e.layer };
|
||||
if (cps.length >= 2) return { z, pts: cps, closed, layer: e.layer };
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── INSERT (Block-Referenz → transformierte Konturen) ─────────────────────────
|
||||
|
||||
/** Tolerantes Zahlenfeld: endliche Zahl oder Default. */
|
||||
function numOr(v: unknown, d: number): number {
|
||||
return typeof v === "number" && Number.isFinite(v) ? v : d;
|
||||
}
|
||||
|
||||
/** Tolerante Koordinate aus einem (unbekannt getippten) Punktobjekt. */
|
||||
function coord(v: unknown, key: "x" | "y"): number {
|
||||
if (v && typeof v === "object") {
|
||||
const n = (v as Record<string, unknown>)[key];
|
||||
if (typeof n === "number" && Number.isFinite(n)) return n;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/** Schutz gegen zyklische/tief verschachtelte Blockreferenzen. */
|
||||
const MAX_INSERT_DEPTH = 8;
|
||||
|
||||
/**
|
||||
* INSERT expandieren: Block-Entities rekursiv zu LOKALEN Konturen sammeln, dann
|
||||
* je Array-Zelle mit der 2D-Transform der Referenz nach Welt abbilden.
|
||||
* Welt(p, off) = Einfügepunkt + Rot(θ) · ( Scale(p − Basispunkt) + off ).
|
||||
* `off` = (Spalte·Spaltenabstand, Zeile·Zeilenabstand) im rotierten Blockraster.
|
||||
*/
|
||||
function expandInsert(
|
||||
e: DxfEntity,
|
||||
blocks: Record<string, DxfBlock>,
|
||||
out: Contour[],
|
||||
depth: number,
|
||||
): void {
|
||||
if (depth >= MAX_INSERT_DEPTH) return;
|
||||
const name = typeof e.name === "string" ? e.name : null;
|
||||
if (!name) return;
|
||||
const block = blocks[name];
|
||||
if (!block || !Array.isArray(block.entities)) return;
|
||||
|
||||
const sx = numOr(e.xScale, 1);
|
||||
const sy = numOr(e.yScale, 1);
|
||||
const rot = (numOr(e.rotation, 0) * Math.PI) / 180; // INSERT-Rotation in GRAD
|
||||
const cos = Math.cos(rot);
|
||||
const sin = Math.sin(rot);
|
||||
const ix = coord(e.position, "x"); // Einfügepunkt (tolerant gelesen)
|
||||
const iy = coord(e.position, "y");
|
||||
const bx = coord(block.position, "x"); // Block-Basispunkt
|
||||
const by = coord(block.position, "y");
|
||||
const nc = Math.max(1, Math.floor(numOr(e.columnCount, 1)));
|
||||
const nr = Math.max(1, Math.floor(numOr(e.rowCount, 1)));
|
||||
const dc = numOr(e.columnSpacing, 0);
|
||||
const dr = numOr(e.rowSpacing, 0);
|
||||
|
||||
// Block-Inhalt EINMAL lokal sammeln (rekursiv), dann je Zelle transformieren.
|
||||
const local: Contour[] = [];
|
||||
for (const be of block.entities) collectContours(be, blocks, local, depth + 1);
|
||||
if (local.length === 0) return;
|
||||
|
||||
for (let row = 0; row < nr; row++) {
|
||||
for (let col = 0; col < nc; col++) {
|
||||
const offx = col * dc;
|
||||
const offy = row * dr;
|
||||
for (const lc of local) {
|
||||
const pts = lc.pts.map((p) => {
|
||||
const vx = (p.x - bx) * sx + offx;
|
||||
const vy = (p.y - by) * sy + offy;
|
||||
return { x: ix + (vx * cos - vy * sin), y: iy + (vx * sin + vy * cos) };
|
||||
});
|
||||
out.push({ z: lc.z, pts, closed: lc.closed, layer: e.layer ?? lc.layer });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gemeinsamer Kontur-Dispatch für eine Entity (Top-Level UND Block-Inhalt).
|
||||
* Mesh-Entities werden hier NICHT behandelt (nur der Top-Level-Pfad erzeugt
|
||||
* Meshes; Block-interne Meshes sind im 2D-Import selten und bleiben aussen vor).
|
||||
*/
|
||||
function collectContours(
|
||||
e: DxfEntity,
|
||||
blocks: Record<string, DxfBlock>,
|
||||
out: Contour[],
|
||||
depth: number,
|
||||
): void {
|
||||
const type = (e.type ?? "").toUpperCase();
|
||||
switch (type) {
|
||||
case "LWPOLYLINE": {
|
||||
const c = polylineContour(e);
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "POLYLINE": {
|
||||
if (!isMeshPolyline(e)) {
|
||||
const c = polylineContour(e);
|
||||
if (c) out.push(c);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case "LINE": {
|
||||
const c = lineContour(e);
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "ARC": {
|
||||
const c = arcContour(e);
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "CIRCLE": {
|
||||
const c = circleContour(e);
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "ELLIPSE": {
|
||||
const c = ellipseContour(e);
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "SPLINE": {
|
||||
const c = splineContour(e);
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "INSERT":
|
||||
expandInsert(e, blocks, out, depth);
|
||||
break;
|
||||
default:
|
||||
// Unbekannte/irrelevante Entity → ignorieren (tolerant).
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user