0ca3b1dd57
- Zweiter Vertex-Pfad [pos,normal,uv] via build_walls_mesh_textured, ABGELEITET aus dem fertigen Mesh (Positionen/Normalen/Indizes 1:1) -> Alt-Pfad [pos,normal,color] bitgleich; per Regressionstest belegt. - Prozedurale 256x256-Schachbrett-Textur (kein Asset, kein image-Crate), Sampler Linear/Repeat, Textur-Bind-Group group 1 (Globals bleibt group 0). - MESH_TEXTURED_WGSL: gleiche Beleuchtung wie MESH_WGSL, Albedo aus textureSample. Pipeline in Depth/MSAA/Color-Target bitidentisch zur Haupt-Pipeline. - UV planar in Metern: Mantel u=entlang Achse/v=Hoehe, Deckel u=x/v=z; weltraumstabil, keine Verzerrung an Gehrungen. 1 Kachel = 1 m. - spike3d: Taste T schaltet Shaded <-> Textured zur Laufzeit (kein Re-Meshing). - Feature-gegatet, Default-Build/-Darstellung unveraendert. cargo test 58 (default) / 59 (--features render, inkl. naga-Test MESH_TEXTURED_WGSL) gruen.
1006 lines
40 KiB
Rust
1006 lines
40 KiB
Rust
// Nativer wgpu-3D-Renderer fuer die CAD-Modellsicht.
|
||
//
|
||
// Aufbau in Schichten (bewusst getrennt, siehe Feature-Flags in Cargo.toml):
|
||
// - `types` : serde-only Eingabe (geflachte Waende, Kamera) + Mesh-Ausgabe.
|
||
// - `mesh` : Wand-Extrusion (Band aus Achse+Dicke, hochgezogen auf Hoehe).
|
||
// GPU-frei, headless per `cargo test` pruefbar. Kern-Port der
|
||
// three.js-`ExtrudeGeometry`-Wanderzeugung.
|
||
// - `math` : Mat4 + Kamera (View/Projektion, Perspektive + Orthografie,
|
||
// die fuenf Presets) + Orbit-Helfer. Handgerechnet, testbar.
|
||
// - `shaders` : WGSL-Quelle (View-Projektion + Directional-Light).
|
||
// - `gpu` : wgpu-Pipeline mit Tiefenpuffer + Backface-Culling (Feature "render").
|
||
// - `bin/spike3d` : winit-Fenster mit Orbit-Kamera (Feature "window").
|
||
//
|
||
// Standard-Build (`cargo test`/`cargo build` ohne Features) enthaelt nur die
|
||
// GPU-freien Schichten und ist damit unabhaengig von einer Display-Session.
|
||
|
||
pub mod edges;
|
||
pub mod grid;
|
||
pub mod math;
|
||
pub mod mesh;
|
||
pub(crate) mod openings;
|
||
pub mod section;
|
||
pub mod section_fill;
|
||
pub mod shaders;
|
||
pub mod types;
|
||
|
||
#[cfg(feature = "render")]
|
||
pub mod gpu;
|
||
|
||
// Browser-Bindings (wasm32 + WebGPU), nur mit Feature "web". Setzt "render" voraus
|
||
// (dieselbe GPU-Schicht wie das native Fenster), fuegt aber KEINE winit-Abhaengigkeit
|
||
// hinzu — die Canvas dient direkt als wgpu-Surface. Muster: render2d/src/web.rs.
|
||
#[cfg(feature = "web")]
|
||
pub mod web;
|
||
|
||
pub use math::{
|
||
look_at, orbit_eye, orthographic, perspective, preset_camera, projection_matrix,
|
||
view_matrix, view_projection, Mat4,
|
||
};
|
||
pub use mesh::{
|
||
append_context_mesh, build_model_mesh, build_scene_mesh, build_walls_mesh,
|
||
build_walls_mesh_textured, extrude_slab, extrude_wall, textured_from_mesh, triangulate,
|
||
};
|
||
pub use section::{
|
||
cut_section, ComponentKind, ComponentRef, CutPolygon, SectionEdge, SectionOutput, SectionPlane,
|
||
};
|
||
pub use types::{
|
||
Camera, CameraPreset, Mesh, MeshInput, MeshKind, Point2, Projection, Rgb, SlabInput,
|
||
TexturedMesh, WallInput, FLOATS_PER_VERTEX, TEXTURED_FLOATS_PER_VERTEX,
|
||
};
|
||
|
||
// --- Tests: Mesh-Erzeugung (Muster wie render2d/tessellate) -------------------
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::mesh::{
|
||
build_walls_mesh, build_walls_mesh_textured, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX,
|
||
};
|
||
use super::types::{
|
||
Hole, Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX, TEXTURED_FLOATS_PER_VERTEX,
|
||
};
|
||
|
||
/// Bequemer Bau einer achsparallelen Wand entlang +X.
|
||
fn wall_x(len: f32, thickness: f32, height: f32) -> WallInput {
|
||
WallInput {
|
||
start: [0.0, 0.0],
|
||
end: [len, 0.0],
|
||
thickness,
|
||
height,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
}
|
||
}
|
||
|
||
/// Liest Position + Normale eines Vertex (index i) aus dem interleaved Puffer.
|
||
fn vert(mesh: &Mesh, i: usize) -> ([f32; 3], [f32; 3]) {
|
||
let b = i * FLOATS_PER_VERTEX;
|
||
(
|
||
[mesh.verts[b], mesh.verts[b + 1], mesh.verts[b + 2]],
|
||
[mesh.verts[b + 3], mesh.verts[b + 4], mesh.verts[b + 5]],
|
||
)
|
||
}
|
||
|
||
#[test]
|
||
fn texturierter_pfad_ist_geometrisch_deckungsgleich_mit_shaded() {
|
||
// Der texturierte Wand-Pfad (`build_walls_mesh_textured`, [pos,normal,uv])
|
||
// muss GEOMETRISCH bitgleich zum Alt-Pfad (`build_walls_mesh`,
|
||
// [pos,normal,color]) sein: gleiche Vertex-/Index-Zahl und -Reihenfolge,
|
||
// identische Positionen und Normalen. Nur der letzte Kanal (Farbe -> UV)
|
||
// unterscheidet sich. So ist belegt, dass der additive UV-Kanal den
|
||
// Alt-Pfad nicht beruehrt.
|
||
let walls = [wall_x(3.0, 0.2, 2.5)];
|
||
let shaded = build_walls_mesh(&walls);
|
||
let tex = build_walls_mesh_textured(&walls);
|
||
assert_eq!(tex.indices, shaded.indices, "Index-Puffer identisch");
|
||
assert_eq!(
|
||
tex.vertex_count(),
|
||
shaded.vertex_count(),
|
||
"gleiche Vertex-Zahl"
|
||
);
|
||
for i in 0..shaded.vertex_count() {
|
||
let sb = i * FLOATS_PER_VERTEX;
|
||
let tb = i * TEXTURED_FLOATS_PER_VERTEX;
|
||
// Position (0..3) und Normale (3..6) bitgleich uebernommen.
|
||
for k in 0..6 {
|
||
assert_eq!(
|
||
shaded.verts[sb + k],
|
||
tex.verts[tb + k],
|
||
"Vertex {i} Kanal {k}: pos/normal muss identisch sein"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn texturierte_uv_ist_weltmassstaeblich() {
|
||
// UV-Massstab: `u`/`v` in Metern (1 Kachel = 1 m). Fuer eine Wand entlang +X
|
||
// liegt die +Z-Mantelflaeche bei Normale (0,0,1); dort ist die Tangente
|
||
// t=(-n.z,0,n.x)=(-1,0,0), also u = -x und v = Hoehe. Die Deckflaeche
|
||
// (Normale +Y) projiziert auf u=x, v=z. Wir pruefen, dass es UEBERHAUPT
|
||
// Vertices mit v == Wandhoehe (2.5) bzw. v == 0 gibt (Hoehe wandert als v
|
||
// mit) — belegt den weltmassstaeblichen Hoehen-Kanal.
|
||
let tex = build_walls_mesh_textured(&[wall_x(3.0, 0.2, 2.5)]);
|
||
let n = tex.vertex_count();
|
||
let mut saw_top = false;
|
||
let mut saw_bottom = false;
|
||
for i in 0..n {
|
||
let b = i * TEXTURED_FLOATS_PER_VERTEX;
|
||
let ny = tex.verts[b + 4];
|
||
let v = tex.verts[b + 7];
|
||
// Nur Mantelflaechen (ny ~ 0) tragen die Hoehe als v.
|
||
if ny.abs() < 0.5 {
|
||
if (v - 2.5).abs() < 1e-5 {
|
||
saw_top = true;
|
||
}
|
||
if v.abs() < 1e-5 {
|
||
saw_bottom = true;
|
||
}
|
||
}
|
||
}
|
||
assert!(saw_top, "Mantel-Vertex mit v == Wandhoehe (2.5 m) erwartet");
|
||
assert!(saw_bottom, "Mantel-Vertex mit v == 0 (Sockel) erwartet");
|
||
}
|
||
|
||
#[test]
|
||
fn eine_wand_hat_quader_zaehlung() {
|
||
// Eine Wand -> ein Quader: 24 Vertices, 36 Indizes (12 Dreiecke).
|
||
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||
assert_eq!(mesh.vertex_count(), VERTS_PER_BOX, "24 Vertices je Quader");
|
||
assert_eq!(mesh.indices.len(), INDICES_PER_BOX, "36 Indizes je Quader");
|
||
assert_eq!(mesh.triangle_count(), 12, "12 Dreiecke je Quader");
|
||
// Kein Index zeigt ausserhalb des Puffers.
|
||
let max_idx = *mesh.indices.iter().max().unwrap();
|
||
assert!((max_idx as usize) < mesh.vertex_count());
|
||
}
|
||
|
||
#[test]
|
||
fn mehrere_waende_addieren_sich() {
|
||
let mesh = build_walls_mesh(&[
|
||
wall_x(3.0, 0.2, 2.5),
|
||
WallInput {
|
||
start: [3.0, 0.0],
|
||
end: [3.0, 4.0],
|
||
thickness: 0.2,
|
||
height: 2.5,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
},
|
||
]);
|
||
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX);
|
||
assert_eq!(mesh.indices.len(), 2 * INDICES_PER_BOX);
|
||
}
|
||
|
||
#[test]
|
||
fn bounding_box_deckt_dicke_laenge_hoehe_ab() {
|
||
// Wand entlang +X, Laenge 3, Dicke 0.2 -> Band in Z von -0.1..0.1;
|
||
// Hoehe 2.5 ab Basis 0. world: (x, y=hoehe, z=grundriss.y).
|
||
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||
let (min, max) = mesh.bounds();
|
||
// X: 0..3 (Achsenlaenge).
|
||
assert!((min[0] - 0.0).abs() < 1e-5, "min x == 0");
|
||
assert!((max[0] - 3.0).abs() < 1e-5, "max x == 3");
|
||
// Y: 0..2.5 (Hoehe).
|
||
assert!((min[1] - 0.0).abs() < 1e-5, "min y == 0");
|
||
assert!((max[1] - 2.5).abs() < 1e-5, "max y == 2.5");
|
||
// Z: -0.1..0.1 (halbe Dicke je Seite).
|
||
assert!((min[2] + 0.1).abs() < 1e-5, "min z == -0.1");
|
||
assert!((max[2] - 0.1).abs() < 1e-5, "max z == 0.1");
|
||
}
|
||
|
||
#[test]
|
||
fn basis_hoehe_verschiebt_in_y() {
|
||
let mut w = wall_x(3.0, 0.2, 2.5);
|
||
w.base_elevation = 3.0; // Obergeschoss.
|
||
let mesh = build_walls_mesh(&[w]);
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[1] - 3.0).abs() < 1e-5, "Unterkante auf Basis 3.0");
|
||
assert!((max[1] - 5.5).abs() < 1e-5, "Oberkante 3.0 + 2.5");
|
||
}
|
||
|
||
#[test]
|
||
fn deckel_normale_zeigt_nach_oben() {
|
||
// Der Deckel wird als erstes Quad angehaengt (Vertices 0..3): Normale +Y.
|
||
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||
let (_, n0) = vert(&mesh, 0);
|
||
assert!((n0[0]).abs() < 1e-6);
|
||
assert!((n0[1] - 1.0).abs() < 1e-6, "Deckel-Normale +Y");
|
||
assert!((n0[2]).abs() < 1e-6);
|
||
}
|
||
|
||
#[test]
|
||
fn mantel_normalen_zeigen_nach_aussen() {
|
||
// Wand entlang +X: die +n-Seite liegt bei z=+0.1 (n = leftNormal von +X =
|
||
// (0,1) im Grundriss -> world +Z), die -n-Seite bei z=-0.1. Fuer JEDE
|
||
// Mantelflaechen-Normale muss gelten: sie zeigt vom Wand-Zentrum weg.
|
||
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||
// Wand-Zentrum in world.
|
||
let center = [1.5f32, 1.25, 0.0];
|
||
// Alle Vertices durchgehen; fuer jede Flaeche muss die Normale eine
|
||
// positive Komponente in Richtung (Vertex - Zentrum) haben (zeigt raus).
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let (p, nor) = vert(&mesh, i);
|
||
let out = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
|
||
let d = out[0] * nor[0] + out[1] * nor[1] + out[2] * nor[2];
|
||
// >= 0: die Normale weist nie ins Innere (Backface-Culling korrekt).
|
||
assert!(
|
||
d >= -1e-5,
|
||
"Vertex {i}: Normale zeigt nach innen (dot={d})"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn diagonale_wand_hat_gleiche_zaehlung() {
|
||
// Auch nicht-achsparallele Waende extrudieren korrekt (nur andere Ecken).
|
||
let w = WallInput {
|
||
start: [0.0, 0.0],
|
||
end: [2.0, 2.0],
|
||
thickness: 0.3,
|
||
height: 3.0,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
};
|
||
let mut mesh = Mesh::default();
|
||
extrude_wall(&mut mesh, &w);
|
||
assert_eq!(mesh.vertex_count(), VERTS_PER_BOX);
|
||
// Hoehe deckt Y 0..3 ab.
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[1]).abs() < 1e-5 && (max[1] - 3.0).abs() < 1e-5);
|
||
}
|
||
|
||
#[test]
|
||
fn degenerierte_wand_erzeugt_nichts() {
|
||
// Start == Ende -> keine Richtung -> uebersprungen (kein Absturz).
|
||
let w = WallInput {
|
||
start: [1.0, 1.0],
|
||
end: [1.0, 1.0],
|
||
thickness: 0.2,
|
||
height: 2.5,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
};
|
||
let mesh = build_walls_mesh(&[w]);
|
||
assert_eq!(mesh.vertex_count(), 0);
|
||
assert!(mesh.indices.is_empty());
|
||
}
|
||
|
||
// --- Oeffnungen (Fenster/Tueren) in der Vollkoerper-Extrusion -------------
|
||
|
||
#[test]
|
||
fn fenster_erzeugt_pfeiler_bruestung_sturz_und_sichtbare_luecke() {
|
||
// Wand entlang +X, Laenge 4, ein Fenster bei x in [1.5,2.5], Bruestung
|
||
// 0.9, Kopfhoehe 2.1 (< Wandhoehe 2.5) -> vier Teil-Quader: Pfeiler
|
||
// 0..1.5 (voll), Bruestung 1.5..2.5 (0..0.9), Sturz 1.5..2.5 (2.1..2.5),
|
||
// Pfeiler 2.5..4 (voll). Kein Vollkoerper mehr ueber die Oeffnung.
|
||
let mut w = wall_x(4.0, 0.2, 2.5);
|
||
w.openings = vec![Opening {
|
||
from: 1.5,
|
||
to: 2.5,
|
||
sill: 0.9,
|
||
height: 1.2,
|
||
}];
|
||
let mesh = build_walls_mesh(&[w]);
|
||
assert_eq!(
|
||
mesh.vertex_count(),
|
||
4 * VERTS_PER_BOX,
|
||
"vier Teil-Quader (2 Pfeiler + Bruestung + Sturz)"
|
||
);
|
||
assert_eq!(mesh.indices.len(), 4 * INDICES_PER_BOX);
|
||
let max_idx = *mesh.indices.iter().max().unwrap();
|
||
assert!((max_idx as usize) < mesh.vertex_count());
|
||
|
||
// Aussenmasse bleiben unveraendert (Pfeiler+Bruestung+Sturz decken
|
||
// zusammen wieder die volle Bounding-Box ab).
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[0]).abs() < 1e-5 && (max[0] - 4.0).abs() < 1e-5);
|
||
assert!((min[1]).abs() < 1e-5 && (max[1] - 2.5).abs() < 1e-5);
|
||
|
||
// Sichtbare Luecke: kein Vertex liegt (mit Sicherheitsabstand) im
|
||
// Oeffnungs-Loch selbst (x in (1.5,2.5) UND y in (0.9,2.1)) — dort darf
|
||
// keine Mesh-Flaeche mehr existieren.
|
||
let margin = 0.05;
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
let inside_hole = p[0] > 1.5 + margin
|
||
&& p[0] < 2.5 - margin
|
||
&& p[1] > 0.9 + margin
|
||
&& p[1] < 2.1 - margin;
|
||
assert!(!inside_hole, "Vertex {i} bei {p:?} liegt im Fensterloch");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn tuer_erreicht_den_boden_keine_bruestung() {
|
||
// Tuer (sill == 0.0) reicht bis zum Boden -> keine Bruestung, nur zwei
|
||
// volle Pfeiler + ein Sturz-Quader ueber der Tuer (drei Teil-Quader).
|
||
let mut w = wall_x(4.0, 0.2, 2.5);
|
||
w.openings = vec![Opening {
|
||
from: 1.0,
|
||
to: 1.8,
|
||
sill: 0.0,
|
||
height: 2.1,
|
||
}];
|
||
let mesh = build_walls_mesh(&[w]);
|
||
assert_eq!(
|
||
mesh.vertex_count(),
|
||
3 * VERTS_PER_BOX,
|
||
"zwei Pfeiler + ein Sturz, keine Bruestung"
|
||
);
|
||
assert_eq!(mesh.indices.len(), 3 * INDICES_PER_BOX);
|
||
|
||
// Im Tuerbereich (x in (1.0,1.8)) darf unterhalb der Kopfhoehe (2.1)
|
||
// keine Geometrie liegen -> jeder Vertex dort liegt auf/ueber dem Sturz.
|
||
let margin = 0.05;
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
let in_door_span = p[0] > 1.0 + margin && p[0] < 1.8 - margin;
|
||
if in_door_span {
|
||
assert!(
|
||
p[1] > 2.1 - 1e-4,
|
||
"Vertex {i} bei {p:?} liegt unter der Tuer-Kopfhoehe"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn oeffnung_ausserhalb_der_wandlaenge_erzeugt_vollkoerper() {
|
||
// Oeffnung komplett ausserhalb [0,length] -> Regressionsfall: ein
|
||
// einzelner Vollkoerper-Quader wie ohne Oeffnungen.
|
||
let mut w = wall_x(4.0, 0.2, 2.5);
|
||
w.openings = vec![Opening {
|
||
from: 5.0,
|
||
to: 6.0,
|
||
sill: 0.9,
|
||
height: 1.2,
|
||
}];
|
||
let mesh = build_walls_mesh(&[w]);
|
||
assert_eq!(mesh.vertex_count(), VERTS_PER_BOX);
|
||
assert_eq!(mesh.indices.len(), INDICES_PER_BOX);
|
||
}
|
||
|
||
// --- Echte Loecher in EINEM Wandkoerper (WallInput::holes) -----------------
|
||
|
||
#[test]
|
||
fn zwei_versetzte_ueberlappende_loecher_in_einem_koerper() {
|
||
// KERNFALL: EINE Wand (Laenge 4, Hoehe 2.5) mit ZWEI Loechern, deren
|
||
// Achsen-Intervalle sich UEBERLAPPEN, aber auf UNTERSCHIEDLICHER Hoehe
|
||
// liegen — Fenster A u=[1.0,2.0] z=[0.9,2.1], Fenster B u=[1.5,2.5]
|
||
// z=[0.3,0.7]. Das kann die Segment-Zerlegung nicht abbilden; als Loecher
|
||
// in EINEM Koerper muss es sauber zerfallen (keine Geometrie in den
|
||
// Loechern, Aussenmasse erhalten, beide Laibungen vollstaendig).
|
||
let mut w = wall_x(4.0, 0.2, 2.5);
|
||
w.holes = vec![
|
||
Hole { from: 1.0, to: 2.0, z_bottom: 0.9, z_top: 2.1 },
|
||
Hole { from: 1.5, to: 2.5, z_bottom: 0.3, z_top: 0.7 },
|
||
];
|
||
let mesh = build_walls_mesh(&[w]);
|
||
|
||
// Mehr als ein Vollkoerper (Loecher fuegen Teilrechtecke + Laibungen hinzu).
|
||
assert!(mesh.vertex_count() > VERTS_PER_BOX);
|
||
// Index-Integritaet.
|
||
let max_idx = *mesh.indices.iter().max().unwrap();
|
||
assert!((max_idx as usize) < mesh.vertex_count());
|
||
|
||
// Aussenmasse unveraendert (die soliden Teilrechtecke decken den Rand ab).
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[0]).abs() < 1e-5 && (max[0] - 4.0).abs() < 1e-5, "u 0..4");
|
||
assert!((min[1]).abs() < 1e-5 && (max[1] - 2.5).abs() < 1e-5, "z 0..2.5");
|
||
assert!((min[2] + 0.1).abs() < 1e-5 && (max[2] - 0.1).abs() < 1e-5, "Dicke +/-0.1");
|
||
|
||
// KEIN Vertex liegt (mit Sicherheitsabstand) im Inneren eines der Loecher.
|
||
let holes = [(1.0f32, 2.0f32, 0.9f32, 2.1f32), (1.5, 2.5, 0.3, 0.7)];
|
||
let m = 0.05;
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
for (a0, a1, zb, zt) in holes {
|
||
let inside =
|
||
p[0] > a0 + m && p[0] < a1 - m && p[1] > zb + m && p[1] < zt - m;
|
||
assert!(!inside, "Vertex {i} bei {p:?} liegt im Loch [{a0},{a1}]x[{zb},{zt}]");
|
||
}
|
||
}
|
||
|
||
// Beide Loecher besitzen ihre vier inneren Laibungen: fuer jede Loch-Kante
|
||
// muss mindestens ein Vertex GENAU auf der Kante (in u bzw. z) UND im
|
||
// jeweils anderen Loch-Intervall liegen (die Laibungsflaeche).
|
||
let has_vert = |pred: &dyn Fn([f32; 3]) -> bool| (0..n).any(|i| pred(vert(&mesh, i).0));
|
||
for (a0, a1, zb, zt) in holes {
|
||
// linke/rechte Laibung: Vertex bei u==a0 bzw. u==a1, z im Loch.
|
||
assert!(
|
||
has_vert(&|p| (p[0] - a0).abs() < 1e-4 && p[1] > zb - 1e-4 && p[1] < zt + 1e-4),
|
||
"linke Laibung fehlt fuer Loch [{a0},{a1}]"
|
||
);
|
||
assert!(
|
||
has_vert(&|p| (p[0] - a1).abs() < 1e-4 && p[1] > zb - 1e-4 && p[1] < zt + 1e-4),
|
||
"rechte Laibung fehlt fuer Loch [{a0},{a1}]"
|
||
);
|
||
// untere/obere Laibung: Vertex bei z==zb bzw. z==zt, u im Loch.
|
||
assert!(
|
||
has_vert(&|p| (p[1] - zb).abs() < 1e-4 && p[0] > a0 - 1e-4 && p[0] < a1 + 1e-4),
|
||
"untere Laibung fehlt fuer Loch [{a0},{a1}]"
|
||
);
|
||
assert!(
|
||
has_vert(&|p| (p[1] - zt).abs() < 1e-4 && p[0] > a0 - 1e-4 && p[0] < a1 + 1e-4),
|
||
"obere Laibung fehlt fuer Loch [{a0},{a1}]"
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn tuer_loch_bis_zum_boden_ohne_untere_laibung() {
|
||
// Tuer-Loch reicht bis zum Boden (z_bottom == base_elevation 0): die
|
||
// untere Kante liegt am Wandrand -> KEINE untere Laibung, stattdessen eine
|
||
// Luecke im Boden-Streifen. Unter der Kopfhoehe (z_top 2.1) darf im
|
||
// Tuerbereich keine Geometrie liegen.
|
||
let mut w = wall_x(4.0, 0.2, 2.5);
|
||
w.holes = vec![Hole { from: 1.0, to: 1.8, z_bottom: 0.0, z_top: 2.1 }];
|
||
let mesh = build_walls_mesh(&[w]);
|
||
let n = mesh.vertex_count();
|
||
let m = 0.05;
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
let in_span = p[0] > 1.0 + m && p[0] < 1.8 - m;
|
||
if in_span {
|
||
assert!(
|
||
p[1] < m || p[1] > 2.1 - 1e-4,
|
||
"Vertex {i} bei {p:?} liegt unter der Tuer-Kopfhoehe im Loch"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Ecken-Verschneidung (Gehrung) -----------------------------------------
|
||
|
||
#[test]
|
||
fn rechtwinklige_ecke_wird_gehrt_kein_ueberstand_kein_spalt() {
|
||
// Zwei Waende entlang +X bzw. +Y, die sich exakt im Ursprung treffen
|
||
// (L-Ecke, Dicke 0.4 -> Halbdicke 0.2). Ohne Gehrung ueberlappen sich die
|
||
// Baender im Eckbereich (jede Wand ragt um die Halbdicke der jeweils
|
||
// anderen ueber den Knoten hinaus); MIT Gehrung muessen alle Vertices IM
|
||
// Eckbereich exakt auf der 45-Grad-Gehrungslinie |x| == |z| liegen —
|
||
// weder darueber hinaus (Ueberstand) noch darunter (Spalt).
|
||
let a = WallInput {
|
||
start: [0.0, 0.0],
|
||
end: [3.0, 0.0],
|
||
thickness: 0.4,
|
||
height: 2.5,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
};
|
||
let b = WallInput {
|
||
start: [0.0, 0.0],
|
||
end: [0.0, 3.0],
|
||
thickness: 0.4,
|
||
height: 2.5,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
};
|
||
let mesh = build_walls_mesh(&[a, b]);
|
||
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX, "weiterhin zwei Quader");
|
||
|
||
// Jeder Vertex nahe dem Knoten (innerhalb der doppelten Halbdicke) muss
|
||
// auf der Gehrungslinie liegen: world.x und world.z (== Grundriss-y)
|
||
// betragsgleich (45-Grad-Winkelhalbierende der beiden Achsen).
|
||
let near_corner_margin = 0.5; // deutlich groesser als die Halbdicke 0.2
|
||
let n = mesh.vertex_count();
|
||
let mut checked_any = false;
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
if p[0].abs() < near_corner_margin && p[2].abs() < near_corner_margin {
|
||
checked_any = true;
|
||
assert!(
|
||
(p[0].abs() - p[2].abs()).abs() < 1e-4,
|
||
"Eck-Vertex {i} bei {p:?} liegt nicht auf der 45-Grad-Gehrungslinie"
|
||
);
|
||
}
|
||
}
|
||
assert!(checked_any, "Test hat keinen Eck-Vertex gefunden (Setup falsch)");
|
||
|
||
// Kein Vertex ragt ueber die Gehrungslinie hinaus: fuer Punkte mit x>0
|
||
// UND z>0 (im "inneren" Quadranten der Ecke) muss min(x,z) <= Halbdicke
|
||
// gelten -- sonst wuerde die Wand ueber die andere hinausragen.
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
if p[0] > 1e-4 && p[2] > 1e-4 && p[0] < near_corner_margin && p[2] < near_corner_margin {
|
||
assert!(
|
||
p[0].min(p[2]) <= 0.2 + 1e-4,
|
||
"Vertex {i} bei {p:?} ragt ueber die Gehrung hinaus"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn freies_wandende_bleibt_rechtwinklig() {
|
||
// Eine einzelne Wand ohne Nachbarn: keine Gehrung, Enden bleiben exakt
|
||
// senkrecht zur Achse -> identisch zum Einzel-Wand-Regressionsfall.
|
||
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[0]).abs() < 1e-5 && (max[0] - 3.0).abs() < 1e-5);
|
||
}
|
||
|
||
#[test]
|
||
fn t_stoss_bleibt_rechtwinklig() {
|
||
// Drei Wandenden treffen sich im selben Knoten (T-Stoss) -> gemaess
|
||
// Moduldoc/`joins.ts` bewusst NICHT gehrt (mehrdeutig), alle drei bleiben
|
||
// rechtwinklig. Regressions-Absicherung: keine Panik, stabile Zaehlung.
|
||
let mk = |s: [f32; 2], e: [f32; 2]| WallInput {
|
||
start: s,
|
||
end: e,
|
||
thickness: 0.2,
|
||
height: 2.5,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
};
|
||
let walls = vec![
|
||
mk([0.0, 0.0], [3.0, 0.0]),
|
||
mk([0.0, 0.0], [0.0, 3.0]),
|
||
mk([0.0, 0.0], [-3.0, 0.0]),
|
||
];
|
||
let mesh = build_walls_mesh(&walls);
|
||
assert_eq!(mesh.vertex_count(), 3 * VERTS_PER_BOX);
|
||
}
|
||
|
||
#[test]
|
||
fn zwei_geschosse_mit_gleichem_grundriss_verschneiden_sich_nicht_ueber_geschosse() {
|
||
// EG (Hoehe 0..2.5) und OG (Basis 2.5, Hoehe 2.5..5.0) mit identischem
|
||
// L-Grundriss: die Hoehenbereiche BERUEHREN sich nur (bei 2.5), ueber-
|
||
// lappen aber nicht -> jedes Geschoss wird NUR gegen sein eigenes
|
||
// Pendant gehrt, nicht ueber die Geschossgrenze hinweg. Absicherung:
|
||
// beide Geschosse zusammen liefern exakt dieselbe Gehrung wie ein
|
||
// einzelnes Geschoss fuer sich (Vertex-Zaehlung + Eck-Vertices je
|
||
// Geschoss weiterhin auf der 45-Grad-Linie relativ zu SEINER Basis).
|
||
let mk = |s: [f32; 2], e: [f32; 2], base: f32| WallInput {
|
||
start: s,
|
||
end: e,
|
||
thickness: 0.4,
|
||
height: 2.5,
|
||
base_elevation: base,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: None,
|
||
holes: vec![],
|
||
};
|
||
let walls = vec![
|
||
mk([0.0, 0.0], [3.0, 0.0], 0.0),
|
||
mk([0.0, 0.0], [0.0, 3.0], 0.0),
|
||
mk([0.0, 0.0], [3.0, 0.0], 2.5),
|
||
mk([0.0, 0.0], [0.0, 3.0], 2.5),
|
||
];
|
||
let mesh = build_walls_mesh(&walls);
|
||
assert_eq!(mesh.vertex_count(), 4 * VERTS_PER_BOX);
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
if p[0].abs() < 0.5 && p[2].abs() < 0.5 {
|
||
assert!(
|
||
(p[0].abs() - p[2].abs()).abs() < 1e-4,
|
||
"Eck-Vertex {i} bei {p:?} (Geschoss-Basis {}) nicht auf der Gehrung",
|
||
p[1]
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Schichten (WallInput::layers) -----------------------------------------
|
||
|
||
#[test]
|
||
fn ohne_layers_entsteht_weiterhin_ein_vollkoerper() {
|
||
// Regressionsfall: `layers: None` extrudiert exakt wie zuvor.
|
||
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||
assert_eq!(mesh.vertex_count(), VERTS_PER_BOX);
|
||
}
|
||
|
||
#[test]
|
||
fn layer_wand_liefert_ein_prisma_je_schicht() {
|
||
// Drei Schichten (Putz/Daemmung/Mauerwerk) -> drei eigenstaendige
|
||
// Quader, je VERTS_PER_BOX Vertices, ohne Oeffnungen.
|
||
let mut w = wall_x(3.0, 0.3, 2.5);
|
||
w.layers = Some(vec![
|
||
WallLayer { thickness: 0.02, color: [1.0, 1.0, 1.0] },
|
||
WallLayer { thickness: 0.18, color: [0.9, 0.85, 0.7] },
|
||
WallLayer { thickness: 0.10, color: [0.6, 0.3, 0.2] },
|
||
]);
|
||
let mesh = build_walls_mesh(&[w]);
|
||
assert_eq!(mesh.vertex_count(), 3 * VERTS_PER_BOX);
|
||
assert_eq!(mesh.indices.len(), 3 * INDICES_PER_BOX);
|
||
|
||
// Die Schichten decken zusammen exakt die Gesamtdicke (0.3, Z in
|
||
// -0.15..0.15) lueckenlos und ueberlappungsfrei ab.
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[2] + 0.15).abs() < 1e-4, "min z == -0.15");
|
||
assert!((max[2] - 0.15).abs() < 1e-4, "max z == 0.15");
|
||
|
||
// Jede Schicht-Farbe taucht in den Vertex-Farben auf (Layer wurden
|
||
// tatsaechlich mit ihrer eigenen Farbe extrudiert, nicht der Wandfarbe).
|
||
let n = mesh.vertex_count();
|
||
let has_color = |target: [f32; 3]| {
|
||
(0..n).any(|i| {
|
||
let b = i * FLOATS_PER_VERTEX;
|
||
let c = [mesh.verts[b + 6], mesh.verts[b + 7], mesh.verts[b + 8]];
|
||
(c[0] - target[0]).abs() < 1e-6
|
||
&& (c[1] - target[1]).abs() < 1e-6
|
||
&& (c[2] - target[2]).abs() < 1e-6
|
||
})
|
||
};
|
||
assert!(has_color([1.0, 1.0, 1.0]), "Putz-Farbe fehlt");
|
||
assert!(has_color([0.9, 0.85, 0.7]), "Daemmungs-Farbe fehlt");
|
||
assert!(has_color([0.6, 0.3, 0.2]), "Mauerwerk-Farbe fehlt");
|
||
}
|
||
|
||
#[test]
|
||
fn layer_ecke_wird_ebenfalls_gehrt() {
|
||
// Zwei mehrschichtige Waende an einer Ecke: JEDE Schicht nutzt dasselbe
|
||
// gehrte Fussabdruck-Band -> auch hier landen alle Eck-Vertices exakt
|
||
// auf der 45-Grad-Gehrungslinie (analog zum Vollkoerper-Test oben).
|
||
let layers = || {
|
||
Some(vec![
|
||
WallLayer { thickness: 0.15, color: [0.9, 0.85, 0.7] },
|
||
WallLayer { thickness: 0.15, color: [0.6, 0.3, 0.2] },
|
||
])
|
||
};
|
||
let a = WallInput {
|
||
start: [0.0, 0.0],
|
||
end: [3.0, 0.0],
|
||
thickness: 0.3,
|
||
height: 2.5,
|
||
base_elevation: 0.0,
|
||
color: [0.8, 0.8, 0.8],
|
||
openings: vec![],
|
||
layers: layers(),
|
||
holes: vec![],
|
||
};
|
||
let mut b = a.clone();
|
||
b.end = [0.0, 3.0];
|
||
|
||
let mesh = build_walls_mesh(&[a, b]);
|
||
let n = mesh.vertex_count();
|
||
assert_eq!(n, 4 * VERTS_PER_BOX, "2 Waende x 2 Schichten");
|
||
for i in 0..n {
|
||
let (p, _) = vert(&mesh, i);
|
||
if p[0].abs() < 0.4 && p[2].abs() < 0.4 {
|
||
assert!(
|
||
(p[0].abs() - p[2].abs()).abs() < 1e-4,
|
||
"Layer-Eck-Vertex {i} bei {p:?} nicht auf der Gehrung"
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// --- Deckenplatten (extrudierte Polygone) ---------------------------------
|
||
|
||
use super::mesh::{build_model_mesh, extrude_slab, triangulate};
|
||
use super::types::SlabInput;
|
||
|
||
/// Quadratischer Decken-Umriss (CCW) mit Kantenlaenge `s`, Ecke im Ursprung.
|
||
fn square_slab(s: f32, z_bottom: f32, z_top: f32) -> SlabInput {
|
||
SlabInput {
|
||
outline: vec![[0.0, 0.0], [s, 0.0], [s, s], [0.0, s]],
|
||
z_bottom,
|
||
z_top,
|
||
color: [0.86, 0.86, 0.88],
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn triangulate_quadrat_gibt_zwei_dreiecke() {
|
||
let tris = triangulate(&[[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]);
|
||
assert_eq!(tris.len(), 6, "Quadrat -> 2 Dreiecke = 6 Indizes");
|
||
}
|
||
|
||
#[test]
|
||
fn triangulate_cw_umriss_ebenfalls_zwei_dreiecke() {
|
||
// CW-Umriss (negative Flaeche) muss genauso trianguliert werden.
|
||
let tris = triangulate(&[[0.0, 0.0], [0.0, 1.0], [1.0, 1.0], [1.0, 0.0]]);
|
||
assert_eq!(tris.len(), 6);
|
||
}
|
||
|
||
#[test]
|
||
fn slab_deckel_und_boden_und_mantel() {
|
||
// Quadratische Platte: Deckel (2 Tri) + Boden (2 Tri) + 4 Mantel-Quads
|
||
// (je 2 Tri) = 4 + 8 = 12 Dreiecke = 36 Vertices (je Tri eigene Vertices).
|
||
let mut mesh = Mesh::default();
|
||
extrude_slab(&mut mesh, &square_slab(4.0, 2.4, 2.65));
|
||
assert_eq!(mesh.triangle_count(), 12, "2+2 Kappen + 8 Mantel");
|
||
assert_eq!(mesh.vertex_count(), 36);
|
||
// Z-Ausdehnung deckt die Deckendicke ab.
|
||
let (min, max) = mesh.bounds();
|
||
assert!((min[1] - 2.4).abs() < 1e-5, "UK 2.4");
|
||
assert!((max[1] - 2.65).abs() < 1e-5, "OK 2.65");
|
||
}
|
||
|
||
#[test]
|
||
fn slab_deckel_normale_zeigt_nach_oben_boden_nach_unten() {
|
||
let mut mesh = Mesh::default();
|
||
extrude_slab(&mut mesh, &square_slab(4.0, 0.0, 0.25));
|
||
// Fuer jedes Vertex mit y==0.25 (Deckel) muss die Normale +Y sein, fuer
|
||
// y==0.0 (Boden) −Y. Mantel-Vertices haben n.y ≈ 0.
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let b = i * FLOATS_PER_VERTEX;
|
||
let py = mesh.verts[b + 1];
|
||
let ny = mesh.verts[b + 4];
|
||
if (py - 0.25).abs() < 1e-6 && (mesh.verts[b + 3]).abs() < 1e-6 && (mesh.verts[b + 5]).abs() < 1e-6 {
|
||
assert!((ny - 1.0).abs() < 1e-5, "Deckel-Normale +Y");
|
||
}
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn slab_mantel_normalen_zeigen_nach_aussen() {
|
||
// Zentriertes Quadrat um den Ursprung: jede Mantel-Normale muss vom
|
||
// Zentrum weg zeigen.
|
||
let slab = SlabInput {
|
||
outline: vec![[-2.0, -2.0], [2.0, -2.0], [2.0, 2.0], [-2.0, 2.0]],
|
||
z_bottom: 0.0,
|
||
z_top: 0.3,
|
||
color: [0.8, 0.8, 0.8],
|
||
};
|
||
let mut mesh = Mesh::default();
|
||
extrude_slab(&mut mesh, &slab);
|
||
let center = [0.0f32, 0.15, 0.0];
|
||
let n = mesh.vertex_count();
|
||
for i in 0..n {
|
||
let b = i * FLOATS_PER_VERTEX;
|
||
let p = [mesh.verts[b], mesh.verts[b + 1], mesh.verts[b + 2]];
|
||
let nor = [mesh.verts[b + 3], mesh.verts[b + 4], mesh.verts[b + 5]];
|
||
let out = [p[0] - center[0], p[1] - center[1], p[2] - center[2]];
|
||
let d = out[0] * nor[0] + out[1] * nor[1] + out[2] * nor[2];
|
||
assert!(d >= -1e-4, "Vertex {i}: Normale zeigt nach innen (dot={d})");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn build_model_mesh_haengt_slabs_an_waende() {
|
||
let wall = build_walls_mesh(&[wall_x(4.0, 0.2, 2.6)]);
|
||
let model = build_model_mesh(&[wall_x(4.0, 0.2, 2.6)], &[square_slab(4.0, 2.6, 2.85)]);
|
||
// Modell = Wand-Vertices + Slab-Vertices.
|
||
assert_eq!(model.vertex_count(), wall.vertex_count() + 36);
|
||
assert!(model.indices.len() > wall.indices.len());
|
||
}
|
||
|
||
#[test]
|
||
fn entarteter_slab_erzeugt_nichts() {
|
||
let mut mesh = Mesh::default();
|
||
// <3 Ecken.
|
||
extrude_slab(&mut mesh, &SlabInput { outline: vec![[0.0, 0.0], [1.0, 0.0]], z_bottom: 0.0, z_top: 0.2, color: [0.8, 0.8, 0.8] });
|
||
assert_eq!(mesh.vertex_count(), 0);
|
||
// Nullhoehe.
|
||
extrude_slab(&mut mesh, &square_slab(4.0, 2.6, 2.6));
|
||
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 ----------------------------------------------------
|
||
|
||
#[test]
|
||
fn look_at_setzt_kamera_ins_zentrum() {
|
||
use super::math::{transform_point, view_matrix};
|
||
use super::types::Camera;
|
||
let cam = Camera {
|
||
eye: [0.0, 0.0, 5.0],
|
||
target: [0.0, 0.0, 0.0],
|
||
up: [0.0, 1.0, 0.0],
|
||
..Camera::default()
|
||
};
|
||
let v = view_matrix(&cam);
|
||
// Das Blickziel (Ursprung) liegt im View-Raum vor der Kamera bei z=-5
|
||
// (Kamera schaut entlang -Z).
|
||
let p = transform_point(&v, [0.0, 0.0, 0.0]);
|
||
assert!((p[0]).abs() < 1e-5 && (p[1]).abs() < 1e-5);
|
||
assert!((p[2] + 5.0).abs() < 1e-5, "Ziel bei view-z = -5");
|
||
}
|
||
|
||
#[test]
|
||
fn perspektive_klemmt_z_in_null_bis_eins() {
|
||
use super::math::{perspective, transform_point};
|
||
let m = perspective(60.0_f32.to_radians(), 1.0, 1.0, 100.0);
|
||
// Punkt auf der Nah-Ebene (view-z = -near) -> clip-z/w == 0.
|
||
let near = transform_point(&m, [0.0, 0.0, -1.0]);
|
||
assert!((near[2] / near[3]).abs() < 1e-4, "Nah-Ebene -> z=0");
|
||
// Punkt auf der Fern-Ebene (view-z = -far) -> clip-z/w == 1.
|
||
let far = transform_point(&m, [0.0, 0.0, -100.0]);
|
||
assert!(((far[2] / far[3]) - 1.0).abs() < 1e-4, "Fern-Ebene -> z=1");
|
||
}
|
||
|
||
#[test]
|
||
fn orbit_eye_haelt_abstand_ein() {
|
||
use super::math::orbit_eye;
|
||
let target = [1.0, 0.0, 2.0];
|
||
let e = orbit_eye(target, 0.7, 0.4, 10.0);
|
||
let d = ((e[0] - target[0]).powi(2)
|
||
+ (e[1] - target[1]).powi(2)
|
||
+ (e[2] - target[2]).powi(2))
|
||
.sqrt();
|
||
assert!((d - 10.0).abs() < 1e-4, "Abstand == dist");
|
||
}
|
||
|
||
#[test]
|
||
fn presets_setzen_projektionsart() {
|
||
use super::math::preset_camera;
|
||
use super::types::{CameraPreset, Projection};
|
||
let t = [0.0, 0.0, 0.0];
|
||
assert_eq!(
|
||
preset_camera(CameraPreset::Top, t, 10.0).projection,
|
||
Projection::Orthographic
|
||
);
|
||
assert_eq!(
|
||
preset_camera(CameraPreset::Front, t, 10.0).projection,
|
||
Projection::Orthographic
|
||
);
|
||
assert_eq!(
|
||
preset_camera(CameraPreset::Side, t, 10.0).projection,
|
||
Projection::Orthographic
|
||
);
|
||
assert_eq!(
|
||
preset_camera(CameraPreset::Iso, t, 10.0).projection,
|
||
Projection::Orthographic
|
||
);
|
||
assert_eq!(
|
||
preset_camera(CameraPreset::Persp, t, 10.0).projection,
|
||
Projection::Perspective
|
||
);
|
||
// Top blickt von oben herab: eye.y > target.y.
|
||
let top = preset_camera(CameraPreset::Top, t, 10.0);
|
||
assert!(top.eye[1] > t[1], "Top-Kamera ueber dem Ziel");
|
||
}
|
||
|
||
/// Validiert die WGSL-Quelle headless ueber naga (Parser + Validator) — faengt
|
||
/// Syntax-/Typfehler ohne GPU/Display ab. Nur mit Feature "render", weil naga
|
||
/// sonst nicht mitgebaut wird (Muster: render2d).
|
||
#[cfg(feature = "render")]
|
||
#[test]
|
||
fn wgsl_quelle_ist_valide() {
|
||
use naga::valid::{Capabilities, ValidationFlags, Validator};
|
||
let src = super::shaders::MESH_WGSL;
|
||
let module = naga::front::wgsl::parse_str(src)
|
||
.unwrap_or_else(|e| panic!("mesh: WGSL-Parse-Fehler: {e:?}"));
|
||
let mut validator = Validator::new(ValidationFlags::all(), Capabilities::all());
|
||
validator
|
||
.validate(&module)
|
||
.unwrap_or_else(|e| panic!("mesh: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
||
|
||
// Bodengitter-Shader ebenso headless validieren.
|
||
let grid_src = super::shaders::GRID_WGSL;
|
||
let grid_module = naga::front::wgsl::parse_str(grid_src)
|
||
.unwrap_or_else(|e| panic!("grid: WGSL-Parse-Fehler: {e:?}"));
|
||
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||
.validate(&grid_module)
|
||
.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:?}"));
|
||
|
||
// Texturierter Wand-Shader (RenderStyle::Textured, Bild-Textur in group 1)
|
||
// ebenso headless validieren — faengt Textur-/Sampler-Bindungsfehler ab.
|
||
let tex_src = super::shaders::MESH_TEXTURED_WGSL;
|
||
let tex_module = naga::front::wgsl::parse_str(tex_src)
|
||
.unwrap_or_else(|e| panic!("textured: WGSL-Parse-Fehler: {e:?}"));
|
||
Validator::new(ValidationFlags::all(), Capabilities::all())
|
||
.validate(&tex_module)
|
||
.unwrap_or_else(|e| panic!("textured: WGSL-Validierung fehlgeschlagen: {e:?}"));
|
||
}
|
||
}
|