Schnittebenen 2D↔3D Phase 1+2, native Fenster, Hell/Dunkel-Theme, SWISSIMAGE-Import

- Schnittebenen: 3D-Live-Schnitt folgt der gewählten Grundriss-Schnittlinie
  (section3dCutId/sectionPlaneFromLevel), Schalter "Im 3D schneiden" im
  Objekt-Info, Doppelklick auf Schnittlinie springt in 2D-Schnittansicht,
  unsichtbare Ebenen blenden ihre Führungslinie aus
- Eigene native Tauri-Fenster für Kontext-Import/Zeichnungsebenen/
  Ebenen-Einstellungen/Ressourcen/Einstellungen + klassische Menüleiste
  (AppMenuBar) neben der Wortmarke
- Hell/Dunkel-Umschalter (Einstellungen → Darstellung), persistiert,
  flackerfrei vor erstem Render gesetzt
- SWISSIMAGE-Luftbild-Import (swisstopo WMS) als Kontext-Hintergrundebene
- UI-Politur: Werkzeug-Panel Symbole/Liste umschaltbar, Topbar-Quick-Access-
  Icons entfernt, Zahnrad→Einstellungen in Panel-Köpfen, Footerbar/
  Snap-Marker/Maß-HUD auf helle Pillen-Sprache umgestellt
