Engine-3D-Presets ueber die TopBar + orthografisch (Top/Front/Seite), Iso korrekt
Die Kamera-Presets liefen zuvor als Viewport-Overlay mit eigenen JS-yaw/pitch- Winkeln, rein perspektivisch (Iso falsch, keine Parallelprojektion). Jetzt haengt der Engine-Viewport am app-weiten view3d-State (TopBar-CameraMenu, wie three.js) und nutzt die vorhandene Rust-preset_camera: - web.rs: set_view_preset(preset) + model_bounds/fit; Front/Top/Side echt orthografisch, Iso/Perspektive perspektivisch - wie das native Fenster. default_fov() pub(crate), damit Fit denselben FOV nutzt. - useWasm3dRenderer: applyViewPreset; render(null) ueberschreibt die Preset-Kamera nicht. - Wasm3DViewport: konsumiert view3d/renderMode als Props (Overlay + lokaler Umschalter entfernt, wasm3dOverlay.css geloescht); Orbit bleibt frei (Preset = Sprung, kein Lock), three.js-Verhalten gespiegelt. - Viewport3D-Switcher reicht view3d/renderMode an den Engine-Viewport durch (kein three.js-Szenencode angefasst).
This commit is contained in:
@@ -145,7 +145,9 @@ pub struct Camera {
|
|||||||
pub far: f32,
|
pub far: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_fov() -> f32 {
|
// `pub(crate)`, damit `web::set_view_preset` denselben Voreinstellungswert
|
||||||
|
// fuers formatfuellende Fit nutzen kann (kein zweiter FOV-Wert zum Pflegen).
|
||||||
|
pub(crate) fn default_fov() -> f32 {
|
||||||
// 50 Grad, wie die three.js-PerspectiveCamera-Voreinstellung.
|
// 50 Grad, wie die three.js-PerspectiveCamera-Voreinstellung.
|
||||||
50.0_f32.to_radians()
|
50.0_f32.to_radians()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,11 +11,15 @@
|
|||||||
// up.x, up.y, up.z, perspective, fovY, orthoHalfHeight);
|
// up.x, up.y, up.z, perspective, fovY, orthoHalfHeight);
|
||||||
// r.resize(px_w, px_h); r.render(); // pro Frame
|
// r.resize(px_w, px_h); r.render(); // pro Frame
|
||||||
//
|
//
|
||||||
// KAMERA: die Fassade nimmt eine FERTIGE Kamera (eye/target/up + Projektionsart)
|
// KAMERA: zwei Wege, dieselbe `Camera`. `set_camera` nimmt eine FERTIGE Kamera
|
||||||
// entgegen — Orbit/Pan/Zoom-Interaktion (Yaw/Pitch/Distanz, Praesets) rechnet die
|
// (eye/target/up + Projektionsart) entgegen — die freie Orbit/Pan/Zoom-Interaktion
|
||||||
// TS-Seite (dieselbe Konvention wie `math::orbit_eye`/`preset_camera`), damit die
|
// (Yaw/Pitch/Distanz) rechnet weiterhin die TS-Seite (`math::orbit_eye`-Konvention),
|
||||||
// Maus-UX 1:1 der bestehenden three.js-OrbitControls-Bedienung folgen kann, ohne
|
// damit die Maus-UX 1:1 der bestehenden three.js-OrbitControls-Bedienung folgt.
|
||||||
// eine zweite Praeset-Tabelle in Rust pflegen zu muessen.
|
// `set_view_preset` wendet stattdessen eines der fuenf `View3d`-Praesete
|
||||||
|
// (front/top/side/iso/perspective) direkt ueber `math::preset_camera` an — Front/
|
||||||
|
// Top/Side werden dabei ECHT orthografisch (`preset_camera`), was eine reine
|
||||||
|
// TS-Orbit-Naeherung nicht leisten kann. Nach einem Praeset-Sprung bleibt die
|
||||||
|
// weitere Navigation frei ueber `set_camera` (kein Lock).
|
||||||
//
|
//
|
||||||
// MODELL-FORMAT: JSON-Struct { walls: WallInput[], slabs: SlabInput[] } — identisch
|
// MODELL-FORMAT: JSON-Struct { walls: WallInput[], slabs: SlabInput[] } — identisch
|
||||||
// zu `RModel3d` (src/plan/toWalls3d.ts) und zum nativen Tauri-Push (nativeSync.ts).
|
// zu `RModel3d` (src/plan/toWalls3d.ts) und zum nativen Tauri-Push (nativeSync.ts).
|
||||||
@@ -25,8 +29,9 @@ use wasm_bindgen::prelude::*;
|
|||||||
use web_sys::HtmlCanvasElement;
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
use crate::gpu::Renderer;
|
use crate::gpu::Renderer;
|
||||||
|
use crate::math::preset_camera;
|
||||||
use crate::section::{cut_section, SectionPlane};
|
use crate::section::{cut_section, SectionPlane};
|
||||||
use crate::types::{Camera, Projection, SlabInput, WallInput};
|
use crate::types::{Camera, CameraPreset, Projection, SlabInput, WallInput};
|
||||||
|
|
||||||
/// JSON-Eingabe des Modells: Waende (bereits in Teilquader zerlegt, siehe
|
/// JSON-Eingabe des Modells: Waende (bereits in Teilquader zerlegt, siehe
|
||||||
/// `projectToWalls3d`) + Deckenplatten. `slabs` optional (leeres Modell ohne
|
/// `projectToWalls3d`) + Deckenplatten. `slabs` optional (leeres Modell ohne
|
||||||
@@ -83,6 +88,54 @@ pub struct WebModelRenderer {
|
|||||||
config: wgpu::SurfaceConfiguration,
|
config: wgpu::SurfaceConfiguration,
|
||||||
renderer: Renderer,
|
renderer: Renderer,
|
||||||
camera: Camera,
|
camera: Camera,
|
||||||
|
/// Achsparallele Bounding-Box (min, max) des zuletzt gesetzten Modells,
|
||||||
|
/// world-Meter (Y-up) — Grundlage fuer `set_view_preset` (formatfuellendes
|
||||||
|
/// Ziel/Distanz, mirror der TS-`fitTargetDist` in Wasm3DViewport.tsx).
|
||||||
|
/// `None` bei leerem Modell (kein Wall-/Slab-Vertex) oder vor `set_model`.
|
||||||
|
bounds: Option<([f32; 3], [f32; 3])>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Achsparallele Bounding-Box des Modells in Welt-Metern (Y-up), aus Waenden +
|
||||||
|
/// Decken. Mirror der TS-`fitTargetDist`-Grow-Schleife (Wasm3DViewport.tsx):
|
||||||
|
/// Wand-Achsenpunkte auf Basis-/Firsthoehe, Decken-Umriss auf Unter-/Oberkante.
|
||||||
|
/// `None` bei leerem Modell.
|
||||||
|
fn model_bounds(walls: &[WallInput], slabs: &[SlabInput]) -> Option<([f32; 3], [f32; 3])> {
|
||||||
|
let mut min = [f32::INFINITY; 3];
|
||||||
|
let mut max = [f32::NEG_INFINITY; 3];
|
||||||
|
let mut grow = |x: f32, y: f32, z: f32| {
|
||||||
|
if x < min[0] {
|
||||||
|
min[0] = x;
|
||||||
|
}
|
||||||
|
if x > max[0] {
|
||||||
|
max[0] = x;
|
||||||
|
}
|
||||||
|
if y < min[1] {
|
||||||
|
min[1] = y;
|
||||||
|
}
|
||||||
|
if y > max[1] {
|
||||||
|
max[1] = y;
|
||||||
|
}
|
||||||
|
if z < min[2] {
|
||||||
|
min[2] = z;
|
||||||
|
}
|
||||||
|
if z > max[2] {
|
||||||
|
max[2] = z;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for w in walls {
|
||||||
|
grow(w.start[0], w.base_elevation, w.start[1]);
|
||||||
|
grow(w.end[0], w.base_elevation + w.height, w.end[1]);
|
||||||
|
}
|
||||||
|
for s in slabs {
|
||||||
|
for p in &s.outline {
|
||||||
|
grow(p[0], s.z_bottom, p[1]);
|
||||||
|
grow(p[0], s.z_top, p[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !min[0].is_finite() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some((min, max))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -165,15 +218,18 @@ impl WebModelRenderer {
|
|||||||
config,
|
config,
|
||||||
renderer,
|
renderer,
|
||||||
camera: Camera::default(),
|
camera: Camera::default(),
|
||||||
|
bounds: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Setzt/aktualisiert das darzustellende Modell aus einem JSON-String
|
/// Setzt/aktualisiert das darzustellende Modell aus einem JSON-String
|
||||||
/// (Format = `{ walls: WallInput[], slabs?: SlabInput[] }`, identisch zum
|
/// (Format = `{ walls: WallInput[], slabs?: SlabInput[] }`, identisch zum
|
||||||
/// nativen Push). Laedt die GPU-Puffer neu hoch; ruft NICHT selbst `render`.
|
/// nativen Push). Laedt die GPU-Puffer neu hoch; ruft NICHT selbst `render`.
|
||||||
|
/// Merkt sich zusaetzlich die Bounding-Box (fuer `set_view_preset`).
|
||||||
pub fn set_model(&mut self, json: &str) -> Result<(), JsValue> {
|
pub fn set_model(&mut self, json: &str) -> Result<(), JsValue> {
|
||||||
let model: ModelInput = serde_json::from_str(json)
|
let model: ModelInput = serde_json::from_str(json)
|
||||||
.map_err(|e| JsValue::from_str(&format!("Modell parsen: {e}")))?;
|
.map_err(|e| JsValue::from_str(&format!("Modell parsen: {e}")))?;
|
||||||
|
self.bounds = model_bounds(&model.walls, &model.slabs);
|
||||||
self.renderer
|
self.renderer
|
||||||
.upload_model(&self.device, &model.walls, &model.slabs);
|
.upload_model(&self.device, &model.walls, &model.slabs);
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -216,6 +272,49 @@ impl WebModelRenderer {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wendet eines der fuenf Kamera-Praesets der three.js-Sicht
|
||||||
|
/// (`front`/`top`/`side`/`iso`/`perspective`, s. `View3d` in TopBar.tsx) auf
|
||||||
|
/// das aktuelle Modell an: Ziel = Modell-Mitte, Distanz = formatfuellend
|
||||||
|
/// (mirror der TS-`fitTargetDist`); Front/Top/Side werden orthografisch,
|
||||||
|
/// Iso/Perspective perspektivisch (`math::preset_camera`). Wirkt erst beim
|
||||||
|
/// naechsten `render`. „Praeset = hinspringen, kein Lock" — manuelles
|
||||||
|
/// Orbit/Pan/Zoom bleibt danach frei ueber `set_camera` (s. Wasm3DViewport.tsx).
|
||||||
|
pub fn set_view_preset(&mut self, preset: &str) -> Result<(), JsValue> {
|
||||||
|
let preset = match preset {
|
||||||
|
"front" => CameraPreset::Front,
|
||||||
|
"top" => CameraPreset::Top,
|
||||||
|
"side" => CameraPreset::Side,
|
||||||
|
"iso" => CameraPreset::Iso,
|
||||||
|
"perspective" => CameraPreset::Persp,
|
||||||
|
other => {
|
||||||
|
return Err(JsValue::from_str(&format!(
|
||||||
|
"unbekanntes Kamera-Praeset: {other}"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (target, dist) = match self.bounds {
|
||||||
|
Some((min, max)) => {
|
||||||
|
let center = [
|
||||||
|
(min[0] + max[0]) * 0.5,
|
||||||
|
(min[1] + max[1]) * 0.5,
|
||||||
|
(min[2] + max[2]) * 0.5,
|
||||||
|
];
|
||||||
|
let (dx, dy, dz) = (max[0] - min[0], max[1] - min[1], max[2] - min[2]);
|
||||||
|
let radius = (dx * dx + dy * dy + dz * dz).sqrt() * 0.5;
|
||||||
|
let radius = if radius > 0.0 { radius } else { 1.0 };
|
||||||
|
// Abstand so, dass die Bounding-Kugel ins vertikale Sichtfeld
|
||||||
|
// passt (+ Rand) — identische Formel wie `fitTargetDist`.
|
||||||
|
let fov_y = crate::types::default_fov();
|
||||||
|
let dist = ((radius / (fov_y * 0.5).tan()) * 1.2).max(4.0);
|
||||||
|
(center, dist)
|
||||||
|
}
|
||||||
|
// Leeres Modell: neutraler Standpunkt (entspricht dem three.js-Start).
|
||||||
|
None => ([2.5_f32, 1.0, 2.0], 14.0_f32),
|
||||||
|
};
|
||||||
|
self.camera = preset_camera(preset, target, dist);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Darstellungsart umschalten: `true` = weisses Clay-Material (beleuchtet,
|
/// Darstellungsart umschalten: `true` = weisses Clay-Material (beleuchtet,
|
||||||
/// ohne Bauteil-Farben — Vectorworks „white"), `false` = normale Material-
|
/// ohne Bauteil-Farben — Vectorworks „white"), `false` = normale Material-
|
||||||
/// farbe (shaded, Default). Wirkt erst beim naechsten `render`.
|
/// farbe (shaded, Default). Wirkt erst beim naechsten `render`.
|
||||||
|
|||||||
@@ -116,7 +116,15 @@ const WASM_ENGINE_ACTIVE: boolean = (() => {
|
|||||||
* (Auswahl, Render-Modi, Editier-Griffe) wirken nur im three.js-Pfad.
|
* (Auswahl, Render-Modi, Editier-Griffe) wirken nur im three.js-Pfad.
|
||||||
*/
|
*/
|
||||||
export function Viewport3D(props: Parameters<typeof ThreeViewport3D>[0]) {
|
export function Viewport3D(props: Parameters<typeof ThreeViewport3D>[0]) {
|
||||||
if (WASM_ENGINE_ACTIVE) return <Wasm3DViewport project={props.project} />;
|
if (WASM_ENGINE_ACTIVE) {
|
||||||
|
return (
|
||||||
|
<Wasm3DViewport
|
||||||
|
project={props.project}
|
||||||
|
view3d={props.view3d}
|
||||||
|
renderMode={props.renderMode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return <ThreeViewport3D {...props} />;
|
return <ThreeViewport3D {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
-183
@@ -5,26 +5,42 @@
|
|||||||
//
|
//
|
||||||
// INTEGRATIONS-SPIKE (Umfang bewusst schmal): Massenmodell (Wände mit Öffnungs-
|
// INTEGRATIONS-SPIKE (Umfang bewusst schmal): Massenmodell (Wände mit Öffnungs-
|
||||||
// Teilquadern + Deckenplatten, alle Geschosse gestapelt) + Orbit-Navigation +
|
// Teilquadern + Deckenplatten, alle Geschosse gestapelt) + Orbit-Navigation +
|
||||||
// Kamera-Presets (Top/Bottom/Front/Back/Left/Right/Iso) + Darstellungsart
|
// Kamera-Presets (`view3d`, von der Oberleiste getrieben — s. u.) + Darstellungs-
|
||||||
// (shaded/white) im Overlay unten (s. VIEW_PRESETS, wasm3dOverlay.css).
|
// art (shaded/white, `renderMode`, ebenfalls Oberleiste). Auswahl/Raycast,
|
||||||
// Auswahl/Raycast, Griffe/Zeichnen sowie die übrigen Darstellungsarten der
|
// Griffe/Zeichnen sowie die übrigen Darstellungsarten der three.js-Sicht
|
||||||
// three.js-Sicht (texturiert/Drahtgitter/Kanten, HLR) folgen später — bis
|
// (texturiert/Drahtgitter/Kanten, HLR) folgen später — bis dahin gilt: LINKS
|
||||||
// dahin gilt: LINKS ohne Funktion, RECHTS unterdrückt nur das Browser-Menü.
|
// ohne Funktion, RECHTS unterdrückt nur das Browser-Menü.
|
||||||
|
//
|
||||||
|
// KAMERA-PRESETS: `view3d`/`renderMode` kommen als Props von der Oberleiste
|
||||||
|
// (TopBar → App.tsx → Viewport3D-Umschalter, dieselbe Quelle wie die three.js-
|
||||||
|
// Sicht). Front/Top/Side/Iso/Perspective rechnet NICHT mehr diese Datei (die
|
||||||
|
// alte VIEW_PRESETS-Tabelle war perspektivisch-only und damit für Front/Top/
|
||||||
|
// Side falsch) — stattdessen `WebModelRenderer.set_view_preset` in Rust
|
||||||
|
// (`web.rs` → `math::preset_camera`): Front/Top/Side werden dort ECHT
|
||||||
|
// orthografisch, Iso/Perspective perspektivisch, exakt wie die native Fenster-
|
||||||
|
// Sicht. „Praeset = hinspringen, kein Lock" (wie `applyView3d` in der three.js-
|
||||||
|
// Sicht): manuelles Orbit/Pan/Zoom bleibt danach frei — es fährt mit der
|
||||||
|
// FREIEN perspektivischen Orbit-Kamera fort (Kugelkoordinaten, s. u.), deren
|
||||||
|
// Start-Blickwinkel VIEW3D_ORBIT so wählt, dass der erste Drag-Frame nahtlos
|
||||||
|
// an die Praeset-Blickrichtung anschliesst (Rust berechnet dieselbe Richtung
|
||||||
|
// für die exakte, evtl. orthografische Praeset-Kamera).
|
||||||
//
|
//
|
||||||
// MAUS-SCHEMA wie die three.js-Sicht (docs/design/context-menu.md):
|
// MAUS-SCHEMA wie die three.js-Sicht (docs/design/context-menu.md):
|
||||||
// MITTE ziehen = Orbit, Shift+MITTE = Pan, RAD = Zoom.
|
// MITTE ziehen = Orbit, Shift+MITTE = Pan, RAD = Zoom.
|
||||||
// Die Orbit-/Pan-Mathematik folgt den OrbitControls-Konventionen (Winkel je
|
// Die Orbit-/Pan-Mathematik folgt den OrbitControls-Konventionen (Winkel je
|
||||||
// Pixel an der Viewport-Höhe normiert), damit sich beide Pfade gleich anfühlen.
|
// Pixel an der Viewport-Höhe normiert), damit sich beide Pfade gleich anfühlen.
|
||||||
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { Project } from "../model/types";
|
import type { Project } from "../model/types";
|
||||||
import { projectToModel3d } from "../plan/toWalls3d";
|
import { projectToModel3d } from "../plan/toWalls3d";
|
||||||
|
import type { RenderMode, View3d } from "../ui/TopBar";
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { useWasm3dRenderer } from "./useWasm3dRenderer";
|
import { useWasm3dRenderer } from "./useWasm3dRenderer";
|
||||||
import type { Camera3d } from "./useWasm3dRenderer";
|
import type { Camera3d } from "./useWasm3dRenderer";
|
||||||
import "./wasm3dOverlay.css";
|
|
||||||
|
|
||||||
/** Vertikaler Öffnungswinkel (Radiant) — wie die three.js-PerspectiveCamera (50°). */
|
/** Vertikaler Öffnungswinkel (Radiant) — wie die three.js-PerspectiveCamera (50°),
|
||||||
|
* auch für die FREIE Orbit-Kamera hier (die Presets selbst nutzen den Rust-
|
||||||
|
* seitigen Default-FOV, s. `types::default_fov`, denselben Wert). */
|
||||||
const FOV_Y = (50 * Math.PI) / 180;
|
const FOV_Y = (50 * Math.PI) / 180;
|
||||||
/** Pitch-Klemme gegen den Gimbal-Flip (identisch zu render3d::math::orbit_eye). */
|
/** Pitch-Klemme gegen den Gimbal-Flip (identisch zu render3d::math::orbit_eye). */
|
||||||
const PITCH_LIMIT = Math.PI / 2 - 0.01;
|
const PITCH_LIMIT = Math.PI / 2 - 0.01;
|
||||||
@@ -41,7 +57,9 @@ interface OrbitState {
|
|||||||
target: [number, number, number];
|
target: [number, number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fertige Kamera aus dem Orbit-Zustand (Konvention wie render3d::math::orbit_eye). */
|
/** Fertige (freie, perspektivische) Kamera aus dem Orbit-Zustand (Konvention
|
||||||
|
* wie render3d::math::orbit_eye). Für die manuelle Navigation NACH einem
|
||||||
|
* Praeset-Sprung — die Presets selbst zeichnen über `set_view_preset`. */
|
||||||
function orbitCamera(o: OrbitState): Camera3d {
|
function orbitCamera(o: OrbitState): Camera3d {
|
||||||
const pitch = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, o.pitch));
|
const pitch = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, o.pitch));
|
||||||
const r = o.dist * Math.cos(pitch);
|
const r = o.dist * Math.cos(pitch);
|
||||||
@@ -61,9 +79,10 @@ function orbitCamera(o: OrbitState): Camera3d {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatfüllendes Ziel + Distanz: Bounding-Box des geflachten Modells (Wände +
|
* Formatfüllendes Ziel + Distanz: Bounding-Box des geflachten Modells (Wände +
|
||||||
* Decken, world: x = Modell-x, z = Modell-y, y = Höhe) einpassen. Gemeinsame
|
* Decken, world: x = Modell-x, z = Modell-y, y = Höhe) einpassen. Mirror der
|
||||||
* Grundlage für den Start-Orbit UND die Kamera-Presets (beide sollen das
|
* Rust-Bounding-Box in `web::model_bounds`/`set_view_preset` — beide Seiten
|
||||||
* Modell formatfüllend zeigen, nur der Blickwinkel unterscheidet sich).
|
* müssen auf dieselbe Distanz/dasselbe Ziel kommen, damit der Orbit-Zustand
|
||||||
|
* (hier) nahtlos an die exakte Praeset-Kamera (dort) anschliesst.
|
||||||
*/
|
*/
|
||||||
function fitTargetDist(project: Project): {
|
function fitTargetDist(project: Project): {
|
||||||
target: [number, number, number];
|
target: [number, number, number];
|
||||||
@@ -94,7 +113,8 @@ function fitTargetDist(project: Project): {
|
|||||||
grow(p[0], s.zTop, p[1]);
|
grow(p[0], s.zTop, p[1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Leeres Modell: neutraler Standpunkt (entspricht dem three.js-Start).
|
// 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)) {
|
if (!Number.isFinite(minX)) {
|
||||||
return { dist: 14, target: [2.5, 1, 2] };
|
return { dist: 14, target: [2.5, 1, 2] };
|
||||||
}
|
}
|
||||||
@@ -111,161 +131,117 @@ function fitTargetDist(project: Project): {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatfüllender Start-Orbit: Blick von schräg oben (wie das three.js-
|
* Orbit-Blickwinkel (yaw/pitch), die DIESELBE Blickrichtung wie das jeweilige
|
||||||
* Perspektive-Preset).
|
* `view3d`-Praeset ergeben (hergeleitet aus `preset_camera` in
|
||||||
*/
|
* render3d/src/math.rs — front/side/top/iso/perspective). Dient NICHT dem
|
||||||
function fitOrbit(project: Project): OrbitState {
|
* Zeichnen der Presets selbst (das macht `set_view_preset` in Rust, inkl.
|
||||||
const { target, dist } = fitTargetDist(project);
|
* echter Orthografie für front/top/side) — nur als Startpunkt für den freien
|
||||||
return { yaw: Math.PI / 4, pitch: 0.5, dist, target };
|
* Orbit-Zustand, damit ein Drag direkt nach einem Praeset-Sprung nicht aus
|
||||||
}
|
* einem unpassenden Blickwinkel losspringt.
|
||||||
|
|
||||||
/**
|
|
||||||
* Kanonische Blickwinkel-Presets (Vectorworks/DOSSIER-Stil, wie
|
|
||||||
* `applyView3d` in Viewport3D.tsx — hier als Kugelkoordinaten yaw/pitch statt
|
|
||||||
* eines Richtungsvektors, weil der Engine-Pfad die Kamera über den Orbit-
|
|
||||||
* Zustand treibt, s. o.). Herleitung (Y-up, world x = Plan-x, world z =
|
|
||||||
* Plan-y):
|
|
||||||
* front/back: Blick entlang ±Z (yaw = ±90°, pitch 0).
|
|
||||||
* left/right: Blick entlang ±X (yaw = 0°/180°, pitch 0).
|
|
||||||
* top/bottom: senkrecht von oben/unten; pitch ans Gimbal-Limit geklemmt
|
|
||||||
* (exakt 90° macht die Kamerabasis singulär — dieselbe Klemme wie beim
|
|
||||||
* freien Orbit, s. PITCH_LIMIT), mit demselben leichten Yaw-Versatz wie
|
|
||||||
* front/back (stabile, vorhersagbare „oben"-Achse am Bildschirm, analog
|
|
||||||
* zum three.js-Kniff `dir=(0,1,0.0001)`).
|
|
||||||
* iso: Ecklage von schräg oben (SW-Iso) — Richtung (1,1,1) normiert, exakt
|
|
||||||
* wie das three.js-Iso-Preset (Elevationswinkel arcsin(1/√3) ≈ 35.26°).
|
|
||||||
*/
|
*/
|
||||||
const ISO_PITCH = Math.asin(1 / Math.sqrt(3));
|
const ISO_PITCH = Math.asin(1 / Math.sqrt(3));
|
||||||
const VIEW_PRESETS: Record<
|
const VIEW3D_ORBIT: Record<View3d, { yaw: number; pitch: number }> = {
|
||||||
"top" | "bottom" | "front" | "back" | "left" | "right" | "iso",
|
|
||||||
{ yaw: number; pitch: number }
|
|
||||||
> = {
|
|
||||||
top: { yaw: Math.PI / 2, pitch: PITCH_LIMIT },
|
|
||||||
bottom: { yaw: -Math.PI / 2, pitch: -PITCH_LIMIT },
|
|
||||||
front: { yaw: Math.PI / 2, pitch: 0 },
|
front: { yaw: Math.PI / 2, pitch: 0 },
|
||||||
back: { yaw: -Math.PI / 2, pitch: 0 },
|
side: { yaw: 0, pitch: 0 },
|
||||||
right: { yaw: 0, pitch: 0 },
|
top: { yaw: Math.PI / 2, pitch: PITCH_LIMIT },
|
||||||
left: { yaw: Math.PI, pitch: 0 },
|
|
||||||
iso: { yaw: Math.PI / 4, pitch: ISO_PITCH },
|
iso: { yaw: Math.PI / 4, pitch: ISO_PITCH },
|
||||||
|
perspective: { yaw: Math.PI / 4, pitch: 0.5 },
|
||||||
};
|
};
|
||||||
type ViewPreset = keyof typeof VIEW_PRESETS;
|
|
||||||
|
|
||||||
/** Labels der Preset-Knöpfe: bestehende i18n-Schlüssel wiederverwendet, wo
|
export function Wasm3DViewport({
|
||||||
* vorhanden (view3d.*, aus der three.js-Oberleiste); für die im engine-Pfad
|
project,
|
||||||
* zusätzlichen Ansichten (bottom/back/left/right, dort nicht vorhanden) ein
|
view3d = "perspective",
|
||||||
* kurzer inline-deutscher Text — keine neuen i18n-Schlüssel (Projektregel).
|
renderMode = "shaded",
|
||||||
* Als Funktion (nicht Modul-Konstante) aufgerufen, damit `t()` bei jedem
|
}: {
|
||||||
* Render die aktuelle Sprache liest (wie der bestehende Fallback-Text unten). */
|
project: Project;
|
||||||
function viewPresetLabel(preset: ViewPreset): string {
|
/** Kanonischer Blickwinkel (Oberleiste) — s. Datei-Kommentar. */
|
||||||
switch (preset) {
|
view3d?: View3d;
|
||||||
case "top":
|
/** Darstellungsart (Oberleiste): nur `shaded`/`white` wirken hier (die
|
||||||
return t("view3d.top");
|
* übrigen Modi der three.js-Sicht sind im Engine-Pfad noch nicht gebaut,
|
||||||
case "bottom":
|
* s. Datei-Kommentar) — alles ausser `white` fällt auf shaded zurück. */
|
||||||
return "Unten";
|
renderMode?: RenderMode;
|
||||||
case "front":
|
}) {
|
||||||
return t("view3d.front");
|
|
||||||
case "back":
|
|
||||||
return "Hinten";
|
|
||||||
case "left":
|
|
||||||
return "Links";
|
|
||||||
case "right":
|
|
||||||
return "Rechts";
|
|
||||||
case "iso":
|
|
||||||
return t("view3d.iso");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const VIEW_PRESET_ORDER: ViewPreset[] = [
|
|
||||||
"top",
|
|
||||||
"bottom",
|
|
||||||
"front",
|
|
||||||
"back",
|
|
||||||
"left",
|
|
||||||
"right",
|
|
||||||
"iso",
|
|
||||||
];
|
|
||||||
|
|
||||||
export function Wasm3DViewport({ project }: { project: Project }) {
|
|
||||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||||
const { render, updateModel, setWhiteMode, ready, failed } = useWasm3dRenderer(
|
const { render, updateModel, setWhiteMode, applyViewPreset, ready, failed } =
|
||||||
canvasRef,
|
useWasm3dRenderer(canvasRef, true);
|
||||||
true,
|
|
||||||
);
|
|
||||||
// Orbit-Zustand in einem Ref (kein React-State): Mausbewegungen zeichnen
|
// Orbit-Zustand in einem Ref (kein React-State): Mausbewegungen zeichnen
|
||||||
// direkt neu, ohne die Komponente je Frame zu re-rendern.
|
// direkt neu, ohne die Komponente je Frame zu re-rendern. Dient NUR der
|
||||||
|
// freien Navigation NACH einem Praeset-Sprung (s. Datei-Kommentar).
|
||||||
const orbitRef = useRef<OrbitState | null>(null);
|
const orbitRef = useRef<OrbitState | null>(null);
|
||||||
|
// Redraw-Strategie des zuletzt aktiven Kamera-Modus (Praeset ODER freier
|
||||||
|
// Orbit) — Resize/Modell-Update ruft diese auf, ohne den Modus zu kennen.
|
||||||
|
const redrawRef = useRef<() => void>(() => {});
|
||||||
// Frisches Projekt für den (stabilen) Init-Effekt unten.
|
// Frisches Projekt für den (stabilen) Init-Effekt unten.
|
||||||
const projectRef = useRef(project);
|
const projectRef = useRef(project);
|
||||||
projectRef.current = project;
|
projectRef.current = project;
|
||||||
|
|
||||||
// Aktiver Blickwinkel-Preset (nur fürs Knopf-Hervorheben) + Darstellungsart
|
// Modell bei Bereitschaft + jeder Projektänderung neu einspielen. Die
|
||||||
// (Overlay-UI, s. u.). Bewusst React-State — anders als der Orbit ändern
|
// Kamera bleibt dabei stehen (gleiches Verhalten wie der three.js-Kamera-
|
||||||
// sich beide selten (Klick, nicht Mausbewegung), ein Re-Render ist unkritisch.
|
// Snapshot) — nur das `view3d`-Effekt unten springt aktiv auf ein Praeset.
|
||||||
const [activePreset, setActivePreset] = useState<ViewPreset | null>(null);
|
|
||||||
const [renderMode, setRenderMode] = useState<"shaded" | "white">("shaded");
|
|
||||||
|
|
||||||
// Modell bei Bereitschaft + jeder Projektänderung neu einspielen. Der Orbit
|
|
||||||
// wird nur beim ERSTEN Mal eingepasst — danach bleibt die Kamera beim
|
|
||||||
// Editieren stehen (gleiches Verhalten wie der three.js-Kamera-Snapshot).
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
updateModel(project);
|
updateModel(project);
|
||||||
if (!orbitRef.current) orbitRef.current = fitOrbit(project);
|
redrawRef.current();
|
||||||
render(orbitCamera(orbitRef.current));
|
}, [ready, project, updateModel]);
|
||||||
}, [ready, project, updateModel, render]);
|
|
||||||
|
// Blickwinkel-Praeset anwenden, wenn `view3d` wechselt (oder der Renderer
|
||||||
|
// bereit wird): springt über `set_view_preset` (Rust) auf die formatfüllende,
|
||||||
|
// ggf. orthografische Praeset-Kamera — „hinspringen, kein Lock", der
|
||||||
|
// Orbit-Zustand wird nur als Startpunkt für spätere manuelle Navigation
|
||||||
|
// nachgezogen (s. VIEW3D_ORBIT).
|
||||||
|
useEffect(() => {
|
||||||
|
if (!ready) return;
|
||||||
|
applyViewPreset(view3d);
|
||||||
|
const { yaw, pitch } = VIEW3D_ORBIT[view3d];
|
||||||
|
const { target, dist } = fitTargetDist(projectRef.current);
|
||||||
|
orbitRef.current = { yaw, pitch, dist, target };
|
||||||
|
redrawRef.current = () => render(null);
|
||||||
|
redrawRef.current();
|
||||||
|
}, [ready, view3d, applyViewPreset, render]);
|
||||||
|
|
||||||
// Darstellungsart (shaded/weiss) an den Renderer weiterreichen, sobald sie
|
// Darstellungsart (shaded/weiss) an den Renderer weiterreichen, sobald sie
|
||||||
// wechselt (Overlay-Klick) oder der Renderer bereit wird — dann neu zeichnen.
|
// wechselt oder der Renderer bereit wird — dann mit der aktuell aktiven
|
||||||
|
// Kamera (Praeset ODER Orbit) neu zeichnen.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ready) return;
|
if (!ready) return;
|
||||||
setWhiteMode(renderMode === "white");
|
setWhiteMode(renderMode === "white");
|
||||||
if (orbitRef.current) render(orbitCamera(orbitRef.current));
|
redrawRef.current();
|
||||||
}, [ready, renderMode, setWhiteMode, render]);
|
}, [ready, renderMode, setWhiteMode]);
|
||||||
|
|
||||||
// Blickwinkel-Preset anwenden: formatfüllend auf die aktuelle Modell-
|
|
||||||
// Bounding-Box (wie fitOrbit), aber mit dem Preset-Blickwinkel statt des
|
|
||||||
// Standard-Schrägblicks. „Preset = dorthin springen, kein Lock" — danach
|
|
||||||
// bleibt Orbit/Pan/Zoom frei (gleiche Philosophie wie Viewport3D.applyView3d).
|
|
||||||
const applyPreset = (name: ViewPreset) => {
|
|
||||||
if (!ready) return;
|
|
||||||
const { yaw, pitch } = VIEW_PRESETS[name];
|
|
||||||
const { target, dist } = fitTargetDist(projectRef.current);
|
|
||||||
orbitRef.current = { yaw, pitch, dist, target };
|
|
||||||
setActivePreset(name);
|
|
||||||
render(orbitCamera(orbitRef.current));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Canvas-Größe beobachten: bei Layout-Änderung mit aktueller Kamera neu
|
// Canvas-Größe beobachten: bei Layout-Änderung mit aktueller Kamera neu
|
||||||
// zeichnen (die Surface-Anpassung übernimmt der Hook im render()).
|
// zeichnen (die Surface-Anpassung übernimmt der Hook im render()).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!ready || !canvas) return;
|
if (!ready || !canvas) return;
|
||||||
const ro = new ResizeObserver(() => {
|
const ro = new ResizeObserver(() => redrawRef.current());
|
||||||
if (orbitRef.current) render(orbitCamera(orbitRef.current));
|
|
||||||
});
|
|
||||||
ro.observe(canvas);
|
ro.observe(canvas);
|
||||||
return () => ro.disconnect();
|
return () => ro.disconnect();
|
||||||
}, [ready, render]);
|
}, [ready]);
|
||||||
|
|
||||||
// Maus-Navigation (MITTE = Orbit, Shift+MITTE = Pan, RAD = Zoom).
|
// Maus-Navigation (MITTE = Orbit, Shift+MITTE = Pan, RAD = Zoom). Verlässt
|
||||||
|
// damit IMMER die Praeset-Kamera und wechselt auf die freie perspektivische
|
||||||
|
// Orbit-Kamera (Kontinuität über den in VIEW3D_ORBIT vorbereiteten Start-
|
||||||
|
// Blickwinkel) — gleiche Philosophie wie OrbitControls in der three.js-Sicht.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!ready || !canvas) return;
|
if (!ready || !canvas) return;
|
||||||
|
|
||||||
let drag: { mode: "orbit" | "pan"; x: number; y: number } | null = null;
|
const useOrbitRedraw = () => {
|
||||||
|
redrawRef.current = () => {
|
||||||
const redraw = () => {
|
|
||||||
if (orbitRef.current) render(orbitCamera(orbitRef.current));
|
if (orbitRef.current) render(orbitCamera(orbitRef.current));
|
||||||
};
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let drag: { mode: "orbit" | "pan"; x: number; y: number } | null = null;
|
||||||
|
|
||||||
|
const redraw = () => redrawRef.current();
|
||||||
|
|
||||||
const onPointerDown = (e: PointerEvent) => {
|
const onPointerDown = (e: PointerEvent) => {
|
||||||
if (e.button !== 1) return; // nur MITTE navigiert (links = Auswahl, s. o.)
|
if (e.button !== 1) return; // nur MITTE navigiert (links = Auswahl, s. o.)
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
drag = { mode: e.shiftKey ? "pan" : "orbit", x: e.clientX, y: e.clientY };
|
drag = { mode: e.shiftKey ? "pan" : "orbit", x: e.clientX, y: e.clientY };
|
||||||
canvas.setPointerCapture(e.pointerId);
|
canvas.setPointerCapture(e.pointerId);
|
||||||
// Freies Navigieren verlässt den Preset-Blickwinkel — Knopf-Hervorhebung
|
useOrbitRedraw();
|
||||||
// löschen (EINMAL je Geste, nicht je Pointermove: kein State-Update im
|
|
||||||
// Bewegungs-Hotpath, s. Kommentar oben zum Orbit-Ref).
|
|
||||||
setActivePreset((prev) => (prev === null ? prev : null));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPointerMove = (e: PointerEvent) => {
|
const onPointerMove = (e: PointerEvent) => {
|
||||||
@@ -319,9 +295,9 @@ export function Wasm3DViewport({ project }: { project: Project }) {
|
|||||||
const o = orbitRef.current;
|
const o = orbitRef.current;
|
||||||
if (!o) return;
|
if (!o) return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
useOrbitRedraw();
|
||||||
// Exponentielles Dolly (rad-deltaY ≈ ±100 je Rastung → ~14 % je Schritt).
|
// Exponentielles Dolly (rad-deltaY ≈ ±100 je Rastung → ~14 % je Schritt).
|
||||||
o.dist = Math.max(0.2, Math.min(2000, o.dist * Math.exp(e.deltaY * 0.0013)));
|
o.dist = Math.max(0.2, Math.min(2000, o.dist * Math.exp(e.deltaY * 0.0013)));
|
||||||
setActivePreset((prev) => (prev === null ? prev : null));
|
|
||||||
redraw();
|
redraw();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -347,63 +323,10 @@ export function Wasm3DViewport({ project }: { project: Project }) {
|
|||||||
return (
|
return (
|
||||||
<div className="viewport">
|
<div className="viewport">
|
||||||
{/* Canvas-Anzeigegröße kommt aus `.viewport canvas` (styles.css); die
|
{/* Canvas-Anzeigegröße kommt aus `.viewport canvas` (styles.css); die
|
||||||
Renderauflösung (DPR) gleicht der Hook je Frame ab. */}
|
Renderauflösung (DPR) gleicht der Hook je Frame ab. Presets +
|
||||||
|
Darstellungsart kommen jetzt von der Oberleiste (TopBar), kein
|
||||||
|
eigenes Overlay mehr hier. */}
|
||||||
<canvas ref={canvasRef} data-engine="render3d" />
|
<canvas ref={canvasRef} data-engine="render3d" />
|
||||||
{!failed && (
|
|
||||||
<div className="wasm3d-overlay">
|
|
||||||
{/* Kamera-Presets (Vectorworks/DOSSIER-Stil, s. VIEW_PRESETS oben) —
|
|
||||||
„dorthin springen", kein Lock: danach bleibt Orbit/Pan/Zoom frei. */}
|
|
||||||
<div
|
|
||||||
className="wasm3d-overlay-group"
|
|
||||||
role="group"
|
|
||||||
aria-label={t("view3d.group")}
|
|
||||||
>
|
|
||||||
{VIEW_PRESET_ORDER.map((key) => (
|
|
||||||
<button
|
|
||||||
key={key}
|
|
||||||
type="button"
|
|
||||||
className={`wasm3d-overlay-btn${activePreset === key ? " active" : ""}`}
|
|
||||||
disabled={!ready}
|
|
||||||
onClick={() => applyPreset(key)}
|
|
||||||
title={viewPresetLabel(key)}
|
|
||||||
aria-label={viewPresetLabel(key)}
|
|
||||||
>
|
|
||||||
{viewPresetLabel(key)}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{/* Darstellungsart: shaded (Bauteil-/Materialfarbe, Default) vs. white
|
|
||||||
(einheitliches beleuchtetes Hellgrau, Clay-Look) — Rust-Uniform
|
|
||||||
`mode.x` (shaders.rs). Texturiert/Drahtgitter/Kanten der three.js-
|
|
||||||
Sicht sind hier (noch) nicht gebaut, s. Datei-Kommentar. */}
|
|
||||||
<div
|
|
||||||
className="wasm3d-overlay-group"
|
|
||||||
role="group"
|
|
||||||
aria-label={t("display.style.group")}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`wasm3d-overlay-btn${renderMode === "shaded" ? " active" : ""}`}
|
|
||||||
disabled={!ready}
|
|
||||||
onClick={() => setRenderMode("shaded")}
|
|
||||||
title={t("display.style.shaded")}
|
|
||||||
aria-label={t("display.style.shaded")}
|
|
||||||
>
|
|
||||||
{t("display.style.shaded")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`wasm3d-overlay-btn${renderMode === "white" ? " active" : ""}`}
|
|
||||||
disabled={!ready}
|
|
||||||
onClick={() => setRenderMode("white")}
|
|
||||||
title={t("display.style.white")}
|
|
||||||
aria-label={t("display.style.white")}
|
|
||||||
>
|
|
||||||
{t("display.style.white")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{failed && (
|
{failed && (
|
||||||
<div className="viewport-fallback">
|
<div className="viewport-fallback">
|
||||||
<strong>{t("viewport3d.engineUnavailable")}</strong>
|
<strong>{t("viewport3d.engineUnavailable")}</strong>
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export interface Camera3d {
|
|||||||
interface WebModelRendererLike {
|
interface WebModelRendererLike {
|
||||||
set_model(json: string): void;
|
set_model(json: string): void;
|
||||||
set_render_mode_white(white: boolean): void;
|
set_render_mode_white(white: boolean): void;
|
||||||
|
set_view_preset(preset: string): void;
|
||||||
set_camera(
|
set_camera(
|
||||||
eyeX: number,
|
eyeX: number,
|
||||||
eyeY: number,
|
eyeY: number,
|
||||||
@@ -144,9 +145,33 @@ export function useWasm3dRenderer(
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/** Einen Frame mit der gegebenen Kamera zeichnen (Canvas-Größe + DPR abgleichen). */
|
/**
|
||||||
|
* Kamera-Praeset anwenden (`front`/`top`/`side`/`iso`/`perspective`, wie
|
||||||
|
* `View3d` in TopBar.tsx). Delegiert die formatfuellende Ziel-/Distanz-
|
||||||
|
* Berechnung + Projektionsart (ortho fuer front/top/side) an Rust
|
||||||
|
* (`web::set_view_preset` -> `math::preset_camera`) — dieselbe Logik wie das
|
||||||
|
* native Fenster, statt einer zweiten Praeset-Tabelle in TS. Wirkt erst beim
|
||||||
|
* naechsten `render`.
|
||||||
|
*/
|
||||||
|
const applyViewPreset = useCallback((preset: string) => {
|
||||||
|
const r = rendererRef.current;
|
||||||
|
if (!r) return;
|
||||||
|
try {
|
||||||
|
r.set_view_preset(preset);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("render3d set_view_preset fehlgeschlagen:", e);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Einen Frame zeichnen (Canvas-Größe + DPR abgleichen). `cam` = null lässt
|
||||||
|
* die zuletzt in Rust gesetzte Kamera unverändert — Fall für `render(null)`
|
||||||
|
* direkt nach `applyViewPreset` (die Kamera kommt dann aus `preset_camera`,
|
||||||
|
* ein `set_camera` hier würde das freie Orbit-Ergebnis darüberschreiben und
|
||||||
|
* die orthografische Projektion der Presets zunichtemachen).
|
||||||
|
*/
|
||||||
const render = useCallback(
|
const render = useCallback(
|
||||||
(cam: Camera3d) => {
|
(cam: Camera3d | null) => {
|
||||||
const r = rendererRef.current;
|
const r = rendererRef.current;
|
||||||
const canvas = canvasRef.current;
|
const canvas = canvasRef.current;
|
||||||
if (!r || !canvas) return;
|
if (!r || !canvas) return;
|
||||||
@@ -160,6 +185,7 @@ export function useWasm3dRenderer(
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
r.resize(w, h);
|
r.resize(w, h);
|
||||||
|
if (cam) {
|
||||||
r.set_camera(
|
r.set_camera(
|
||||||
cam.eye[0],
|
cam.eye[0],
|
||||||
cam.eye[1],
|
cam.eye[1],
|
||||||
@@ -174,6 +200,7 @@ export function useWasm3dRenderer(
|
|||||||
cam.fovY,
|
cam.fovY,
|
||||||
cam.orthoHalfHeight,
|
cam.orthoHalfHeight,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
r.render();
|
r.render();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("render3d render fehlgeschlagen:", e);
|
console.warn("render3d render fehlgeschlagen:", e);
|
||||||
@@ -182,5 +209,5 @@ export function useWasm3dRenderer(
|
|||||||
[canvasRef],
|
[canvasRef],
|
||||||
);
|
);
|
||||||
|
|
||||||
return { render, updateModel, setWhiteMode, ready, failed };
|
return { render, updateModel, setWhiteMode, applyViewPreset, ready, failed };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
/* Steuer-Overlay des engine-3D-Viewports (Wasm3DViewport): Kamera-Presets +
|
|
||||||
* Darstellungsart. Bewusst eine EIGENE Datei (nicht styles.css) — dieser
|
|
||||||
* Viewport-Pfad wird unabhängig von der three.js-Sicht weiterentwickelt.
|
|
||||||
* Nutzt dieselben CSS-Variablen wie die übrige App (in :root definiert), damit
|
|
||||||
* sich der Overlay optisch nahtlos einfügt (auch im Dunkelmodus). */
|
|
||||||
|
|
||||||
.wasm3d-overlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
left: 10px;
|
|
||||||
z-index: 3;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wasm3d-overlay-group {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 3px;
|
|
||||||
padding: 4px;
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 8px;
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
|
|
||||||
pointer-events: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wasm3d-overlay-btn {
|
|
||||||
min-width: 34px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
font-size: 11px;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--ink, #ddd);
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
border-radius: 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.wasm3d-overlay-btn:hover {
|
|
||||||
background: var(--accent-dim);
|
|
||||||
border-color: var(--accent-border);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.wasm3d-overlay-btn.active {
|
|
||||||
background: var(--accent-dim);
|
|
||||||
border-color: var(--accent);
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user