9705890fd2
Root Cause für „Gebäude-Import funktioniert nicht": Die installierte dxf-parser-Version liefert Face-Vertex-Indizes NICHT als `faces`-Array, sondern als vier einzelne Felder (Gruppencodes 71–74: faceA/faceB/faceC/ faceD). Unser addPolyfaceMesh/isMeshPolyline prüfte auf ein `faces`-Array, das in dieser Version nie existiert — jede POLYLINE-Polyface-Mesh (exakt das Format der swissBUILDINGS3D-DXF-Kacheln) ergab dadurch 0 Dreiecke, obwohl Positionen geschrieben wurden. Live gegen echte swissBUILDINGS3D-Kacheln verifiziert: vorher 0 Meshes, jetzt tausende Dreiecke mit realistischen Schweizer Höhenwerten. +3 Tests (rohes DXF, nicht gemockt, deckt auch die zugrundeliegende Bibliothek ab), Suite 794 grün.
1009 lines
35 KiB
TypeScript
1009 lines
35 KiB
TypeScript
// DXF-Import → Kontext-Geometrie (three-frei).
|
||
//
|
||
// Importierte Geometrie ist „dumme" KONTEXT-Geometrie (Anzeige + späteres
|
||
// Snap-Ziel), NICHT semantisch. Dieser Parser liefert nur rohe Buffer-Daten
|
||
// (positions/indices) bzw. Konturen (Vec2 + Z), die in `Project.context` landen.
|
||
//
|
||
// ── DWG-Hinweis (bewusst KEIN DWG-Parser) ──────────────────────────────────
|
||
// DWG ist ein geschlossenes Binärformat ohne robusten Pure-JS-Parser; wir bauen
|
||
// hier KEINEN DWG-Parser. Der Import-Weg führt über DXF:
|
||
// DWG → DXF konvertieren (gratis „ODA File Converter" von der Open Design
|
||
// Alliance), dann die DXF-Datei hier importieren.
|
||
// LibreDWG-WASM ist nicht zuverlässig genug und wird bewusst nicht verfolgt.
|
||
//
|
||
// Unterstützte DXF-Entities:
|
||
// • 3DFACE → Dreiecks-Mesh (3 bzw. 4 Ecken → 1–2 Tri).
|
||
// • POLYLINE (Polyface/PolygonMesh)→ Dreiecks-Mesh (Faces bzw. Gitter).
|
||
// • MESH → Dreiecks-Mesh (Vertices + Face-Liste).
|
||
// • LWPOLYLINE / POLYLINE (2D/3D) → Kontur (z aus elevation/Vertex-Z).
|
||
// • LINE → Kontur (zwei-Punkt-Linienzug).
|
||
// • 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).
|
||
// • HATCH → gefüllte Kontur(en) je Randpfad (Custom-Handler,
|
||
// Polyline- + Linien-/Bogen-/Ellipsen-/Spline-Kanten; Fill via `Contour.filled`).
|
||
// • TEXT / MTEXT → ImportedText (Position/Höhe/Winkel) → Drawing2D `{shape:"text"}`.
|
||
|
||
import DxfParser from "dxf-parser";
|
||
import type { Contour, ContourSet, ImportedMesh, ImportedText, Vec2 } from "../model/types";
|
||
|
||
/**
|
||
* Optionale Import-Diagnose: Histogramm der gelesenen Entity-Typen (Typ → Anzahl)
|
||
* und die Gesamtzahl. Wird vor allem vom DWG-Parser gefüllt, damit beim Fall
|
||
* „keine Geometrie gefunden" sichtbar wird, WORAN es liegt (z. B. „0 Entities" =
|
||
* Encoding-/Versions-Problem vs. „500 ARC, 200 INSERT" = Abdeckungslücke).
|
||
*/
|
||
export interface ImportDiagnostics {
|
||
/** Anzahl je (Modellraum-)Entity-Typ, inkl. expandierter Block-Entities. */
|
||
entityCounts: Record<string, number>;
|
||
/** Gesamtzahl betrachteter Entities. */
|
||
total: number;
|
||
}
|
||
|
||
/** Ergebnis eines DXF-Imports: rohe Meshes + Konturen-Sätze + Texte. */
|
||
export interface DxfImportResult {
|
||
meshes: ImportedMesh[];
|
||
contours: ContourSet[];
|
||
/** Importierte TEXT/MTEXT-Elemente (optional; der DWG-Parser füllt sie nicht). */
|
||
texts?: ImportedText[];
|
||
/** Optionale Diagnose (Entity-Typ-Histogramm); vom DWG-Parser gefüllt. */
|
||
diagnostics?: ImportDiagnostics;
|
||
}
|
||
|
||
// Lose getippte Sicht auf die dxf-parser-Ausgabe (das Paket liefert keine engen
|
||
// Typen); wir greifen tolerant auf die Felder zu, die wir brauchen.
|
||
interface DxfVertex {
|
||
x?: number;
|
||
y?: number;
|
||
z?: number;
|
||
// Polyface-Mesh-Face-Record (VERTEX mit polyfaceMeshVertex-Flag): 1-basierte,
|
||
// ggf. negative Vertex-Indizes (negativ = unsichtbare Kante) — die installierte
|
||
// dxf-parser-Version liefert sie NICHT als `faces`-Array, sondern als vier
|
||
// EINZELNE Felder (Gruppencodes 71–74, ein Dreieck nutzt nur faceA–faceC,
|
||
// ein Viereck alle vier). `polyfaceMeshVertex` markiert einen Face- statt
|
||
// Geometrie-Vertex (Gruppencode-70-Bit 128, s. isMeshPolyline/addPolyfaceMesh).
|
||
faceA?: number;
|
||
faceB?: number;
|
||
faceC?: number;
|
||
faceD?: number;
|
||
polyfaceMeshVertex?: boolean;
|
||
}
|
||
interface DxfEntity {
|
||
type?: string;
|
||
layer?: string;
|
||
elevation?: number;
|
||
vertices?: DxfVertex[];
|
||
// MESH-Entity-Felder (variieren je nach Parser-Version):
|
||
position?: DxfVertex[];
|
||
faces?: number[][];
|
||
isPolyfaceMesh?: boolean;
|
||
is3dPolygonMesh?: boolean;
|
||
// Kurven-Entities (ARC/CIRCLE/ELLIPSE). Winkel liefert dxf-parser in RADIANT
|
||
// (ARC/CIRCLE Grad→rad umgerechnet; ELLIPSE-Parameterwinkel roh in Radiant).
|
||
center?: DxfVertex;
|
||
radius?: number;
|
||
startAngle?: number;
|
||
endAngle?: number;
|
||
/** ELLIPSE: Hauptachsen-Endpunkt RELATIV zum Center. */
|
||
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;
|
||
// HATCH: rohe Gruppencodes (vom Custom-Handler gesammelt, siehe HatchHandler).
|
||
rawCodes?: RawCode[];
|
||
// TEXT/MTEXT. `position` (MTEXT-Einfügepunkt) wird tolerant gelesen (oben als
|
||
// MESH-Vertexliste getippt). `rotation` (Grad) ist bereits oben deklariert.
|
||
startPoint?: DxfVertex; // TEXT-Einfügepunkt
|
||
textHeight?: number; // TEXT-Höhe (Code 40)
|
||
height?: number; // MTEXT-Höhe (Code 40)
|
||
text?: string; // Textinhalt (Code 1, MTEXT auch 3)
|
||
[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;
|
||
position?: DxfVertex;
|
||
entities?: DxfEntity[];
|
||
}
|
||
interface DxfDocument {
|
||
entities?: DxfEntity[];
|
||
blocks?: Record<string, DxfBlock>;
|
||
}
|
||
|
||
let importSeq = 0;
|
||
const nextId = (prefix: string) =>
|
||
`${prefix}-${Date.now()}-${importSeq++}`;
|
||
|
||
/**
|
||
* Parst DXF-Text in Kontext-Geometrie. Tolerant gegen fehlende Felder; nicht
|
||
* unterstützte/leere Entities werden übersprungen. Ein fataler Parse-Fehler wird
|
||
* geworfen (der Aufrufer kann ihn anzeigen) — pro-Entity-Fehler nicht.
|
||
*/
|
||
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 ?? [];
|
||
const blocks = doc?.blocks ?? {};
|
||
|
||
const meshTriangles: number[] = []; // gesammelte Mesh-Positions (x,y,z…)
|
||
const meshIndices: number[] = [];
|
||
const contours: Contour[] = [];
|
||
const texts: ImportedText[] = [];
|
||
|
||
for (const e of entities) {
|
||
const type = (e.type ?? "").toUpperCase();
|
||
// 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;
|
||
}
|
||
if (type === "TEXT" || type === "MTEXT") {
|
||
const it = textEntity(e, type);
|
||
if (it) texts.push(it);
|
||
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[] = [];
|
||
if (meshIndices.length > 0) {
|
||
meshes.push({
|
||
id: nextId("imported"),
|
||
type: "importedMesh",
|
||
name: "DXF-Mesh",
|
||
positions: meshTriangles,
|
||
indices: meshIndices,
|
||
});
|
||
}
|
||
|
||
const contourSets: ContourSet[] = [];
|
||
if (contours.length > 0) {
|
||
contourSets.push({
|
||
id: nextId("contours"),
|
||
type: "contourSet",
|
||
name: "DXF-Konturen",
|
||
contours,
|
||
});
|
||
}
|
||
|
||
return { meshes, contours: contourSets, texts };
|
||
}
|
||
|
||
// ── TEXT / MTEXT ─────────────────────────────────────────────────────────────
|
||
|
||
/** Fallback-Schrifthöhe (Meter), wenn die DXF-Höhe fehlt/0 ist. */
|
||
const DEFAULT_TEXT_HEIGHT = 0.25;
|
||
|
||
/**
|
||
* MTEXT-Inhalt von Formatierungscodes säubern (grober erster Wurf): \P → Leer-
|
||
* zeichen, \~ → Leerzeichen, `\f…;`/`\H…;`/`\C…;`/`\A…;`-Sequenzen und die {}-
|
||
* Gruppenklammern entfernen, `\{`/`\}`/`\\` entschärfen. Komplexe Verschachte-
|
||
* lungen werden flachgeklopft (dokumentierte Grenze).
|
||
*/
|
||
function sanitizeMText(s: string): string {
|
||
return s
|
||
.replace(/\\P/gi, " ")
|
||
.replace(/\\~/g, " ")
|
||
.replace(/\\[A-Za-z][^;\\]*;/g, "")
|
||
.replace(/[{}]/g, "")
|
||
.replace(/\\([{}\\])/g, "$1")
|
||
.trim();
|
||
}
|
||
|
||
/** TEXT/MTEXT-Entity → ImportedText (Position/Höhe/Winkel), oder null bei leer. */
|
||
function textEntity(e: DxfEntity, type: string): ImportedText | null {
|
||
const rawText = typeof e.text === "string" ? e.text : "";
|
||
const content = type === "MTEXT" ? sanitizeMText(rawText) : rawText;
|
||
if (!content) return null;
|
||
const at: Vec2 =
|
||
type === "MTEXT"
|
||
? { x: coord(e.position, "x"), y: coord(e.position, "y") }
|
||
: { x: e.startPoint?.x ?? 0, y: e.startPoint?.y ?? 0 };
|
||
const rawHeight = type === "MTEXT" ? numOr(e.height, 0) : numOr(e.textHeight, 0);
|
||
const height = rawHeight > 0 ? rawHeight : DEFAULT_TEXT_HEIGHT;
|
||
const angle = (numOr(e.rotation, 0) * Math.PI) / 180; // DXF: Grad CCW → Radiant
|
||
return { at, text: content, height, angle, layer: e.layer };
|
||
}
|
||
|
||
// ── Mesh-Entities ────────────────────────────────────────────────────────────
|
||
|
||
/** Fügt einen Vertex zu positions hinzu und liefert seinen Index. */
|
||
function pushVertex(
|
||
positions: number[],
|
||
x: number,
|
||
y: number,
|
||
z: number,
|
||
): number {
|
||
const idx = positions.length / 3;
|
||
positions.push(x, y, z);
|
||
return idx;
|
||
}
|
||
|
||
/**
|
||
* 3DFACE: 3 oder 4 Eckpunkte. Bei 4 Punkten zwei Dreiecke (Fan). Entartete
|
||
* (degenerierte) Punkte (4. == 3.) ergeben nur ein Dreieck.
|
||
*/
|
||
function addFace(positions: number[], indices: number[], e: DxfEntity): void {
|
||
const vs = (e.vertices ?? []).filter(
|
||
(v) => Number.isFinite(v.x) && Number.isFinite(v.y),
|
||
);
|
||
if (vs.length < 3) return;
|
||
const idx = vs.map((v) =>
|
||
pushVertex(positions, v.x ?? 0, v.y ?? 0, v.z ?? 0),
|
||
);
|
||
// Dreieck (0,1,2).
|
||
pushTri(indices, positions, idx[0], idx[1], idx[2]);
|
||
// Viereck → zweites Dreieck (0,2,3), falls 4. Punkt vorhanden und ≠ 3.
|
||
if (idx.length >= 4) pushTri(indices, positions, idx[0], idx[2], idx[3]);
|
||
}
|
||
|
||
/**
|
||
* MESH-Entity: explizite Vertex-Liste + Face-Liste. Die Face-Liste kann Drei-
|
||
* oder Vierecke enthalten (jeweils 0-basierte Vertex-Indizes); Vierecke werden
|
||
* fan-trianguliert.
|
||
*/
|
||
function addMesh(positions: number[], indices: number[], e: DxfEntity): void {
|
||
const verts = e.position ?? e.vertices ?? [];
|
||
if (verts.length === 0) return;
|
||
const base = positions.length / 3;
|
||
for (const v of verts) {
|
||
positions.push(v.x ?? 0, v.y ?? 0, v.z ?? 0);
|
||
}
|
||
const faces = Array.isArray(e.faces) ? e.faces : [];
|
||
for (const face of faces) {
|
||
if (!Array.isArray(face) || face.length < 3) continue;
|
||
for (let i = 1; i + 1 < face.length; i++) {
|
||
pushTri(
|
||
indices,
|
||
positions,
|
||
base + face[0],
|
||
base + face[i],
|
||
base + face[i + 1],
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Rohe Face-Vertex-Indizes eines VERTEX-Records (faceA..faceD, nur gesetzte,
|
||
* von Null verschiedene Felder — ein Dreieck belegt nur faceA–faceC). */
|
||
function faceIndicesOf(v: DxfVertex): number[] {
|
||
return [v.faceA, v.faceB, v.faceC, v.faceD].filter(
|
||
(i): i is number => typeof i === "number" && i !== 0,
|
||
);
|
||
}
|
||
|
||
/** Ob eine POLYLINE eine Mesh-Variante ist (Polyface- oder Polygon-Gitter). */
|
||
function isMeshPolyline(e: DxfEntity): boolean {
|
||
if (e.isPolyfaceMesh === true || e.is3dPolygonMesh === true) return true;
|
||
// Heuristik: enthält irgendein Vertex Face-Indizes → Polyface-Mesh.
|
||
return (e.vertices ?? []).some((v) => faceIndicesOf(v).length > 0);
|
||
}
|
||
|
||
/**
|
||
* Polyface-Mesh (POLYLINE, Flag 64): erst Geometrie-Vertices, dann Face-Records
|
||
* (VERTEX mit `polyfaceMeshVertex`-Flag, s. {@link DxfVertex}) mit 1-basierten
|
||
* (ggf. negativen = unsichtbare Kante) Vertex-Indizes in faceA..faceD. Wir
|
||
* trennen Geometrie- von Face-Vertices und fan-triangulieren jede Face.
|
||
*/
|
||
function addPolyfaceMesh(
|
||
positions: number[],
|
||
indices: number[],
|
||
e: DxfEntity,
|
||
): void {
|
||
const all = e.vertices ?? [];
|
||
const geom = all.filter((v) => faceIndicesOf(v).length === 0);
|
||
const faceRecs = all.filter((v) => faceIndicesOf(v).length > 0);
|
||
if (geom.length < 3) return;
|
||
const base = positions.length / 3;
|
||
for (const v of geom) {
|
||
positions.push(v.x ?? 0, v.y ?? 0, v.z ?? 0);
|
||
}
|
||
for (const fr of faceRecs) {
|
||
// 1-basierte Indizes; Vorzeichen markiert (un)sichtbare Kanten → abs().
|
||
const ring = faceIndicesOf(fr)
|
||
.map((i) => Math.abs(i))
|
||
.filter((i) => i >= 1)
|
||
.map((i) => base + (i - 1));
|
||
if (ring.length < 3) continue;
|
||
for (let i = 1; i + 1 < ring.length; i++) {
|
||
pushTri(indices, positions, ring[0], ring[i], ring[i + 1]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Fügt ein Dreieck hinzu, sofern es nicht entartet ist (zwei gleiche Indizes
|
||
* oder kollineare/Null-Flächen-Ecken). Hält das importierte Mesh sauber.
|
||
*/
|
||
function pushTri(
|
||
indices: number[],
|
||
positions: number[],
|
||
a: number,
|
||
b: number,
|
||
c: number,
|
||
): void {
|
||
if (a === b || b === c || a === c) return;
|
||
const n = positions.length / 3;
|
||
if (a < 0 || b < 0 || c < 0 || a >= n || b >= n || c >= n) return;
|
||
indices.push(a, b, c);
|
||
}
|
||
|
||
// ── Kontur-Entities ──────────────────────────────────────────────────────────
|
||
|
||
/**
|
||
* LWPOLYLINE / 2D-/3D-POLYLINE → Kontur. Z-Höhe: bei 3D-Polylinie aus den
|
||
* Vertex-Z (erster gültiger), sonst aus `elevation`, sonst 0. Die 2D-Punkte sind
|
||
* (x,y) jedes Vertex. Bei < 2 Punkten wird nichts erzeugt.
|
||
*/
|
||
function polylineContour(e: DxfEntity): Contour | null {
|
||
const vs = e.vertices ?? [];
|
||
const pts: Vec2[] = [];
|
||
let zFromVertex: number | undefined;
|
||
for (const v of vs) {
|
||
if (!Number.isFinite(v.x) || !Number.isFinite(v.y)) continue;
|
||
pts.push({ x: v.x ?? 0, y: v.y ?? 0 });
|
||
if (zFromVertex === undefined && Number.isFinite(v.z)) zFromVertex = v.z;
|
||
}
|
||
if (pts.length < 2) return null;
|
||
const z = zFromVertex ?? (Number.isFinite(e.elevation) ? (e.elevation as number) : 0);
|
||
// closed-Flag aus dem Parser ableiten (LWPOLYLINE: `shape`, POLYLINE: `shape`).
|
||
const closed = e.shape === true || e.closed === true;
|
||
return { z, pts, closed, layer: e.layer };
|
||
}
|
||
|
||
/** LINE → zweipunktige (offene) Kontur. Z aus dem Start-Vertex (sonst 0). */
|
||
function lineContour(e: DxfEntity): Contour | null {
|
||
const vs = e.vertices ?? [];
|
||
if (vs.length < 2) return null;
|
||
const a = vs[0];
|
||
const b = vs[1];
|
||
if (
|
||
!Number.isFinite(a.x) ||
|
||
!Number.isFinite(a.y) ||
|
||
!Number.isFinite(b.x) ||
|
||
!Number.isFinite(b.y)
|
||
) {
|
||
return null;
|
||
}
|
||
const z = Number.isFinite(a.z) ? (a.z as number) : 0;
|
||
return {
|
||
z,
|
||
pts: [
|
||
{ x: a.x ?? 0, y: a.y ?? 0 },
|
||
{ x: b.x ?? 0, y: b.y ?? 0 },
|
||
],
|
||
closed: false,
|
||
layer: e.layer,
|
||
};
|
||
}
|
||
|
||
// ── Kurven-Entities (ARC/CIRCLE/ELLIPSE) → tessellierte Konturen ──────────────
|
||
|
||
/** Winkelauflösung der Tessellierung (~5.6° pro Segment). */
|
||
const CURVE_STEP = Math.PI / 32;
|
||
/** Obergrenze der Segmentzahl (Schutz gegen entartete Eingaben). */
|
||
const CURVE_MAX_SEG = 256;
|
||
|
||
/** Segmentzahl für eine Winkelspanne (Radiant): mind. 2, gedeckelt. */
|
||
function segmentsFor(sweep: number): number {
|
||
return Math.max(2, Math.min(CURVE_MAX_SEG, Math.ceil(Math.abs(sweep) / CURVE_STEP)));
|
||
}
|
||
|
||
/**
|
||
* ARC → offener Bogen-Linienzug. `startAngle`/`endAngle` in Radiant (dxf-parser
|
||
* rechnet Grad→rad). Bögen laufen CCW; eine nicht-positive Spanne wird um 2π
|
||
* ergänzt (voller-Kreis-Fall bleibt 2π).
|
||
*/
|
||
function arcContour(e: DxfEntity): Contour | null {
|
||
const c = e.center;
|
||
const r = e.radius;
|
||
if (
|
||
!c ||
|
||
!Number.isFinite(c.x) ||
|
||
!Number.isFinite(c.y) ||
|
||
typeof r !== "number" ||
|
||
!Number.isFinite(r) ||
|
||
r <= 0
|
||
) {
|
||
return null;
|
||
}
|
||
const start = Number.isFinite(e.startAngle) ? (e.startAngle as number) : 0;
|
||
const end = Number.isFinite(e.endAngle) ? (e.endAngle as number) : Math.PI * 2;
|
||
let sweep = end - start;
|
||
if (sweep <= 0) sweep += Math.PI * 2;
|
||
const cx = c.x ?? 0;
|
||
const cy = c.y ?? 0;
|
||
const z = Number.isFinite(c.z) ? (c.z as number) : 0;
|
||
const segs = segmentsFor(sweep);
|
||
const pts: Vec2[] = [];
|
||
for (let i = 0; i <= segs; i++) {
|
||
const t = start + (sweep * i) / segs;
|
||
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
||
}
|
||
return {
|
||
z,
|
||
pts,
|
||
closed: false,
|
||
layer: e.layer,
|
||
curve: { kind: "arc", cx, cy, r, a0: start, a1: start + sweep },
|
||
};
|
||
}
|
||
|
||
/** CIRCLE → geschlossener Kreis-Linienzug (voller Umlauf, letzter Punkt weggelassen). */
|
||
function circleContour(e: DxfEntity): Contour | null {
|
||
const c = e.center;
|
||
const r = e.radius;
|
||
if (
|
||
!c ||
|
||
!Number.isFinite(c.x) ||
|
||
!Number.isFinite(c.y) ||
|
||
typeof r !== "number" ||
|
||
!Number.isFinite(r) ||
|
||
r <= 0
|
||
) {
|
||
return null;
|
||
}
|
||
const cx = c.x ?? 0;
|
||
const cy = c.y ?? 0;
|
||
const z = Number.isFinite(c.z) ? (c.z as number) : 0;
|
||
const segs = segmentsFor(Math.PI * 2);
|
||
const pts: Vec2[] = [];
|
||
// 0..2π ohne Schluss-Duplikat (closed schließt den Ring).
|
||
for (let i = 0; i < segs; i++) {
|
||
const t = (Math.PI * 2 * i) / segs;
|
||
pts.push({ x: cx + r * Math.cos(t), y: cy + r * Math.sin(t) });
|
||
}
|
||
return {
|
||
z,
|
||
pts,
|
||
closed: true,
|
||
layer: e.layer,
|
||
curve: { kind: "circle", cx, cy, r, a0: 0, a1: Math.PI * 2 },
|
||
};
|
||
}
|
||
|
||
/**
|
||
* ELLIPSE → Linienzug. Hauptachse = `majorAxisEndPoint` (Vektor relativ zum
|
||
* Center), Nebenachse = ⟂ dazu · `axisRatio`. `startAngle`/`endAngle` sind
|
||
* PARAMETERwinkel in Radiant; ein voller Umlauf (Spanne ≈ 2π) wird geschlossen.
|
||
* Punkt(t) = Center + cos t · Haupt + sin t · Neben.
|
||
*/
|
||
function ellipseContour(e: DxfEntity): Contour | null {
|
||
const c = e.center;
|
||
const maj = e.majorAxisEndPoint;
|
||
const ratio = e.axisRatio;
|
||
if (
|
||
!c ||
|
||
!maj ||
|
||
!Number.isFinite(c.x) ||
|
||
!Number.isFinite(c.y) ||
|
||
!Number.isFinite(maj.x) ||
|
||
!Number.isFinite(maj.y) ||
|
||
typeof ratio !== "number" ||
|
||
!Number.isFinite(ratio)
|
||
) {
|
||
return null;
|
||
}
|
||
const cx = c.x ?? 0;
|
||
const cy = c.y ?? 0;
|
||
const ax = maj.x ?? 0; // Hauptachsen-Vektor (relativ Center)
|
||
const ay = maj.y ?? 0;
|
||
const bx = -ay * ratio; // Nebenachse = Linksnormale der Hauptachse · Verhältnis
|
||
const by = ax * ratio;
|
||
const start = Number.isFinite(e.startAngle) ? (e.startAngle as number) : 0;
|
||
const end = Number.isFinite(e.endAngle) ? (e.endAngle as number) : Math.PI * 2;
|
||
let sweep = end - start;
|
||
if (sweep <= 0) sweep += Math.PI * 2;
|
||
const full = Math.abs(sweep - Math.PI * 2) < 1e-9;
|
||
const z = Number.isFinite(c.z) ? (c.z as number) : 0;
|
||
const segs = segmentsFor(sweep);
|
||
const pts: Vec2[] = [];
|
||
// Bei vollem Umlauf Schluss-Duplikat weglassen (closed schließt den Ring).
|
||
const last = full ? segs - 1 : segs;
|
||
for (let i = 0; i <= last; i++) {
|
||
const t = start + (sweep * i) / segs;
|
||
const ct = Math.cos(t);
|
||
const st = Math.sin(t);
|
||
pts.push({ x: cx + ax * ct + bx * st, y: cy + ay * ct + by * st });
|
||
}
|
||
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];
|
||
}
|
||
|
||
/**
|
||
* 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
|
||
* 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);
|
||
|
||
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;
|
||
}
|
||
|
||
// ── 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 "HATCH":
|
||
for (const c of hatchContours(e)) out.push(c);
|
||
break;
|
||
case "INSERT":
|
||
expandInsert(e, blocks, out, depth);
|
||
break;
|
||
default:
|
||
// Unbekannte/irrelevante Entity → ignorieren (tolerant).
|
||
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) });
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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.
|
||
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);
|
||
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-, 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;
|
||
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;
|
||
}
|