# truck-Integration — Profil-Extrusion (B-Rep → 3D-Mesh)

> Übergabe-Dokument für die Implementierung. Lies zuerst CONVENTIONS.md.

## Ziel

Nutzer zeichnen im Grundriss ein geschlossenes Polygon (z. B. L-Profil einer Stütze)
und können es als 3D-Körper auf eine Höhe extrudieren. Das Ergebnis erscheint sofort
im 3D-Viewport neben Wänden und Decken.

**MVP-Scope (dieser Auftrag):**
- Neue Rust/WASM-Crate `trucksolid` mit zwei Funktionen: `extrude_polygon` + `extrude_circle`
- Neue TS-Wrapper-Datei `src/engine/truckSolid.ts` (WASM laden + typisierte API)
- KEIN neues Werkzeug, KEIN UI — nur die Geometrie-Schicht. UI kommt in einem
  separaten Folgeauftrag.

---

## Architektur-Kontext

```
src-tauri/
  render3d/          ← bestehend: wgpu-Renderer (wasm-pack → pkg3d/)
  kernel2d/          ← bestehend: 2D-Geometrie-Kern
  trucksolid/        ← NEU (dieser Auftrag)
    Cargo.toml
    src/
      lib.rs

src/engine/
  pkg3d/             ← render3d WASM-Output
  pkgTruck/          ← NEU: trucksolid WASM-Output (wasm-pack Ziel)
  truckSolid.ts      ← NEU: TS-Wrapper

src/plan/
  toWalls3d.ts       ← bestehend: baut RenderScene; emitMeshes() muss erweitert werden
```

Das Koordinatensystem in DOSSIER ist **Modell-Meter: X = rechts, Y = oben (Grundriss),
Z = Höhe**. render3d erwartet `positions` im gleichen System — `mesh.rs` macht intern
`(x, y, z) → (x, z, y)` für wgpu (Y-up world). Truck arbeitet in 3D; wir bauen
das Polygon in der XY-Ebene (z=0) und extrudieren nach +Z.

---

## 1 — Rust-Crate `src-tauri/trucksolid/`

### 1.1 Cargo.toml

Exakt dasselbe Muster wie `src-tauri/kernel2d/Cargo.toml`:

```toml
[workspace]   # entkoppelt vom cad-tauri-Workspace (Pflicht)

[package]
name = "trucksolid"
version = "0.1.0"
edition = "2021"
description = "Profil-Extrusion via truck (B-Rep → tesselliertes Mesh für render3d)"

[lib]
crate-type = ["cdylib", "rlib"]

[features]
default = []
web = [
    "dep:wasm-bindgen",
    "dep:serde_json",
    "dep:console_error_panic_hook",
]

[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = { version = "1", optional = true }
wasm-bindgen = { version = "0.2", optional = true }
console_error_panic_hook = { version = "0.1", optional = true }

# truck: nur die stabilen Crates (KEIN truck-modeling — Booleans instabil)
truck-geometry  = "0.3"
truck-topology  = "0.3"
truck-rendimesh = "0.3"

[dev-dependencies]
serde_json = "1"
```

**Wichtig:** `truck-modeling` (die Crate mit Boolean-Operatoren) wird NICHT eingebunden.
Nur `truck-geometry`, `truck-topology` und `truck-rendimesh` — die sind stabil.

### 1.2 src/lib.rs — Kern-Logik

#### Eingabe/Ausgabe-Typen

```rust
use serde::{Deserialize, Serialize};

/// Input: flaches Array [x0,y0, x1,y1, ...] in Modell-Metern (Grundriss-XY).
/// Muss ≥ 3 Punkte enthalten; Wiederholung des ersten Punkts am Ende optional.
/// Reihenfolge CCW oder CW — truck normalisiert selbst.
#[derive(Deserialize)]
pub struct ExtrudePolyInput {
    pub points: Vec<f64>,  // flat: [x0,y0, x1,y1, ...]
    pub height: f64,       // Extrusionshöhe in Metern (> 0)
}

/// Output: trianguliertes Mesh, kompatibel mit render3d::types::MeshInput.
/// positions: flat [x0,y0,z0, x1,y1,z1, ...] in Modell-Metern
/// indices:   Dreiecks-Indizes (je 3 = 1 Dreieck), 0-basiert
#[derive(Serialize)]
pub struct MeshOutput {
    pub positions: Vec<f32>,
    pub indices:   Vec<u32>,
}
```

