DXF-Import: HATCH via Custom-Handler -> gefuellte geschlossene Drawing2D-Flaeche
This commit is contained in:
+105
-1
@@ -3,7 +3,8 @@
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { parseDxf } from "./dxfParser";
|
||||
import type { Contour, Vec2 } from "../model/types";
|
||||
import { contoursToDrawings } from "./dxfToDrawings";
|
||||
import type { Contour, ContourSet, Vec2 } from "../model/types";
|
||||
|
||||
/** Minimales DXF aus Gruppencode/Wert-Paaren; nur eine ENTITIES-Sektion. */
|
||||
function dxf(...entities: string[][]): string {
|
||||
@@ -286,3 +287,106 @@ describe("parseDxf — INSERT", () => {
|
||||
expect(c.pts[1].y).toBeCloseTo(3, 9);
|
||||
});
|
||||
});
|
||||
|
||||
// ── HATCH (gefüllte Flächen) ──────────────────────────────────────────────────
|
||||
|
||||
/** HATCH mit Polyline-Randpfad (Flag 3 = external+polyline). */
|
||||
function hatchPoly(verts: Array<[number, number]>): string[] {
|
||||
const g = ["0", "HATCH", "8", "0", "2", "SOLID", "70", "1", "91", "1", "92", "3", "72", "0", "73", "1", "93", `${verts.length}`];
|
||||
for (const [x, y] of verts) g.push("10", `${x}`, "20", `${y}`);
|
||||
g.push("97", "0");
|
||||
return g;
|
||||
}
|
||||
|
||||
/** HATCH mit Kanten-Randpfad aus Linienkanten (Flag 1 = external, kein Polyline-Bit). */
|
||||
function hatchLineEdges(verts: Array<[number, number]>): string[] {
|
||||
const g = ["0", "HATCH", "8", "0", "2", "SOLID", "70", "1", "91", "1", "92", "1", "93", `${verts.length}`];
|
||||
for (let k = 0; k < verts.length; k++) {
|
||||
const a = verts[k];
|
||||
const b = verts[(k + 1) % verts.length];
|
||||
g.push("72", "1", "10", `${a[0]}`, "20", `${a[1]}`, "11", `${b[0]}`, "21", `${b[1]}`);
|
||||
}
|
||||
g.push("97", "0");
|
||||
return g;
|
||||
}
|
||||
|
||||
/** HATCH mit einer Bogenkante (Kantentyp 2), Winkel in Grad. */
|
||||
function hatchArc(cx: number, cy: number, r: number, s: number, e: number): string[] {
|
||||
return [
|
||||
"0", "HATCH", "8", "0", "2", "SOLID", "70", "1",
|
||||
"91", "1", "92", "1", "93", "1",
|
||||
"72", "2", "10", `${cx}`, "20", `${cy}`, "40", `${r}`, "50", `${s}`, "51", `${e}`, "73", "1",
|
||||
"97", "0",
|
||||
];
|
||||
}
|
||||
|
||||
describe("parseDxf — HATCH", () => {
|
||||
it("Polyline-Rand → geschlossene gefüllte Kontur", () => {
|
||||
const c = onlyContour(dxf(hatchPoly([[0, 0], [10, 0], [10, 10], [0, 10]])));
|
||||
expect(c.closed).toBe(true);
|
||||
expect(c.filled).toBe(true);
|
||||
expect(c.pts).toHaveLength(4);
|
||||
expect(c.pts[1].x).toBeCloseTo(10, 9);
|
||||
expect(c.pts[1].y).toBeCloseTo(0, 9);
|
||||
expect(c.pts[2].x).toBeCloseTo(10, 9);
|
||||
expect(c.pts[2].y).toBeCloseTo(10, 9);
|
||||
});
|
||||
|
||||
it("Linienkanten-Rand → geschlossene gefüllte Kontur (Eckpunkte)", () => {
|
||||
const c = onlyContour(dxf(hatchLineEdges([[0, 0], [4, 0], [4, 4], [0, 4]])));
|
||||
expect(c.closed).toBe(true);
|
||||
expect(c.filled).toBe(true);
|
||||
expect(c.pts).toHaveLength(4);
|
||||
const xs = c.pts.map((p) => p.x).sort((a, b) => a - b);
|
||||
expect(xs[0]).toBeCloseTo(0, 9);
|
||||
expect(xs[3]).toBeCloseTo(4, 9);
|
||||
});
|
||||
|
||||
it("Bogenkante → tessellierter Halbkreis auf Radius", () => {
|
||||
const c = onlyContour(dxf(hatchArc(0, 0, 5, 0, 180)));
|
||||
expect(c.filled).toBe(true);
|
||||
for (const p of c.pts) expect(Math.hypot(p.x, p.y)).toBeCloseTo(5, 6);
|
||||
expect(c.pts[0].x).toBeCloseTo(5, 6);
|
||||
expect(c.pts[0].y).toBeCloseTo(0, 6);
|
||||
const last = c.pts[c.pts.length - 1];
|
||||
expect(last.x).toBeCloseTo(-5, 6);
|
||||
expect(last.y).toBeCloseTo(0, 6);
|
||||
});
|
||||
|
||||
it("mehrere Randpfade → mehrere Loops (Insel als eigener Ring)", () => {
|
||||
// Zwei Polyline-Pfade in EINER HATCH: numPaths = 2.
|
||||
const g = [
|
||||
"0", "HATCH", "8", "0", "2", "SOLID", "70", "1", "91", "2",
|
||||
"92", "3", "72", "0", "73", "1", "93", "4",
|
||||
"10", "0", "20", "0", "10", "10", "20", "0", "10", "10", "20", "10", "10", "0", "20", "10",
|
||||
"97", "0",
|
||||
"92", "3", "72", "0", "73", "1", "93", "4",
|
||||
"10", "3", "20", "3", "10", "7", "20", "3", "10", "7", "20", "7", "10", "3", "20", "7",
|
||||
"97", "0",
|
||||
];
|
||||
const res = parseDxf(dxf(g));
|
||||
expect(res.contours[0].contours).toHaveLength(2);
|
||||
expect(res.contours[0].contours.every((c) => c.filled && c.closed)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("contoursToDrawings — HATCH-Füllung", () => {
|
||||
it("gefüllte Kontur → Drawing2D mit fillColor und polyline-Form", () => {
|
||||
const set: ContourSet = {
|
||||
id: "s", type: "contourSet", name: "h",
|
||||
contours: [{ z: 0, closed: true, filled: true, pts: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }] }],
|
||||
};
|
||||
const [d] = contoursToDrawings([set], "lvl", "active", "CODE", (s) => s);
|
||||
expect(d.geom.shape).toBe("polyline");
|
||||
expect(d.fillColor).toBeTruthy();
|
||||
});
|
||||
|
||||
it("ungefüllte Kontur → Drawing2D ohne fillColor", () => {
|
||||
const set: ContourSet = {
|
||||
id: "s", type: "contourSet", name: "h",
|
||||
contours: [{ z: 0, closed: true, pts: [{ x: 0, y: 0 }, { x: 1, y: 0 }, { x: 1, y: 1 }] }],
|
||||
};
|
||||
const [d] = contoursToDrawings([set], "lvl", "active", "CODE", (s) => s);
|
||||
expect(d.fillColor).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
// • ELLIPSE → Kontur (Ellipsenbogen/-umlauf, tesselliert).
|
||||
// • 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`).
|
||||
|
||||
import DxfParser from "dxf-parser";
|
||||
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
|
||||
@@ -93,8 +95,15 @@ interface DxfEntity {
|
||||
rowCount?: number;
|
||||
columnSpacing?: number;
|
||||
rowSpacing?: number;
|
||||
// HATCH: rohe Gruppencodes (vom Custom-Handler gesammelt, siehe HatchHandler).
|
||||
rawCodes?: RawCode[];
|
||||
[k: string]: unknown;
|
||||
}
|
||||
/** Ein rohes DXF-Gruppencode/Wert-Paar (HATCH-Randpfad-Auswertung). */
|
||||
interface RawCode {
|
||||
code: number;
|
||||
value: number | string;
|
||||
}
|
||||
/** Ein Block (BLOCKS-Tabelle): Basispunkt `position` + eigene Entity-Liste. */
|
||||
interface DxfBlock {
|
||||
name?: string;
|
||||
@@ -117,6 +126,12 @@ const nextId = (prefix: string) =>
|
||||
*/
|
||||
export function parseDxf(text: string): DxfImportResult {
|
||||
const parser = new DxfParser();
|
||||
// dxf-parser bringt keinen HATCH-Handler mit (verwirft HATCH sonst stumm).
|
||||
// Eigenen registrieren, der die rohen Gruppencodes einsammelt; die Auswertung
|
||||
// der Randpfade läuft separat und testbar in `hatchContours`.
|
||||
(parser as unknown as {
|
||||
registerEntityHandler: (h: new () => unknown) => void;
|
||||
}).registerEntityHandler(HatchHandler);
|
||||
// parseSync wirft bei strukturell kaputtem DXF; das soll nach oben.
|
||||
const doc = parser.parseSync(text) as unknown as DxfDocument;
|
||||
const entities = doc?.entities ?? [];
|
||||
@@ -668,6 +683,9 @@ function collectContours(
|
||||
if (c) out.push(c);
|
||||
break;
|
||||
}
|
||||
case "HATCH":
|
||||
for (const c of hatchContours(e)) out.push(c);
|
||||
break;
|
||||
case "INSERT":
|
||||
expandInsert(e, blocks, out, depth);
|
||||
break;
|
||||
@@ -676,3 +694,176 @@ function collectContours(
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ── HATCH (Custom-Handler + Randpfad-Auswertung → gefüllte Konturen) ──────────
|
||||
|
||||
/**
|
||||
* Custom dxf-parser-Handler für HATCH: sammelt die rohen Gruppencodes der Entity
|
||||
* (bis zum nächsten Code 0), OHNE zu interpretieren. Muster wie die eingebauten
|
||||
* Handler (Schleife bricht bei Code 0; `lastReadGroup` bleibt darauf stehen).
|
||||
* Die eigentliche Randpfad-Logik liegt testbar in `hatchContours`.
|
||||
*/
|
||||
class HatchHandler {
|
||||
ForEntityName = "HATCH";
|
||||
parseEntity(
|
||||
scanner: {
|
||||
next: () => { code: number; value: number | string };
|
||||
isEOF: () => boolean;
|
||||
},
|
||||
curr: { code: number; value: number | string },
|
||||
): DxfEntity {
|
||||
const raw: RawCode[] = [];
|
||||
const entity: DxfEntity = { type: String(curr.value), rawCodes: raw };
|
||||
curr = scanner.next();
|
||||
while (!scanner.isEOF()) {
|
||||
if (curr.code === 0) break;
|
||||
if (curr.code === 8) entity.layer = String(curr.value);
|
||||
raw.push({ code: curr.code, value: curr.value });
|
||||
curr = scanner.next();
|
||||
}
|
||||
return entity;
|
||||
}
|
||||
}
|
||||
|
||||
/** Zahlenwert eines Gruppencodes (tolerant). */
|
||||
function rc(v: number | string | undefined): number {
|
||||
if (typeof v === "number") return Number.isFinite(v) ? v : 0;
|
||||
const n = typeof v === "string" ? Number.parseFloat(v) : NaN;
|
||||
return Number.isFinite(n) ? n : 0;
|
||||
}
|
||||
|
||||
/** Erster Wert eines Codes aus der Kanten-Code-Sammlung. */
|
||||
function firstOf(m: Map<number, number[]>, code: number): number | undefined {
|
||||
const a = m.get(code);
|
||||
return a && a.length > 0 ? a[0] : undefined;
|
||||
}
|
||||
|
||||
/** Bogenkante (Kantentyp 2): Zentrum/Radius, Winkel in GRAD, ggf. im Uhrzeigersinn. */
|
||||
function pushArcEdge(pts: Vec2[], m: Map<number, number[]>): void {
|
||||
const cx = firstOf(m, 10);
|
||||
const cy = firstOf(m, 20);
|
||||
const r = firstOf(m, 40);
|
||||
if (cx === undefined || cy === undefined || r === undefined || r <= 0) return;
|
||||
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;
|
||||
// Spanne in Laufrichtung auf (0, 2π] normieren.
|
||||
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);
|
||||
// Startpunkt weglassen, wenn er den vorherigen Kanten-Endpunkt dupliziert.
|
||||
for (let i = 0; i <= segs; i++) {
|
||||
const t = s + dir * (sweep * i) / segs;
|
||||
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
||||
}
|
||||
}
|
||||
|
||||
/** Punkte einer HATCH-Randkante an die Loop-Punkte anhängen (Typ 1 Linie, 2 Bogen). */
|
||||
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.
|
||||
const x = firstOf(m, 10);
|
||||
const y = firstOf(m, 20);
|
||||
if (x !== undefined && y !== undefined) pts.push({ x, y });
|
||||
// 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.
|
||||
}
|
||||
|
||||
/**
|
||||
* 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).
|
||||
*/
|
||||
function hatchContours(e: DxfEntity): Contour[] {
|
||||
const raw = e.rawCodes;
|
||||
if (!raw || raw.length === 0) return [];
|
||||
const n = raw.length;
|
||||
const layer = e.layer;
|
||||
|
||||
// Zum ersten Code 91 (Anzahl Randpfade).
|
||||
let i = 0;
|
||||
while (i < n && raw[i].code !== 91) i++;
|
||||
if (i >= n) return [];
|
||||
const numPaths = rc(raw[i].value);
|
||||
i++;
|
||||
|
||||
const loops: Contour[] = [];
|
||||
for (let p = 0; p < numPaths && i < n; p++) {
|
||||
// Zum Pfadtyp-Flag (92) des nächsten Pfads.
|
||||
while (i < n && raw[i].code !== 92) i++;
|
||||
if (i >= n) break;
|
||||
const flag = rc(raw[i].value);
|
||||
i++;
|
||||
const isPolyline = (flag & 2) !== 0;
|
||||
const pts: Vec2[] = [];
|
||||
|
||||
if (isPolyline) {
|
||||
// Optionale 72/73 überspringen, bis 93 (Vertex-Anzahl).
|
||||
while (i < n && raw[i].code !== 93) i++;
|
||||
if (i >= n) break;
|
||||
const numVerts = rc(raw[i].value);
|
||||
i++;
|
||||
let cx: number | null = null;
|
||||
let got = 0;
|
||||
while (i < n && got < numVerts) {
|
||||
const c = raw[i];
|
||||
if (c.code === 91 || c.code === 92) break; // Sicherheitsnetz
|
||||
if (c.code === 10) {
|
||||
cx = rc(c.value);
|
||||
} else if (c.code === 20 && cx !== null) {
|
||||
pts.push({ x: cx, y: rc(c.value) });
|
||||
cx = null;
|
||||
got++;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
// Kanten-Pfad: 93 = Anzahl Kanten.
|
||||
while (i < n && raw[i].code !== 93) i++;
|
||||
if (i >= n) break;
|
||||
const numEdges = rc(raw[i].value);
|
||||
i++;
|
||||
for (let ed = 0; ed < numEdges && i < n; ed++) {
|
||||
// Zum Kantentyp (72), aber nicht über den Pfad/Kanten-Block hinaus.
|
||||
while (
|
||||
i < n &&
|
||||
raw[i].code !== 72 &&
|
||||
raw[i].code !== 97 &&
|
||||
raw[i].code !== 92 &&
|
||||
raw[i].code !== 91
|
||||
) {
|
||||
i++;
|
||||
}
|
||||
if (i >= n || raw[i].code !== 72) break;
|
||||
const edgeType = rc(raw[i].value);
|
||||
i++;
|
||||
// Kanten-Codes bis zur nächsten Kante/Blockgrenze sammeln.
|
||||
const m = new Map<number, number[]>();
|
||||
while (i < n) {
|
||||
const c = raw[i];
|
||||
if (c.code === 72 || c.code === 97 || c.code === 92 || c.code === 91) break;
|
||||
const arr = m.get(c.code) ?? [];
|
||||
arr.push(rc(c.value));
|
||||
m.set(c.code, arr);
|
||||
i++;
|
||||
}
|
||||
appendEdge(pts, edgeType, m);
|
||||
}
|
||||
}
|
||||
|
||||
if (pts.length >= 2) {
|
||||
loops.push({ z: 0, pts, closed: true, layer, filled: true });
|
||||
}
|
||||
}
|
||||
return loops;
|
||||
}
|
||||
|
||||
+11
-2
@@ -23,6 +23,9 @@ export type CategoryMode = "active" | "byLayer";
|
||||
let drawingSeq = 0;
|
||||
const nextId = (): string => `dxf2d-${Date.now()}-${drawingSeq++}`;
|
||||
|
||||
/** Default-Füllfarbe für aus HATCH importierte Flächen (neutral, restylebar). */
|
||||
const DXF_HATCH_FILL = "#c8c8c8";
|
||||
|
||||
/**
|
||||
* Wandelt einen Satz Konturen-Sätze in Drawing2D-Elemente für die Ziel-Ebene
|
||||
* `levelId` um. Bei `categoryMode === "byLayer"` liefert `layerToCode` den
|
||||
@@ -45,13 +48,19 @@ export function contoursToDrawings(
|
||||
categoryMode === "byLayer" && layerName
|
||||
? layerToCode(layerName)
|
||||
: fallbackCode;
|
||||
out.push({
|
||||
const drawing: Drawing2D = {
|
||||
id: nextId(),
|
||||
type: "drawing2d",
|
||||
levelId,
|
||||
categoryCode,
|
||||
geom,
|
||||
});
|
||||
};
|
||||
// Aus einer HATCH stammende Kontur → geschlossene, gefüllte Form. Vollton-
|
||||
// Füllung als restylebarer Default (neutrales Grau); Umriss bleibt Kategorie.
|
||||
if (contour.filled && geom.shape === "polyline") {
|
||||
drawing.fillColor = DXF_HATCH_FILL;
|
||||
}
|
||||
out.push(drawing);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
|
||||
Reference in New Issue
Block a user