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:
@@ -0,0 +1,243 @@
|
||||
# Tauri-Migration + Compute-Boundary — Aufgabe für nächste Instanz
|
||||
|
||||
**Entscheidung (fix, 2026-07-01):** Browser-CAD → **Desktop Tauri-App mit Rust-Backend + wgpu-Rendering.**
|
||||
|
||||
**Grund:** komplexe Möbel mit vielen Polygonen (100k+) + mehrere parallele rechenintensive Ops → WebGL/three.js wird zum Blocker. **wgpu** (low-level GPU-API) + Rust-Compute skaliert native.
|
||||
|
||||
**Scope:** Desktop-only Release (Milestone 1). WASM-Fallback für Browser später wenn gebraucht.
|
||||
|
||||
**Rendering-Engine:** three.js → **wgpu** (Milestone 2)
|
||||
|
||||
---
|
||||
|
||||
## Aufgabe: Shell aufsetzen + Compute-Boundary + erste Op migrieren
|
||||
|
||||
### Schritt 0 — Compute-Boundary (TS-Kontrakt)
|
||||
|
||||
**Neu:** `src/compute/index.ts` — die einzige Stelle, durch die alle rechenintensiven Ops laufen.
|
||||
|
||||
```typescript
|
||||
// src/compute/index.ts — einheitliche Schnittstelle
|
||||
export async function computeKernel2D(op: 'offset'|'trim', …): Promise<Polyline[]> { … }
|
||||
export async function detectRooms(…): Promise<Room[]> { … }
|
||||
export async function computeJoins(…): Promise<JoinInfo[]> { … } // ← erste Op
|
||||
export async function parseShapeFromDwg(…): Promise<DwgGeometry> { … }
|
||||
```
|
||||
|
||||
**Hinter der Boundary:**
|
||||
- Erst Tauri-invoke zu Rust `#[tauri::command]`
|
||||
- Fallback auf lokale TS-Impl (bleibt unberührt, bis Rust stabil)
|
||||
- `catch(err) → console.warn('Rust failed, using TS fallback'); return tsImpl(…)`
|
||||
|
||||
**Effekt:** Aufrufort im Code bleibt stabil; Caller sieht nicht, ob Op schon in Rust ist oder noch TS.
|
||||
|
||||
---
|
||||
|
||||
### Schritt 1 — Tauri-Shell aufsetzen
|
||||
|
||||
**Struktur:**
|
||||
```
|
||||
repo/
|
||||
src-tauri/ ← neue Rust-Seite (Tauri-Konvention)
|
||||
src/
|
||||
main.rs ← Tauri window + invoke handlers
|
||||
geometry.rs ← compute_joins() + weitere Ops später
|
||||
...
|
||||
Cargo.toml
|
||||
src/ ← React/TS (unverändert)
|
||||
compute/
|
||||
index.ts ← Compute-Boundary
|
||||
...
|
||||
vite.config.ts ← Tauri plugin integrieren
|
||||
package.json ← tauri scripts
|
||||
```
|
||||
|
||||
**Setup:**
|
||||
1. `cargo init --name cad-tauri src-tauri` (oder `src-tauri` manuell anlegen)
|
||||
2. `Cargo.toml`: Tauri v2 einbinden mit Feature `webkit2gtk-6.0` (GTK4)
|
||||
```toml
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = ["webkit2gtk-6.0"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
```
|
||||
3. `src-tauri/src/main.rs`: Minimal-Fenster, invoke-Handler registrieren
|
||||
```rust
|
||||
#[tauri::command]
|
||||
async fn compute_joins(input: JoinInput) -> Result<JoinOutput, String> {
|
||||
// Rust-Impl
|
||||
geometry::compute_joins(input).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![compute_joins])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
```
|
||||
4. `vite.config.ts`: Tauri plugin + dev-server-Integration
|
||||
```typescript
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: { port: 5173 } // Tauri dev zeigt hier drauf
|
||||
})
|
||||
```
|
||||
5. `package.json`: Tauri scripts hinzufügen
|
||||
```json
|
||||
"scripts": {
|
||||
"tauri": "tauri",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 2 — Erste Op migrieren: `computeJoins` (Wand-Eckverbindungen)
|
||||
|
||||
**Warum `computeJoins` zuerst?**
|
||||
- Läuft häufig (bei jedem Wall-Edit)
|
||||
- Klein und fokussiert (~50 Zeilen Kernlogik)
|
||||
- Proof-of-Concept für Tauri-invoke-Flow
|
||||
- Danach `kernel2d` parallel hochfahren
|
||||
|
||||
**Rust-Impl:** `src-tauri/src/geometry.rs`
|
||||
|
||||
```rust
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Vec2 { pub x: f64, pub y: f64 }
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WallJoinInput {
|
||||
pub walls: Vec<Wall>,
|
||||
pub joints: Vec<(usize, usize)>, // wall indices
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct JoinInfo { /* … */ }
|
||||
|
||||
pub fn compute_joins(input: WallJoinInput) -> Result<Vec<JoinInfo>, Box<dyn std::error::Error>> {
|
||||
// Port der Logik aus src/model/joins.ts
|
||||
// L-Ecken, T-Stösse, +-Kreuzungen
|
||||
Ok(vec![]) // Placeholder
|
||||
}
|
||||
```
|
||||
|
||||
**TS-Fallback bleibt:** `src/model/joins.ts` (LS vor Rust-Port)
|
||||
|
||||
**Compute-Boundary:** `src/compute/index.ts`
|
||||
```typescript
|
||||
export async function computeJoins(walls: Wall[], joints: Array<[number, number]>): Promise<JoinInfo[]> {
|
||||
try {
|
||||
return await invoke<JoinInfo[]>('compute_joins', { walls, joints });
|
||||
} catch (err) {
|
||||
console.warn('Rust compute_joins failed, using TS fallback:', err);
|
||||
return joinsTS(walls, joints); // Fallback
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 3 — Migrations-Parität testen
|
||||
|
||||
**Test-Fixtures:**
|
||||
- Aus `src/model/sampleProject.ts` exportieren: Wand-Arrays mit bekannten L/T/±-Konfigurationen
|
||||
- Rust-Unit-Tests: gleiche Fixtures → gleiche JoinInfo-Outputs
|
||||
- Vergleich: `actual == expected`
|
||||
|
||||
**Beispiel (Rust):**
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_l_corner() {
|
||||
let input = WallJoinInput { /* L-shaped walls */ };
|
||||
let result = compute_joins(input).unwrap();
|
||||
assert_eq!(result[0].kind, JoinKind::LCorner);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Schritt 4 — Vite/Tauri-Integration
|
||||
|
||||
**Dev-Workflow:**
|
||||
```bash
|
||||
npm run tauri:dev
|
||||
# → Vite dev-server (localhost:5173) lädt React-App
|
||||
# → Tauri-window zeigt auf :5173
|
||||
# → invoke() ruft Rust-Commands auf
|
||||
```
|
||||
|
||||
**Build-Workflow:**
|
||||
```bash
|
||||
npm run build # Vite → dist/
|
||||
npm run tauri:build # Tauri packt dist/ + Rust-Binary
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deliverable (Proof-of-Concept)
|
||||
|
||||
**Git-Stand nach dieser Aufgabe:**
|
||||
|
||||
- [ ] `src-tauri/` Verzeichnis mit `Cargo.toml` + `src/main.rs` + `src/geometry.rs`
|
||||
- [ ] `Cargo.toml` buildet sauber (`cargo check` 0 Fehler)
|
||||
- [ ] `src/compute/index.ts` mit `computeJoins()` Schnittstelle (invoke + TS-fallback)
|
||||
- [ ] Rust `compute_joins()` implementiert, Tests pass (`cargo test`)
|
||||
- [ ] `package.json` `tauri` scripts hinzugefügt
|
||||
- [ ] **App läuft:** `npm run tauri:dev` → Tauri-Fenster öffnet, Wand-Edit triggert Rust-Op, Output identisch TS-Version
|
||||
- [ ] **Trace-Scan sauber** (kein TS/Rust-Code übrig, beide Impl. aktiv)
|
||||
- [ ] HANDOVER.md aktualisiert: `computeJoins` migriert, nächste Ops in Queue
|
||||
|
||||
**Verification:**
|
||||
```bash
|
||||
# Build-Check
|
||||
cargo check # 0 Fehler
|
||||
|
||||
# Rust-Tests
|
||||
cargo test
|
||||
|
||||
# App end-to-end
|
||||
npm run tauri:dev
|
||||
# → Wand zeichnen + editieren → computeJoins() über Rust aufgerufen
|
||||
# → Plan + 3D aktualisiert wie vorher
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nächste Ops (Priorisierung)
|
||||
|
||||
Nach `computeJoins` stabil:
|
||||
|
||||
1. **`kernel2d`** (Offset/Trim/Extend) — großer Hebel, läuft häufig
|
||||
2. **DXF/DWG-Parser** (Geometrie) — heavy, aber niedrige Frequenz
|
||||
3. **`detectRooms`** (SIA-Raumerkennung) — async, kann auf Hintergrund ziehen
|
||||
4. **`booleanOps`** (Union/Differenz/Schnitt) — Kandidat für später
|
||||
|
||||
---
|
||||
|
||||
## Offene Punkte (NICHT jetzt)
|
||||
|
||||
- **GPU-Compute (wgpu):** Erst nach CPU-Ops stabil (kernel2d, joins, parsing)
|
||||
- **WASM-Fallback:** Nur wenn Browser-Support nötig wird
|
||||
- **Flatpak-Distribution:** Nach Tauri-Shell stable + erste Ops migriert
|
||||
|
||||
---
|
||||
|
||||
## Referenzen
|
||||
|
||||
- Tauri v2 Docs: https://tauri.app/v1/guides/getting-started/prerequisites
|
||||
- Serde: https://serde.rs/
|
||||
- CONVENTIONS.md: Identifiers englisch, UI-Text via `t()`, …
|
||||
- Commit-Regel: kein AI-Attribution im Repo
|
||||
Reference in New Issue
Block a user