#### Polygon-Extrusion (truck-API)

```rust
use truck_geometry::prelude::*;
use truck_topology::*;

pub fn extrude_polygon_core(pts: &[(f64, f64)], height: f64) -> Result<MeshOutput, String> {
    if pts.len() < 3 { return Err("min 3 Punkte".into()); }
    if height <= 0.0  { return Err("height muss > 0 sein".into()); }

    // 1. Punkte in der XY-Ebene (z=0) als truck-Vertices
    let verts: Vec<Vertex<Point3>> = pts.iter()
        .map(|(x, y)| Vertex::new(Point3::new(*x, *y, 0.0)))
        .collect();

    // 2. Kanten: je zwei aufeinanderfolgende Vertices verbinden, Ring schliessen
    let edges: Vec<Edge<_, Line<Point3>>> = verts.windows(2)
        .chain(std::iter::once([verts.last().unwrap(), &verts[0]].as_slice()))
        .map(|w| {
            let p0 = *w[0].point();
            let p1 = *w[1].point();
            Edge::new(&w[0], &w[1], Line(p0, p1))
        })
        .collect();

    // 3. Wire (geschlossener Kantenzug)
    let wire = Wire::from_iter(edges);

    // 4. Planare Face aus dem Wire
    //    truck-topology::Face::new braucht die äussere Boundary + optional Holes
    let face = Face::new(vec![wire]);

    // 5. Lineare Extrusion: tsweep entlang +Z um `height`
    let solid = face.tsweep(&Vector3::new(0.0, 0.0, height));

    // 6. Tessellieren (chord-tolerance in Metern — 0.005 = 5 mm)
    use truck_rendimesh::MeshedShape;
    let mesh = solid.triangulation(0.005).to_polygon();

    // 7. positions + indices extrahieren
    let positions: Vec<f32> = mesh.positions().iter()
        .flat_map(|p| [p.x as f32, p.y as f32, p.z as f32])
        .collect();
    let indices: Vec<u32> = mesh.tri_faces().iter()
        .flat_map(|tri| tri.iter().map(|idx| idx.pos as u32))
        .collect();

    Ok(MeshOutput { positions, indices })
}
```

> **Achtung:** Die exakte truck-API (Methoden-Namen, Trait-Imports, tsweep-Signatur)
> kann je nach veröffentlichter Version leicht abweichen. Prüfe die Docs von
> `truck-topology 0.3` und `truck-rendimesh 0.3` auf docs.rs. Die Struktur oben
> ist das Ziel-Pattern — passe Methoden-Signaturen an, wenn der Compiler es verlangt.
> Ändere NICHT die Eingabe/Ausgabe-JSON-Struktur.

#### Kreis-Extrusion (Zylinder)

```rust
pub fn extrude_circle_core(cx: f64, cy: f64, r: f64, height: f64) -> Result<MeshOutput, String> {
    // Kreis in XY-Ebene tessellieren (N Segmente je nach r),
    // dann wie extrude_polygon_core aufrufen.
    // Alternativ: truck-geometry::Ellipse/Circle direkt nutzen, falls vorhanden.
    let n = (2.0 * std::f64::consts::PI * r / 0.02).ceil().max(16.0) as usize;
    let pts: Vec<(f64, f64)> = (0..n)
        .map(|i| {
            let a = 2.0 * std::f64::consts::PI * i as f64 / n as f64;
            (cx + r * a.cos(), cy + r * a.sin())
        })
        .collect();
    extrude_polygon_core(&pts, height)
}
```

#### WASM-Bindings (Feature "web")

