DXF-HATCH: Ellipsen- + Spline-Randkanten tesselliert (B-Spline-Sampling extrahiert)
This commit is contained in:
@@ -353,6 +353,47 @@ describe("parseDxf — HATCH", () => {
|
||||
expect(last.y).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it("Ellipsenkante → tessellierter Halbumlauf mit korrekten Halbachsen", () => {
|
||||
// Center (0,0), Hauptachse (10,0), Verhältnis 0.5, 0°→180° ccw.
|
||||
const g = [
|
||||
"0", "HATCH", "8", "0", "2", "SOLID", "70", "1",
|
||||
"91", "1", "92", "1", "93", "1",
|
||||
"72", "3", "10", "0", "20", "0", "11", "10", "21", "0", "40", "0.5", "50", "0", "51", "180", "73", "1",
|
||||
"97", "0",
|
||||
];
|
||||
const c = onlyContour(dxf(g));
|
||||
expect(c.filled).toBe(true);
|
||||
// t=0 → Center + Hauptachse = (10,0).
|
||||
expect(c.pts[0].x).toBeCloseTo(10, 6);
|
||||
expect(c.pts[0].y).toBeCloseTo(0, 6);
|
||||
// Scheitel bei t=90° → Nebenachse (0,5).
|
||||
const maxY = Math.max(...c.pts.map((p) => p.y));
|
||||
expect(maxY).toBeCloseTo(5, 6);
|
||||
const last = c.pts[c.pts.length - 1];
|
||||
expect(last.x).toBeCloseTo(-10, 6);
|
||||
expect(last.y).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it("Spline-Kante (Grad 1) → Kontrollpolygon nachgezeichnet", () => {
|
||||
// 2 CPs (0,0)-(10,0), Grad 1, clamped-Knoten [0,0,1,1].
|
||||
const g = [
|
||||
"0", "HATCH", "8", "0", "2", "SOLID", "70", "1",
|
||||
"91", "1", "92", "1", "93", "1",
|
||||
"72", "4", "94", "1", "95", "4", "96", "2",
|
||||
"40", "0", "40", "0", "40", "1", "40", "1",
|
||||
"10", "0", "20", "0", "10", "10", "20", "0",
|
||||
"97", "0",
|
||||
];
|
||||
const c = onlyContour(dxf(g));
|
||||
expect(c.filled).toBe(true);
|
||||
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);
|
||||
for (const p of c.pts) expect(p.y).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
it("mehrere Randpfade → mehrere Loops (Insel als eigener Ring)", () => {
|
||||
// Zwei Polyline-Pfade in EINER HATCH: numPaths = 2.
|
||||
const g = [
|
||||
|
||||
+83
-23
@@ -23,7 +23,7 @@
|
||||
// • SPLINE → Kontur (B-Spline via De Boor; Fallback fitPoints).
|
||||
// • INSERT → Block-Konturen, transformiert (Scale/Rot/Array, verschachtelt).
|
||||
// • HATCH → gefüllte Kontur(en) je Randpfad (Custom-Handler,
|
||||
// Polyline- + Linien-/Bogen-Kanten; Fill via `Contour.filled`).
|
||||
// Polyline- + Linien-/Bogen-/Ellipsen-/Spline-Kanten; Fill via `Contour.filled`).
|
||||
|
||||
import DxfParser from "dxf-parser";
|
||||
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
|
||||
@@ -527,6 +527,27 @@ function deBoor(cps: Vec2[], U: number[], p: number, t: number): Vec2 {
|
||||
return d[p];
|
||||
}
|
||||
|
||||
/**
|
||||
* Nicht-rationale B-Spline über die Domäne [U[p], U[n+1]] abtasten (De Boor),
|
||||
* wenn Kontrollpunkte/Knoten/Grad konsistent sind (|U| = |P| + Grad + 1). Sonst
|
||||
* null. ~8 Abtastpunkte je Kontrollpunkt, gedeckelt.
|
||||
*/
|
||||
function sampleBSpline(cps: Vec2[], knots: number[], degree: number): Vec2[] | null {
|
||||
if (cps.length < 2 || degree < 1 || knots.length !== cps.length + degree + 1) {
|
||||
return null;
|
||||
}
|
||||
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 pts;
|
||||
}
|
||||
|
||||
/**
|
||||
* SPLINE → Linienzug. Primär echte B-Spline-Auswertung (Kontrollpunkte + Knoten
|
||||
* + Grad, wenn der Knotenvektor konsistent ist: |U| = |P| + Grad + 1). Sonst
|
||||
@@ -543,19 +564,8 @@ function splineContour(e: DxfEntity): Contour | null {
|
||||
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 };
|
||||
}
|
||||
const sampled = sampleBSpline(cps, knots, degree);
|
||||
if (sampled) return { z, pts: sampled, 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;
|
||||
@@ -760,7 +770,59 @@ function pushArcEdge(pts: Vec2[], m: Map<number, number[]>): void {
|
||||
}
|
||||
}
|
||||
|
||||
/** Punkte einer HATCH-Randkante an die Loop-Punkte anhängen (Typ 1 Linie, 2 Bogen). */
|
||||
/**
|
||||
* Ellipsenkante (Kantentyp 3): Zentrum (10/20), Hauptachsen-Endpunkt relativ
|
||||
* (11/21), Achsverhältnis (40), Start/End-PARAMETERwinkel in GRAD (50/51), ggf.
|
||||
* im Uhrzeigersinn (73). Punkt(t) = Center + cos t·Haupt + sin t·Neben.
|
||||
*/
|
||||
function pushEllipseEdge(pts: Vec2[], m: Map<number, number[]>): void {
|
||||
const cx = firstOf(m, 10);
|
||||
const cy = firstOf(m, 20);
|
||||
const mx = firstOf(m, 11);
|
||||
const my = firstOf(m, 21);
|
||||
const ratio = firstOf(m, 40);
|
||||
if (cx === undefined || cy === undefined || mx === undefined || my === undefined || ratio === undefined) {
|
||||
return;
|
||||
}
|
||||
const bx = -my * ratio; // Nebenachse = Linksnormale der Hauptachse · Verhältnis
|
||||
const by = mx * ratio;
|
||||
const s = (firstOf(m, 50) ?? 0) * (Math.PI / 180);
|
||||
const e = (firstOf(m, 51) ?? 360) * (Math.PI / 180);
|
||||
const ccw = (firstOf(m, 73) ?? 1) !== 0;
|
||||
let sweep = ccw ? e - s : -(e - s);
|
||||
sweep = ((sweep % (Math.PI * 2)) + Math.PI * 2) % (Math.PI * 2);
|
||||
if (sweep === 0) sweep = Math.PI * 2;
|
||||
const dir = ccw ? 1 : -1;
|
||||
const segs = segmentsFor(sweep);
|
||||
for (let i = 0; i <= segs; i++) {
|
||||
const t = s + (dir * (sweep * i)) / segs;
|
||||
const ct = Math.cos(t);
|
||||
const st = Math.sin(t);
|
||||
pts.push({ x: cx + mx * ct + bx * st, y: cy + my * ct + by * st });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Spline-Kante (Kantentyp 4): Grad (94), Knoten (40×), Kontrollpunkte (10/20×).
|
||||
* Über De Boor abgetastet (rationale Gewichte 42 ignoriert). Fallback: rohe
|
||||
* Kontrollpunkte als Polygonzug.
|
||||
*/
|
||||
function pushSplineEdge(pts: Vec2[], m: Map<number, number[]>): void {
|
||||
const xs = m.get(10) ?? [];
|
||||
const ys = m.get(20) ?? [];
|
||||
const cps: Vec2[] = [];
|
||||
for (let i = 0; i < Math.min(xs.length, ys.length); i++) cps.push({ x: xs[i], y: ys[i] });
|
||||
const knots = m.get(40) ?? [];
|
||||
const degree = firstOf(m, 94) ?? 3;
|
||||
const sampled = sampleBSpline(cps, knots, degree);
|
||||
if (sampled) {
|
||||
for (const p of sampled) pts.push(p);
|
||||
} else {
|
||||
for (const p of cps) pts.push(p);
|
||||
}
|
||||
}
|
||||
|
||||
/** Punkte einer HATCH-Randkante an die Loop-Punkte anhängen (Typ 1/2/3/4). */
|
||||
function appendEdge(pts: Vec2[], edgeType: number, m: Map<number, number[]>): void {
|
||||
if (edgeType === 1) {
|
||||
// Linie: Start (10/20) — der Endpunkt ist der Start der nächsten Kante.
|
||||
@@ -770,19 +832,17 @@ function appendEdge(pts: Vec2[], edgeType: number, m: Map<number, number[]>): vo
|
||||
// Bei der LETZTEN Kante fehlt sonst der Endpunkt; der closed-Ring schließt ihn.
|
||||
return;
|
||||
}
|
||||
if (edgeType === 2) {
|
||||
pushArcEdge(pts, m);
|
||||
return;
|
||||
}
|
||||
// Kantentyp 3 (Ellipse) / 4 (Spline): im ersten Wurf nicht unterstützt.
|
||||
if (edgeType === 2) pushArcEdge(pts, m);
|
||||
else if (edgeType === 3) pushEllipseEdge(pts, m);
|
||||
else if (edgeType === 4) pushSplineEdge(pts, m);
|
||||
}
|
||||
|
||||
/**
|
||||
* HATCH-Entity (rohe Gruppencodes) → gefüllte, geschlossene Konturen (ein Loop
|
||||
* je Randpfad). Unterstützt Polyline-Randpfade (Flag-Bit 2) sowie Kanten-Pfade
|
||||
* mit Linien- und Bogenkanten. Bulges an Polyline-Rändern und Ellipse-/Spline-
|
||||
* Kanten werden im ersten Wurf ignoriert (als Sehne bzw. übersprungen). Insel-
|
||||
* Loops entstehen als eigene geschlossene Ringe (keine echten Löcher).
|
||||
* mit Linien-, Bogen-, Ellipsen- und Spline-Kanten (alle tesselliert). Bulges an
|
||||
* Polyline-Rändern werden ignoriert (als Sehne); Insel-Loops entstehen als eigene
|
||||
* geschlossene Ringe (keine echten Löcher).
|
||||
*/
|
||||
function hatchContours(e: DxfEntity): Contour[] {
|
||||
const raw = e.rawCodes;
|
||||
|
||||
Reference in New Issue
Block a user