- Neues Dachziegel-Material (RoofingTiles013A)
This commit is contained in:
2026-07-20 10:51:37 +02:00
parent 51e57c368b
commit b9e330ecb7
89 changed files with 7723 additions and 1848 deletions
+67 -54
View File
@@ -274,6 +274,14 @@ function fitTargetDist(project: Project): {
grow(p[0], s.zTop, p[1]);
}
}
// Meshes (Dächer, Extrusionen, Stützen UND importierte Gebäude/Terrain) —
// ohne die kann „Alles einpassen" den (evtl. weit entfernten) Import nicht
// rahmen: model (x,y,z=Höhe) → world (x, z, y). Nutzer-Report: „im 3D
// funktioniert das Fokussieren / Alles-anzeigen nicht".
for (const m of model.meshes) {
const pos = m.positions;
for (let i = 0; i + 2 < pos.length; i += 3) grow(pos[i], pos[i + 2], pos[i + 1]);
}
// Leeres Modell: neutraler Standpunkt (entspricht dem three.js-Start und dem
// Rust-Fallback in `set_view_preset` bei fehlender Bounding-Box).
if (!Number.isFinite(minX)) {
@@ -334,6 +342,8 @@ export function Wasm3DViewport({
onEditRoofPitch,
onEditWallTop,
onEditEnd,
section3d = null,
onClearSection3d,
}: {
project: Project;
/** Kanonischer Blickwinkel (Oberleiste) — s. Datei-Kommentar. */
@@ -393,6 +403,12 @@ export function Wasm3DViewport({
onEditWallTop?: (wallId: string, topZ: number) => void;
/** Ende eines Griff-Drags. */
onEditEnd?: () => void;
/** Aktive 3D-Schnittebene (world point+normal) aus der gebundenen Schnittlinie
* (App-State, `sectionPlaneFromLevel`), oder `null` = kein Live-Schnitt.
* Ersetzt den früheren lokalen Hardcode-Toggle. */
section3d?: { point: [number, number, number]; normal: [number, number, number] } | null;
/** Klick auf den Viewport-Schnitt-Knopf: hebt die Bindung auf (App löscht die Id). */
onClearSection3d?: () => void;
}) {
const canvasRef = useRef<HTMLCanvasElement | null>(null);
// Pick-Callback in einem Ref spiegeln, damit der (einmal registrierte)
@@ -438,11 +454,9 @@ export function Wasm3DViewport({
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);
// Live-Schnitt: die aktive Ebene kommt jetzt aus `section3d` (App-State, an
// eine Schnittlinie gebunden — `sectionPlaneFromLevel`) statt aus einem
// lokalen Hardcode-Toggle. `null` = kein Schnitt.
// 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).
@@ -515,36 +529,33 @@ export function Wasm3DViewport({
}, [ready, renderMode, setRenderStyle]);
// Boden-Referenzraster: bei Bereitschaft oder Wechsel von Sichtbarkeit/
// Geschoss-Hoehe neu setzen + zeichnen. Fixe Ausdehnung/Abstand (40 m /
// 1 m) — ausreichend fuer die ueblichen Grundriss-Groessen dieses Spikes.
// Geschoss-Hoehe neu setzen + zeichnen. Fixe Ausdehnung/Abstand (300 m /
// 1 m) — bei den bisherigen 40 m lief man bei flachem Blickwinkel (Iso/
// Perspektive) schnell auf den harten Gitterrand (wirkte wie ein Clipping-
// Bug, war aber nur das Ende der Flaeche). 300 m ist bei gleicher
// Liniendichte immer noch sehr billig (nur ein LineList-Puffer).
useEffect(() => {
if (!ready) return;
setGroundGrid(gridVisible, groundElevation, 40, 1);
setGroundGrid(gridVisible, groundElevation, 300, 1);
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.
// Live-Schnitt: die gebundene Schnittlinie liefert `section3d` = world
// point+normal (vertikale Ebene entlang der Grundriss-Linie, Blickrichtung
// aus `directionSign` — s. sectionPlaneFromLevel). Ohne Bindung (`null`) kein
// Schnitt. Bei aktivem Schnitt baut Rust die Schnittflaechen-Schraffur (Caps)
// aus den gecachten Waenden/Decken. Neu setzen bei Bereitschaft, Wechsel der
// Ebene oder Modelländerung (Caps folgen dem aktuellen Modell).
useEffect(() => {
if (!ready) return;
// Bei inaktivem Schnitt braucht es kein Ziel (Ebene ist ohnehin unsichtbar)
// — `fitTargetDist` NICHT aufrufen, das würde sonst bei JEDER Projekt-
// Änderung (auch waehrend eines Griff-Drags) unnoetig die komplette
// Wand-/Oeffnungs-/Decken-Geometrie erneut flatten, nur um eine Bounding-
// Box zu ziehen, die `updateModel` fuer denselben `project`-Stand gerade
// erst berechnet hat.
if (!sectionActive) {
if (!section3d) {
setSectionPlane(false, [0, 0, 0], [0, 0, 1]);
redrawRef.current();
return;
}
const { target } = fitTargetDist(project);
setSectionPlane(true, target, [0, 0, 1]);
setSectionPlane(true, section3d.point, section3d.normal);
redrawRef.current();
}, [ready, sectionActive, project, setSectionPlane]);
}, [ready, section3d, project, setSectionPlane]);
// Auswahl-Hervorhebung: bei Bereitschaft oder Änderung der Highlight-Linien
// (Auswahl gewechselt/aufgehoben) neu setzen + zeichnen. Die Engine hält das
@@ -1030,37 +1041,39 @@ export function Wasm3DViewport({
}}
/>
))}
{/* 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={t("viewport3d.section.label")}
onClick={() => setSectionActive((v) => !v)}
title={t(sectionActive ? "viewport3d.section.hide" : "viewport3d.section.show")}
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>
{/* Live-Schnitt-Knopf: nur sichtbar, wenn eine Schnittlinie den 3D-Schnitt
treibt (`section3d` gebunden, Bindung im Objekt-Info gesetzt). Klick
hebt die Bindung auf. Sitzt ueber dem Grid-Button (falls vorhanden). */}
{section3d && (
<button
type="button"
aria-label={t("viewport3d.section.label")}
onClick={() => onClearSection3d?.()}
title={t("viewport3d.section.bound")}
aria-pressed
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: "rgba(255,255,255,0.14)",
color: "#f2f2f2",
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