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:
@@ -16,7 +16,9 @@ use crate::edges::{build_mesh_edges, EDGE_FLOATS_PER_VERTEX};
|
|||||||
use crate::grid::{build_ground_grid, GRID_FLOATS_PER_VERTEX};
|
use crate::grid::{build_ground_grid, GRID_FLOATS_PER_VERTEX};
|
||||||
use crate::math::{view_projection, Mat4};
|
use crate::math::{view_projection, Mat4};
|
||||||
use crate::mesh::{build_scene_mesh, build_walls_mesh};
|
use crate::mesh::{build_scene_mesh, build_walls_mesh};
|
||||||
use crate::shaders::{GRID_WGSL, MESH_WGSL};
|
use crate::section::SectionPlane;
|
||||||
|
use crate::section_fill::{build_cut_caps, CAP_FLOATS_PER_VERTEX};
|
||||||
|
use crate::shaders::{CAP_WGSL, GRID_WGSL, MESH_WGSL};
|
||||||
use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
||||||
|
|
||||||
/// Darstellungsart des 3D-Renderers (Oberleiste: shaded/white/textured/
|
/// Darstellungsart des 3D-Renderers (Oberleiste: shaded/white/textured/
|
||||||
@@ -90,8 +92,13 @@ struct Globals {
|
|||||||
sky_color: [f32; 4],
|
sky_color: [f32; 4],
|
||||||
ground_color: [f32; 4],
|
ground_color: [f32; 4],
|
||||||
sun_color: [f32; 4],
|
sun_color: [f32; 4],
|
||||||
/// x = Darstellungsart (0 = shaded/Material, 1 = weiss/Clay); siehe shaders.rs.
|
/// x = Darstellungsart (0 = shaded/Material, 1 = weiss/Clay, 2 = hidden);
|
||||||
|
/// y = Live-Schnitt aktiv (0/1). Siehe shaders.rs.
|
||||||
mode: [f32; 4],
|
mode: [f32; 4],
|
||||||
|
/// Live-Schnittebene: xyz = Ebenennormale (= Blickrichtung, normiert),
|
||||||
|
/// w = -dot(normal, point). Ein Fragment mit `dot(world, xyz) + w > 0` liegt
|
||||||
|
/// hinter der Ebene und wird (bei aktivem Schnitt, mode.y > 0.5) verworfen.
|
||||||
|
section_plane: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Globals {
|
impl Default for Globals {
|
||||||
@@ -107,6 +114,8 @@ impl Default for Globals {
|
|||||||
// Directional-Sonne: warmweiss, moderat.
|
// Directional-Sonne: warmweiss, moderat.
|
||||||
sun_color: [0.55, 0.53, 0.49, 1.0],
|
sun_color: [0.55, 0.53, 0.49, 1.0],
|
||||||
mode: [0.0, 0.0, 0.0, 0.0],
|
mode: [0.0, 0.0, 0.0, 0.0],
|
||||||
|
// Default: kein Schnitt (mode.y = 0). Neutrale Ebene.
|
||||||
|
section_plane: [0.0, 0.0, 1.0, 0.0],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,6 +140,14 @@ struct LineBuffers {
|
|||||||
vertex_count: u32,
|
vertex_count: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GPU-seitiger Vertex-Buffer der Schnittflaechen-Kappen (Cut-Caps, TriangleList,
|
||||||
|
/// kein Indexpuffer). Format `[pos vec3, uv vec2]` (`CAP_FLOATS_PER_VERTEX`) —
|
||||||
|
/// die (u,v)-Ebenenkoordinaten treiben die prozedurale Schraffur (CAP_WGSL).
|
||||||
|
struct CapBuffers {
|
||||||
|
vbo: wgpu::Buffer,
|
||||||
|
vertex_count: u32,
|
||||||
|
}
|
||||||
|
|
||||||
/// Der native 3D-Renderer: haelt die Pipeline, das Uniform (View-Projektion +
|
/// Der native 3D-Renderer: haelt die Pipeline, das Uniform (View-Projektion +
|
||||||
/// Licht) und das aktuell hochgeladene Mesh. Ein Tiefenpuffer wird passend zur
|
/// Licht) und das aktuell hochgeladene Mesh. Ein Tiefenpuffer wird passend zur
|
||||||
/// Ziel-Groesse (neu) angelegt.
|
/// Ziel-Groesse (neu) angelegt.
|
||||||
@@ -150,6 +167,11 @@ pub struct Renderer {
|
|||||||
/// (`depth_compare: Always`, kein Depth-Write): die Auswahl-Highlight-Linien
|
/// (`depth_compare: Always`, kein Depth-Write): die Auswahl-Highlight-Linien
|
||||||
/// liegen immer sichtbar obenauf und scheinen auch hinter Waenden durch.
|
/// liegen immer sichtbar obenauf und scheinen auch hinter Waenden durch.
|
||||||
highlight_pipeline: wgpu::RenderPipeline,
|
highlight_pipeline: wgpu::RenderPipeline,
|
||||||
|
/// Schnittflaechen-Kappen (TriangleList, CAP_WGSL, eigenes Vertex-Layout
|
||||||
|
/// [pos vec3, uv vec2]). Teilt sich die `Globals`-Bind-Group; leichter
|
||||||
|
/// Depth-Bias zur Kamera hin gegen Z-Fighting mit den an der Ebene
|
||||||
|
/// per-`discard` gekappten Wandflaechen. Backface-Culling AUS (CullMode::None).
|
||||||
|
cap_pipeline: wgpu::RenderPipeline,
|
||||||
bind_group: wgpu::BindGroup,
|
bind_group: wgpu::BindGroup,
|
||||||
uniform: wgpu::Buffer,
|
uniform: wgpu::Buffer,
|
||||||
mesh: Option<MeshBuffers>,
|
mesh: Option<MeshBuffers>,
|
||||||
@@ -165,6 +187,10 @@ pub struct Renderer {
|
|||||||
/// `set_highlight_lines` gesetzt/geloescht; im Render-Pass als ALLERLETZTES
|
/// `set_highlight_lines` gesetzt/geloescht; im Render-Pass als ALLERLETZTES
|
||||||
/// (ohne Tiefentest) gezeichnet.
|
/// (ohne Tiefentest) gezeichnet.
|
||||||
highlight: Option<LineBuffers>,
|
highlight: Option<LineBuffers>,
|
||||||
|
/// Schnittflaechen-Kappen (Cut-Caps) des Live-Schnitts. None = kein Schnitt
|
||||||
|
/// aktiv / keine Schnittgeometrie. Ueber `set_cut_caps` gesetzt/geloescht,
|
||||||
|
/// gezeichnet NACH den Flaechen, VOR Kanten/Grid/Highlight.
|
||||||
|
caps: Option<CapBuffers>,
|
||||||
/// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false —
|
/// Ob das Bodengitter im naechsten `render` gezeichnet wird. Default false —
|
||||||
/// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter.
|
/// bestehende Aufrufer ohne `set_ground_grid` bekommen KEIN Gitter.
|
||||||
grid_visible: bool,
|
grid_visible: bool,
|
||||||
@@ -372,6 +398,70 @@ impl Renderer {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Schnittflaechen-Kappen-Pipeline: eigener Shader (CAP_WGSL), eigenes
|
||||||
|
// schlankes Vertex-Layout [pos vec3, uv vec2], TriangleList. Dieselbe
|
||||||
|
// Bind-Group (Globals) und dasselbe Depth-/MSAA-/Farbziel wie oben.
|
||||||
|
let cap_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("cap.wgsl"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(CAP_WGSL.into()),
|
||||||
|
});
|
||||||
|
let cap_vertex_layout = wgpu::VertexBufferLayout {
|
||||||
|
array_stride: (CAP_FLOATS_PER_VERTEX * 4) as u64,
|
||||||
|
step_mode: wgpu::VertexStepMode::Vertex,
|
||||||
|
attributes: &wgpu::vertex_attr_array![0 => Float32x3, 1 => Float32x2],
|
||||||
|
};
|
||||||
|
let cap_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("cap.pipeline"),
|
||||||
|
layout: Some(&pipeline_layout),
|
||||||
|
vertex: wgpu::VertexState {
|
||||||
|
module: &cap_module,
|
||||||
|
entry_point: Some("vs_main"),
|
||||||
|
buffers: &[cap_vertex_layout],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
},
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &cap_module,
|
||||||
|
entry_point: Some("fs_main"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState {
|
||||||
|
format: color_format,
|
||||||
|
blend: Some(wgpu::BlendState::REPLACE),
|
||||||
|
write_mask: wgpu::ColorWrites::ALL,
|
||||||
|
})],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
primitive: wgpu::PrimitiveState {
|
||||||
|
topology: wgpu::PrimitiveTopology::TriangleList,
|
||||||
|
// Die Cap ist eine flache Flaeche und soll aus BEIDEN Richtungen
|
||||||
|
// sichtbar sein (das Winding der Rueckprojektion ist nicht
|
||||||
|
// kamerakonsistent garantiert) -> kein Backface-Culling.
|
||||||
|
cull_mode: None,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format: DEPTH_FORMAT,
|
||||||
|
depth_write_enabled: Some(true),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::Less),
|
||||||
|
stencil: wgpu::StencilState::default(),
|
||||||
|
// Leicht ZUR Kamera ziehen (negativer Bias, Gegenteil des
|
||||||
|
// hidden_face_pipeline-Bias): die Caps liegen exakt auf der
|
||||||
|
// Schnittebene, an der die Wandflaechen weggeschnitten werden —
|
||||||
|
// der negative Bias vermeidet Z-Fighting mit an der Grenze
|
||||||
|
// gerade noch stehen gebliebenen Wandfragmenten.
|
||||||
|
bias: wgpu::DepthBiasState {
|
||||||
|
constant: -4,
|
||||||
|
slope_scale: -1.0,
|
||||||
|
clamp: 0.0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState {
|
||||||
|
count: SAMPLE_COUNT,
|
||||||
|
mask: !0,
|
||||||
|
alpha_to_coverage_enabled: false,
|
||||||
|
},
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
});
|
||||||
|
|
||||||
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
label: Some("globals.buffer"),
|
label: Some("globals.buffer"),
|
||||||
size: std::mem::size_of::<Globals>() as u64,
|
size: std::mem::size_of::<Globals>() as u64,
|
||||||
@@ -392,12 +482,14 @@ impl Renderer {
|
|||||||
hidden_face_pipeline,
|
hidden_face_pipeline,
|
||||||
grid_pipeline,
|
grid_pipeline,
|
||||||
highlight_pipeline,
|
highlight_pipeline,
|
||||||
|
cap_pipeline,
|
||||||
bind_group,
|
bind_group,
|
||||||
uniform,
|
uniform,
|
||||||
mesh: None,
|
mesh: None,
|
||||||
edges: None,
|
edges: None,
|
||||||
grid: None,
|
grid: None,
|
||||||
highlight: None,
|
highlight: None,
|
||||||
|
caps: None,
|
||||||
grid_visible: false,
|
grid_visible: false,
|
||||||
style: RenderStyle::Shaded,
|
style: RenderStyle::Shaded,
|
||||||
depth: None,
|
depth: None,
|
||||||
@@ -556,6 +648,61 @@ impl Renderer {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Aktiviert/deaktiviert den Live-Schnitt und setzt die Schnittebene. `active`
|
||||||
|
/// steuert `mode.y` (der Mesh-/Grid-Shader kappt die Geometrie nur bei > 0.5);
|
||||||
|
/// `point`/`normal` (world-Meter, Normale = Blickrichtung) fuellen
|
||||||
|
/// `section_plane = [n, -dot(n, point)]`. Nur die Uniform-Werte — die Cap-
|
||||||
|
/// Geometrie setzt der Aufrufer separat via `set_cut_caps`.
|
||||||
|
pub fn set_section_plane(&mut self, active: bool, point: [f32; 3], normal: [f32; 3]) {
|
||||||
|
let n = crate::math::normalize(normal);
|
||||||
|
let w = -(n[0] * point[0] + n[1] * point[1] + n[2] * point[2]);
|
||||||
|
self.globals.section_plane = [n[0], n[1], n[2], w];
|
||||||
|
self.globals.mode[1] = if active { 1.0 } else { 0.0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Setzt/loescht die Schnittflaechen-Kappen (Cut-Caps). `verts` = interleaved
|
||||||
|
/// `[px,py,pz, u,v, ...]` (world-Meter + Ebenenkoordinaten, TriangleList,
|
||||||
|
/// `CAP_FLOATS_PER_VERTEX`), wie `section_fill::build_cut_caps` liefert. Leeres
|
||||||
|
/// Slice loescht die Caps. Wirkt erst beim naechsten `render`.
|
||||||
|
pub fn set_cut_caps(&mut self, device: &wgpu::Device, verts: &[f32]) {
|
||||||
|
if verts.is_empty() {
|
||||||
|
self.caps = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let vbo = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||||
|
label: Some("caps.vbo"),
|
||||||
|
contents: bytemuck::cast_slice(verts),
|
||||||
|
usage: wgpu::BufferUsages::VERTEX,
|
||||||
|
});
|
||||||
|
self.caps = Some(CapBuffers {
|
||||||
|
vbo,
|
||||||
|
vertex_count: (verts.len() / CAP_FLOATS_PER_VERTEX) as u32,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Komfort: Schnittebene setzen UND die Cap-Geometrie aus den (gecachten)
|
||||||
|
/// Waenden/Decken neu bauen — bei `active == false` werden Uniform-Flag und
|
||||||
|
/// Caps geloescht. Buendelt `set_section_plane` + `build_cut_caps` +
|
||||||
|
/// `set_cut_caps`, damit `web.rs` nur EINEN Aufruf braucht.
|
||||||
|
pub fn update_section(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
active: bool,
|
||||||
|
point: [f32; 3],
|
||||||
|
normal: [f32; 3],
|
||||||
|
walls: &[WallInput],
|
||||||
|
slabs: &[SlabInput],
|
||||||
|
) {
|
||||||
|
self.set_section_plane(active, point, normal);
|
||||||
|
if active {
|
||||||
|
let plane = SectionPlane::new(point, normal);
|
||||||
|
let caps = build_cut_caps(&plane, walls, slabs);
|
||||||
|
self.set_cut_caps(device, &caps);
|
||||||
|
} else {
|
||||||
|
self.set_cut_caps(device, &[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Stellt sicher, dass ein Tiefenpuffer passend zur Ziel-Groesse existiert.
|
/// Stellt sicher, dass ein Tiefenpuffer passend zur Ziel-Groesse existiert.
|
||||||
fn ensure_depth(&mut self, device: &wgpu::Device, w: u32, h: u32) {
|
fn ensure_depth(&mut self, device: &wgpu::Device, w: u32, h: u32) {
|
||||||
let ok = matches!(&self.depth, Some(d) if d.width == w && d.height == h);
|
let ok = matches!(&self.depth, Some(d) if d.width == w && d.height == h);
|
||||||
@@ -692,6 +839,17 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 1b) Schnittflaechen-Kappen (Cut-Caps) NACH den Flaechen, VOR den
|
||||||
|
// Kanten: sie fuellen die an der Schnittebene weggeschnittene Wunde
|
||||||
|
// mit der Diagonalschraffur (CAP_WGSL). Werden selbst NICHT gekappt.
|
||||||
|
// Nur vorhanden, wenn der Live-Schnitt aktiv ist (set_cut_caps).
|
||||||
|
if let Some(c) = &self.caps {
|
||||||
|
pass.set_pipeline(&self.cap_pipeline);
|
||||||
|
pass.set_bind_group(0, &self.bind_group, &[]);
|
||||||
|
pass.set_vertex_buffer(0, c.vbo.slice(..));
|
||||||
|
pass.draw(0..c.vertex_count, 0..1);
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Modell-Kanten (wireframe/hidden) — LineList-Pipeline (wie Grid).
|
// 2) Modell-Kanten (wireframe/hidden) — LineList-Pipeline (wie Grid).
|
||||||
// Im Hidden-Stil obenauf, tiefengetestet gegen die (gebiasten)
|
// Im Hidden-Stil obenauf, tiefengetestet gegen die (gebiasten)
|
||||||
// Flaechen -> verdeckte Kanten fallen weg. Im Wireframe ohne
|
// Flaechen -> verdeckte Kanten fallen weg. Im Wireframe ohne
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub mod math;
|
|||||||
pub mod mesh;
|
pub mod mesh;
|
||||||
pub(crate) mod openings;
|
pub(crate) mod openings;
|
||||||
pub mod section;
|
pub mod section;
|
||||||
|
pub mod section_fill;
|
||||||
pub mod shaders;
|
pub mod shaders;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
@@ -818,5 +819,13 @@ mod tests {
|
|||||||
Validator::new(ValidationFlags::all(), Capabilities::all())
|
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||||
.validate(&grid_module)
|
.validate(&grid_module)
|
||||||
.unwrap_or_else(|e| panic!("grid: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
.unwrap_or_else(|e| panic!("grid: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
||||||
|
|
||||||
|
// Schnittflaechen-Kappen-Shader (Live-Schnitt-Schraffur) ebenso.
|
||||||
|
let cap_src = super::shaders::CAP_WGSL;
|
||||||
|
let cap_module = naga::front::wgsl::parse_str(cap_src)
|
||||||
|
.unwrap_or_else(|e| panic!("cap: WGSL-Parse-Fehler: {e:?}"));
|
||||||
|
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||||||
|
.validate(&cap_module)
|
||||||
|
.unwrap_or_else(|e| panic!("cap: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,9 @@ impl SectionPlane {
|
|||||||
|
|
||||||
/// u-Achse der Projektion: horizontal, senkrecht zur Blickrichtung (siehe
|
/// u-Achse der Projektion: horizontal, senkrecht zur Blickrichtung (siehe
|
||||||
/// Moduldoc — dieselbe Konvention wie `math::look_at`s `right`-Vektor).
|
/// Moduldoc — dieselbe Konvention wie `math::look_at`s `right`-Vektor).
|
||||||
fn u_axis(&self) -> [f32; 3] {
|
/// `pub(crate)`, damit `section_fill` die (u,v)->Welt-Ruecktransformation
|
||||||
|
/// mit EXAKT derselben Achsdefinition (inkl. Entartungs-Fallback) rechnet.
|
||||||
|
pub(crate) fn u_axis(&self) -> [f32; 3] {
|
||||||
let up = [0.0, 1.0, 0.0];
|
let up = [0.0, 1.0, 0.0];
|
||||||
let u = math::cross(self.normal, up);
|
let u = math::cross(self.normal, up);
|
||||||
if math::length(u) > 1e-6 {
|
if math::length(u) > 1e-6 {
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
//! Schnittflaechen-Geometrie fuer den Live-3D-Schnitt: wandelt die analytischen
|
||||||
|
//! Cut-Polygone aus `section::cut_section` (achsparallele (u,v)-Rechtecke, siehe
|
||||||
|
//! `section.rs`-Moduldoc) in kamerataugliche Dreiecks-Geometrie fuer die Cap-
|
||||||
|
//! Pipeline (`gpu::cap_pipeline`, Schraffur in `shaders::CAP_WGSL`).
|
||||||
|
//!
|
||||||
|
//! ## Ruecktransformation (u,v) -> Welt
|
||||||
|
//!
|
||||||
|
//! `SectionPlane::project(p) = [dot(p - point, u_axis), p.y]` (nur vertikale
|
||||||
|
//! Ebenen). Die Inverse fuer einen Punkt AUF der Ebene ist damit
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! world = point + u * u_axis (horizontale Lage), world.y = v
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! `u_axis` ist horizontal (y = 0), also traegt `u * u_axis` nichts zur Hoehe
|
||||||
|
//! bei — `world.y = v` gilt exakt, und der Punkt liegt auf der Ebene, weil
|
||||||
|
//! `u_axis` und die Welt-Hoehenachse [0,1,0] beide senkrecht auf der (horizon-
|
||||||
|
//! talen) Normale stehen. Wir nutzen `SectionPlane::u_axis()` (pub(crate)),
|
||||||
|
//! damit die Achsdefinition (inkl. Entartungs-Fallback) mit `section.rs`
|
||||||
|
//! identisch bleibt.
|
||||||
|
//!
|
||||||
|
//! ## Ausgabeformat
|
||||||
|
//!
|
||||||
|
//! Interleaved `[world_x, world_y, world_z, u, v]` je Vertex (5 f32,
|
||||||
|
//! `CAP_FLOATS_PER_VERTEX`), Dreiecksliste. `(u,v)` in Modell-Metern wandern als
|
||||||
|
//! Attribut mit, damit die Schraffur im Fragment-Shader ein durchgaengiges,
|
||||||
|
//! blickwinkel-unabhaengiges Muster IN der Schnittebene zeichnet (nicht im
|
||||||
|
//! Bildschirmraum). Ein Cut-Polygon ist stets ein konvexes Rechteck (4 Ecken,
|
||||||
|
//! siehe `section.rs`) — Fan-Triangulierung genuegt.
|
||||||
|
|
||||||
|
use crate::section::{cut_section, SectionPlane};
|
||||||
|
use crate::types::{SlabInput, WallInput};
|
||||||
|
|
||||||
|
/// f32 pro Cap-Vertex: [pos.x, pos.y, pos.z, u, v]. Spiegel des Vertex-Layouts
|
||||||
|
/// der `cap_pipeline` (gpu.rs) und des `VsIn` in `CAP_WGSL` (shaders.rs).
|
||||||
|
pub const CAP_FLOATS_PER_VERTEX: usize = 5;
|
||||||
|
|
||||||
|
/// Erzeugt die Schnittflaechen-Geometrie (Cut-Caps) fuer eine vertikale
|
||||||
|
/// Schnittebene: ruft `cut_section` und trianguliert jedes (u,v)-Rechteck in die
|
||||||
|
/// Welt zurueck. Rueckgabe = interleaved Vertexpuffer (`CAP_FLOATS_PER_VERTEX`),
|
||||||
|
/// leer, wenn die Ebene das Modell nicht schneidet.
|
||||||
|
///
|
||||||
|
/// WINDING: bewusst nicht garantiert kamerakonsistent — die Cap-Pipeline
|
||||||
|
/// zeichnet ohne Backface-Culling (`CullMode::None`, siehe gpu.rs), sodass die
|
||||||
|
/// flache Schnittflaeche aus beiden Richtungen sichtbar ist. Die Schraffur haengt
|
||||||
|
/// nicht von der Normale ab.
|
||||||
|
pub fn build_cut_caps(
|
||||||
|
plane: &SectionPlane,
|
||||||
|
walls: &[WallInput],
|
||||||
|
slabs: &[SlabInput],
|
||||||
|
) -> Vec<f32> {
|
||||||
|
let out = cut_section(plane, walls, slabs);
|
||||||
|
let point = plane.point;
|
||||||
|
let u_axis = plane.u_axis();
|
||||||
|
// (u, v) -> Welt: horizontale Lage aus point + u*u_axis, Hoehe = v.
|
||||||
|
let to_world = |uv: [f32; 2]| -> [f32; 3] {
|
||||||
|
[
|
||||||
|
point[0] + uv[0] * u_axis[0],
|
||||||
|
uv[1],
|
||||||
|
point[2] + uv[0] * u_axis[2],
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut verts: Vec<f32> = Vec::new();
|
||||||
|
for poly in &out.cut_polygons {
|
||||||
|
let pts = &poly.pts;
|
||||||
|
if pts.len() < 3 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut push_vertex = |uv: [f32; 2]| {
|
||||||
|
let w = to_world(uv);
|
||||||
|
verts.extend_from_slice(&[w[0], w[1], w[2], uv[0], uv[1]]);
|
||||||
|
};
|
||||||
|
// Fan-Triangulierung (konvexe Rechtecke): (0, i, i+1).
|
||||||
|
for i in 1..pts.len() - 1 {
|
||||||
|
push_vertex(pts[0]);
|
||||||
|
push_vertex(pts[i]);
|
||||||
|
push_vertex(pts[i + 1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
verts
|
||||||
|
}
|
||||||
@@ -31,12 +31,13 @@
|
|||||||
/// + Lambert-Directional — GPU-Aequivalent eines Architektur-Massenmodell-Lichts.
|
/// + Lambert-Directional — GPU-Aequivalent eines Architektur-Massenmodell-Lichts.
|
||||||
pub const MESH_WGSL: &str = r#"
|
pub const MESH_WGSL: &str = r#"
|
||||||
struct Globals {
|
struct Globals {
|
||||||
view_proj : mat4x4<f32>,
|
view_proj : mat4x4<f32>,
|
||||||
light_dir : vec4<f32>,
|
light_dir : vec4<f32>,
|
||||||
sky_color : vec4<f32>,
|
sky_color : vec4<f32>,
|
||||||
ground_color : vec4<f32>,
|
ground_color : vec4<f32>,
|
||||||
sun_color : vec4<f32>,
|
sun_color : vec4<f32>,
|
||||||
mode : vec4<f32>,
|
mode : vec4<f32>,
|
||||||
|
section_plane : vec4<f32>,
|
||||||
};
|
};
|
||||||
@group(0) @binding(0) var<uniform> globals : Globals;
|
@group(0) @binding(0) var<uniform> globals : Globals;
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ struct VsOut {
|
|||||||
@builtin(position) clip_pos : vec4<f32>,
|
@builtin(position) clip_pos : vec4<f32>,
|
||||||
@location(0) world_normal : vec3<f32>,
|
@location(0) world_normal : vec3<f32>,
|
||||||
@location(1) color : vec3<f32>,
|
@location(1) color : vec3<f32>,
|
||||||
|
@location(2) world_pos : vec3<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
@@ -58,11 +60,21 @@ fn vs_main(in : VsIn) -> VsOut {
|
|||||||
out.clip_pos = globals.view_proj * vec4<f32>(in.position, 1.0);
|
out.clip_pos = globals.view_proj * vec4<f32>(in.position, 1.0);
|
||||||
out.world_normal = in.normal;
|
out.world_normal = in.normal;
|
||||||
out.color = in.color;
|
out.color = in.color;
|
||||||
|
out.world_pos = in.position;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||||
|
// Live-Schnitt: mode.y > 0.5 aktiviert die Kappung. section_plane.xyz =
|
||||||
|
// Ebenennormale (= Blickrichtung), .w = -dot(n, point). depth > 0 heisst
|
||||||
|
// "hinter der Ebene" (in Blickrichtung) -> dieses Material wird weggeschnitten,
|
||||||
|
// die Schnittflaechen-Caps (cap_pipeline) fuellen die entstehende Wunde.
|
||||||
|
if (globals.mode.y > 0.5) {
|
||||||
|
if (dot(in.world_pos, globals.section_plane.xyz) + globals.section_plane.w > 0.0) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
let n = normalize(in.world_normal);
|
let n = normalize(in.world_normal);
|
||||||
let l = normalize(globals.light_dir.xyz);
|
let l = normalize(globals.light_dir.xyz);
|
||||||
let style = globals.mode.x;
|
let style = globals.mode.x;
|
||||||
@@ -120,12 +132,13 @@ fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
|||||||
/// gemeinsamen Tiefenpuffer, sodass das Modell das Gitter korrekt verdeckt.
|
/// gemeinsamen Tiefenpuffer, sodass das Modell das Gitter korrekt verdeckt.
|
||||||
pub const GRID_WGSL: &str = r#"
|
pub const GRID_WGSL: &str = r#"
|
||||||
struct Globals {
|
struct Globals {
|
||||||
view_proj : mat4x4<f32>,
|
view_proj : mat4x4<f32>,
|
||||||
light_dir : vec4<f32>,
|
light_dir : vec4<f32>,
|
||||||
sky_color : vec4<f32>,
|
sky_color : vec4<f32>,
|
||||||
ground_color : vec4<f32>,
|
ground_color : vec4<f32>,
|
||||||
sun_color : vec4<f32>,
|
sun_color : vec4<f32>,
|
||||||
mode : vec4<f32>,
|
mode : vec4<f32>,
|
||||||
|
section_plane : vec4<f32>,
|
||||||
};
|
};
|
||||||
@group(0) @binding(0) var<uniform> globals : Globals;
|
@group(0) @binding(0) var<uniform> globals : Globals;
|
||||||
|
|
||||||
@@ -136,7 +149,8 @@ struct VsIn {
|
|||||||
|
|
||||||
struct VsOut {
|
struct VsOut {
|
||||||
@builtin(position) clip_pos : vec4<f32>,
|
@builtin(position) clip_pos : vec4<f32>,
|
||||||
@location(0) color : vec3<f32>,
|
@location(0) color : vec3<f32>,
|
||||||
|
@location(1) world_pos : vec3<f32>,
|
||||||
};
|
};
|
||||||
|
|
||||||
@vertex
|
@vertex
|
||||||
@@ -144,11 +158,79 @@ fn vs_main(in : VsIn) -> VsOut {
|
|||||||
var out : VsOut;
|
var out : VsOut;
|
||||||
out.clip_pos = globals.view_proj * vec4<f32>(in.position, 1.0);
|
out.clip_pos = globals.view_proj * vec4<f32>(in.position, 1.0);
|
||||||
out.color = in.color;
|
out.color = in.color;
|
||||||
|
out.world_pos = in.position;
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||||
|
// Gleiche Kappung wie die Mesh-Pipeline: Grid-, Modell-Kanten- und
|
||||||
|
// Highlight-Linien (alle teilen diesen Shader) werden an der Schnittebene
|
||||||
|
// weggeschnitten, damit auf der abgeschnittenen Seite keine Kanten stehen
|
||||||
|
// bleiben (mode.y > 0.5 = aktiv).
|
||||||
|
if (globals.mode.y > 0.5) {
|
||||||
|
if (dot(in.world_pos, globals.section_plane.xyz) + globals.section_plane.w > 0.0) {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
}
|
||||||
return vec4<f32>(in.color, 1.0);
|
return vec4<f32>(in.color, 1.0);
|
||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
|
/// WGSL der Schnittflaechen-Kappen (Cut-Caps, section_fill.rs / gpu::cap_pipeline).
|
||||||
|
/// Eigene TriangleList-Pipeline: schlankes Vertex-Layout [pos vec3, uv vec2],
|
||||||
|
/// dieselbe `Globals`-Bind-Group (group(0) binding(0)) wie Mesh/Grid. Die Caps
|
||||||
|
/// werden NICHT geclippt (sie SIND die Schnittflaeche). Fragment: prozedurale
|
||||||
|
/// 45-Grad-Diagonalschraffur aus den Ebenen-Koordinaten (u,v) in Modell-Metern —
|
||||||
|
/// dunkle Tinte auf hellem Papier, passend zur 2D-Schnittkonvention.
|
||||||
|
pub const CAP_WGSL: &str = r#"
|
||||||
|
struct Globals {
|
||||||
|
view_proj : mat4x4<f32>,
|
||||||
|
light_dir : vec4<f32>,
|
||||||
|
sky_color : vec4<f32>,
|
||||||
|
ground_color : vec4<f32>,
|
||||||
|
sun_color : vec4<f32>,
|
||||||
|
mode : vec4<f32>,
|
||||||
|
section_plane : vec4<f32>,
|
||||||
|
};
|
||||||
|
@group(0) @binding(0) var<uniform> globals : Globals;
|
||||||
|
|
||||||
|
struct VsIn {
|
||||||
|
@location(0) position : vec3<f32>,
|
||||||
|
@location(1) uv : vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct VsOut {
|
||||||
|
@builtin(position) clip_pos : vec4<f32>,
|
||||||
|
@location(0) uv : vec2<f32>,
|
||||||
|
};
|
||||||
|
|
||||||
|
@vertex
|
||||||
|
fn vs_main(in : VsIn) -> VsOut {
|
||||||
|
var out : VsOut;
|
||||||
|
out.clip_pos = globals.view_proj * vec4<f32>(in.position, 1.0);
|
||||||
|
out.uv = in.uv;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fs_main(in : VsOut) -> @location(0) vec4<f32> {
|
||||||
|
// 45-Grad-Diagonalschraffur in Modell-Metern: Linien bei ganzzahligen
|
||||||
|
// Vielfachen von `spacing` entlang (u + v). `dist` = Abstand (in Einheiten
|
||||||
|
// von `spacing`) zur naechsten Linie; `fwidth` liefert eine bildschirm-
|
||||||
|
// groessenrichtige Kantenglaettung (kein Aliasing bei flachem Blickwinkel).
|
||||||
|
let spacing = 0.08; // Linienabstand ~8 cm
|
||||||
|
let half = 0.004; // halbe Linienbreite ~4 mm (Strich ~8 mm)
|
||||||
|
let ink = vec3<f32>(0.06, 0.06, 0.06);
|
||||||
|
let paper = vec3<f32>(0.94, 0.94, 0.94);
|
||||||
|
|
||||||
|
let s = (in.uv.x + in.uv.y) / spacing;
|
||||||
|
let frac = fract(s);
|
||||||
|
let dist = min(frac, 1.0 - frac); // 0 auf der Linie
|
||||||
|
let half_s = half / spacing;
|
||||||
|
let aa = max(fwidth(s), 1e-5);
|
||||||
|
let line = 1.0 - smoothstep(half_s, half_s + aa, dist);
|
||||||
|
let color = mix(paper, ink, line);
|
||||||
|
return vec4<f32>(color, 1.0);
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|||||||
@@ -98,6 +98,15 @@ pub struct WebModelRenderer {
|
|||||||
/// Ziel/Distanz, mirror der TS-`fitTargetDist` in Wasm3DViewport.tsx).
|
/// Ziel/Distanz, mirror der TS-`fitTargetDist` in Wasm3DViewport.tsx).
|
||||||
/// `None` bei leerem Modell (kein Wall-/Slab-Vertex) oder vor `set_model`.
|
/// `None` bei leerem Modell (kein Wall-/Slab-Vertex) oder vor `set_model`.
|
||||||
bounds: Option<([f32; 3], [f32; 3])>,
|
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 +
|
/// Achsparallele Bounding-Box des Modells in Welt-Metern (Y-up), aus Waenden +
|
||||||
@@ -224,6 +233,9 @@ impl WebModelRenderer {
|
|||||||
renderer,
|
renderer,
|
||||||
camera: Camera::default(),
|
camera: Camera::default(),
|
||||||
bounds: None,
|
bounds: None,
|
||||||
|
walls: Vec::new(),
|
||||||
|
slabs: Vec::new(),
|
||||||
|
section: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,6 +249,47 @@ impl WebModelRenderer {
|
|||||||
self.bounds = model_bounds(&model.walls, &model.slabs);
|
self.bounds = model_bounds(&model.walls, &model.slabs);
|
||||||
self.renderer
|
self.renderer
|
||||||
.upload_model(&self.device, &model.walls, &model.slabs, &model.meshes);
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
// 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 } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import type { Project } from "../model/types";
|
import type { Project } from "../model/types";
|
||||||
import { projectToModel3d, pickGeometry } from "../plan/toWalls3d";
|
import { projectToModel3d, pickGeometry } from "../plan/toWalls3d";
|
||||||
import type { RenderMode, View3d } from "../ui/TopBar";
|
import type { RenderMode, View3d } from "../ui/TopBar";
|
||||||
@@ -220,10 +220,16 @@ export function Wasm3DViewport({
|
|||||||
setRenderStyle,
|
setRenderStyle,
|
||||||
applyViewPreset,
|
applyViewPreset,
|
||||||
setGroundGrid,
|
setGroundGrid,
|
||||||
|
setSectionPlane,
|
||||||
setHighlightLines,
|
setHighlightLines,
|
||||||
ready,
|
ready,
|
||||||
failed,
|
failed,
|
||||||
} = useWasm3dRenderer(canvasRef, true);
|
} = 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
|
// Orbit-Zustand in einem Ref (kein React-State): Mausbewegungen zeichnen
|
||||||
// direkt neu, ohne die Komponente je Frame zu re-rendern. Dient NUR der
|
// direkt neu, ohne die Komponente je Frame zu re-rendern. Dient NUR der
|
||||||
// freien Navigation NACH einem Praeset-Sprung (s. Datei-Kommentar).
|
// freien Navigation NACH einem Praeset-Sprung (s. Datei-Kommentar).
|
||||||
@@ -277,6 +283,18 @@ export function Wasm3DViewport({
|
|||||||
redrawRef.current();
|
redrawRef.current();
|
||||||
}, [ready, gridVisible, groundElevation, setGroundGrid]);
|
}, [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-Hervorhebung: bei Bereitschaft oder Änderung der Highlight-Linien
|
||||||
// (Auswahl gewechselt/aufgehoben) neu setzen + zeichnen. Die Engine hält das
|
// (Auswahl gewechselt/aufgehoben) neu setzen + zeichnen. Die Engine hält das
|
||||||
// Highlight, bis es erneut gesetzt wird — Kamerabewegungen (Orbit/Pan/Zoom)
|
// 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
|
Darstellungsart kommen von der Oberleiste (TopBar); das Grid-Overlay
|
||||||
unten ist der einzige eigene Umschalter hier. */}
|
unten ist der einzige eigene Umschalter hier. */}
|
||||||
<canvas ref={canvasRef} data-engine="render3d" />
|
<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 && (
|
{onToggleGrid && (
|
||||||
// Dezenter Overlay-Button (eigener DOM-Knoten neben dem Canvas, `.viewport`
|
// Dezenter Overlay-Button (eigener DOM-Knoten neben dem Canvas, `.viewport`
|
||||||
// ist `position: relative` — s. styles.css), stört die Maus-Navigation
|
// ist `position: relative` — s. styles.css), stört die Maus-Navigation
|
||||||
|
|||||||
@@ -53,6 +53,20 @@ interface WebModelRendererLike {
|
|||||||
extent_m: number,
|
extent_m: number,
|
||||||
spacing_m: number,
|
spacing_m: number,
|
||||||
): void;
|
): 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
|
* Auswahl-Hervorhebung setzen: ein LineList-Vertex-Array (interleaved
|
||||||
* `[px,py,pz, r,g,b, ...]`, world-Meter, je 2 Vertices = 1 Linie), das die
|
* `[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
|
* Auswahl-Hervorhebung setzen (Umriss-Linien des gewählten Bauteils, immer
|
||||||
* obenauf) — reicht das fertige LineList-Vertex-Array 1:1 an
|
* obenauf) — reicht das fertige LineList-Vertex-Array 1:1 an
|
||||||
@@ -274,6 +320,7 @@ export function useWasm3dRenderer(
|
|||||||
setRenderStyle,
|
setRenderStyle,
|
||||||
applyViewPreset,
|
applyViewPreset,
|
||||||
setGroundGrid,
|
setGroundGrid,
|
||||||
|
setSectionPlane,
|
||||||
setHighlightLines,
|
setHighlightLines,
|
||||||
ready,
|
ready,
|
||||||
failed,
|
failed,
|
||||||
|
|||||||
Reference in New Issue
Block a user