35299307d6
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.
497 lines
21 KiB
Rust
497 lines
21 KiB
Rust
// Browser-Bindings (wasm32 + WebGPU): fuehrt die native GPU-Schicht (`gpu::Renderer`)
|
||
// als WASM-Modul in die App-UI, mit einer HTML-Canvas als wgpu-Surface. Bewusst
|
||
// duenn — die gesamte Zeichenlogik (Wand-/Decken-Extrusion, Pipeline, Licht) ist
|
||
// mit dem nativen Fenster-Spike (bin/spike3d) geteilt; hier liegt nur die Canvas-/
|
||
// WebGPU-Anbindung + die wasm-bindgen-Fassade. Muster: render2d/src/web.rs.
|
||
//
|
||
// Ablauf im Frontend (siehe src/viewport/useWasm3dRenderer.ts):
|
||
// const r = await WebModelRenderer.new(canvas);
|
||
// r.set_model(JSON.stringify(projectToModel3d(project))); // bei Modelländerung
|
||
// r.set_camera(eye.x, eye.y, eye.z, target.x, target.y, target.z,
|
||
// up.x, up.y, up.z, perspective, fovY, orthoHalfHeight);
|
||
// r.resize(px_w, px_h); r.render(); // pro Frame
|
||
//
|
||
// KAMERA: zwei Wege, dieselbe `Camera`. `set_camera` nimmt eine FERTIGE Kamera
|
||
// (eye/target/up + Projektionsart) entgegen — die freie Orbit/Pan/Zoom-Interaktion
|
||
// (Yaw/Pitch/Distanz) rechnet weiterhin die TS-Seite (`math::orbit_eye`-Konvention),
|
||
// damit die Maus-UX 1:1 der bestehenden three.js-OrbitControls-Bedienung folgt.
|
||
// `set_view_preset` wendet stattdessen eines der `View3d`-Praesete (front/back/
|
||
// top/side/left/iso/isoFrontLeft/isoBackRight/isoBackLeft/perspective) direkt
|
||
// ueber `math::preset_camera` an — alle ausser Perspective werden dabei ECHT
|
||
// orthografisch (`preset_camera`; die Iso-Varianten = echte Isometrie ohne
|
||
// perspektivische Verzerrung), 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
|
||
// zu `RModel3d` (src/plan/toWalls3d.ts) und zum nativen Tauri-Push (nativeSync.ts).
|
||
|
||
use serde::Deserialize;
|
||
use wasm_bindgen::prelude::*;
|
||
use web_sys::HtmlCanvasElement;
|
||
|
||
use crate::gpu::Renderer;
|
||
use crate::math::preset_camera;
|
||
use crate::section::{cut_section, SectionPlane};
|
||
use crate::types::{Camera, CameraPreset, MeshInput, Projection, SlabInput, WallInput};
|
||
|
||
/// JSON-Eingabe des Modells: Waende (bereits in Teilquader zerlegt, siehe
|
||
/// `projectToWalls3d`) + Deckenplatten. `slabs` optional (leeres Modell ohne
|
||
/// Decken bleibt gueltig).
|
||
#[derive(Deserialize)]
|
||
struct ModelInput {
|
||
walls: Vec<WallInput>,
|
||
#[serde(default)]
|
||
slabs: Vec<SlabInput>,
|
||
/// Rohe Kontext-Meshes (Terrain/importierte Volumen). `#[serde(default)]`
|
||
/// haelt aeltere Payloads ohne `meshes` gueltig.
|
||
#[serde(default)]
|
||
meshes: Vec<MeshInput>,
|
||
}
|
||
|
||
/// Berechnet einen Schnitt/eine Ansicht (`section::cut_section`) gegen das Modell
|
||
/// und gibt das `SectionOutput` als JSON-String zurueck. GPU-frei — reine
|
||
/// Prismen-Arithmetik (siehe `section.rs`); braucht KEINEN `WebModelRenderer`
|
||
/// (kein WebGPU-Kontext), nur das geladene WASM-Modul. Aufruf aus JS:
|
||
///
|
||
/// ```js
|
||
/// const json = mod.cut_section_json(
|
||
/// JSON.stringify({ walls, slabs }), // gleiches Format wie set_model
|
||
/// px, py, pz, // Punkt auf der Schnittebene (world-Meter, Y-up)
|
||
/// nx, ny, nz, // Ebenennormale = Blickrichtung (wird normalisiert)
|
||
/// );
|
||
/// const out = JSON.parse(json); // { cutPolygons, visibleEdges, hiddenEdges }
|
||
/// ```
|
||
///
|
||
/// Koordinaten der Ausgabe: (u, v) in Metern — `u` horizontal entlang der
|
||
/// Ebene, `v = world.y` (absolute Hoehe). Siehe `section.rs`-Moduldoc.
|
||
#[wasm_bindgen]
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn cut_section_json(
|
||
model_json: &str,
|
||
px: f32,
|
||
py: f32,
|
||
pz: f32,
|
||
nx: f32,
|
||
ny: f32,
|
||
nz: f32,
|
||
) -> Result<String, JsValue> {
|
||
let model: ModelInput = serde_json::from_str(model_json)
|
||
.map_err(|e| JsValue::from_str(&format!("Modell parsen: {e}")))?;
|
||
let plane = SectionPlane::new([px, py, pz], [nx, ny, nz]);
|
||
let out = cut_section(&plane, &model.walls, &model.slabs);
|
||
serde_json::to_string(&out)
|
||
.map_err(|e| JsValue::from_str(&format!("SectionOutput serialisieren: {e}")))
|
||
}
|
||
|
||
/// Der Browser-Renderer: haelt Surface + Device/Queue + den geteilten `Renderer`
|
||
/// und die aktuelle Kamera. Wird per `WebModelRenderer.new(canvas)` aus JS erzeugt.
|
||
#[wasm_bindgen]
|
||
pub struct WebModelRenderer {
|
||
surface: wgpu::Surface<'static>,
|
||
device: wgpu::Device,
|
||
queue: wgpu::Queue,
|
||
config: wgpu::SurfaceConfiguration,
|
||
renderer: Renderer,
|
||
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])>,
|
||
/// Gecachte Wand-/Decken-Eingabe des zuletzt gesetzten Modells. `set_model`
|
||
/// verwirft sie nach dem GPU-Upload NICHT (anders als der Renderer selbst),
|
||
/// weil der Live-Schnitt (`set_section_plane`) daraus jederzeit die
|
||
/// Cut-Cap-Geometrie neu berechnen koennen muss (GPU-frei, `cut_section`).
|
||
walls: Vec<WallInput>,
|
||
slabs: Vec<SlabInput>,
|
||
/// Aktueller Live-Schnitt-Zustand: aktiv? + Ebene (point/normal). Gemerkt,
|
||
/// damit ein `set_model` bei aktivem Schnitt die Caps automatisch neu baut.
|
||
section: 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]
|
||
impl WebModelRenderer {
|
||
/// Erzeugt den Renderer asynchron (Adapter/Device via WebGPU).
|
||
/// Aus JS: `await WebModelRenderer.new(canvasEl)`.
|
||
#[allow(clippy::new_ret_no_self)]
|
||
pub async fn new(canvas: HtmlCanvasElement) -> Result<WebModelRenderer, JsValue> {
|
||
// Panics als lesbare Konsolen-Fehler statt "unreachable" ausgeben.
|
||
console_error_panic_hook::set_once();
|
||
|
||
let width = canvas.width().max(1);
|
||
let height = canvas.height().max(1);
|
||
|
||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
|
||
backends: wgpu::Backends::BROWSER_WEBGPU,
|
||
..wgpu::InstanceDescriptor::new_without_display_handle()
|
||
});
|
||
let surface = instance
|
||
.create_surface(wgpu::SurfaceTarget::Canvas(canvas))
|
||
.map_err(|e| JsValue::from_str(&format!("create_surface: {e:?}")))?;
|
||
|
||
let adapter = instance
|
||
.request_adapter(&wgpu::RequestAdapterOptions {
|
||
power_preference: wgpu::PowerPreference::HighPerformance,
|
||
force_fallback_adapter: false,
|
||
compatible_surface: Some(&surface),
|
||
})
|
||
.await
|
||
.map_err(|e| {
|
||
JsValue::from_str(&format!("kein WebGPU-Adapter (navigator.gpu?): {e}"))
|
||
})?;
|
||
|
||
let (device, queue) = adapter
|
||
.request_device(&wgpu::DeviceDescriptor {
|
||
label: Some("render3d.web.device"),
|
||
required_features: wgpu::Features::empty(),
|
||
required_limits: wgpu::Limits::downlevel_webgl2_defaults()
|
||
.using_resolution(adapter.limits()),
|
||
experimental_features: wgpu::ExperimentalFeatures::disabled(),
|
||
memory_hints: wgpu::MemoryHints::Performance,
|
||
trace: wgpu::Trace::Off,
|
||
})
|
||
.await
|
||
.map_err(|e| JsValue::from_str(&format!("request_device: {e:?}")))?;
|
||
|
||
// WebGPU-Canvas liefert i.d.R. bgra8unorm (kein sRGB-Format fuer die
|
||
// Surface) — Format vom Kontext abfragen statt hart zu kodieren (dieselbe
|
||
// Wahl wie render2d: erstes NICHT-sRGB-Format, sonst das erste angebotene).
|
||
let caps = surface.get_capabilities(&adapter);
|
||
let format = caps
|
||
.formats
|
||
.iter()
|
||
.copied()
|
||
.find(|f| !f.is_srgb())
|
||
.unwrap_or(caps.formats[0]);
|
||
let config = wgpu::SurfaceConfiguration {
|
||
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
|
||
format,
|
||
width,
|
||
height,
|
||
present_mode: wgpu::PresentMode::Fifo,
|
||
alpha_mode: caps.alpha_modes[0],
|
||
view_formats: vec![],
|
||
desired_maximum_frame_latency: 2,
|
||
};
|
||
surface.configure(&device, &config);
|
||
|
||
let renderer = Renderer::new(&device, format);
|
||
|
||
web_sys::console::log_1(&JsValue::from_str(&format!(
|
||
"render3d WebGPU-Viewport initialisiert ({}x{}, {:?})",
|
||
width, height, format
|
||
)));
|
||
|
||
Ok(WebModelRenderer {
|
||
surface,
|
||
device,
|
||
queue,
|
||
config,
|
||
renderer,
|
||
camera: Camera::default(),
|
||
bounds: None,
|
||
walls: Vec::new(),
|
||
slabs: Vec::new(),
|
||
section: None,
|
||
})
|
||
}
|
||
|
||
/// Setzt/aktualisiert das darzustellende Modell aus einem JSON-String
|
||
/// (Format = `{ walls: WallInput[], slabs?: SlabInput[] }`, identisch zum
|
||
/// 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> {
|
||
let model: ModelInput = serde_json::from_str(json)
|
||
.map_err(|e| JsValue::from_str(&format!("Modell parsen: {e}")))?;
|
||
self.bounds = model_bounds(&model.walls, &model.slabs);
|
||
self.renderer
|
||
.upload_model(&self.device, &model.walls, &model.slabs, &model.meshes);
|
||
// Wand-/Decken-Eingabe fuer den Live-Schnitt cachen (Klon, da der Renderer
|
||
// sie nur zu GPU-Puffern verarbeitet und verwirft).
|
||
self.walls = model.walls;
|
||
self.slabs = model.slabs;
|
||
// Ist der Schnitt aktiv, die Caps am neuen Modell neu aufbauen.
|
||
if let Some((point, normal)) = self.section {
|
||
self.renderer.update_section(
|
||
&self.device,
|
||
true,
|
||
point,
|
||
normal,
|
||
&self.walls,
|
||
&self.slabs,
|
||
);
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
/// Live-Schnitt ein-/ausschalten und die Schnittebene setzen. `active=false`
|
||
/// hebt den Schnitt auf (Modell wieder komplett, keine Caps). `p*` = Punkt auf
|
||
/// der Ebene, `n*` = Ebenennormale = Blickrichtung (world-Meter, Y-up; wird
|
||
/// normalisiert). NUR VERTIKALE Ebenen sind sinnvoll (Normale ohne Y-Anteil —
|
||
/// Annahme von `section.rs`). Kappt Mesh/Kanten/Grid per Shader-`discard` und
|
||
/// baut aus den gecachten Waenden/Decken die Schnittflaechen-Schraffur (Caps).
|
||
/// Wirkt erst beim naechsten `render`.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn set_section_plane(
|
||
&mut self,
|
||
active: bool,
|
||
px: f32,
|
||
py: f32,
|
||
pz: f32,
|
||
nx: f32,
|
||
ny: f32,
|
||
nz: f32,
|
||
) -> Result<(), JsValue> {
|
||
let point = [px, py, pz];
|
||
let normal = [nx, ny, nz];
|
||
self.section = if active { Some((point, normal)) } else { None };
|
||
self.renderer
|
||
.update_section(&self.device, active, point, normal, &self.walls, &self.slabs);
|
||
Ok(())
|
||
}
|
||
|
||
/// Setzt die Kamera fuer den naechsten `render`-Aufruf. `eye`/`target`/`up`
|
||
/// in world-Metern (Y-up, wie `types::Camera`). `perspective=true` waehlt eine
|
||
/// perspektivische Projektion (`fov_y` in Radiant), sonst orthografisch
|
||
/// (`ortho_half_height` = halbe Sichthoehe in Metern). Die TS-Seite berechnet
|
||
/// `eye` aus Orbit-Winkeln/Distanz oder einem Praeset (front/top/side/iso/persp)
|
||
/// — siehe `useWasm3dRenderer`/`Wasm3DViewport`.
|
||
#[allow(clippy::too_many_arguments)]
|
||
pub fn set_camera(
|
||
&mut self,
|
||
eye_x: f32,
|
||
eye_y: f32,
|
||
eye_z: f32,
|
||
target_x: f32,
|
||
target_y: f32,
|
||
target_z: f32,
|
||
up_x: f32,
|
||
up_y: f32,
|
||
up_z: f32,
|
||
perspective: bool,
|
||
fov_y: f32,
|
||
ortho_half_height: f32,
|
||
) {
|
||
self.camera = Camera {
|
||
eye: [eye_x, eye_y, eye_z],
|
||
target: [target_x, target_y, target_z],
|
||
up: [up_x, up_y, up_z],
|
||
projection: if perspective {
|
||
Projection::Perspective
|
||
} else {
|
||
Projection::Orthographic
|
||
},
|
||
fov_y,
|
||
ortho_half_height,
|
||
..Camera::default()
|
||
};
|
||
}
|
||
|
||
/// Wendet eines der Kardinal-/Iso-Kamera-Praesets der three.js-Sicht
|
||
/// (`front`/`back`/`top`/`side`/`left`/`iso`/`isoFrontLeft`/`isoBackRight`/
|
||
/// `isoBackLeft`/`perspective`, s. `View3d` in TopBar.tsx) auf das aktuelle
|
||
/// Modell an: Ziel = Modell-Mitte, Distanz = formatfuellend (mirror der
|
||
/// TS-`fitTargetDist`); alle Presets ausser Perspective werden orthografisch,
|
||
/// nur Perspective ist 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,
|
||
"back" => CameraPreset::Back,
|
||
"top" => CameraPreset::Top,
|
||
"side" => CameraPreset::Side,
|
||
"left" => CameraPreset::Left,
|
||
"iso" => CameraPreset::Iso,
|
||
"isoFrontLeft" => CameraPreset::IsoFrontLeft,
|
||
"isoBackRight" => CameraPreset::IsoBackRight,
|
||
"isoBackLeft" => CameraPreset::IsoBackLeft,
|
||
"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 der Oberleiste setzen. Gueltige Werte:
|
||
/// - `"shaded"` — beleuchtete Bauteilfarben (Default).
|
||
/// - `"white"` — Clay-Look: einheitlich helles Material, beleuchtet
|
||
/// (identisch zum bisherigen `set_render_mode_white(true)`).
|
||
/// - `"textured"` — Material-Farbkarten je Wand-Band aus dem Textur-Array
|
||
/// (`set_material_textures`), Fallback-Schachbrett fuer Baender ohne Material.
|
||
/// - `"wireframe"`— nur Modell-Kanten (Feature-Edges), keine Flaechen.
|
||
/// - `"hidden"` — Hidden-Line: flach-weisse Flaechen fuellen den Depth-
|
||
/// Buffer, dunkle Kanten liegen tiefengetestet obenauf (nur sichtbare).
|
||
/// Unbekannte Werte fallen auf `"shaded"` zurueck. Wirkt erst beim naechsten
|
||
/// `render`. Loest `set_render_mode_white` ab (das bleibt kompatibel).
|
||
pub fn set_render_style(&mut self, style: &str) -> Result<(), JsValue> {
|
||
self.renderer
|
||
.set_render_style(crate::gpu::RenderStyle::from_str_or_shaded(style));
|
||
Ok(())
|
||
}
|
||
|
||
/// Darstellungsart umschalten: `true` = weisses Clay-Material (beleuchtet,
|
||
/// ohne Bauteil-Farben — Vectorworks „white"), `false` = normale Material-
|
||
/// farbe (shaded, Default). Wirkt erst beim naechsten `render`. RUECKWAERTS-
|
||
/// KOMPATIBEL — bildet intern auf denselben `render_style`-Zustand ab wie
|
||
/// `set_render_style` (true→„white", false→„shaded"), sodass es keine zwei
|
||
/// widersprechenden Zustaende gibt.
|
||
pub fn set_render_mode_white(&mut self, white: bool) {
|
||
self.renderer.set_white_mode(white);
|
||
}
|
||
|
||
/// Schaltet das Referenz-Bodengitter (grid.rs) ein/aus und setzt seine
|
||
/// Parameter. `elevation_m` = Hoehe der XZ-Gitterebene (world-Meter, i. d. R.
|
||
/// die z=0-Ebene des aktiven Geschosses); `extent_m` = quadratische Kantenlaenge
|
||
/// (auf ein Vielfaches von `spacing_m` gerundet); `spacing_m` = Linienabstand.
|
||
/// Das Gitter ist zentriert um den Welt-Ursprung (x=0, z=0). Wirkt erst beim
|
||
/// naechsten `render`. Default ohne Aufruf: unsichtbar (bestehende Aufrufer
|
||
/// bleiben unveraendert). Jede 5. Linie (inkl. Mittelachse) ist eine Major-Linie.
|
||
pub fn set_ground_grid(
|
||
&mut self,
|
||
visible: bool,
|
||
elevation_m: f32,
|
||
extent_m: f32,
|
||
spacing_m: f32,
|
||
) -> Result<(), JsValue> {
|
||
self.renderer
|
||
.set_ground_grid(&self.device, visible, elevation_m, extent_m, spacing_m);
|
||
Ok(())
|
||
}
|
||
|
||
/// Setzt/loescht die Auswahl-Highlight-Linien (Umriss/Kanten des selektierten
|
||
/// Bauteils). `vertices` = interleaved LineList-Vertices
|
||
/// `[px,py,pz, r,g,b, ...]` (world-Meter, je 2 Vertices = 1 Segment) — exakt
|
||
/// dasselbe Format wie die Grid-/Edge-Vertices. Ein leeres Slice loescht das
|
||
/// Highlight (nichts wird gezeichnet). Die Linien liegen im Render-Pass ohne
|
||
/// Tiefentest immer sichtbar obenauf (scheinen auch hinter Waenden durch).
|
||
/// Wirkt erst beim naechsten `render`.
|
||
pub fn set_highlight_lines(&mut self, vertices: &[f32]) -> Result<(), JsValue> {
|
||
self.renderer.set_highlight_lines(&self.device, vertices);
|
||
Ok(())
|
||
}
|
||
|
||
/// Laedt die Material-Farbkarten fuer den Stil `Textured` als Textur-Array hoch
|
||
/// (`Renderer::set_material_textures`). `rgba` = dicht gepackte RGBA8-Bytes von
|
||
/// `layer_count` Ebenen, JEDE exakt 256×256 (der JS-Aufrufer dekodiert/skaliert
|
||
/// die Material-Bilder, siehe `viewport/materialTextures.ts`). Ebene 0 bleibt
|
||
/// intern das Fallback-Schachbrett; die hochgeladenen Karten belegen die Ebenen
|
||
/// 1..N, auf die `WallInput::materialIndex` (1-basiert) zeigt. `layer_count == 0`
|
||
/// oder zu kleine Daten setzen auf das reine Schachbrett zurueck. Wirkt erst beim
|
||
/// naechsten `render`.
|
||
pub fn set_material_textures(&mut self, rgba: &[u8], layer_count: u32) -> Result<(), JsValue> {
|
||
self.renderer
|
||
.set_material_textures(&self.device, &self.queue, rgba, layer_count);
|
||
Ok(())
|
||
}
|
||
|
||
/// Surface an eine neue Pixelgroesse anpassen (DPR beachtet der Aufrufer).
|
||
pub fn resize(&mut self, width: u32, height: u32) {
|
||
let (w, h) = (width.max(1), height.max(1));
|
||
if self.config.width == w && self.config.height == h {
|
||
return;
|
||
}
|
||
self.config.width = w;
|
||
self.config.height = h;
|
||
self.surface.configure(&self.device, &self.config);
|
||
}
|
||
|
||
/// Zeichnet EINEN Frame in die Surface-Textur.
|
||
pub fn render(&mut self) {
|
||
// wgpu 29: `get_current_texture` liefert ein Enum statt eines Results —
|
||
// Suboptimal ist weiterhin praesentierbar, Lost/Outdated -> rekonfigurieren.
|
||
let frame = match self.surface.get_current_texture() {
|
||
wgpu::CurrentSurfaceTexture::Success(f)
|
||
| wgpu::CurrentSurfaceTexture::Suboptimal(f) => f,
|
||
wgpu::CurrentSurfaceTexture::Lost | wgpu::CurrentSurfaceTexture::Outdated => {
|
||
self.surface.configure(&self.device, &self.config);
|
||
return;
|
||
}
|
||
other => {
|
||
web_sys::console::warn_1(&JsValue::from_str(&format!(
|
||
"render3d Surface-Fehler: {other:?}"
|
||
)));
|
||
return;
|
||
}
|
||
};
|
||
let view = frame
|
||
.texture
|
||
.create_view(&wgpu::TextureViewDescriptor::default());
|
||
self.renderer.render(
|
||
&self.device,
|
||
&self.queue,
|
||
&view,
|
||
&self.camera,
|
||
(self.config.width, self.config.height),
|
||
);
|
||
frame.present();
|
||
}
|
||
}
|