Nordstern-3D: Live-Schnittebene mit schraffierten Schnittflaechen

Der 3D-Viewport kann das Modell jetzt live an einer vertikalen Ebene
aufschneiden (Overlay-Button 'Schnittebene', MVP: Ebene durch die
Modellmitte, Blick +Y):
- Globals um section_plane vec4 erweitert (xyz=Normale, w=-n*p; Enable
  in mode.y); MESH_/GRID_WGSL discarden Fragmente vor der Ebene
  (world_pos als neuer Varying) - Flaechen, Kanten, Grid, Highlight.
- section_fill.rs (neu): build_cut_caps ruft cut_section und trianguliert
  jedes (u,v)-Rechteck zurueck in Weltkoordinaten ([x,y,z,u,v]).
- CAP_WGSL: prozedurale 45-Grad-Diagonalschraffur aus (u,v) in Modell-
  Metern (fwidth-AA), Tinte auf Papier wie die 2D-Konvention; eigene
  cap_pipeline (CullMode::None, Depth-Bias Richtung Kamera gegen
  Z-Fighting), gezeichnet nach den Flaechen, nicht geclippt.
- web.rs cached walls/slabs; set_section_plane(active, p, n) aktualisiert
  Uniform + Caps; set_model baut Caps bei aktivem Schnitt neu.
- useWasm3dRenderer.setSectionPlane + Toggle in Wasm3DViewport.

