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).
This commit is contained in:
2026-07-02 00:12:39 +02:00
parent cfe5249440
commit 3d2d4d6321
184 changed files with 29421 additions and 669 deletions
+101
View File
@@ -0,0 +1,101 @@
// Compute-Grenze: der EINE Einstiegspunkt für rechenintensive Operationen.
// Läuft die App unter Tauri, werden Ops an einen Rust-`#[tauri::command]`
// geroutet; sonst greift die bestehende TS-Implementierung. In dieser PoC ist
// nur `computeJoins` real an Rust angebunden — die übrigen Funktionen sind
// dünne Pass-throughs auf ihre TS-Impls (Grenze etabliert, Migration
// inkrementell).
import type { Project, Wall } from "../model/types";
import { getWallType, wallTypeThickness } from "../model/types";
import { wallReferenceOffset } from "../model/wall";
import { computeJoins as computeJoinsTS } from "../model/joins";
import type { WallCuts } from "../model/joins";
import type { Line } from "../model/geometry";
import { detectRooms as detectRoomsTS } from "../geometry/roomBoundary";
import type { WallSegment, DetectRoomsOptions } from "../geometry/roomBoundary";
import { parseDwg as parseDwgTS } from "../io/dwgParser";
import type { DxfImportResult } from "../io/dxfParser";
import type { Vec2 } from "../model/types";
// Tauri-invoke, wenn vorhanden — sonst null (Browser/kein Tauri). Dynamischer,
// GEGUARDETER Import, damit der Build ohne @tauri-apps/api grün bleibt.
async function tauriInvoke<T>(cmd: string, args: Record<string, unknown>): Promise<T | null> {
try {
// @ts-ignore optionales Paket
const mod = await import(/* @vite-ignore */ "@tauri-apps/api/core").catch(() => null);
if (!mod || typeof (mod as any).invoke !== "function") return null;
return await (mod as any).invoke(cmd, args) as T;
} catch {
return null;
}
}
/** Serialisierbare Wand-Repräsentation für den Rust-Kern. */
interface FlatWall {
id: string;
start: Vec2;
end: Vec2;
thickness: number;
referenceOffset: number;
}
/** Rohantwort des Rust-Befehls `compute_joins`. */
interface RustWallCut {
wallId: string;
startCut: Line | null;
endCut: Line | null;
}
/**
* Gehrungs-Schnittlinien pro Wand. Unter Tauri via Rust-Kern, sonst TS-Fallback.
* Die Signatur ist synchron-kompatibel zur TS-Impl, aber async (Rust-Aufruf).
*/
export async function computeJoins(
project: Project,
walls: Wall[],
): Promise<Map<string, WallCuts>> {
try {
const flattened: FlatWall[] = walls.map((w) => {
const thickness = wallTypeThickness(getWallType(project, w));
return {
id: w.id,
start: w.start,
end: w.end,
thickness,
referenceOffset: wallReferenceOffset(w, thickness),
};
});
const res = await tauriInvoke<RustWallCut[]>("compute_joins", {
input: { walls: flattened },
});
if (res != null) {
const map = new Map<string, WallCuts>();
for (const c of res) {
map.set(c.wallId, { startCut: c.startCut, endCut: c.endCut });
}
return map;
}
} catch {
// fällt unten in den TS-Pfad
}
console.warn("Rust compute_joins nicht verfügbar, TS-Fallback");
return computeJoinsTS(project, walls);
}
// ── Pass-throughs (noch reines TS; bereit für spätere Rust-Migration) ─────────
/** Raumerkennung aus Wandsegmenten. Aktuell TS; Grenze für spätere Migration. */
export async function detectRooms(
walls: WallSegment[],
options: DetectRoomsOptions = {},
): Promise<Vec2[][]> {
return detectRoomsTS(walls, options);
}
/** DWG-Import (LibreDWG-WASM). Aktuell TS; Grenze für spätere Migration. */
export async function parseShapeFromDwg(
data: ArrayBuffer,
): Promise<DxfImportResult> {
return parseDwgTS(data);
}
+88
View File
@@ -0,0 +1,88 @@
// TS<->Rust-Paritaet fuer compute_joins: dieselben Sample-Waende, flach gemacht
// wie in der Compute-Boundary, einmal durch die TS-Implementierung und einmal
// durch das Rust-geometry-Crate (examples/parity) — die Gehrungs-Schnittlinien
// muessen numerisch identisch sein. Belegt, dass der Rust-Port die Semantik
// erhaelt (Kernbeweis der Migration).
// @ts-ignore -- node:child_process ohne globales @types/node; im Vitest-Node-Lauf vorhanden
import { execFileSync } from "node:child_process";
import { describe, expect, it } from "vitest";
import { sampleProject } from "../model/sampleProject";
import { computeJoins as computeJoinsTS } from "../model/joins";
import type { WallCuts } from "../model/joins";
import type { Line } from "../model/geometry";
import { getWallType, wallTypeThickness } from "../model/types";
import { wallReferenceOffset } from "../model/wall";
const EPS = 1e-9;
/** Flach wie in src/compute/index.ts (Boundary) — Eingang fuer das Rust-Crate. */
function flatten() {
return sampleProject.walls.map((w) => {
const thickness = wallTypeThickness(getWallType(sampleProject, w));
return {
id: w.id,
start: w.start,
end: w.end,
thickness,
referenceOffset: wallReferenceOffset(w, thickness),
};
});
}
/** Rust-Ausgabe ueber das geometry-Beispiel (liest stdin-JSON, druckt stdout-JSON). */
function runRust(input: unknown): Map<string, WallCuts> {
const out = execFileSync(
"cargo",
[
"run",
"-q",
"--manifest-path",
"src-tauri/geometry/Cargo.toml",
"--example",
"parity",
],
{ input: JSON.stringify(input), encoding: "utf8", timeout: 180000 },
);
const arr = JSON.parse(out) as Array<{
wallId: string;
startCut: Line | null;
endCut: Line | null;
}>;
const map = new Map<string, WallCuts>();
for (const c of arr) map.set(c.wallId, { startCut: c.startCut, endCut: c.endCut });
return map;
}
function lineClose(a: Line | null, b: Line | null): boolean {
if (a === null || b === null) return a === b;
return (
Math.abs(a.point.x - b.point.x) < EPS &&
Math.abs(a.point.y - b.point.y) < EPS &&
Math.abs(a.dir.x - b.dir.x) < EPS &&
Math.abs(a.dir.y - b.dir.y) < EPS
);
}
describe("compute_joins TS<->Rust Paritaet", () => {
it("liefert fuer alle Sample-Waende identische Gehrungslinien", () => {
const walls = sampleProject.walls;
const ts = computeJoinsTS(sampleProject, walls);
const rust = runRust({ walls: flatten() });
expect(rust.size).toBe(ts.size);
// Mindestens eine Wand muss eine echte Gehrung haben (sonst prueft der Test nichts).
let cutsSeen = 0;
for (const w of walls) {
const t = ts.get(w.id)!;
const r = rust.get(w.id);
expect(r, `Wand ${w.id} fehlt in Rust-Ausgabe`).toBeDefined();
expect(lineClose(t.startCut, r!.startCut), `startCut ${w.id}`).toBe(true);
expect(lineClose(t.endCut, r!.endCut), `endCut ${w.id}`).toBe(true);
if (t.startCut) cutsSeen++;
if (t.endCut) cutsSeen++;
}
expect(cutsSeen, "keine einzige Gehrung im Sample — Test waere aussagelos").toBeGreaterThan(0);
});
});