Files
DOSSIER-STANDALONE/src/export/dxfWriter.ts
T
karim 8fd8987b70 2D-Plan-Renderer auf WebGL2 (GPU) + akkumulierter Funktionsstand
Neuer GPU-Renderer fuer den Grundriss (src/plan/glPlan/): Earcut-Tessellierung
(konkav-faehig), gehrte Linienzuege (Miter), echte Papier-mm-Strichbreiten im
Massstab (repliziert den SVG-printStrokeVb-Pfad), Hybrid mit scharfem SVG-Text-
Overlay. GPU ist der Standardpfad; der SVG-Renderer bleibt automatischer Fallback,
falls WebGL2/Shader nicht verfuegbar sind. Imperativer Pan (rAF + CSS-transform)
fuer fluessige Interaktion ohne React-Re-Render je Frame.

Enthaelt zudem den bisher nicht committeten Arbeitsstand des Browser-BIM
(Oeffnungen, Treppen, Raeume, Decken, DXF-Export, Materialbibliothek, Kontext-
Import, Tauri-Compute-Boundary-PoC).
2026-07-02 00:12:39 +02:00

281 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Schlanker, handgeschriebener DXF-Writer (ASCII, AutoCAD R2000/AC1015-kompatibel).
// Erzeugt eine gültige Datei mit HEADER-, TABLES- (LAYER) und ENTITIES-Sektion und
// korrektem EOF. Geometrie wird in MODELL-SPACE in ECHTEN Metern geschrieben
// ($INSUNITS = 6 = Meter); nachgelagerte Nutzer skalieren beim Plot.
//
// Bewusst minimaler Funktionsumfang (LINE, LWPOLYLINE, CIRCLE, ARC, TEXT), aber mit
// korrekten Gruppen-Codes, damit AutoCAD/LibreCAD/andere Viewer die Datei sauber
// öffnen. Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
/** Ein Layer im DXF (Kategorie): Name, ACI-Farbindex, Linienstärke (mm × 100). */
export interface DxfLayer {
/** Layer-Name (keine Leerzeichen/Sonderzeichen → werden bereinigt). */
name: string;
/** AutoCAD Color Index (1..255). */
color: number;
/** True-Color (0xRRGGBB) für Viewer mit 24-Bit-Farbe; optional. */
trueColor?: number;
/** Linienstärke in 1/100 mm (DXF-LWEIGHT-Konvention), z. B. 25 = 0.25 mm. */
lineWeight: number;
}
/** 2D-Punkt (Meter, Modell-Space). */
export interface DxfPoint {
x: number;
y: number;
}
/**
* Sammelt Entities und Layer und serialisiert sie zu einem gültigen DXF-String.
* Alle Koordinaten sind Meter (Modell-Space). Der Y-Wert wird 1:1 übernommen
* (DXF-Y zeigt wie das Weltmodell nach oben — keine Spiegelung wie beim SVG).
*/
export class DxfWriter {
private layers = new Map<string, DxfLayer>();
private entities: string[] = [];
private handle = 0x100;
/** Nächstes eindeutiges Entity-Handle (Hex), fortlaufend. */
private nextHandle(): string {
return (this.handle++).toString(16).toUpperCase();
}
/** Registriert einen Layer (idempotent über den Namen). */
addLayer(layer: DxfLayer): void {
const name = sanitizeLayerName(layer.name);
if (!this.layers.has(name)) {
this.layers.set(name, { ...layer, name });
}
}
/** Stellt sicher, dass ein Layer existiert; liefert den bereinigten Namen. */
private ensureLayer(name: string): string {
const clean = sanitizeLayerName(name);
if (!this.layers.has(clean)) {
this.layers.set(clean, { name: clean, color: 7, lineWeight: 25 });
}
return clean;
}
addLine(layer: string, a: DxfPoint, b: DxfPoint): void {
const l = this.ensureLayer(layer);
this.entities.push(
tag(0, "LINE"),
tag(5, this.nextHandle()),
tag(100, "AcDbEntity"),
tag(8, l),
tag(100, "AcDbLine"),
num(10, a.x),
num(20, a.y),
num(30, 0),
num(11, b.x),
num(21, b.y),
num(31, 0),
);
}
/**
* LWPOLYLINE (leichte 2D-Polylinie). `closed` setzt das Ringflag (Bit 1 in
* Code 70), sodass die letzte Kante zurück zum ersten Punkt geschlossen wird.
*/
addPolyline(layer: string, pts: DxfPoint[], closed: boolean): void {
if (pts.length < 2) return;
const l = this.ensureLayer(layer);
this.entities.push(
tag(0, "LWPOLYLINE"),
tag(5, this.nextHandle()),
tag(100, "AcDbEntity"),
tag(8, l),
tag(100, "AcDbPolyline"),
tag(90, String(pts.length)),
tag(70, closed ? "1" : "0"),
);
for (const p of pts) {
this.entities.push(num(10, p.x), num(20, p.y));
}
}
addCircle(layer: string, center: DxfPoint, r: number): void {
const l = this.ensureLayer(layer);
this.entities.push(
tag(0, "CIRCLE"),
tag(5, this.nextHandle()),
tag(100, "AcDbEntity"),
tag(8, l),
tag(100, "AcDbCircle"),
num(10, center.x),
num(20, center.y),
num(30, 0),
num(40, r),
);
}
/**
* ARC — Bogen um `center` mit Radius `r`, von `startAngle` bis `endAngle`
* (Grad, gegen den Uhrzeigersinn, DXF-Konvention).
*/
addArc(
layer: string,
center: DxfPoint,
r: number,
startAngle: number,
endAngle: number,
): void {
const l = this.ensureLayer(layer);
this.entities.push(
tag(0, "ARC"),
tag(5, this.nextHandle()),
tag(100, "AcDbEntity"),
tag(8, l),
tag(100, "AcDbCircle"),
num(10, center.x),
num(20, center.y),
num(30, 0),
num(40, r),
tag(100, "AcDbArc"),
num(50, startAngle),
num(51, endAngle),
);
}
/** TEXT — einzeiliger Text, Höhe in Metern, an `at` (unten links). */
addText(layer: string, at: DxfPoint, height: number, value: string): void {
const l = this.ensureLayer(layer);
this.entities.push(
tag(0, "TEXT"),
tag(5, this.nextHandle()),
tag(100, "AcDbEntity"),
tag(8, l),
tag(100, "AcDbText"),
num(10, at.x),
num(20, at.y),
num(30, 0),
num(40, height),
tag(1, sanitizeText(value)),
tag(100, "AcDbText"),
);
}
/** Anzahl bislang gesammelter Entities (für Tests/Validierung). */
get entityCount(): number {
// Jede Entity beginnt mit einem "0"-Gruppencode + Typ-Zeile.
let n = 0;
for (let i = 0; i < this.entities.length; i += 2) {
if (this.entities[i] === " 0") n++;
}
return n;
}
/** Serialisiert die vollständige DXF-Datei als String (mit EOF). */
build(): string {
const out: string[] = [];
out.push(...this.headerSection());
out.push(...this.tablesSection());
out.push(...this.entitiesSection());
out.push(tag(0, "EOF"));
return out.join("\r\n") + "\r\n";
}
private headerSection(): string[] {
return [
tag(0, "SECTION"),
tag(2, "HEADER"),
tag(9, "$ACADVER"),
tag(1, "AC1015"), // AutoCAD 2000
tag(9, "$INSUNITS"),
tag(70, "6"), // 6 = Meter
tag(9, "$MEASUREMENT"),
tag(70, "1"), // 1 = metrisch
tag(0, "ENDSEC"),
];
}
private tablesSection(): string[] {
const out: string[] = [tag(0, "SECTION"), tag(2, "TABLES")];
// LTYPE-Tabelle mit dem obligatorischen Continuous-Linetype (Viewer erwarten ihn).
out.push(
tag(0, "TABLE"),
tag(2, "LTYPE"),
tag(70, "1"),
tag(0, "LTYPE"),
tag(2, "CONTINUOUS"),
tag(70, "0"),
tag(3, "Solid line"),
tag(72, "65"),
tag(73, "0"),
num(40, 0),
tag(0, "ENDTAB"),
);
// LAYER-Tabelle: Layer 0 (Pflicht) + alle Kategorie-Layer.
const layers = [...this.layers.values()];
out.push(tag(0, "TABLE"), tag(2, "LAYER"), tag(70, String(layers.length + 1)));
out.push(...this.layerRecord({ name: "0", color: 7, lineWeight: 25 }));
for (const layer of layers) out.push(...this.layerRecord(layer));
out.push(tag(0, "ENDTAB"));
out.push(tag(0, "ENDSEC"));
return out;
}
/** Ein LAYER-Datensatz (Name/Farbe/Linetype/Linienstärke, ggf. True-Color). */
private layerRecord(layer: DxfLayer): string[] {
const rec = [
tag(0, "LAYER"),
tag(5, this.nextHandle()),
tag(100, "AcDbSymbolTableRecord"),
tag(100, "AcDbLayerTableRecord"),
tag(2, layer.name),
tag(70, "0"), // Flags: nicht gefroren/gesperrt
tag(62, String(layer.color)), // ACI-Farbindex
tag(6, "CONTINUOUS"),
tag(370, String(layer.lineWeight)), // Linienstärke in 1/100 mm
];
// True-Color (Code 420, 24-Bit) für Viewer mit voller Farbtiefe.
if (layer.trueColor != null) {
rec.push(tag(420, String(layer.trueColor & 0xffffff)));
}
return rec;
}
private entitiesSection(): string[] {
return [tag(0, "SECTION"), tag(2, "ENTITIES"), ...this.entities, tag(0, "ENDSEC")];
}
}
/** Gruppen-Code-Zeile mit String-Wert (Code rechtsbündig auf 3 Stellen). */
function tag(code: number, value: string): string {
return `${String(code).padStart(3, " ")}\n${value}`;
}
/** Gruppen-Code-Zeile mit Zahl-Wert (feste, kompakte Dezimaldarstellung). */
function num(code: number, value: number): string {
return `${String(code).padStart(3, " ")}\n${fmt(value)}`;
}
/** Kompakte, verlustarme Zahlformatierung (bis 6 Nachkommastellen, ohne Nullen). */
function fmt(v: number): string {
if (!Number.isFinite(v)) return "0.0";
const s = v.toFixed(6);
// Nachlaufende Nullen entfernen, aber mindestens eine Nachkommastelle behalten.
return s.replace(/(\.\d*?)0+$/, "$1").replace(/\.$/, ".0");
}
/**
* Bereinigt einen Layer-Namen: DXF-verbotene Zeichen (< > / \ " : ; ? * | = `)
* und Leerzeichen werden zu „_". Leerer Name → "0".
*/
export function sanitizeLayerName(name: string): string {
const clean = name
.trim()
.replace(/[<>/\\":;?*|=`]/g, "_")
.replace(/\s+/g, "_");
return clean.length ? clean : "0";
}
/** Entfernt Zeilenumbrüche aus TEXT-Werten (einzeilige TEXT-Entity). */
function sanitizeText(value: string): string {
return value.replace(/[\r\n]+/g, " ");
}