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 7b3b597abc
commit 8fd8987b70
184 changed files with 29421 additions and 669 deletions
+65 -21
View File
@@ -29,11 +29,21 @@ import type {
* Brücke zur App: liefert den Live-Kontext + Snapping und wendet die Ergebnisse
* an (Vorschau zeigen, committen). So bleibt die Engine frei von React/Store.
*/
/** Modifikatoren (Shift=Ortho, Ctrl=Snap aus) beim Snap eines Befehlsschritts. */
export interface EngineMods {
shift: boolean;
ctrl: boolean;
}
export interface EngineHost {
/** Aktueller Befehls-Kontext (Projekt, aktive Ebene/Kategorie, lastPoint). */
context(lastPoint: Vec2 | null): CommandContext;
/** Snap für einen rohen Cursor-Punkt (oder null), inkl. der Draft-Punkte. */
snap(raw: Vec2, draftPoints: Vec2[]): SnapResult | null;
/**
* Snap für einen rohen Cursor-Punkt (oder null), inkl. der Draft-Punkte. `mods`
* trägt Shift (Ortho) / Ctrl (Snap aus) durch — auch der ERSTE Punkt (ohne
* Draft) wird so gesnappt bzw. per Ctrl unterdrückt.
*/
snap(raw: Vec2, draftPoints: Vec2[], mods?: EngineMods): SnapResult | null;
/** Aktuelle Pixel/Meter-Skala (für Snap-Toleranz). */
pxPerMeter(): number;
/** Vorschau setzen (oder löschen). */
@@ -57,10 +67,17 @@ export interface EngineView {
/**
* Geordnete Zahlenfelder des aktuellen Schritts (Tab-Feld-Zyklus §2.7). Leer,
* wenn der aktive Befehl/Schritt keine Felder anbietet. `value` ist der
* gelockte Wert (oder null = folgt der Maus), `active` markiert das aktuelle
* Tab-Ziel.
* gelockte Wert ODER — wenn ungelockt — der LIVE-Wert unter der Maus (oder null,
* wenn (noch) keiner bestimmbar ist). `locked` unterscheidet beide für die UI;
* `active` markiert das aktuelle Tab-Ziel.
*/
fields: { id: string; labelKey: string; value: number | null; active: boolean }[];
fields: {
id: string;
labelKey: string;
value: number | null;
locked: boolean;
active: boolean;
}[];
}
export class CommandEngine {
@@ -104,12 +121,26 @@ export class CommandEngine {
}
this.syncFields();
const defs = this.currentFields();
const fields = defs.map((f, i) => ({
id: f.id,
labelKey: f.labelKey as string,
value: f.id in this.locks ? this.locks[f.id] : null,
active: i === this.activeFieldIndex,
}));
// Live-Werte (Maus-Vorschau) für ungelockte Felder, falls der Befehl sie
// liefert — so zeigt die Befehlszeile beim Zeichnen die folgende Länge/Winkel.
const live = this.command.fieldValues
? this.command.fieldValues(this.state, this.locks, this.lastCursor)
: {};
const fields = defs.map((f, i) => {
const locked = f.id in this.locks;
const value = locked
? this.locks[f.id]
: f.id in live
? live[f.id]
: null;
return {
id: f.id,
labelKey: f.labelKey as string,
value,
locked,
active: i === this.activeFieldIndex,
};
});
return {
active: true,
promptKey: this.command.prompt(this.state),
@@ -224,6 +255,13 @@ export class CommandEngine {
this.lastCursor = null;
this.resetFields();
this.host.setDraft(null);
// Sofort-Befehl (Join/Split u. Ä.): direkt ausführen, ohne auf einen Pick zu
// warten. So sind Tastenkürzel und getippter Befehl EIN Pfad (§Task C).
if (cmd.autoRun) {
const res = cmd.autoRun(this.context());
this.applyResult(res);
return true;
}
this.emit();
return true;
}
@@ -324,10 +362,10 @@ export class CommandEngine {
}
/** Maus-Pick aus PlanView (linker Klick) — gesnappter Modellpunkt. */
pick(raw: Vec2): void {
pick(raw: Vec2, mods?: EngineMods): void {
if (!this.command || !this.state) return;
if (!this.command.accepts(this.state).includes("point")) return;
const snap = this.host.snap(raw, this.draftPoints());
const snap = this.host.snap(raw, this.draftPoints(), mods);
const point = snap?.point ?? raw;
this.lastCursor = point;
// Tab-Feld-Modus: gelockte Felder überschreiben den Klick (z. B. Länge fix,
@@ -341,9 +379,9 @@ export class CommandEngine {
}
/** Hover/Drag aus PlanView — nur Vorschau (kein Commit). */
move(raw: Vec2): void {
move(raw: Vec2, mods?: EngineMods): void {
if (!this.command || !this.state) return;
const snap = this.host.snap(raw, this.draftPoints());
const snap = this.host.snap(raw, this.draftPoints(), mods);
const point = snap?.point ?? raw;
this.lastCursor = point;
const ctx = this.context();
@@ -379,12 +417,18 @@ export class CommandEngine {
this.repeatLast();
return;
}
// Tab-Feld-Modus (§2.7): existieren Felder, committet Enter den Schritt mit
// dem aus (Locks + Cursor) gebildeten Punkt — statt den Befehl abzubrechen.
const fieldPt = this.fieldResultPoint(this.lastCursor);
if (fieldPt) {
this.feed({ kind: "point", point: fieldPt });
return;
// Tab-Feld-Modus (§2.7): ist ein Feld GELOCKT (getippte Länge/Winkel/Dicke),
// committet Enter den Schritt mit dem aus (Locks + Cursor) gebildeten Punkt —
// statt den Befehl zu beenden. OHNE Lock ist Enter/Rechtsklick das normale
// Bestätigen: mehrteilige Befehle (Wand/Polylinie) beenden ihren Zug über
// onConfirm (sonst feuerte die Feld-Auflösung einen Cursor-Punkt und der Zug
// liesse sich nie abschliessen).
if (Object.keys(this.locks).length > 0) {
const fieldPt = this.fieldResultPoint(this.lastCursor);
if (fieldPt) {
this.feed({ kind: "point", point: fieldPt });
return;
}
}
const ctx = this.context();
const [st, res] = this.command.onConfirm(this.state, ctx);