```rust
#[cfg(feature = "web")]
mod web {
    use super::*;
    use wasm_bindgen::prelude::*;

    #[wasm_bindgen(start)]
    pub fn init() {
        console_error_panic_hook::set_once();
    }

    /// Extrudiert ein Polygon-Profil.
    /// `input_json`: `{ "points": [x0,y0,…], "height": 2.5 }`
    /// Rückgabe: `{ "positions": […], "indices": […] }` oder throws JsError.
    #[wasm_bindgen]
    pub fn extrude_polygon(input_json: &str) -> Result<String, JsError> {
        let input: ExtrudePolyInput = serde_json::from_str(input_json)
            .map_err(|e| JsError::new(&e.to_string()))?;
        let pts: Vec<(f64, f64)> = input.points.chunks(2)
            .map(|c| (c[0], c[1]))
            .collect();
        let mesh = extrude_polygon_core(&pts, input.height)
            .map_err(|e| JsError::new(&e))?;
        serde_json::to_string(&mesh).map_err(|e| JsError::new(&e.to_string()))
    }

    /// Extrudiert einen Kreis-Querschnitt (Zylinder).
    /// `input_json`: `{ "cx": 0, "cy": 0, "r": 0.15, "height": 3.0 }`
    #[wasm_bindgen]
    pub fn extrude_circle(input_json: &str) -> Result<String, JsError> {
        #[derive(serde::Deserialize)]
        struct In { cx: f64, cy: f64, r: f64, height: f64 }
        let i: In = serde_json::from_str(input_json)
            .map_err(|e| JsError::new(&e.to_string()))?;
        let mesh = extrude_circle_core(i.cx, i.cy, i.r, i.height)
            .map_err(|e| JsError::new(&e))?;
        serde_json::to_string(&mesh).map_err(|e| JsError::new(&e.to_string()))
    }
}
```

### 1.3 Tests (headless, ohne Feature "web")

Mindestens diese drei Tests müssen mit `cargo test` grün sein:

```rust
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn quad_extrusion_vertex_count() {
        // 1m × 1m Quadrat, 2m hoch → 8 Eckpunkte minimum (6 Flächen × 2 Dreiecke)
        let pts = vec![(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)];
        let m = extrude_polygon_core(&pts, 2.0).unwrap();
        assert!(m.positions.len() >= 8 * 3);       // ≥ 8 Vertices × 3 floats
        assert_eq!(m.indices.len() % 3, 0);         // vollständige Dreiecke
        assert!(m.indices.len() >= 12 * 3);         // ≥ 12 Dreiecke (Quader)
    }

    #[test]
    fn l_profile_extrusion() {
        // L-Profil: 6 Punkte
        let pts = vec![
            (0.0,0.0),(0.3,0.0),(0.3,0.1),(0.1,0.1),(0.1,0.3),(0.0,0.3),
        ];
        let m = extrude_polygon_core(&pts, 3.0).unwrap();
        assert_eq!(m.indices.len() % 3, 0);
        assert!(m.positions.len() > 0);
    }

    #[test]
    fn cylinder_extrusion() {
        let m = extrude_circle_core(0.0, 0.0, 0.15, 3.0).unwrap();
        assert_eq!(m.indices.len() % 3, 0);
        assert!(m.positions.len() > 0);
    }

    #[test]
    fn rejects_too_few_points() {
        assert!(extrude_polygon_core(&[(0.0,0.0),(1.0,0.0)], 1.0).is_err());
    }

    #[test]
    fn rejects_zero_height() {
        let pts = vec![(0.0,0.0),(1.0,0.0),(0.5,1.0)];
        assert!(extrude_polygon_core(&pts, 0.0).is_err());
    }
}
```

---

## 2 — Build-Script (package.json)

Füge in `package.json` unter `"scripts"` hinzu:

```json
"build:truck": "wasm-pack build src-tauri/trucksolid --release --target web --out-dir ../../src/engine/pkgTruck --out-name trucksolid --no-default-features --features web"
```

Und in `src-tauri/Cargo.toml` unter `exclude`:

```toml
exclude = ["render2d", "render3d", "geometry", "kernel2d", "dwgimport", "trucksolid"]
```

---

## 3 — TS-Wrapper `src/engine/truckSolid.ts`

