Nordstern-3D: Fenster/Tueren als echte Boolean-Loecher in EINEM Wandkoerper

Bisher zerlegte emitWall eine Wand um jede Oeffnung in Pfeiler/Bruestung/Sturz-
Teilquader -> sichtbare Segment-Naehte im 3D, Oeffnung war semantisch kein Loch,
und versetzt uebereinanderliegende Fenster liessen sich gar nicht abbilden. Neu:
im 3D-Pfad (layered=true) EIN RWall pro Schicht-Band ueber die volle Achse mit
allen Oeffnungen als rechteckige holes; render3d stanzt sie per achsparalleler
Rechteck-Gitter-Zerlegung der Langseiten aus und setzt 4 Laibungsquads je Loch.
holes und openings schliessen sich im Emitter gegenseitig aus. Tuer = Loch bis
zum Wand-zBottom. Schnitt-Pfad (layered=false) segmentiert unveraendert weiter
(wallSegmentOwners bleibt synchron, toSection.ts unangetastet).

Zusatz: trimWallTopForCeilings (Deckentrim, e4b8df6) gilt jetzt NUR im 3D-Pfad
(layered=true, Z-Fighting-Vermeidung) und wird im Schnitt-Pfad uebersprungen -
der Schnitt braucht die volle Wandhoehe fuer seine schichtweise Prioritaets-
Subtraktion (Kontrolle von Schichteinzug/Bodenaufbau am Wand-Decken-Anschluss).

RWall.holes / WallInput.holes additiv (#[serde(default)]). Pflicht-Testfaelle:
zwei ueberlappende, hoehenversetzte Fenster in einem Koerper; Tuer-Loch bis Boden
ohne untere Laibung. cargo test 56 gruen, vitest 230 (+3), build:engine3d + tsc
sauber.
This commit is contained in:
2026-07-04 22:17:21 +02:00
parent 87a30b7061
commit 05cfade483
8 changed files with 670 additions and 17 deletions
+101 -1
View File
@@ -54,7 +54,7 @@ pub use types::{
#[cfg(test)]
mod tests {
use super::mesh::{build_walls_mesh, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX};
use super::types::{Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX};
use super::types::{Hole, Mesh, Opening, WallInput, WallLayer, FLOATS_PER_VERTEX};
/// Bequemer Bau einer achsparallelen Wand entlang +X.
fn wall_x(len: f32, thickness: f32, height: f32) -> WallInput {
@@ -67,6 +67,7 @@ mod tests {
color: [0.8, 0.8, 0.8],
openings: vec![],
layers: None,
holes: vec![],
}
}
@@ -104,6 +105,7 @@ mod tests {
color: [0.8, 0.8, 0.8],
openings: vec![],
layers: None,
holes: vec![],
},
]);
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX);
@@ -182,6 +184,7 @@ mod tests {
color: [0.8, 0.8, 0.8],
openings: vec![],
layers: None,
holes: vec![],
};
let mut mesh = Mesh::default();
extrude_wall(&mut mesh, &w);
@@ -203,6 +206,7 @@ mod tests {
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);
@@ -306,6 +310,97 @@ mod tests {
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]
@@ -325,6 +420,7 @@ mod tests {
color: [0.8, 0.8, 0.8],
openings: vec![],
layers: None,
holes: vec![],
};
let b = WallInput {
start: [0.0, 0.0],
@@ -335,6 +431,7 @@ mod tests {
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");
@@ -394,6 +491,7 @@ mod tests {
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]),
@@ -422,6 +520,7 @@ mod tests {
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),
@@ -510,6 +609,7 @@ mod tests {
color: [0.8, 0.8, 0.8],
openings: vec![],
layers: layers(),
holes: vec![],
};
let mut b = a.clone();
b.end = [0.0, 3.0];