Nordstern: Geo-Kontext-Meshes (Terrain/Import) rendern
Bisher zeigte nur three.js importierte Gebaeude/DXF-Meshes und das Terrain-TIN (Project.context). Neuer MeshInput-Typ + Kontext-Mesh-Pfad in render3d (Flat-Normalen, doppelseitig gegen unbekanntes Winding), projectToModel3d speist project.context jetzt in beide Renderer (nativ + WASM) ein. Nebenbei: fehlendes layers-Feld in demo_walls() behoben, das den native3d-Feature-Build zuvor schon brach.
This commit is contained in:
@@ -13,9 +13,9 @@ use bytemuck::{Pod, Zeroable};
|
|||||||
use wgpu::util::DeviceExt;
|
use wgpu::util::DeviceExt;
|
||||||
|
|
||||||
use crate::math::{view_projection, Mat4};
|
use crate::math::{view_projection, Mat4};
|
||||||
use crate::mesh::{build_model_mesh, build_walls_mesh};
|
use crate::mesh::{build_scene_mesh, build_walls_mesh};
|
||||||
use crate::shaders::MESH_WGSL;
|
use crate::shaders::MESH_WGSL;
|
||||||
use crate::types::{Camera, Mesh, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
use crate::types::{Camera, Mesh, MeshInput, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
||||||
|
|
||||||
/// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar).
|
/// Tiefenformat des Z-Puffers (32 Bit Float, ueberall verfuegbar).
|
||||||
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
pub const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
|
||||||
@@ -219,9 +219,16 @@ impl Renderer {
|
|||||||
self.upload_mesh(device, build_walls_mesh(walls));
|
self.upload_mesh(device, build_walls_mesh(walls));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Erzeugt das Mesh aus Waenden UND Deckenplatten und laedt die Puffer hoch.
|
/// Erzeugt das Mesh aus Waenden, Deckenplatten UND rohen Kontext-Meshes
|
||||||
pub fn upload_model(&mut self, device: &wgpu::Device, walls: &[WallInput], slabs: &[SlabInput]) {
|
/// (Terrain/importierte Volumen, siehe `MeshInput`) und laedt die Puffer hoch.
|
||||||
self.upload_mesh(device, build_model_mesh(walls, slabs));
|
pub fn upload_model(
|
||||||
|
&mut self,
|
||||||
|
device: &wgpu::Device,
|
||||||
|
walls: &[WallInput],
|
||||||
|
slabs: &[SlabInput],
|
||||||
|
meshes: &[MeshInput],
|
||||||
|
) {
|
||||||
|
self.upload_mesh(device, build_scene_mesh(walls, slabs, meshes));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Laedt ein fertiges Mesh in die GPU-Puffer (oder loescht es bei leer).
|
/// Laedt ein fertiges Mesh in die GPU-Puffer (oder loescht es bei leer).
|
||||||
|
|||||||
@@ -34,12 +34,16 @@ pub use math::{
|
|||||||
look_at, orbit_eye, orthographic, perspective, preset_camera, projection_matrix,
|
look_at, orbit_eye, orthographic, perspective, preset_camera, projection_matrix,
|
||||||
view_matrix, view_projection, Mat4,
|
view_matrix, view_projection, Mat4,
|
||||||
};
|
};
|
||||||
pub use mesh::{build_model_mesh, build_walls_mesh, extrude_slab, extrude_wall, triangulate};
|
pub use mesh::{
|
||||||
|
append_context_mesh, build_model_mesh, build_scene_mesh, build_walls_mesh, extrude_slab,
|
||||||
|
extrude_wall, triangulate,
|
||||||
|
};
|
||||||
pub use section::{
|
pub use section::{
|
||||||
cut_section, ComponentKind, ComponentRef, CutPolygon, SectionEdge, SectionOutput, SectionPlane,
|
cut_section, ComponentKind, ComponentRef, CutPolygon, SectionEdge, SectionOutput, SectionPlane,
|
||||||
};
|
};
|
||||||
pub use types::{
|
pub use types::{
|
||||||
Camera, CameraPreset, Mesh, Point2, Projection, Rgb, SlabInput, WallInput, FLOATS_PER_VERTEX,
|
Camera, CameraPreset, Mesh, MeshInput, MeshKind, Point2, Projection, Rgb, SlabInput, WallInput,
|
||||||
|
FLOATS_PER_VERTEX,
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Tests: Mesh-Erzeugung (Muster wie render2d/tessellate) -------------------
|
// --- Tests: Mesh-Erzeugung (Muster wie render2d/tessellate) -------------------
|
||||||
@@ -624,6 +628,98 @@ mod tests {
|
|||||||
assert_eq!(mesh.vertex_count(), 0);
|
assert_eq!(mesh.vertex_count(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Rohe Kontext-Meshes (Terrain / importierte Volumen) ------------------
|
||||||
|
|
||||||
|
use super::mesh::{append_context_mesh, build_scene_mesh};
|
||||||
|
use super::types::{MeshInput, MeshKind};
|
||||||
|
|
||||||
|
/// Ein flaches Dreieck auf Modell-Hoehe `z` (Modell x,y,z=Hoehe).
|
||||||
|
fn flat_context_tri(z: f32, kind: MeshKind, color: Option<[f32; 3]>) -> MeshInput {
|
||||||
|
MeshInput {
|
||||||
|
positions: vec![0.0, 0.0, z, 1.0, 0.0, z, 0.0, 1.0, z],
|
||||||
|
indices: vec![0, 1, 2],
|
||||||
|
kind,
|
||||||
|
color,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_mesh_ist_doppelseitig_und_mappt_hoehe_nach_y() {
|
||||||
|
let mut mesh = Mesh::default();
|
||||||
|
append_context_mesh(&mut mesh, &flat_context_tri(5.0, MeshKind::Terrain, None));
|
||||||
|
// Ein Quell-Dreieck -> zwei (Vorder-/Rueckseite) = 6 Vertices.
|
||||||
|
assert_eq!(mesh.triangle_count(), 2);
|
||||||
|
assert_eq!(mesh.vertex_count(), 6);
|
||||||
|
// model (x,y,z) -> world (x, z, y): die Hoehe z=5 liegt auf world.y=5.
|
||||||
|
let (min, max) = mesh.bounds();
|
||||||
|
assert!((min[1] - 5.0).abs() < 1e-5 && (max[1] - 5.0).abs() < 1e-5, "flach auf y=5");
|
||||||
|
// Doppelseitig: eine Normale +Y, eine −Y.
|
||||||
|
let mut saw_up = false;
|
||||||
|
let mut saw_down = false;
|
||||||
|
for i in 0..mesh.vertex_count() {
|
||||||
|
let ny = mesh.verts[i * FLOATS_PER_VERTEX + 4];
|
||||||
|
if ny > 0.5 {
|
||||||
|
saw_up = true;
|
||||||
|
}
|
||||||
|
if ny < -0.5 {
|
||||||
|
saw_down = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(saw_up && saw_down, "Vorder- UND Rueckseite (±Y)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_mesh_default_farbe_je_kind() {
|
||||||
|
// Ohne explizite Farbe greift die kind-Default-Farbe (three.js mats.*).
|
||||||
|
let mut mesh = Mesh::default();
|
||||||
|
append_context_mesh(&mut mesh, &flat_context_tri(0.0, MeshKind::Terrain, None));
|
||||||
|
let c = [mesh.verts[6], mesh.verts[7], mesh.verts[8]];
|
||||||
|
assert_eq!(c, MeshKind::Terrain.default_color());
|
||||||
|
// Explizite Farbe schlaegt den Default.
|
||||||
|
let mut mesh2 = Mesh::default();
|
||||||
|
append_context_mesh(&mut mesh2, &flat_context_tri(0.0, MeshKind::Imported, Some([0.1, 0.2, 0.3])));
|
||||||
|
assert_eq!([mesh2.verts[6], mesh2.verts[7], mesh2.verts[8]], [0.1, 0.2, 0.3]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn context_mesh_ueberspringt_kaputte_indizes_und_entartung() {
|
||||||
|
let mut mesh = Mesh::default();
|
||||||
|
// Index 9 out-of-range (nur 3 Vertices) -> Dreieck verworfen, kein Panic.
|
||||||
|
append_context_mesh(
|
||||||
|
&mut mesh,
|
||||||
|
&MeshInput {
|
||||||
|
positions: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0],
|
||||||
|
indices: vec![0, 1, 9],
|
||||||
|
kind: MeshKind::Imported,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(mesh.vertex_count(), 0);
|
||||||
|
// Kollineares (entartetes) Dreieck -> keine Normale, verworfen.
|
||||||
|
append_context_mesh(
|
||||||
|
&mut mesh,
|
||||||
|
&MeshInput {
|
||||||
|
positions: vec![0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 2.0, 0.0, 0.0],
|
||||||
|
indices: vec![0, 1, 2],
|
||||||
|
kind: MeshKind::Imported,
|
||||||
|
color: None,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert_eq!(mesh.vertex_count(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn build_scene_mesh_haengt_kontext_meshes_hinten_an() {
|
||||||
|
let model = build_model_mesh(&[wall_x(4.0, 0.2, 2.6)], &[square_slab(4.0, 2.6, 2.85)]);
|
||||||
|
let scene = build_scene_mesh(
|
||||||
|
&[wall_x(4.0, 0.2, 2.6)],
|
||||||
|
&[square_slab(4.0, 2.6, 2.85)],
|
||||||
|
&[flat_context_tri(0.0, MeshKind::Terrain, None)],
|
||||||
|
);
|
||||||
|
// Szene = Modell (Wand+Slab) + 6 Kontext-Vertices (doppelseitiges Dreieck).
|
||||||
|
assert_eq!(scene.vertex_count(), model.vertex_count() + 6);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Kamera / Matrizen ----------------------------------------------------
|
// --- Kamera / Matrizen ----------------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -58,7 +58,9 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
||||||
use crate::types::{Mesh, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer, FLOATS_PER_VERTEX};
|
use crate::types::{
|
||||||
|
Mesh, MeshInput, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer, FLOATS_PER_VERTEX,
|
||||||
|
};
|
||||||
|
|
||||||
/// Ein Quader-Mesh besteht aus 6 Seiten (Boden, Deckel, 4 Waende) zu je 2
|
/// Ein Quader-Mesh besteht aus 6 Seiten (Boden, Deckel, 4 Waende) zu je 2
|
||||||
/// Dreiecken = 12 Dreiecke, mit flachen Normalen also 24 Vertices (je Seite 4,
|
/// Dreiecken = 12 Dreiecke, mit flachen Normalen also 24 Vertices (je Seite 4,
|
||||||
@@ -584,6 +586,94 @@ pub fn build_model_mesh(walls: &[WallInput], slabs: &[SlabInput]) -> Mesh {
|
|||||||
mesh
|
mesh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Baut die volle Szene: Waende + Deckenplatten (`build_model_mesh`) und haengt
|
||||||
|
/// danach die rohen Kontext-Meshes (Terrain/importierte Volumen, siehe
|
||||||
|
/// `MeshInput`) an — alles in EINEN Puffer. Die Wand-/Slab-Reihenfolge bleibt
|
||||||
|
/// vorne (deterministische Zaehlung fuer die bestehenden Tests); die Kontext-
|
||||||
|
/// Meshes kommen hinten dran.
|
||||||
|
pub fn build_scene_mesh(walls: &[WallInput], slabs: &[SlabInput], meshes: &[MeshInput]) -> Mesh {
|
||||||
|
let mut mesh = build_model_mesh(walls, slabs);
|
||||||
|
for m in meshes {
|
||||||
|
append_context_mesh(&mut mesh, m);
|
||||||
|
}
|
||||||
|
mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Rohe Kontext-Meshes (Terrain / importierte Volumen) ──────────────────────
|
||||||
|
|
||||||
|
/// Haengt EIN rohes Dreiecks-Mesh (Terrain-TIN oder importiertes Volumen, siehe
|
||||||
|
/// `MeshInput`) an `mesh` an. `positions` ist ein flaches (x,y,z)-Array in
|
||||||
|
/// MODELL-Metern (z = Hoehe), `indices` sind Dreiecks-Indizes. Jede Dreiecks-
|
||||||
|
/// flaeche bekommt eigene Vertices mit ihrer Flaechen-Normale (Flat Shading, wie
|
||||||
|
/// Wand/Slab) — so ist das Ergebnis unabhaengig von geteilten Vertices im
|
||||||
|
/// Quell-Mesh.
|
||||||
|
///
|
||||||
|
/// Koordinaten: model `(x, y, z)` -> world `(x, z, y)` (Hoehe entlang +Y), exakt
|
||||||
|
/// wie `extrude_layer_segment`/`extrude_slab` und `Viewport3D.tsx`.
|
||||||
|
///
|
||||||
|
/// DOPPELSEITIG: jedes Dreieck wird ZWEIMAL emittiert (Vorderseite `a,b,c` mit
|
||||||
|
/// Normale `+n`, Rueckseite `a,c,b` mit `-n`), weil das Quell-Winding von
|
||||||
|
/// Terrain/Import nicht garantiert ist (three.js rendert sie mit `DoubleSide`).
|
||||||
|
/// Da der Pfad Rueckseiten cullt (`gpu.rs`), traegt stets das zum Betrachter
|
||||||
|
/// zeigende Dreieck die korrekte Normale, das andere wird verworfen — keine
|
||||||
|
/// Z-Fighting (beide liegen deckungsgleich, nur eines ist front-facing).
|
||||||
|
pub fn append_context_mesh(mesh: &mut Mesh, input: &MeshInput) {
|
||||||
|
let pos = &input.positions;
|
||||||
|
let vcount = pos.len() / 3;
|
||||||
|
let color = input.effective_color();
|
||||||
|
// world-Position aus Modell (x, y, z=Hoehe): (x, hoehe, y).
|
||||||
|
let vworld = |i: usize| -> [f32; 3] {
|
||||||
|
let b = i * 3;
|
||||||
|
[pos[b], pos[b + 2], pos[b + 1]]
|
||||||
|
};
|
||||||
|
for t in input.indices.chunks_exact(3) {
|
||||||
|
let (ia, ib, ic) = (t[0] as usize, t[1] as usize, t[2] as usize);
|
||||||
|
// Fehlerhafte Indizes robust ueberspringen (kein Panic bei kaputter Eingabe).
|
||||||
|
if ia >= vcount || ib >= vcount || ic >= vcount {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let a = vworld(ia);
|
||||||
|
let b = vworld(ib);
|
||||||
|
let c = vworld(ic);
|
||||||
|
// Flaechen-Normale (b-a) x (c-a); entartete Dreiecke ueberspringen.
|
||||||
|
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||||
|
let ac = [c[0] - a[0], c[1] - a[1], c[2] - a[2]];
|
||||||
|
let gn = [
|
||||||
|
ab[1] * ac[2] - ab[2] * ac[1],
|
||||||
|
ab[2] * ac[0] - ab[0] * ac[2],
|
||||||
|
ab[0] * ac[1] - ab[1] * ac[0],
|
||||||
|
];
|
||||||
|
let len = (gn[0] * gn[0] + gn[1] * gn[1] + gn[2] * gn[2]).sqrt();
|
||||||
|
if len < 1e-12 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let n = [gn[0] / len, gn[1] / len, gn[2] / len];
|
||||||
|
// Vorderseite + Rueckseite (siehe Moduldoc: doppelseitig).
|
||||||
|
push_ctx_tri(mesh, a, b, c, n, color);
|
||||||
|
push_ctx_tri(mesh, a, c, b, [-n[0], -n[1], -n[2]], color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Haengt EIN Dreieck (drei world-Ecken) mit gegebener Normale + Farbe an —
|
||||||
|
/// Winding wie uebergeben (im Gegensatz zu `push_tri_oriented`, das umsortiert).
|
||||||
|
/// Wird von `append_context_mesh` fuer beide Seiten genutzt.
|
||||||
|
fn push_ctx_tri(
|
||||||
|
mesh: &mut Mesh,
|
||||||
|
a: [f32; 3],
|
||||||
|
b: [f32; 3],
|
||||||
|
c: [f32; 3],
|
||||||
|
normal: [f32; 3],
|
||||||
|
color: Rgb,
|
||||||
|
) {
|
||||||
|
let base = (mesh.verts.len() / FLOATS_PER_VERTEX) as u32;
|
||||||
|
for p in [a, b, c] {
|
||||||
|
mesh.verts.extend_from_slice(&[
|
||||||
|
p[0], p[1], p[2], normal[0], normal[1], normal[2], color[0], color[1], color[2],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
mesh.indices.extend_from_slice(&[base, base + 1, base + 2]);
|
||||||
|
}
|
||||||
|
|
||||||
// ── Deckenplatten (extrudierte Polygone) ─────────────────────────────────────
|
// ── Deckenplatten (extrudierte Polygone) ─────────────────────────────────────
|
||||||
|
|
||||||
/// Signierte Flaeche eines Grundriss-Polygons (Shoelace) in Modell-Koordinaten.
|
/// Signierte Flaeche eines Grundriss-Polygons (Shoelace) in Modell-Koordinaten.
|
||||||
|
|||||||
@@ -128,6 +128,74 @@ fn default_slab_color() -> Rgb {
|
|||||||
[0.86, 0.86, 0.88]
|
[0.86, 0.86, 0.88]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Art eines rohen Kontext-Meshes (aus `Project.context`) — bestimmt die
|
||||||
|
/// Default-Einfaerbung, wenn kein explizites `color` mitkommt. Werte sind
|
||||||
|
/// serde-kompatibel mit der TS-Seite (lowercase).
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
|
||||||
|
#[serde(rename_all = "lowercase")]
|
||||||
|
pub enum MeshKind {
|
||||||
|
/// Gelaende-TIN (offene Flaeche) — gedecktes Lehm-/Gelaendegruen-grau.
|
||||||
|
Terrain,
|
||||||
|
/// Importiertes Volumen (Gebaeude/DXF) — neutrales Hellgrau.
|
||||||
|
#[default]
|
||||||
|
Imported,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshKind {
|
||||||
|
/// Default-Albedo (RGB 0..1), wenn `MeshInput::color` fehlt. Werte 1:1 aus
|
||||||
|
/// der three.js-Sicht (`src/viewport/Viewport3D.tsx`, `mats.terrain`/
|
||||||
|
/// `mats.mesh`), damit beide Renderer denselben Kontext-Farbton zeigen.
|
||||||
|
pub fn default_color(self) -> Rgb {
|
||||||
|
match self {
|
||||||
|
// 0xa9b0a2 — gedaempftes Lehm-/Gelaendegruen-grau (Clay-Look).
|
||||||
|
MeshKind::Terrain => [0.663, 0.690, 0.635],
|
||||||
|
// 0x9fa6ae — neutrales Hellgrau fuer importierte Meshes.
|
||||||
|
MeshKind::Imported => [0.624, 0.651, 0.682],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein rohes Dreiecks-Mesh aus dem Geo-Kontext (`Project.context`): entweder ein
|
||||||
|
/// Gelaende-TIN (`TerrainMesh`) oder ein importiertes Volumen (`ImportedMesh`,
|
||||||
|
/// z. B. extrudierte OSM-Gebaeude/DXF). Anders als `WallInput`/`SlabInput` bringt
|
||||||
|
/// es die Geometrie bereits FERTIG TRIANGULIERT mit — `mesh.rs` berechnet daraus
|
||||||
|
/// nur noch die Flaechen-Normalen (Flat Shading, wie Wand/Slab) und speist es in
|
||||||
|
/// denselben GPU-Pfad ein.
|
||||||
|
///
|
||||||
|
/// KOORDINATEN wie WallInput/SlabInput: die TS-Seite (`projectToModel3d`) liefert
|
||||||
|
/// `positions` im MODELL-System als flaches `[x, y, z, ...]`-Array (z = Hoehe, in
|
||||||
|
/// Metern); `mesh.rs` bildet jeden Vertex nach world ab: model `(x, y, z)` ->
|
||||||
|
/// world `(x, z, y)`, d. h. Grundriss in der XZ-Ebene, Hoehe entlang +Y (exakt wie
|
||||||
|
/// `Viewport3D.tsx` die rohen positions konsumiert).
|
||||||
|
///
|
||||||
|
/// DOPPELSEITIG: Terrain-/Import-Meshes tragen KEIN garantiertes Winding (die
|
||||||
|
/// three.js-Sicht rendert sie mit `DoubleSide`, siehe `io/geoContext.ts`). Da der
|
||||||
|
/// Nordstern-Pfad Rueckseiten cullt, emittiert `mesh.rs` jedes Dreieck ZWEIMAL
|
||||||
|
/// (beide Windings, entgegengesetzte Normale) — so bleibt die Flaeche aus jeder
|
||||||
|
/// Blickrichtung sichtbar und korrekt beleuchtet.
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct MeshInput {
|
||||||
|
/// Flaches Positions-Array in MODELL-Metern: `[x0,y0,z0, x1,y1,z1, ...]`
|
||||||
|
/// (z = Hoehe). Wird von `mesh.rs` nach world (x, z, y) abgebildet.
|
||||||
|
pub positions: Vec<f32>,
|
||||||
|
/// Dreiecks-Indizes (0-basiert auf die Positions-Vertices, je 3 ein Dreieck).
|
||||||
|
pub indices: Vec<u32>,
|
||||||
|
/// Art des Meshes (Default-Einfaerbung, siehe `MeshKind`). Default `Imported`.
|
||||||
|
#[serde(default)]
|
||||||
|
pub kind: MeshKind,
|
||||||
|
/// Optionale explizite Albedo-Farbe (RGB 0..1). Fehlt sie, greift die
|
||||||
|
/// Default-Farbe je `kind` (siehe `effective_color`).
|
||||||
|
#[serde(default)]
|
||||||
|
pub color: Option<Rgb>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeshInput {
|
||||||
|
/// Effektive Albedo-Farbe: explizit gesetzt (`color`) oder Default je `kind`.
|
||||||
|
pub fn effective_color(&self) -> Rgb {
|
||||||
|
self.color.unwrap_or_else(|| self.kind.default_color())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Projektionsart der Kamera. Die three.js-Sicht schaltet zwischen perspektivisch
|
/// Projektionsart der Kamera. Die three.js-Sicht schaltet zwischen perspektivisch
|
||||||
/// (freies Orbit) und orthografisch (die achsparallelen Presets front/top/side um).
|
/// (freies Orbit) und orthografisch (die achsparallelen Presets front/top/side um).
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ use web_sys::HtmlCanvasElement;
|
|||||||
use crate::gpu::Renderer;
|
use crate::gpu::Renderer;
|
||||||
use crate::math::preset_camera;
|
use crate::math::preset_camera;
|
||||||
use crate::section::{cut_section, SectionPlane};
|
use crate::section::{cut_section, SectionPlane};
|
||||||
use crate::types::{Camera, CameraPreset, Projection, SlabInput, WallInput};
|
use crate::types::{Camera, CameraPreset, MeshInput, 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
|
||||||
@@ -42,6 +42,10 @@ struct ModelInput {
|
|||||||
walls: Vec<WallInput>,
|
walls: Vec<WallInput>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
slabs: Vec<SlabInput>,
|
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
|
/// Berechnet einen Schnitt/eine Ansicht (`section::cut_section`) gegen das Modell
|
||||||
@@ -232,7 +236,7 @@ impl WebModelRenderer {
|
|||||||
.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.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, &model.meshes);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+11
-6
@@ -92,7 +92,7 @@ pub fn push_walls(value: serde_json::Value) {
|
|||||||
fn parse_model3d(value: serde_json::Value) -> Result<Model3d, serde_json::Error> {
|
fn parse_model3d(value: serde_json::Value) -> Result<Model3d, serde_json::Error> {
|
||||||
if value.is_array() {
|
if value.is_array() {
|
||||||
let walls = serde_json::from_value::<Vec<WallInput>>(value)?;
|
let walls = serde_json::from_value::<Vec<WallInput>>(value)?;
|
||||||
Ok(Model3d { walls, slabs: Vec::new() })
|
Ok(Model3d { walls, slabs: Vec::new(), meshes: Vec::new() })
|
||||||
} else {
|
} else {
|
||||||
serde_json::from_value::<Model3d>(value)
|
serde_json::from_value::<Model3d>(value)
|
||||||
}
|
}
|
||||||
@@ -232,7 +232,7 @@ use render3d::gpu::Renderer as Renderer3d;
|
|||||||
#[cfg(feature = "native3d")]
|
#[cfg(feature = "native3d")]
|
||||||
use render3d::math::orbit_eye;
|
use render3d::math::orbit_eye;
|
||||||
#[cfg(feature = "native3d")]
|
#[cfg(feature = "native3d")]
|
||||||
use render3d::types::{Camera, Projection, SlabInput, WallInput};
|
use render3d::types::{Camera, MeshInput, Projection, SlabInput, WallInput};
|
||||||
#[cfg(feature = "native3d")]
|
#[cfg(feature = "native3d")]
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
@@ -245,6 +245,10 @@ struct Model3d {
|
|||||||
walls: Vec<WallInput>,
|
walls: Vec<WallInput>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
slabs: Vec<SlabInput>,
|
slabs: Vec<SlabInput>,
|
||||||
|
/// Rohe Kontext-Meshes (Terrain/importierte Volumen). `#[serde(default)]`
|
||||||
|
/// haelt aeltere Payloads ohne `meshes` gueltig.
|
||||||
|
#[serde(default)]
|
||||||
|
meshes: Vec<MeshInput>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "native3d")]
|
#[cfg(feature = "native3d")]
|
||||||
@@ -262,7 +266,7 @@ impl GpuState3d {
|
|||||||
fn new(window: Arc<Window>, model: &Model3d) -> Self {
|
fn new(window: Arc<Window>, model: &Model3d) -> Self {
|
||||||
let (surface, device, queue, config) = configure_surface(&window, "3d.device");
|
let (surface, device, queue, config) = configure_surface(&window, "3d.device");
|
||||||
let mut renderer = Renderer3d::new(&device, config.format);
|
let mut renderer = Renderer3d::new(&device, config.format);
|
||||||
renderer.upload_model(&device, &model.walls, &model.slabs);
|
renderer.upload_model(&device, &model.walls, &model.slabs, &model.meshes);
|
||||||
renderer.set_light([6.0, 12.0, 4.0]);
|
renderer.set_light([6.0, 12.0, 4.0]);
|
||||||
Self { surface, device, queue, config, renderer, window }
|
Self { surface, device, queue, config, renderer, window }
|
||||||
}
|
}
|
||||||
@@ -315,12 +319,12 @@ fn load_model() -> Model3d {
|
|||||||
Ok(model) => model,
|
Ok(model) => model,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("native3d: Modell-Parse-Fehler ({WALLS_PATH}): {e} — nutze Demo-Waende");
|
eprintln!("native3d: Modell-Parse-Fehler ({WALLS_PATH}): {e} — nutze Demo-Waende");
|
||||||
Model3d { walls: demo_walls(), slabs: Vec::new() }
|
Model3d { walls: demo_walls(), slabs: Vec::new(), meshes: Vec::new() }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("native3d: Modell nicht ladbar ({WALLS_PATH}): {e} — nutze Demo-Waende");
|
eprintln!("native3d: Modell nicht ladbar ({WALLS_PATH}): {e} — nutze Demo-Waende");
|
||||||
Model3d { walls: demo_walls(), slabs: Vec::new() }
|
Model3d { walls: demo_walls(), slabs: Vec::new(), meshes: Vec::new() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -339,6 +343,7 @@ fn demo_walls() -> Vec<WallInput> {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: grey,
|
color: grey,
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
};
|
};
|
||||||
vec![
|
vec![
|
||||||
mk([0.0, 0.0], [6.0, 0.0]),
|
mk([0.0, 0.0], [6.0, 0.0]),
|
||||||
@@ -715,7 +720,7 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
self.pending3d = Some(model);
|
self.pending3d = Some(model);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
state.renderer.upload_model(&state.device, &model.walls, &model.slabs);
|
state.renderer.upload_model(&state.device, &model.walls, &model.slabs, &model.meshes);
|
||||||
if !self.nav3d {
|
if !self.nav3d {
|
||||||
self.orbit = Some(Orbit::framed(&model));
|
self.orbit = Some(Orbit::framed(&model));
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-3
@@ -59,16 +59,38 @@ export interface RSlab {
|
|||||||
color: RRgb;
|
color: RRgb;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Modell-Bündel für den nativen 3D-Renderer (Wände + Decken). */
|
/** Art eines rohen Kontext-Meshes (bestimmt in render3d die Default-Einfärbung). */
|
||||||
|
export type RMeshKind = "terrain" | "imported";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ein rohes Dreiecks-Mesh aus dem Geo-Kontext (Terrain-TIN oder importiertes
|
||||||
|
* Gebäude-/DXF-Volumen). `positions` ist das flache (x,y,z=Höhe…)-Array in
|
||||||
|
* MODELL-Metern — genau wie `ImportedMesh`/`TerrainMesh` es hält; render3d bildet
|
||||||
|
* es selbst nach world (x, z, y) ab (analog Wände/Decken). Matcht
|
||||||
|
* render3d::types::MeshInput.
|
||||||
|
*/
|
||||||
|
export interface RMesh {
|
||||||
|
positions: number[];
|
||||||
|
indices: number[];
|
||||||
|
kind: RMeshKind;
|
||||||
|
color: RRgb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Modell-Bündel für den nativen 3D-Renderer (Wände + Decken + Kontext-Meshes). */
|
||||||
export interface RModel3d {
|
export interface RModel3d {
|
||||||
walls: RWall[];
|
walls: RWall[];
|
||||||
slabs: RSlab[];
|
slabs: RSlab[];
|
||||||
|
meshes: RMesh[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Warmer, neutraler Wand-Grundton (wie render3d default). */
|
/** Warmer, neutraler Wand-Grundton (wie render3d default). */
|
||||||
const WALL_RGB: RRgb = [0.82, 0.8, 0.76];
|
const WALL_RGB: RRgb = [0.82, 0.8, 0.76];
|
||||||
/** Etwas hellerer, kühlerer Ton für Deckenplatten (heben sich von Wänden ab). */
|
/** Etwas hellerer, kühlerer Ton für Deckenplatten (heben sich von Wänden ab). */
|
||||||
const SLAB_RGB: RRgb = [0.86, 0.86, 0.88];
|
const SLAB_RGB: RRgb = [0.86, 0.86, 0.88];
|
||||||
|
/** Gedämpftes Lehm-/Geländegrün-grau (three.js mats.terrain = 0xa9b0a2). */
|
||||||
|
const TERRAIN_RGB: RRgb = [169 / 255, 176 / 255, 162 / 255];
|
||||||
|
/** Neutrales Hellgrau für importierte Meshes (three.js mats.mesh = 0x9fa6ae). */
|
||||||
|
const IMPORTED_RGB: RRgb = [159 / 255, 166 / 255, 174 / 255];
|
||||||
|
|
||||||
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
||||||
const EPS = 1e-4;
|
const EPS = 1e-4;
|
||||||
@@ -219,6 +241,30 @@ function emitSlabs(project: Project): RSlab[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emittiert die rohen Kontext-Meshes eines Projekts: Gelände-TINs (`terrainMesh`)
|
||||||
|
* und importierte Volumen (`importedMesh`) aus `project.context`. `contourSet`
|
||||||
|
* bleibt außen vor — das sind nur 2D-Linien (Höhenlinien/Footprints), keine
|
||||||
|
* Dreiecksmeshes. Die `positions`/`indices` werden 1:1 durchgereicht (Modell-
|
||||||
|
* Koordinaten, x,y,z=Höhe); render3d bildet sie selbst nach world ab.
|
||||||
|
*/
|
||||||
|
function emitMeshes(project: Project): RMesh[] {
|
||||||
|
const out: RMesh[] = [];
|
||||||
|
for (const obj of project.context ?? []) {
|
||||||
|
if (obj.type === "terrainMesh" || obj.type === "importedMesh") {
|
||||||
|
// Leere/entartete Meshes überspringen (render3d würde sie ohnehin verwerfen).
|
||||||
|
if (obj.positions.length < 9 || obj.indices.length < 3) continue;
|
||||||
|
out.push({
|
||||||
|
positions: obj.positions,
|
||||||
|
indices: obj.indices,
|
||||||
|
kind: obj.type === "terrainMesh" ? "terrain" : "imported",
|
||||||
|
color: obj.type === "terrainMesh" ? TERRAIN_RGB : IMPORTED_RGB,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände
|
* Nur die Wände (Rückwärts-Kompatibilität / bestehende Aufrufer). Zerlegt Wände
|
||||||
* mit Öffnungen in Teilquader.
|
* mit Öffnungen in Teilquader.
|
||||||
@@ -229,7 +275,11 @@ export function projectToWalls3d(project: Project): RWall[] {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten. */
|
/** Das volle 3D-Modell: Wände (mit Öffnungen) + Deckenplatten + Kontext-Meshes. */
|
||||||
export function projectToModel3d(project: Project): RModel3d {
|
export function projectToModel3d(project: Project): RModel3d {
|
||||||
return { walls: projectToWalls3d(project), slabs: emitSlabs(project) };
|
return {
|
||||||
|
walls: projectToWalls3d(project),
|
||||||
|
slabs: emitSlabs(project),
|
||||||
|
meshes: emitMeshes(project),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user