Gleiche Lade-Pattern wie `src/plan/useWasmPlanRenderer.ts` (render2d) und
`src/viewport/useWasm3dRenderer.ts` (render3d):

```typescript
// Lazy-Singleton: WASM einmalig laden, dann gecacht.
let modulePromise: Promise<typeof import("../../engine/pkgTruck/trucksolid")> | null = null;

async function getModule() {
  if (!modulePromise) {
    modulePromise = import("../../engine/pkgTruck/trucksolid").then(async (m) => {
      await m.default(); // WASM-Binary initialisieren
      return m;
    });
  }
  return modulePromise;
}

export interface ExtrudedMesh {
  positions: number[];
  indices: number[];
}

/** Extrudiert ein geschlossenes Polygon-Profil (Modell-Meter XY) um `height` m. */
export async function extrudePolygon(
  points: number[],  // flat [x0,y0, x1,y1, ...]
  height: number,
): Promise<ExtrudedMesh> {
  const m = await getModule();
  const json = m.extrude_polygon(JSON.stringify({ points, height }));
  return JSON.parse(json) as ExtrudedMesh;
}

/** Extrudiert einen Kreis-Querschnitt (Zylinder). */
export async function extrudeCircle(
  cx: number, cy: number, r: number, height: number,
): Promise<ExtrudedMesh> {
  const m = await getModule();
  const json = m.extrude_circle(JSON.stringify({ cx, cy, r, height }));
  return JSON.parse(json) as ExtrudedMesh;
}
```

---

## 4 — Integration in toWalls3d.ts (Vorbereitung)

In `src/plan/toWalls3d.ts` ist `RMeshKind` bereits definiert:

```typescript
export type RMeshKind = "terrain" | "imported";
```

Erweitere auf `"extrusion"` (damit der Renderer später eine eigene Farbe/Darstellung
wählen kann, auch wenn heute noch kein Unterschied besteht):

```typescript
export type RMeshKind = "terrain" | "imported" | "extrusion";
```

Die `emitMeshes()`-Funktion liest bereits `project.drawings2d` und ähnliche Arrays.
Für extrudierte Körper wird es später ein `project.extrudedSolids`-Array geben
(oder ähnlich — das ist Teil des UI-Folgeauftrags). Die `emitMeshes()`-Erweiterung
kommt dann.

---

## 5 — Verifikations-Checkliste

Bevor du als fertig meldest, müssen alle Punkte grün sein:

- [ ] `cd src-tauri/trucksolid && cargo test` → alle 5 Tests grün, keine Warnings
- [ ] `npm run build:truck` → `src/engine/pkgTruck/trucksolid.js` + `.wasm` erzeugt
- [ ] `npx tsc --noEmit` (aus Root) → 0 Fehler
- [ ] `npx vitest run` → alle bestehenden Tests weiterhin grün (keine Regression)
- [ ] `src-tauri/Cargo.toml` hat `"trucksolid"` im `exclude`-Array
- [ ] `package.json` hat `"build:truck"` im `"scripts"`-Block

---

## 6 — Was NICHT in diesem Auftrag

- Kein neues UI/Werkzeug (kommt später)
- Kein `truck-modeling` (Boolean-Operationen — instabil upstream)
- Kein STEP-Export (kommt in Folgeauftrag wenn Basisschicht steht)
- Kein Eintrag in `project.extrudedSolids` oder Store (UI-Auftrag)
- Keine Änderungen an render3d, generatePlan, App.tsx

---

## 7 — Koordinatensystem-Reminder

| DOSSIER-Modell | truck (im Code) | render3d (wgpu) |
|---|---|---|
| X = rechts | X = rechts | X = rechts |
| Y = oben (Grundriss) | Y = oben (Grundriss) | Z = oben (Y-up swapped) |
| Z = Höhe | Z = Höhe | Y = Höhe |

`mesh.rs` in render3d macht bereits `(model.x, model.y, model.z) → (x, z, y)` beim
Hochladen. Du musst also in truck DOSSIER-Koordinaten verwenden (XY-Ebene = Grundriss,
Z = Höhe) — das ist konsistent mit der bestehenden `MeshInput.positions`-Konvention.
