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:
@@ -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 → 1–2 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,
|
||||
};
|
||||
}
|
||||
@@ -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?.();
|
||||
}
|
||||
@@ -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 → 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).
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
Vendored
+19
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user