// 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; /** 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; } 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)[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, 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, 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, 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): 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): 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): 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): 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(); 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; }