Schnittebenen 2D↔3D Phase 1+2, native Fenster, Hell/Dunkel-Theme, SWISSIMAGE-Import

- Schnittebenen: 3D-Live-Schnitt folgt der gewählten Grundriss-Schnittlinie
  (section3dCutId/sectionPlaneFromLevel), Schalter "Im 3D schneiden" im
  Objekt-Info, Doppelklick auf Schnittlinie springt in 2D-Schnittansicht,
  unsichtbare Ebenen blenden ihre Führungslinie aus
- Eigene native Tauri-Fenster für Kontext-Import/Zeichnungsebenen/
  Ebenen-Einstellungen/Ressourcen/Einstellungen + klassische Menüleiste
  (AppMenuBar) neben der Wortmarke
- Hell/Dunkel-Umschalter (Einstellungen → Darstellung), persistiert,
  flackerfrei vor erstem Render gesetzt
- SWISSIMAGE-Luftbild-Import (swisstopo WMS) als Kontext-Hintergrundebene
- UI-Politur: Werkzeug-Panel Symbole/Liste umschaltbar, Topbar-Quick-Access-
  Icons entfernt, Zahnrad→Einstellungen in Panel-Köpfen, Footerbar/
  Snap-Marker/Maß-HUD auf helle Pillen-Sprache umgestellt
