Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)

Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.

Enthalten (Details in PENDENZEN.md ✅-Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
  Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
  Prioritäts-Verschneidung (section_boolean.rs), einstellbare
  Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
  Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
  Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
  Öffnungen/Treppen, Snap-Symbol-Restyle.
This commit is contained in:
2026-07-09 00:57:29 +02:00
parent 889cbb2c12
commit 35299307d6
131 changed files with 26501 additions and 852 deletions
+82
View File
@@ -7,6 +7,8 @@ import {
type RayCamera,
type PickWall,
type PickSlab,
type PickOpening,
type PickStair,
} from "./raycast3d";
// Kamera bei (0,1,10), Blick entlang -Z auf (0,1,0). Zentraler Strahl (ndc 0,0)
@@ -35,6 +37,24 @@ describe("cameraRay", () => {
// normalisiert
expect(Math.hypot(...ray.dir)).toBeCloseTo(1, 6);
});
it("orthografisch (perspective:false): alle Strahlen sind parallel zur Blickrichtung", () => {
const orthoCam: RayCamera = { ...CAM, perspective: false, orthoHalfHeight: 5 };
const center = cameraRay(orthoCam, 0, 0, 16 / 9);
const offset = cameraRay(orthoCam, 0.5, 0.5, 1);
// Beide Strahlen zeigen exakt in dieselbe Richtung (parallel, keine Konvergenz).
expect(offset.dir).toEqual(center.dir);
expect(center.dir[2]).toBeCloseTo(-1, 6);
// Der Ursprung wandert stattdessen lateral mit dem Pixel (Kehrwert von worldToScreen).
expect(offset.origin[0]).toBeCloseTo(2.5, 6); // ndcX·aspect·hh = 0.5·1·5
expect(offset.origin[1]).toBeCloseTo(3.5, 6); // eye.y(1) + ndcY·hh = 1 + 0.5·5
expect(offset.origin[2]).toBeCloseTo(10, 6); // unveraendert (Achse senkrecht zur Blickrichtung)
});
it("orthografisch ohne perspective-Feld bleibt rückwärtskompatibel perspektivisch", () => {
const ray = cameraRay(CAM, 0.5, 0.5, 1);
expect(ray.origin).toEqual([0, 1, 10]); // Ursprung bleibt eye (Perspektive)
});
});
// Wand: Achse model (-1,5)→(1,5) → world x∈[-1,1], z≈5. Dicke 0.2, Höhe 2 ab 0.
@@ -125,3 +145,65 @@ describe("pickNearest", () => {
expect(hit).toBeNull();
});
});
// Öffnung: dieselbe Achse/Front wie WALL (model y=5 → world z≈5), aber nur über
// die Fenster-Ausdehnung (Brüstung 0.9m..1.9m) statt der vollen Wandhöhe.
const OPENING: PickOpening = {
start: [-0.5, 5],
end: [0.5, 5],
thickness: 0.2,
height: 1.0,
baseElevation: 0.9,
openingId: "op1",
};
// Treppe: Fußabdruck-Rechteck model (0,0)-(2,3), Bounding-Prisma bis 2.6m Rise.
const STAIR: PickStair = {
outline: [
[0, 0],
[2, 0],
[2, 3],
[0, 3],
],
zBottom: 0,
zTop: 2.6,
stairId: "st1",
};
describe("pickNearest — Öffnungen und Treppen", () => {
it("Strahl auf eine Öffnung liefert openingId", () => {
// Wie der Wand-Treffer (Front z=5.1 → t=4.9), aber y=1 liegt in der
// Fenster-Ausdehnung (0.9..1.9) → Öffnung statt Wand.
const hit = pickNearest({ origin: [0, 1, 10], dir: [0, 0, -1] }, [], [], [OPENING]);
expect(hit).not.toBeNull();
expect(hit!.kind).toBe("opening");
expect(hit!.openingId).toBe("op1");
expect(hit!.t).toBeCloseTo(4.9, 6);
});
it("Strahl auf eine Treppe liefert stairId", () => {
// Senkrecht von oben ins Fußabdruck-Rechteck: Deckel bei y=2.6 → t=7.4.
const hit = pickNearest(
{ origin: [1, 10, 1], dir: [0, -1, 0] },
[],
[],
[],
[STAIR],
);
expect(hit).not.toBeNull();
expect(hit!.kind).toBe("stair");
expect(hit!.stairId).toBe("st1");
expect(hit!.t).toBeCloseTo(7.4, 6);
});
it("bei näherer Wand gewinnt die Wand (kleinstes t) trotz konkurrierender Öffnung", () => {
// farOpening liegt bei model y=2 (world z=2) → Front z=2.1 → t=7.9, WEITER
// hinten als die Wand (t=4.9) → die Wand muss trotz der Öffnung-Priorität
// bei Gleichstand gewinnen, weil hier gar kein Gleichstand vorliegt.
const farOpening: PickOpening = { ...OPENING, start: [-0.5, 2], end: [0.5, 2], openingId: "op-far" };
const hit = pickNearest({ origin: [0, 1, 10], dir: [0, 0, -1] }, [WALL], [], [farOpening]);
expect(hit).not.toBeNull();
expect(hit!.kind).toBe("wall");
expect(hit!.id).toBe("w1");
});
});