Browser-BIM (cad): semantisches Modell, abgeleitete 2D/3D-Sichten, Zeichenwerkzeuge

Standalone-Browser-Port von DOSSIER. Enthaelt das semantische Modell mit
Plan-/3D-Ableitung, Zeichen- und Editierwerkzeuge, Rhino-artiges Befehlssystem,
dockbares Panel-System, Resource-Manager, DXF/.lin/.pat-Import, i18n (de/en)
sowie Projektdokumentation und Probe-Harness.
This commit is contained in:
2026-06-30 20:52:27 +02:00
commit ca859c4aa4
157 changed files with 37921 additions and 0 deletions
+353
View File
@@ -0,0 +1,353 @@
// DWG-Import → Kontext-Geometrie (three-frei), echtes In-Browser-Parsing.
//
// DWG ist ein geschlossenes Binärformat; wir lesen es hier über LibreDWG-WASM
// (`@mlightcad/libredwg-web`) DIREKT im Browser — KEIN ODA-Zwischenschritt mehr.
// Ergebnis ist dasselbe `{meshes, contours}`-Format wie der DXF-Parser, damit der
// gesamte nachgelagerte Import-Fluss (Dialog, Ziel-Ebene, Layer-Handling,
// Mesh→Kontext) unverändert wiederverwendet wird.
//
// API (aus den .d.ts der Lib abgelesen, NICHT geraten):
// LibreDwg.create() → Promise<LibreDwgEx> (lädt WASM)
// dwg.dwg_read_data(buffer, Dwg_File_Type.DWG) → Dwg_Data-Pointer
// dwg.convert(ptr) → DwgDatabase (stark typisiert)
// dwg.dwg_free(ptr) → Speicher freigeben
// Der WASM-Build dieser Lib hat DXF-Schreiben/-Lesen DEAKTIVIERT (disable-dxf),
// daher gehen wir bewusst NICHT über DWG→DXF-Text, sondern mappen die geparste
// DwgDatabase direkt (ein Mapping, gespiegelt aus dxfParser).
//
// Unterstützte Entities (Modellraum):
// • 3DFACE → Dreiecks-Mesh (3 bzw. 4 Ecken → 12 Tri).
// • POLYLINE2D (Polyface, Flag 64) → Dreiecks-Mesh (Polyface-Faces).
// • LWPOLYLINE / POLYLINE2D / 3D → Kontur (z aus elevation/Vertex-Z).
// • LINE → Kontur (zwei-Punkt-Linienzug).
//
// Lazy: Das Paket wird per dynamischem import() geladen (kein Bloat im Initial-
// Bundle); WASM startet erst beim ersten DWG-Import.
//
// Bezeichner englisch, Kommentare deutsch (CLAUDE.md).
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
import type { DxfImportResult } from "./dxfParser";
// Lose getippte Sicht auf die libredwg-web-Entities (wir greifen tolerant auf die
// Felder zu, die wir brauchen; alle Punkte sind {x,y,z}).
interface DwgPoint {
x: number;
y: number;
z?: number;
}
// Polyface-/Polyline-Vertex: Punktkoordinaten + optionale Polyface-Indizes.
interface DwgVertexLike extends DwgPoint {
polyfaceIndex0?: number;
polyfaceIndex1?: number;
polyfaceIndex2?: number;
polyfaceIndex3?: number;
flag?: number;
}
interface DwgEntityLike {
type?: string;
layer?: string;
flag?: number;
elevation?: number;
startPoint?: DwgPoint;
endPoint?: DwgPoint;
corner1?: DwgPoint;
corner2?: DwgPoint;
corner3?: DwgPoint;
corner4?: DwgPoint;
vertices?: DwgVertexLike[];
[k: string]: unknown;
}
// Schmale Sicht auf die LibreDWG-Instanz (nur die Methoden, die wir nutzen).
interface LibreDwgApi {
dwg_read_data(data: ArrayBuffer, fileType: number): number | undefined;
convert(ptr: number): { entities?: unknown[] };
dwg_free(ptr: number): void;
}
// Singleton der WASM-Instanz (einmal laden, dann wiederverwenden).
let libredwgPromise: Promise<LibreDwgApi> | null = null;
/**
* Lädt + initialisiert LibreDWG-WASM (lazy, einmalig). Wir laden die Emscripten-
* Glue + die `.wasm`-URL explizit über Vite (`?url`), damit die WASM-Datei in
* Dev UND Build mit korrektem MIME-Type aufgelöst wird (sonst liefert der Dev-
* Server `index.html` statt der WASM → „Incorrect response MIME type").
*/
function createLibreDwg(): Promise<LibreDwgApi> {
if (!libredwgPromise) {
libredwgPromise = (async () => {
const [{ LibreDwg }, glue, wasmUrlMod] = await Promise.all([
import("@mlightcad/libredwg-web"),
// Glue + WASM über ein lokales virtuelles Modul (Alias in vite.config),
// da das Paket seine `wasm/`-Dateien nicht über `exports` freigibt.
import("virtual:libredwg-glue"),
// Vite gibt die WASM als gehashtes Asset aus und liefert ihre URL.
import("virtual:libredwg-wasm-url"),
]);
const createModule = glue.default;
const wasmUrl = wasmUrlMod.default;
const wasmInstance = await createModule({
// locateFile bekommt den Original-Dateinamen; wir liefern die von Vite
// aufgelöste (gehashte) URL zurück → korrektes Laden in Dev + Build.
locateFile: () => wasmUrl,
});
// createByWasmInstance erwartet das (intern getippte) MainModule; die
// Glue liefert es als unknown → bewusster Cast, Rückgabe schmal getippt.
return LibreDwg.createByWasmInstance(
wasmInstance as Parameters<typeof LibreDwg.createByWasmInstance>[0],
) as unknown as LibreDwgApi;
})();
}
return libredwgPromise;
}
let importSeq = 0;
const nextId = (prefix: string): string => `${prefix}-${Date.now()}-${importSeq++}`;
// POLYLINE-Flag 64 = Polyface-Mesh (siehe DwgPolylineFlag in der Lib).
const POLYFACE_FLAG = 64;
// POLYLINE-Flag 1 = geschlossen.
const CLOSED_FLAG = 1;
/**
* Parst eine DWG-Datei (als ArrayBuffer) in dasselbe Kontext-Geometrie-Format
* wie der DXF-Parser. Lädt LibreDWG-WASM lazy. Wirft bei fatalen Fehlern (der
* Aufrufer fängt das ab und zeigt den ODA-Fallback) — pro-Entity-Fehler nicht.
*/
export async function parseDwg(data: ArrayBuffer): Promise<DxfImportResult> {
const libredwg = await createLibreDwg();
let dwgPtr: number | undefined;
try {
// Dwg_File_Type.DWG === 0 (siehe enums.d.ts der Lib). Literal, damit wir den
// Enum-Wert nicht zusätzlich in den lazy Chunk ziehen.
const FILE_TYPE_DWG = 0;
dwgPtr = libredwg.dwg_read_data(data, FILE_TYPE_DWG);
if (dwgPtr == null) {
throw new Error("LibreDWG: dwg_read_data lieferte keinen Pointer.");
}
const db = libredwg.convert(dwgPtr);
const entities = (db?.entities ?? []) as unknown as DwgEntityLike[];
const meshTriangles: number[] = [];
const meshIndices: number[] = [];
const contours: Contour[] = [];
for (const e of entities) {
const type = (e.type ?? "").toUpperCase();
switch (type) {
case "3DFACE":
addFace(meshTriangles, meshIndices, e);
break;
case "POLYLINE2D":
case "POLYLINE3D":
if (isPolyfaceMesh(e)) {
addPolyfaceMesh(meshTriangles, meshIndices, e);
} else {
const ct = polylineContour(e);
if (ct) contours.push(ct);
}
break;
case "LWPOLYLINE": {
const ct = polylineContour(e);
if (ct) contours.push(ct);
break;
}
case "LINE": {
const ct = lineContour(e);
if (ct) contours.push(ct);
break;
}
default:
// Unbekannte/irrelevante Entity → ignorieren (tolerant).
break;
}
}
const meshes: ImportedMesh[] = [];
if (meshIndices.length > 0) {
meshes.push({
id: nextId("imported"),
type: "importedMesh",
name: "DWG-Mesh",
positions: meshTriangles,
indices: meshIndices,
});
}
const contourSets: ContourSet[] = [];
if (contours.length > 0) {
contourSets.push({
id: nextId("contours"),
type: "contourSet",
name: "DWG-Konturen",
contours,
});
}
return { meshes, contours: contourSets };
} finally {
// Speicher der WASM-Instanz freigeben (auch im Fehlerfall).
if (dwgPtr != null) {
try {
libredwg.dwg_free(dwgPtr);
} catch {
// Freigabe-Fehler ignorieren — nicht fatal.
}
}
}
}
// ── 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 (corner1..4). Bei 4 ≠ 3 Punkten zwei Dreiecke (Fan).
*/
function addFace(
positions: number[],
indices: number[],
e: DwgEntityLike,
): void {
const corners = [e.corner1, e.corner2, e.corner3, e.corner4].filter(
(c): c is DwgPoint => !!c && Number.isFinite(c.x) && Number.isFinite(c.y),
);
if (corners.length < 3) return;
const idx = corners.map((c) =>
pushVertex(positions, c.x, c.y, c.z ?? 0),
);
pushTri(indices, positions, idx[0], idx[1], idx[2]);
if (idx.length >= 4) pushTri(indices, positions, idx[0], idx[2], idx[3]);
}
/** Ob eine POLYLINE eine Polyface-Mesh-Variante ist (Flag 64 oder Polyface-Indizes). */
function isPolyfaceMesh(e: DwgEntityLike): boolean {
if (((e.flag ?? 0) & POLYFACE_FLAG) !== 0) return true;
return (e.vertices ?? []).some(
(v) =>
Number.isFinite(v.polyfaceIndex0) ||
Number.isFinite(v.polyfaceIndex1) ||
Number.isFinite(v.polyfaceIndex2) ||
Number.isFinite(v.polyfaceIndex3),
);
}
/**
* Polyface-Mesh (POLYLINE, Flag 64): Geometrie-Vertices (mit Koordinaten, keine
* Polyface-Indizes) und Face-Records (mit 1-basierten, ggf. negativen = unsicht-
* bare Kante Polyface-Indizes). Wir trennen beide und fan-triangulieren jede Face.
*/
function addPolyfaceMesh(
positions: number[],
indices: number[],
e: DwgEntityLike,
): void {
const all = e.vertices ?? [];
const hasFaceIdx = (v: DwgVertexLike): boolean =>
!!(
v.polyfaceIndex0 ||
v.polyfaceIndex1 ||
v.polyfaceIndex2 ||
v.polyfaceIndex3
);
const geom = all.filter((v) => !hasFaceIdx(v));
const faceRecs = all.filter((v) => hasFaceIdx(v));
if (geom.length < 3) return;
const base = positions.length / 3;
for (const v of geom) positions.push(v.x, v.y, v.z ?? 0);
for (const fr of faceRecs) {
// 1-basierte Indizes; Vorzeichen markiert (un)sichtbare Kanten → abs().
const ring = [
fr.polyfaceIndex0,
fr.polyfaceIndex1,
fr.polyfaceIndex2,
fr.polyfaceIndex3,
]
.map((i) => Math.abs(i ?? 0))
.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 (gleiche/ungültige
* Indizes). 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 / POLYLINE2D / POLYLINE3D → Kontur. Z-Höhe: erster gültiger
* Vertex-Z, sonst `elevation`, sonst 0. `closed` aus Flag 1. < 2 Punkte → null.
*/
function polylineContour(e: DwgEntityLike): 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, y: v.y });
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);
const closed = ((e.flag ?? 0) & CLOSED_FLAG) !== 0;
return { z, pts, closed, layer: e.layer };
}
/** LINE → zweipunktige (offene) Kontur. Z aus dem Startpunkt (sonst 0). */
function lineContour(e: DwgEntityLike): Contour | null {
const a = e.startPoint;
const b = e.endPoint;
if (
!a ||
!b ||
!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, y: a.y },
{ x: b.x, y: b.y },
],
closed: false,
layer: e.layer,
};
}
+22
View File
@@ -0,0 +1,22 @@
// Kleiner Modul-Hook, der das (in App montierte) versteckte DXF-Datei-Input
// mit dem `import`-Befehl entkoppelt verbindet. App registriert beim Mount eine
// Trigger-Funktion (öffnet den Datei-Dialog); der Befehl ruft sie auf, ohne die
// Command-Engine an React/DOM zu koppeln. So bleibt der Import-Befehl additiv.
//
// Bewusst KEIN window-Global: ein Modul-Singleton genügt (eine App-Instanz).
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
type DxfImportTrigger = () => void;
let trigger: DxfImportTrigger | null = null;
/** App registriert hier den Datei-Dialog-Öffner (oder hebt ihn beim Unmount auf). */
export function setDxfImportTrigger(fn: DxfImportTrigger | null): void {
trigger = fn;
}
/** Öffnet den DXF-Datei-Dialog, falls App ihn registriert hat. */
export function openDxfImport(): void {
trigger?.();
}
+295
View File
@@ -0,0 +1,295 @@
// 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 → 12 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).
import DxfParser from "dxf-parser";
import type { Contour, ContourSet, ImportedMesh, Vec2 } from "../model/types";
/** Ergebnis eines DXF-Imports: rohe Meshes + Konturen-Sätze. */
export interface DxfImportResult {
meshes: ImportedMesh[];
contours: ContourSet[];
}
// 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: 1-basierte, ggf. negative Vertex-Indizes (negativ = unsichtbare Kante).
faces?: number[];
}
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;
[k: string]: unknown;
}
interface DxfDocument {
entities?: DxfEntity[];
}
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();
// parseSync wirft bei strukturell kaputtem DXF; das soll nach oben.
const doc = parser.parseSync(text) as unknown as DxfDocument;
const entities = doc?.entities ?? [];
const meshTriangles: number[] = []; // gesammelte Mesh-Positions (x,y,z…)
const meshIndices: number[] = [];
const contours: Contour[] = [];
for (const e of entities) {
const type = (e.type ?? "").toUpperCase();
switch (type) {
case "3DFACE":
addFace(meshTriangles, meshIndices, e);
break;
case "MESH":
addMesh(meshTriangles, meshIndices, e);
break;
case "POLYLINE":
// POLYLINE ist mehrdeutig: Polyface/PolygonMesh → Mesh; sonst → Kontur.
if (isMeshPolyline(e)) {
addPolyfaceMesh(meshTriangles, meshIndices, e);
} else {
const ct = polylineContour(e);
if (ct) contours.push(ct);
}
break;
case "LWPOLYLINE": {
const ct = polylineContour(e);
if (ct) contours.push(ct);
break;
}
case "LINE": {
const ct = lineContour(e);
if (ct) contours.push(ct);
break;
}
default:
// Unbekannte/irrelevante Entity → ignorieren (tolerant).
break;
}
}
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 };
}
// ── 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],
);
}
}
}
/** 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 eine `faces`-Liste → Polyface-Mesh.
return (e.vertices ?? []).some(
(v) => Array.isArray(v.faces) && v.faces.length > 0,
);
}
/**
* Polyface-Mesh (POLYLINE, Flag 64): erst Geometrie-Vertices, dann Face-Records
* mit 1-basierten (ggf. negativen = unsichtbare Kante) Vertex-Indizes. 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) => !(Array.isArray(v.faces) && v.faces.length));
const faceRecs = all.filter((v) => Array.isArray(v.faces) && v.faces.length);
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 = (fr.faces ?? [])
.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,
};
}
+89
View File
@@ -0,0 +1,89 @@
// DXF-Konturen → semantische 2D-Zeichengeometrie (Drawing2D).
//
// Der DXF-Parser liefert „dumme" Konturen (Vec2 + Z + optionaler Layer-Name).
// Beim Import in eine ZEICHNUNGSEBENE werden daraus echte Drawing2D-Elemente
// (line/polyline). Die Z-Höhe der Kontur wird in 2D bewusst IGNORIERT (eine
// Zeichnungsebene ist flach). Das `closed`-Flag steuert line vs. polyline:
// • genau zwei Punkte, nicht geschlossen → `line`.
// • sonst → `polyline` (mit `closed`-Flag).
//
// Kategorie-Zuordnung (categoryMode):
// • "active" — alles bekommt die aktive Kategorie (activeCategoryCode).
// • "byLayer" — der DXF-Layer-Name wird auf einen Kategorie-Code gemappt.
// `layerToCode` liefert (deterministisch) einen Code je
// Layer-Name; unbekannte/leere Layer fallen auf `fallbackCode`.
//
// Bezeichner englisch, Kommentare deutsch (CLAUDE.md).
import type { Contour, ContourSet, Drawing2D, Drawing2DGeom } from "../model/types";
/** Wie DXF-Layer auf Kategorien abgebildet werden. */
export type CategoryMode = "active" | "byLayer";
let drawingSeq = 0;
const nextId = (): string => `dxf2d-${Date.now()}-${drawingSeq++}`;
/**
* Wandelt einen Satz Konturen-Sätze in Drawing2D-Elemente für die Ziel-Ebene
* `levelId` um. Bei `categoryMode === "byLayer"` liefert `layerToCode` den
* Kategorie-Code je DXF-Layer-Name; sonst bekommt alles `fallbackCode`.
*/
export function contoursToDrawings(
sets: ContourSet[],
levelId: string,
categoryMode: CategoryMode,
fallbackCode: string,
layerToCode: (layerName: string) => string,
): Drawing2D[] {
const out: Drawing2D[] = [];
for (const set of sets) {
for (const contour of set.contours) {
const geom = contourGeom(contour);
if (!geom) continue;
const layerName = contour.layer ?? set.layer ?? "";
const categoryCode =
categoryMode === "byLayer" && layerName
? layerToCode(layerName)
: fallbackCode;
out.push({
id: nextId(),
type: "drawing2d",
levelId,
categoryCode,
geom,
});
}
}
return out;
}
/** Eine Kontur → 2D-Geometrie (line bei 2 offenen Punkten, sonst polyline). */
function contourGeom(contour: Contour): Drawing2DGeom | null {
const pts = contour.pts;
if (pts.length < 2) return null;
if (pts.length === 2 && !contour.closed) {
return { shape: "line", a: { ...pts[0] }, b: { ...pts[1] } };
}
return {
shape: "polyline",
pts: pts.map((p) => ({ ...p })),
closed: contour.closed,
};
}
/** Liste der unterschiedlichen DXF-Layer-Namen über alle Konturen-Sätze. */
export function distinctLayers(sets: ContourSet[]): string[] {
const seen = new Set<string>();
for (const set of sets) {
for (const contour of set.contours) {
const name = contour.layer ?? set.layer ?? "";
if (name) seen.add(name);
}
}
return [...seen];
}
/** Gesamtzahl der Konturen über alle Sätze (für die Datei-Zusammenfassung). */
export function countContours(sets: ContourSet[]): number {
return sets.reduce((sum, s) => sum + s.contours.length, 0);
}
+19
View File
@@ -0,0 +1,19 @@
// Ambient-Deklarationen für die WASM-Glue-/Asset-Importe von
// @mlightcad/libredwg-web. Das Paket gibt seine `wasm/`-Dateien NICHT über
// `exports` frei, daher laden wir sie über zwei virtuelle Module (Aliase in
// vite.config.ts → echte Dateien im Paket). Hier nur schmale Modul-Stubs.
// (Identifier englisch.)
declare module "virtual:libredwg-glue" {
/** Emscripten-Glue: erzeugt die WASM-Instanz; `locateFile` löst die .wasm-URL. */
const createModule: (opts?: {
locateFile?: (filename: string, scriptDir: string) => string;
}) => Promise<unknown>;
export default createModule;
}
declare module "virtual:libredwg-wasm-url" {
/** Von Vite aufgelöste URL der WASM-Datei (gehashtes Asset im Build). */
const url: string;
export default url;
}
+88
View File
@@ -0,0 +1,88 @@
// Parser für AutoCAD .lin Linientyp-Definitionsdateien.
// Liefert benannte Strichmuster (dash patterns) zur späteren Übernahme in die LineStyle-Bibliothek.
export interface LinPattern {
name: string;
description: string;
dash: number[] | null;
}
// Punkt (0) wird als sehr kurzer Strich dargestellt, da SVG dasharray keine echten Punkte kennt.
const DOT_LENGTH = 0.01;
// Zerlegt eine Musterzeile in Tokens und entfernt eingeklammerte Shape-/Text-Segmente.
function extractNumericSegments(patternLine: string): number[] {
// Klammer-Segmente (z. B. ["TEXT",STYLE,...]) entfernen.
const withoutBrackets = patternLine.replace(/\[[^\]]*\]/g, '');
const tokens = withoutBrackets.split(',');
const numbers: number[] = [];
for (const raw of tokens) {
const token = raw.trim();
if (token === '') continue;
// Alignment-Token überspringen (üblicherweise 'A').
if (/^[A-Za-z]/.test(token)) continue;
const value = Number(token);
if (Number.isFinite(value)) {
numbers.push(value);
}
// Nicht-numerische Tokens werden ignoriert.
}
return numbers;
}
// Wandelt die Roh-Segmente in ein positives alternierendes dash/gap-Array um.
function toDashArray(segments: number[]): number[] | null {
if (segments.length === 0) return null;
const dash: number[] = [];
for (const seg of segments) {
if (seg === 0) {
dash.push(DOT_LENGTH);
} else {
dash.push(Math.abs(seg));
}
}
return dash.length > 0 ? dash : null;
}
export function parseLin(text: string): LinPattern[] {
const result: LinPattern[] = [];
// CRLF/CR vereinheitlichen.
const lines = text.replace(/\r\n?/g, '\n').split('\n');
for (let i = 0; i < lines.length; i++) {
const line = lines[i].trim();
if (line === '') continue;
if (line.startsWith(';')) continue;
if (!line.startsWith('*')) continue;
// Definitionszeile: *NAME,Description
const body = line.slice(1);
const commaIndex = body.indexOf(',');
const name = (commaIndex >= 0 ? body.slice(0, commaIndex) : body).trim();
const description = commaIndex >= 0 ? body.slice(commaIndex + 1).trim() : '';
if (name === '') continue;
// Folgende Musterzeile suchen (Kommentare/Leerzeilen überspringen).
let j = i + 1;
while (j < lines.length) {
const candidate = lines[j].trim();
if (candidate === '' || candidate.startsWith(';')) {
j++;
continue;
}
break;
}
// Keine folgende Zeile oder direkt nächste Definition -> Eintrag überspringen.
if (j >= lines.length || lines[j].trim().startsWith('*')) {
continue;
}
const patternLine = lines[j].trim();
const segments = extractNumericSegments(patternLine);
const dash = toDashArray(segments);
result.push({ name, description, dash });
i = j;
}
return result;
}
+75
View File
@@ -0,0 +1,75 @@
// Parser für AutoCAD .pat Schraffur-Musterdateien.
// Liefert strukturierte Linienfamilien zur späteren Übernahme in die Hatch-Bibliothek.
export interface PatLineFamily {
angle: number; // Grad
x: number;
y: number; // Ursprung
dx: number;
dy: number; // Verschiebung entlang / senkrecht
dashes: number[]; // ggf. leer (durchgezogene Familie)
}
export interface PatPattern {
name: string;
description: string;
families: PatLineFamily[];
}
// Zerlegt eine Familienzeile in numerische Tokens.
function parseNumbers(line: string): number[] {
const tokens = line.split(',');
const numbers: number[] = [];
for (const raw of tokens) {
const token = raw.trim();
if (token === '') continue;
const value = Number(token);
numbers.push(value);
}
return numbers;
}
export function parsePat(text: string): PatPattern[] {
const result: PatPattern[] = [];
// CRLF/CR vereinheitlichen.
const lines = text.replace(/\r\n?/g, '\n').split('\n');
let current: PatPattern | null = null;
for (const rawLine of lines) {
const line = rawLine.trim();
if (line === '') continue;
if (line.startsWith(';')) continue;
if (line.startsWith('*')) {
// Header: *PATTERNNAME, optional description
const body = line.slice(1);
const commaIndex = body.indexOf(',');
const name = (commaIndex >= 0 ? body.slice(0, commaIndex) : body).trim();
const description = commaIndex >= 0 ? body.slice(commaIndex + 1).trim() : '';
current = { name, description, families: [] };
result.push(current);
continue;
}
// Familienzeile nur gültig innerhalb eines Pattern-Blocks.
if (!current) continue;
const numbers = parseNumbers(line);
// Mindestens angle, x, y, dx, dy erforderlich.
if (numbers.length < 5) continue;
if (!numbers.slice(0, 5).every((n) => Number.isFinite(n))) continue;
const [angle, x, y, dx, dy] = numbers;
const dashes: number[] = [];
for (let k = 5; k < numbers.length; k++) {
if (Number.isFinite(numbers[k])) {
dashes.push(numbers[k]);
}
}
current.families.push({ angle, x, y, dx, dy, dashes });
}
return result;
}