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);
|
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).
|
// • ARC → Kontur (offener Bogen, tesselliert).
|
||||||
// • CIRCLE → Kontur (geschlossener Kreis, tesselliert).
|
// • CIRCLE → Kontur (geschlossener Kreis, tesselliert).
|
||||||
// • ELLIPSE → Kontur (Ellipsenbogen/-umlauf, 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 DxfParser from "dxf-parser";
|
||||||
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
|
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
|
||||||
@@ -74,10 +76,34 @@ interface DxfEntity {
|
|||||||
majorAxisEndPoint?: DxfVertex;
|
majorAxisEndPoint?: DxfVertex;
|
||||||
/** ELLIPSE: Verhältnis Neben-/Hauptachse (b/a). */
|
/** ELLIPSE: Verhältnis Neben-/Hauptachse (b/a). */
|
||||||
axisRatio?: number;
|
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;
|
[k: string]: unknown;
|
||||||
}
|
}
|
||||||
|
/** Ein Block (BLOCKS-Tabelle): Basispunkt `position` + eigene Entity-Liste. */
|
||||||
|
interface DxfBlock {
|
||||||
|
name?: string;
|
||||||
|
position?: DxfVertex;
|
||||||
|
entities?: DxfEntity[];
|
||||||
|
}
|
||||||
interface DxfDocument {
|
interface DxfDocument {
|
||||||
entities?: DxfEntity[];
|
entities?: DxfEntity[];
|
||||||
|
blocks?: Record<string, DxfBlock>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let importSeq = 0;
|
let importSeq = 0;
|
||||||
@@ -94,6 +120,7 @@ export function parseDxf(text: string): DxfImportResult {
|
|||||||
// parseSync wirft bei strukturell kaputtem DXF; das soll nach oben.
|
// parseSync wirft bei strukturell kaputtem DXF; das soll nach oben.
|
||||||
const doc = parser.parseSync(text) as unknown as DxfDocument;
|
const doc = parser.parseSync(text) as unknown as DxfDocument;
|
||||||
const entities = doc?.entities ?? [];
|
const entities = doc?.entities ?? [];
|
||||||
|
const blocks = doc?.blocks ?? {};
|
||||||
|
|
||||||
const meshTriangles: number[] = []; // gesammelte Mesh-Positions (x,y,z…)
|
const meshTriangles: number[] = []; // gesammelte Mesh-Positions (x,y,z…)
|
||||||
const meshIndices: number[] = [];
|
const meshIndices: number[] = [];
|
||||||
@@ -101,51 +128,22 @@ export function parseDxf(text: string): DxfImportResult {
|
|||||||
|
|
||||||
for (const e of entities) {
|
for (const e of entities) {
|
||||||
const type = (e.type ?? "").toUpperCase();
|
const type = (e.type ?? "").toUpperCase();
|
||||||
switch (type) {
|
// Mesh-Entities in die Dreiecks-Puffer; POLYLINE nur als Mesh-Variante.
|
||||||
case "3DFACE":
|
if (type === "3DFACE") {
|
||||||
addFace(meshTriangles, meshIndices, e);
|
addFace(meshTriangles, meshIndices, e);
|
||||||
break;
|
continue;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
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[] = [];
|
const meshes: ImportedMesh[] = [];
|
||||||
@@ -456,3 +454,225 @@ function ellipseContour(e: DxfEntity): Contour | null {
|
|||||||
}
|
}
|
||||||
return { z, pts, closed: full, layer: e.layer };
|
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