- Neues Dachziegel-Material (RoofingTiles013A)
This commit is contained in:
2026-07-20 10:51:37 +02:00
parent 0ab0361e3d
commit 68a0459d0e
90 changed files with 7730 additions and 1849 deletions
+146 -2
View File
@@ -55,10 +55,13 @@ pub use types::{
#[cfg(test)]
mod tests {
use super::mesh::{
build_walls_mesh, build_walls_mesh_textured, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX,
build_free_wall_end_caps, build_scene_mesh_textured, build_walls_mesh,
build_walls_mesh_textured, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX,
};
use super::section_fill::CAP_FLOATS_PER_VERTEX;
use super::types::{
Hole, Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX, TEXTURED_FLOATS_PER_VERTEX,
Hatch, Hole, Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX,
TEXTURED_FLOATS_PER_VERTEX,
};
/// Bequemer Bau einer achsparallelen Wand entlang +X.
@@ -161,6 +164,65 @@ mod tests {
assert!((max_idx as usize) < mesh.vertex_count());
}
/// `wall_x` mit gesetzter Bauteil-Schraffur (Voraussetzung fuer die Kappen).
fn wall_x_hatched(len: f32, thickness: f32, height: f32) -> WallInput {
WallInput {
hatch: Some(Hatch { pattern: 3, angle: 45.0, scale: 1.0, line_weight: 0.13 }),
..wall_x(len, thickness, height)
}
}
#[test]
fn isolierte_wand_ohne_hatch_bekommt_keine_kappen() {
// Bestehende Test-Fixtures (kein `hatch`) bleiben unangetastet: keine
// Kappen-Geometrie, unabhaengig davon, dass beide Enden frei sind.
let caps = build_free_wall_end_caps(&[wall_x(3.0, 0.2, 2.5)]);
assert!(caps.is_empty());
}
#[test]
fn isolierte_wand_mit_hatch_bekommt_kappen_an_beiden_freien_enden() {
// Eine einzelne Wand hat KEINE Nachbarn -> beide Achsenenden sind frei ->
// je Ende eine Schraffur-Kappe (1 Schicht x 2 Dreiecke x 3 Vertices = 6
// Vertices je Ende, 12 insgesamt).
let caps = build_free_wall_end_caps(&[wall_x_hatched(3.0, 0.2, 2.5)]);
assert_eq!(caps.len() / CAP_FLOATS_PER_VERTEX, 12);
// Jeder Kappen-Vertex traegt die Schraffur-Parameter (Muster 3 = crosshatch).
for v in caps.chunks_exact(CAP_FLOATS_PER_VERTEX) {
assert_eq!(v[5], 3.0, "pattern");
assert!((v[6] - 45.0_f32.to_radians()).abs() < 1e-5, "angle_rad");
}
}
#[test]
fn verbundene_wandenden_bekommen_keine_kappe_dort() {
// Zwei Waende treffen sich rechtwinklig bei (3,0) -> DIESES Ende ist bei
// beiden Waenden NICHT frei (Gehrung/Anschluss), die beiden anderen
// (freien) Enden bekommen weiterhin je eine Kappe -> 2 Kappen gesamt
// (nicht 4), macht 12 Vertices statt 24.
let a = wall_x_hatched(3.0, 0.2, 2.5); // (0,0) frei .. (3,0) verbunden
let b = WallInput {
start: [3.0, 0.0],
end: [3.0, 4.0],
..wall_x_hatched(4.0, 0.2, 2.5) // (3,0) verbunden .. (3,4) frei
};
let caps = build_free_wall_end_caps(&[a, b]);
assert_eq!(caps.len() / CAP_FLOATS_PER_VERTEX, 12, "nur die 2 freien Enden -> 2 Kappen");
}
#[test]
fn t_stoss_bekommt_ebenfalls_keine_kappe_am_knoten() {
// Drei Waende treffen sich am selben Punkt (T-Stoss, Gehrung bleibt
// rechtwinklig, s. compute_wall_miters) -> trotzdem gilt der Knoten als
// "beruehrt", KEINE der drei dortigen Enden bekommt eine Kappe.
let a = wall_x_hatched(3.0, 0.2, 2.5); // endet bei (3,0)
let b = WallInput { start: [3.0, 0.0], end: [3.0, 4.0], ..wall_x_hatched(4.0, 0.2, 2.5) };
let c = WallInput { start: [3.0, 0.0], end: [6.0, 0.0], ..wall_x_hatched(3.0, 0.2, 2.5) };
let caps = build_free_wall_end_caps(&[a, b, c]);
// Freie Enden: a.start(0,0), b.end(3,4), c.end(6,0) -> 3 Kappen (nicht 6).
assert_eq!(caps.len() / CAP_FLOATS_PER_VERTEX, 18);
}
#[test]
fn mehrere_waende_addieren_sich() {
let mesh = build_walls_mesh(&[
@@ -840,6 +902,8 @@ mod tests {
indices: vec![0, 1, 2],
kind,
color,
material_index: None,
vertex_colors: Vec::new(),
}
}
@@ -881,6 +945,73 @@ mod tests {
assert_eq!([mesh2.verts[6], mesh2.verts[7], mesh2.verts[8]], [0.1, 0.2, 0.3]);
}
#[test]
fn context_mesh_pro_vertex_farbe_wird_uebernommen() {
// Luftbild-Drape: jeder Vertex traegt eine eigene Farbe. Wird 1:1 an die
// Ausgabe-Vertices durchgereicht (statt der Einzelfarbe).
let mut mesh = Mesh::default();
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, 2],
kind: MeshKind::Terrain,
color: None,
material_index: None,
vertex_colors: vec![
1.0, 0.0, 0.0, // Vertex 0 rot
0.0, 1.0, 0.0, // Vertex 1 gruen
0.0, 0.0, 1.0, // Vertex 2 blau
],
},
);
// Vorderseite = erste 3 Vertices (Winding a,b,c): Farben 0,1,2.
assert_eq!([mesh.verts[6], mesh.verts[7], mesh.verts[8]], [1.0, 0.0, 0.0]);
assert_eq!([mesh.verts[15], mesh.verts[16], mesh.verts[17]], [0.0, 1.0, 0.0]);
assert_eq!([mesh.verts[24], mesh.verts[25], mesh.verts[26]], [0.0, 0.0, 1.0]);
// Zu kurzes vertex_colors-Array -> Einzelfarbe (kein Panic, kein Teilbezug).
let mut mesh2 = Mesh::default();
append_context_mesh(
&mut mesh2,
&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, 2],
kind: MeshKind::Terrain,
color: None,
material_index: None,
vertex_colors: vec![1.0, 0.0, 0.0], // nur 1 Vertex -> ignoriert
},
);
assert_eq!(
[mesh2.verts[6], mesh2.verts[7], mesh2.verts[8]],
MeshKind::Terrain.default_color()
);
}
#[test]
fn kontext_mesh_materialindex_wandert_in_die_textured_layer() {
// Daecher (und potenziell andere Kontext-Meshes) koennen jetzt einen
// Material-Textur-Index tragen (`MeshInput::material_index`) — vorher
// war er fuer ALLE Kontext-Meshes hart auf -1 (Schachbrett) gesetzt, der
// Stil „Textured" konnte also nie eine echte Dachziegel-Textur zeigen.
let m = 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, 2],
kind: MeshKind::Extrusion,
color: None,
material_index: Some(2),
vertex_colors: Vec::new(),
};
let tex = build_scene_mesh_textured(&[], &[], std::slice::from_ref(&m));
assert!(!tex.layers.is_empty());
assert!(tex.layers.iter().all(|&l| l == 2.0), "alle Vertices Ebene 2");
// Ohne material_index bleibt der Fallback (-1 -> Schachbrett).
let m2 = MeshInput { material_index: None, ..m };
let tex2 = build_scene_mesh_textured(&[], &[], std::slice::from_ref(&m2));
assert!(tex2.layers.iter().all(|&l| l == -1.0));
}
#[test]
fn context_mesh_ueberspringt_kaputte_indizes_und_entartung() {
let mut mesh = Mesh::default();
@@ -892,6 +1023,8 @@ mod tests {
indices: vec![0, 1, 9],
kind: MeshKind::Imported,
color: None,
material_index: None,
vertex_colors: Vec::new(),
},
);
assert_eq!(mesh.vertex_count(), 0);
@@ -903,6 +1036,8 @@ mod tests {
indices: vec![0, 1, 2],
kind: MeshKind::Imported,
color: None,
material_index: None,
vertex_colors: Vec::new(),
},
);
assert_eq!(mesh.vertex_count(), 0);
@@ -1078,5 +1213,14 @@ mod tests {
Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&tex_module)
.unwrap_or_else(|e| panic!("textured: WGSL-Validierung fehlgeschlagen: {e:?}"));
// Luftbild-Drape-Shader (Terrain-Overlay, Bild-Textur + Bbox-Uniform in
// group 1) ebenso headless validieren.
let aerial_src = super::shaders::MESH_AERIAL_WGSL;
let aerial_module = naga::front::wgsl::parse_str(aerial_src)
.unwrap_or_else(|e| panic!("aerial: WGSL-Parse-Fehler: {e:?}"));
Validator::new(ValidationFlags::all(), Capabilities::all())
.validate(&aerial_module)
.unwrap_or_else(|e| panic!("aerial: WGSL-Validierung fehlgeschlagen: {e:?}"));
}
}