Bekannte Luecken (dokumentiert): Oeffnungen im 3D-Solid nicht ausgespart
(cut_section spart aus -> Mismatch bei Schnitt durchs Fenster; wird mit
dem Boolean-Loecher-Slice geloest), nur vertikale Ebenen, ein globales
Muster (per-Bauteil = Stufe 2). cargo 56 Tests + naga-WGSL-Validierung,
222 vitest gruen, WASM neu gebaut (pkg3d gitignored).
This commit is contained in:
2026-07-04 15:08:41 +02:00
parent 07042ebc75
commit 77f32f14ce
8 changed files with 499 additions and 17 deletions
+50 -1
View File
@@ -42,7 +42,7 @@
// Die Orbit-/Pan-Mathematik folgt den OrbitControls-Konventionen (Winkel je
// Pixel an der Viewport-Höhe normiert), damit sich beide Pfade gleich anfühlen.
import { useEffect, useRef } from "react";
import { useEffect, useRef, useState } from "react";
import type { Project } from "../model/types";
import { projectToModel3d, pickGeometry } from "../plan/toWalls3d";
import type { RenderMode, View3d } from "../ui/TopBar";
@@ -220,10 +220,16 @@ export function Wasm3DViewport({
setRenderStyle,
applyViewPreset,
setGroundGrid,
setSectionPlane,
setHighlightLines,
ready,
failed,
} = useWasm3dRenderer(canvasRef, true);
// Live-Schnitt-Zustand (lokaler Overlay-Umschalter, MVP): vertikale
// Schnittebene durch die Modellmitte, Blick +Y (Grundriss-Y = world Z, s.
// section.rs `looking_plus_y`: normal = [0,0,1]). Kein Store-/App-Prop —
// reine Viewport-Ansichts-Option analog zum Grid-Overlay.
const [sectionActive, setSectionActive] = useState(false);
// Orbit-Zustand in einem Ref (kein React-State): Mausbewegungen zeichnen
// direkt neu, ohne die Komponente je Frame zu re-rendern. Dient NUR der
// freien Navigation NACH einem Praeset-Sprung (s. Datei-Kommentar).
@@ -277,6 +283,18 @@ export function Wasm3DViewport({
redrawRef.current();
}, [ready, gridVisible, groundElevation, setGroundGrid]);
// Live-Schnitt: bei Bereitschaft, Umschalten (`sectionActive`) oder
// Modelländerung neu setzen. MVP-Ebene = vertikal durch die Modellmitte
// (`fitTargetDist`-Ziel), Blick nach +Y -> Normale [0,0,1] (Grundriss-Y =
// world Z, s. section.rs `looking_plus_y`). Bei aktivem Schnitt baut Rust die
// Schnittflaechen-Schraffur (Caps) aus den gecachten Waenden/Decken.
useEffect(() => {
if (!ready) return;
const { target } = fitTargetDist(project);
setSectionPlane(sectionActive, target, [0, 0, 1]);
redrawRef.current();
}, [ready, sectionActive, project, setSectionPlane]);
// Auswahl-Hervorhebung: bei Bereitschaft oder Änderung der Highlight-Linien
// (Auswahl gewechselt/aufgehoben) neu setzen + zeichnen. Die Engine hält das
// Highlight, bis es erneut gesetzt wird — Kamerabewegungen (Orbit/Pan/Zoom)
@@ -504,6 +522,37 @@ export function Wasm3DViewport({
Darstellungsart kommen von der Oberleiste (TopBar); das Grid-Overlay
unten ist der einzige eigene Umschalter hier. */}
<canvas ref={canvasRef} data-engine="render3d" />
{/* Live-Schnitt-Umschalter (eigener Overlay-Button, MVP): vertikale
Schnittebene durch die Modellmitte, Blick +Y. Sitzt ueber dem
Grid-Button (falls vorhanden). Reine Ansichts-Option des Viewports. */}
<button
type="button"
aria-label="Schnittebene"
onClick={() => setSectionActive((v) => !v)}
title={sectionActive ? "Schnittebene aus" : "Schnittebene ein"}
aria-pressed={sectionActive}
style={{
position: "absolute",
right: 10,
bottom: onToggleGrid ? 48 : 10,
width: 30,
height: 30,
display: "flex",
alignItems: "center",
justifyContent: "center",
padding: 0,
borderRadius: 7,
border: "1px solid rgba(255,255,255,0.14)",
background: sectionActive ? "rgba(255,255,255,0.14)" : "rgba(20,20,24,0.65)",
color: sectionActive ? "#f2f2f2" : "#9a9a9a",
cursor: "pointer",
backdropFilter: "blur(4px)",
}}
>
<span className="material-symbols-outlined" aria-hidden="true" style={{ fontSize: 18 }}>
content_cut
</span>
</button>
{onToggleGrid && (
// Dezenter Overlay-Button (eigener DOM-Knoten neben dem Canvas, `.viewport`
// ist `position: relative` — s. styles.css), stört die Maus-Navigation
+47
View File
@@ -53,6 +53,20 @@ interface WebModelRendererLike {
extent_m: number,
spacing_m: number,
): void;
/** Live-Schnitt ein-/ausschalten (Rust-seitig, `web::set_section_plane`).
* `active=false` hebt den Schnitt auf. `p*` = Punkt auf der Ebene, `n*` =
* Ebenennormale = Blickrichtung (world-Meter, Y-up; nur vertikale Ebenen).
* Kappt das Modell per Shader-`discard` und fuellt die Schnittflaeche mit der
* Diagonalschraffur (Caps). Wirkt erst beim naechsten `render`. */
set_section_plane(
active: boolean,
px: number,
py: number,
pz: number,
nx: number,
ny: number,
nz: number,
): void;
/**
* Auswahl-Hervorhebung setzen: ein LineList-Vertex-Array (interleaved
* `[px,py,pz, r,g,b, ...]`, world-Meter, je 2 Vertices = 1 Linie), das die
@@ -187,6 +201,38 @@ export function useWasm3dRenderer(
[],
);
/**
* Live-Schnitt setzen/aufheben (Overlay-Umschalter im Viewport, s.
* Wasm3DViewport.tsx). `active=false` zeigt das Modell wieder komplett.
* `point` = Punkt auf der Schnittebene, `normal` = Ebenennormale =
* Blickrichtung (world-Meter, Y-up; nur vertikale Ebenen sinnvoll). Wirkt
* erst beim nächsten `render`.
*/
const setSectionPlane = useCallback(
(
active: boolean,
point: [number, number, number],
normal: [number, number, number],
) => {
const r = rendererRef.current;
if (!r) return;
try {
r.set_section_plane(
active,
point[0],
point[1],
point[2],
normal[0],
normal[1],
normal[2],
);
} catch (e) {
console.warn("render3d set_section_plane fehlgeschlagen:", e);
}
},
[],
);
/**
* Auswahl-Hervorhebung setzen (Umriss-Linien des gewählten Bauteils, immer
* obenauf) — reicht das fertige LineList-Vertex-Array 1:1 an
@@ -274,6 +320,7 @@ export function useWasm3dRenderer(
setRenderStyle,
applyViewPreset,
setGroundGrid,
setSectionPlane,
setHighlightLines,
ready,
failed,