render3d: Öffnungen im 3D-Vollkörper (mesh.rs) — geteilte Zerlegung mit section.rs
- openings.rs (neu): split_range_by_voids() — die bisher nur in section.rs liegende Pfeiler/Brüstung/Sturz-Zerlegung, herausgezogen als koordinaten- neutraler Helfer. section.rs und mesh.rs teilen jetzt EIN Öffnungsmodell. - section.rs: wall_cut_rectangles delegiert den Sortier-/Rechteck-Teil an openings::split_range_by_voids (plane-spezifisches Clipping bleibt lokal). - mesh.rs: extrude_wall emittiert nicht mehr einen Vollblock je Wand, sondern Teilkörper (Pfeiler in voller Höhe + Brüstung/Sturz je Öffnung) → echtes Loch; Tür (sill==0) lässt die Brüstung weg und reicht bis zum Boden. Ohne Öffnung exakt EIN Block, byte-identische Geometrie zu vorher (Alt-Tests unverändert grün). Gates: cargo test 32/32 (29+3 neue), wasm32 --features web check grün, nativ grün.
This commit is contained in:
@@ -16,6 +16,7 @@
|
|||||||
|
|
||||||
pub mod math;
|
pub mod math;
|
||||||
pub mod mesh;
|
pub mod mesh;
|
||||||
|
pub(crate) mod openings;
|
||||||
pub mod section;
|
pub mod section;
|
||||||
pub mod shaders;
|
pub mod shaders;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
@@ -46,7 +47,7 @@ pub use types::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::mesh::{build_walls_mesh, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX};
|
use super::mesh::{build_walls_mesh, extrude_wall, INDICES_PER_BOX, VERTS_PER_BOX};
|
||||||
use super::types::{Mesh, WallInput, FLOATS_PER_VERTEX};
|
use super::types::{Mesh, Opening, WallInput, FLOATS_PER_VERTEX};
|
||||||
|
|
||||||
/// Bequemer Bau einer achsparallelen Wand entlang +X.
|
/// Bequemer Bau einer achsparallelen Wand entlang +X.
|
||||||
fn wall_x(len: f32, thickness: f32, height: f32) -> WallInput {
|
fn wall_x(len: f32, thickness: f32, height: f32) -> WallInput {
|
||||||
@@ -197,6 +198,103 @@ mod tests {
|
|||||||
assert!(mesh.indices.is_empty());
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Deckenplatten (extrudierte Polygone) ---------------------------------
|
// --- Deckenplatten (extrudierte Polygone) ---------------------------------
|
||||||
|
|
||||||
use super::mesh::{build_model_mesh, extrude_slab, triangulate};
|
use super::mesh::{build_model_mesh, extrude_slab, triangulate};
|
||||||
|
|||||||
+105
-20
@@ -9,13 +9,24 @@
|
|||||||
// `base_elevation` bis `base_elevation + height` extrudiert — analog zur
|
// `base_elevation` bis `base_elevation + height` extrudiert — analog zur
|
||||||
// three.js-`ExtrudeGeometry`, die den 2D-`clippedBand` hochzieht (Viewport3D.tsx
|
// three.js-`ExtrudeGeometry`, die den 2D-`clippedBand` hochzieht (Viewport3D.tsx
|
||||||
// `addLayerPrism`). M1 nimmt die stumpfe (rechtwinklige) Band-Form; Gehrungen
|
// `addLayerPrism`). M1 nimmt die stumpfe (rechtwinklige) Band-Form; Gehrungen
|
||||||
// (`computeJoins`/`clippedBand`) und Oeffnungen kommen in spaeteren Milestones.
|
// (`computeJoins`/`clippedBand`) sind ein spaeterer Milestone.
|
||||||
|
//
|
||||||
|
// OEFFNUNGEN (Tueren/Fenster, `WallInput::openings`): eine Wand mit Oeffnungen
|
||||||
|
// wird NICHT als ein Vollkoerper-Quader extrudiert, sondern in mehrere
|
||||||
|
// Teil-Quader zerlegt — volle Hoehe zwischen Oeffnungen (Pfeiler), Bruestung
|
||||||
|
// unterhalb und Sturz oberhalb jeder Oeffnung (`wall_solid_segments`). Das ist
|
||||||
|
// exakt dieselbe Bruestungs-/Sturz-Zerlegung wie `section::wall_cut_rectangles`
|
||||||
|
// fuer die Schnitt-Cut-Polygone (gemeinsame Basis: `crate::openings::
|
||||||
|
// split_range_by_voids`), nur direkt in Wandachsen-Metern statt in der
|
||||||
|
// Projektions-u-Achse einer Schnittebene — damit zeigt die 3D-Vollansicht
|
||||||
|
// (`?engine=wasm`) dieselbe Oeffnung wie der Schnitt.
|
||||||
//
|
//
|
||||||
// KOORDINATEN: model [x, y] -> world (x, elevation, y). Also Grundriss in der
|
// KOORDINATEN: model [x, y] -> world (x, elevation, y). Also Grundriss in der
|
||||||
// XZ-Ebene, Extrusion entlang +Y (Y-up), exakt wie Viewport3D.tsx:
|
// XZ-Ebene, Extrusion entlang +Y (Y-up), exakt wie Viewport3D.tsx:
|
||||||
// "Modell (x,y,z) -> Three (x, z, y) (Z = Hoehe nach oben)".
|
// "Modell (x,y,z) -> Three (x, z, y) (Z = Hoehe nach oben)".
|
||||||
|
|
||||||
use crate::types::{Mesh, Point2, Rgb, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
||||||
|
use crate::types::{Mesh, Opening, Point2, Rgb, SlabInput, WallInput, 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,
|
||||||
@@ -35,30 +46,101 @@ fn left_normal(start: Point2, end: Point2) -> Point2 {
|
|||||||
[-dy / len, dx / len]
|
[-dy / len, dx / len]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrudiert EINE Wand zu einem Quader-Mesh und haengt sie an `mesh` an.
|
/// Extrudiert EINE Wand zu einem oder mehreren Quader-Meshes und haengt sie an
|
||||||
///
|
/// `mesh` an. Ohne Oeffnungen (Regressionsfall) entsteht wie zuvor GENAU ein
|
||||||
/// Aufbau: die vier Grundriss-Ecken des Bandes (CCW) bilden das Deckel-Rechteck.
|
/// Quader (`VERTS_PER_BOX`/`INDICES_PER_BOX`); mit Oeffnungen wird die Wand
|
||||||
/// Boden = gleiche XZ auf `y0`, Deckel = auf `y1`. Jede der sechs Seiten bekommt
|
/// zuerst in Achsen-Teilstuecke (Pfeiler/Bruestung/Sturz) zerlegt und jedes
|
||||||
/// eigene Vertices mit ihrer (nach aussen zeigenden) Flaechen-Normale — daher
|
/// Teilstueck einzeln als Quader extrudiert (siehe `wall_solid_segments` und
|
||||||
/// flaches Shading und korrektes Backface-Culling.
|
/// die Oeffnungen-Erklaerung im Moduldoc oben).
|
||||||
pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
||||||
let n = left_normal(wall.start, wall.end);
|
let n = left_normal(wall.start, wall.end);
|
||||||
// Degenerierte Wand (Start == Ende) ueberspringen.
|
// Degenerierte Wand (Start == Ende) ueberspringen.
|
||||||
if n == [0.0, 0.0] {
|
if n == [0.0, 0.0] {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let dx = wall.end[0] - wall.start[0];
|
||||||
|
let dy = wall.end[1] - wall.start[1];
|
||||||
|
let length = (dx * dx + dy * dy).sqrt();
|
||||||
|
|
||||||
|
let z0 = wall.base_elevation;
|
||||||
|
let z1 = wall.base_elevation + wall.height;
|
||||||
|
for (a0, a1, y0, y1) in wall_solid_segments(length, &wall.openings, z0, z1) {
|
||||||
|
extrude_wall_segment(mesh, wall, n, length, a0, a1, y0, y1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Zerlegt eine Wandachse (`0..length`, volle Hoehe `[z0,z1]`) an ihren
|
||||||
|
/// `openings` in Voll-/Bruestungs-/Sturz-Rechtecke `(achse0, achse1, y0, y1)` —
|
||||||
|
/// direkt in Wandachsen-Metern, keine Schnittebenen-Projektion noetig (anders
|
||||||
|
/// als `section::wall_cut_rectangles`, das dieselbe Zerlegung fuer eine
|
||||||
|
/// Cut-Ebenen-u-Achse braucht). Ohne Oeffnungen (oder wenn alle ausserhalb
|
||||||
|
/// `[0,length]` liegen) liefert das das eine Vollstueck `[0,length]x[z0,z1]`
|
||||||
|
/// (Regressionsfall, identisch zur bisherigen Vollkoerper-Extrusion).
|
||||||
|
fn wall_solid_segments(
|
||||||
|
length: f32,
|
||||||
|
openings: &[Opening],
|
||||||
|
z0: f32,
|
||||||
|
z1: f32,
|
||||||
|
) -> Vec<(f32, f32, f32, f32)> {
|
||||||
|
if openings.is_empty() {
|
||||||
|
return vec![(0.0, length, z0, z1)];
|
||||||
|
}
|
||||||
|
let mut voids: Vec<(f32, f32, &Opening)> = Vec::new();
|
||||||
|
for op in openings {
|
||||||
|
let from = op.from.max(0.0);
|
||||||
|
let to = op.to.min(length);
|
||||||
|
if to - from > MIN_SPAN {
|
||||||
|
voids.push((from, to, op));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
split_range_by_voids(0.0, length, z0, z1, voids)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Punkt entlang der Wandachse bei Achsen-Position `a` (Meter ab `wall.start`),
|
||||||
|
/// linear zwischen `wall.start` und `wall.end` interpoliert.
|
||||||
|
fn axis_point(wall: &WallInput, length: f32, a: f32) -> Point2 {
|
||||||
|
if length < 1e-9 {
|
||||||
|
return wall.start;
|
||||||
|
}
|
||||||
|
let t = a / length;
|
||||||
|
[
|
||||||
|
wall.start[0] + (wall.end[0] - wall.start[0]) * t,
|
||||||
|
wall.start[1] + (wall.end[1] - wall.start[1]) * t,
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extrudiert EIN Achsen-Teilstueck `[a0,a1]` (Hoehe `[y0,y1]`) einer Wand zu
|
||||||
|
/// einem Quader-Mesh und haengt ihn an `mesh` an. Fuer `a0=0, a1=length,
|
||||||
|
/// y0=base_elevation, y1=base_elevation+height` (keine Oeffnungen) ist das
|
||||||
|
/// exakt die bisherige Vollkoerper-Extrusion.
|
||||||
|
///
|
||||||
|
/// Aufbau: die vier Grundriss-Ecken des Bandes (CCW) bilden das Deckel-Rechteck.
|
||||||
|
/// Boden = gleiche XZ auf `y0`, Deckel = auf `y1`. Jede der sechs Seiten bekommt
|
||||||
|
/// eigene Vertices mit ihrer (nach aussen zeigenden) Flaechen-Normale — daher
|
||||||
|
/// flaches Shading und korrektes Backface-Culling.
|
||||||
|
fn extrude_wall_segment(
|
||||||
|
mesh: &mut Mesh,
|
||||||
|
wall: &WallInput,
|
||||||
|
n: Point2,
|
||||||
|
length: f32,
|
||||||
|
a0: f32,
|
||||||
|
a1: f32,
|
||||||
|
y0: f32,
|
||||||
|
y1: f32,
|
||||||
|
) {
|
||||||
let h = wall.thickness * 0.5;
|
let h = wall.thickness * 0.5;
|
||||||
|
|
||||||
// Vier Band-Ecken im Grundriss (CCW, wie `wallCorners(start,end,thickness)`):
|
// Vier Band-Ecken im Grundriss (CCW, wie `wallCorners(start,end,thickness)`),
|
||||||
// p0 = start + n*h, p1 = end + n*h, p2 = end - n*h, p3 = start - n*h.
|
// aber auf Basis der Achsen-Punkte des Teilstuecks statt Wandanfang/-ende:
|
||||||
|
// p0 = p(a0) + n*h, p1 = p(a1) + n*h, p2 = p(a1) - n*h, p3 = p(a0) - n*h.
|
||||||
let off = |p: Point2, s: f32| -> Point2 { [p[0] + n[0] * s * h, p[1] + n[1] * s * h] };
|
let off = |p: Point2, s: f32| -> Point2 { [p[0] + n[0] * s * h, p[1] + n[1] * s * h] };
|
||||||
let g0 = off(wall.start, 1.0);
|
let p0 = axis_point(wall, length, a0);
|
||||||
let g1 = off(wall.end, 1.0);
|
let p1 = axis_point(wall, length, a1);
|
||||||
let g2 = off(wall.end, -1.0);
|
let g0 = off(p0, 1.0);
|
||||||
let g3 = off(wall.start, -1.0);
|
let g1 = off(p1, 1.0);
|
||||||
|
let g2 = off(p1, -1.0);
|
||||||
|
let g3 = off(p0, -1.0);
|
||||||
|
|
||||||
let y0 = wall.base_elevation;
|
|
||||||
let y1 = wall.base_elevation + wall.height;
|
|
||||||
let color = wall.color;
|
let color = wall.color;
|
||||||
|
|
||||||
// world-Position aus Grundriss-XZ + Hoehe: (x, y, z=grundriss.y).
|
// world-Position aus Grundriss-XZ + Hoehe: (x, y, z=grundriss.y).
|
||||||
@@ -83,10 +165,13 @@ pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
|||||||
// Vier Mantelflaechen. Aussen-Normalen aus dem Grundriss:
|
// Vier Mantelflaechen. Aussen-Normalen aus dem Grundriss:
|
||||||
// Kante g0->g1 liegt auf der +n-Seite -> Normale +n.
|
// Kante g0->g1 liegt auf der +n-Seite -> Normale +n.
|
||||||
// Kante g2->g3 liegt auf der -n-Seite -> Normale -n.
|
// Kante g2->g3 liegt auf der -n-Seite -> Normale -n.
|
||||||
// Kappen an Start (g3->g0) und Ende (g1->g2) liegen an den Achsen-Enden.
|
// Kappen an a0 (g3->g0) und a1 (g1->g2) liegen an den Achsen-Enden des
|
||||||
|
// Teilstuecks (bei einem inneren Teilstueck also am Rand der Oeffnung
|
||||||
|
// -> Leibungsflaeche/Reveal statt Wandende).
|
||||||
let np = [n[0], 0.0, n[1]]; // +n-Seite (world)
|
let np = [n[0], 0.0, n[1]]; // +n-Seite (world)
|
||||||
let nn = [-n[0], 0.0, -n[1]]; // -n-Seite
|
let nn = [-n[0], 0.0, -n[1]]; // -n-Seite
|
||||||
// Achsenrichtung (fuer die End-Kappen-Normalen).
|
// Achsenrichtung der GESAMTEN Wand (fuer die End-Kappen-Normalen) — konstant
|
||||||
|
// ueber alle Teilstuecke, unabhaengig von `a0`/`a1`.
|
||||||
let ux = wall.end[0] - wall.start[0];
|
let ux = wall.end[0] - wall.start[0];
|
||||||
let uz = wall.end[1] - wall.start[1];
|
let uz = wall.end[1] - wall.start[1];
|
||||||
let ul = (ux * ux + uz * uz).sqrt().max(1e-9);
|
let ul = (ux * ux + uz * uz).sqrt().max(1e-9);
|
||||||
@@ -97,9 +182,9 @@ pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
|||||||
push_quad(mesh, b0, b1, t1, t0, np, color);
|
push_quad(mesh, b0, b1, t1, t0, np, color);
|
||||||
// -n-Wand: aussen betrachtet CCW = b2, b3, t3, t2.
|
// -n-Wand: aussen betrachtet CCW = b2, b3, t3, t2.
|
||||||
push_quad(mesh, b2, b3, t3, t2, nn, color);
|
push_quad(mesh, b2, b3, t3, t2, nn, color);
|
||||||
// End-Kappe (Achsen-Ende, Normale +u): b1, b2, t2, t1.
|
// End-Kappe bei a1, Normale +u: b1, b2, t2, t1.
|
||||||
push_quad(mesh, b1, b2, t2, t1, u, color);
|
push_quad(mesh, b1, b2, t2, t1, u, color);
|
||||||
// Start-Kappe (Achsen-Anfang, Normale -u): b3, b0, t0, t3.
|
// End-Kappe bei a0, Normale -u: b3, b0, t0, t3.
|
||||||
push_quad(mesh, b3, b0, t0, t3, neg_u, color);
|
push_quad(mesh, b3, b0, t0, t3, neg_u, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
//! Gemeinsame Rechteck-Zerlegung fuer Wand-Oeffnungen (Fenster/Tueren).
|
||||||
|
//!
|
||||||
|
//! Kern-Idee: ein Achsen-Intervall voller Hoehe (`[z0, z1]`) wird an den darin
|
||||||
|
//! liegenden Oeffnungen in Voll-/Bruestungs-/Sturz-Rechtecke gesplittet —
|
||||||
|
//! Bruestung `[z0, sill]` unterhalb, Sturz `[sill+height, z1]` oberhalb der
|
||||||
|
//! Oeffnung, dazwischen eine Luecke (das Loch). Diese Zerlegung ist
|
||||||
|
//! koordinatenraum-neutral: sie braucht nur ein 1D-Achsen-Intervall plus die
|
||||||
|
//! Oeffnungen als bereits geclippte `(v_lo, v_hi, &Opening)`-Teilstuecke in
|
||||||
|
//! DEMSELBEN Koordinatenraum wie `lo`/`hi`.
|
||||||
|
//!
|
||||||
|
//! Zwei Aufrufer, zwei Koordinatenraeume:
|
||||||
|
//! - `section::wall_cut_rectangles`: Cut-Ebene — die Achsen-Koordinate ist die
|
||||||
|
//! Projektions-u-Achse der Schnittebene (`axis_map` rechnet Wandachsen-Meter
|
||||||
|
//! dorthin um).
|
||||||
|
//! - `mesh::wall_solid_segments`: 3D-Solid-Extrusion — die Achsen-Koordinate
|
||||||
|
//! ist direkt die Wandachse in Metern (0..Wandlaenge), keine Projektion
|
||||||
|
//! noetig.
|
||||||
|
//!
|
||||||
|
//! Beide nutzen `split_range_by_voids` fuer die eigentliche Bruestungs-/
|
||||||
|
//! Sturz-Logik, damit Schnitt-Ansicht und 3D-Solid-Mesh garantiert dasselbe
|
||||||
|
//! Oeffnungsmodell (siehe `types::Opening`) auswerten statt zweier
|
||||||
|
//! divergierender Implementierungen.
|
||||||
|
|
||||||
|
use crate::types::Opening;
|
||||||
|
|
||||||
|
/// Mindestlaenge eines Oeffnungs-/Rest-Teilstuecks, unterhalb derer es als
|
||||||
|
/// vernachlaessigbar (Rand-/Rundungsfall) ignoriert wird.
|
||||||
|
pub(crate) const MIN_SPAN: f32 = 1e-4;
|
||||||
|
|
||||||
|
/// Zerlegt EIN Achsen-Intervall `[lo, hi]` (volle Hoehe `[z0, z1]`) an den in
|
||||||
|
/// `voids` uebergebenen, bereits auf `[lo,hi]` geclippten Oeffnungs-
|
||||||
|
/// Teilstuecken (`(v_lo, v_hi, opening)`, Reihenfolge beliebig) in Rechtecke
|
||||||
|
/// `(u0, u1, v0, v1)`: volle Hoehe ausserhalb von Oeffnungen, Bruestung
|
||||||
|
/// darunter, Sturz darueber (siehe `Opening::sill`/`Opening::height`). Ohne
|
||||||
|
/// Oeffnungen (leeres `voids`) liefert es das eine Vollrechteck `[lo,hi]x[z0,z1]`
|
||||||
|
/// (Regressionsfall).
|
||||||
|
pub(crate) fn split_range_by_voids(
|
||||||
|
lo: f32,
|
||||||
|
hi: f32,
|
||||||
|
z0: f32,
|
||||||
|
z1: f32,
|
||||||
|
mut voids: Vec<(f32, f32, &Opening)>,
|
||||||
|
) -> Vec<(f32, f32, f32, f32)> {
|
||||||
|
if voids.is_empty() {
|
||||||
|
return vec![(lo, hi, z0, z1)];
|
||||||
|
}
|
||||||
|
voids.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||||
|
|
||||||
|
let mut rects = Vec::new();
|
||||||
|
let mut cursor = lo;
|
||||||
|
for (v_lo, v_hi, op) in &voids {
|
||||||
|
if *v_lo > cursor + MIN_SPAN {
|
||||||
|
rects.push((cursor, *v_lo, z0, z1)); // Pfeiler zwischen Oeffnungen
|
||||||
|
}
|
||||||
|
let sill_abs = z0 + op.sill.max(0.0);
|
||||||
|
let head_abs = (sill_abs + op.height).min(z1);
|
||||||
|
if sill_abs - z0 > MIN_SPAN {
|
||||||
|
rects.push((*v_lo, *v_hi, z0, sill_abs)); // Bruestung
|
||||||
|
}
|
||||||
|
if z1 - head_abs > MIN_SPAN {
|
||||||
|
rects.push((*v_lo, *v_hi, head_abs, z1)); // Sturz
|
||||||
|
}
|
||||||
|
cursor = cursor.max(*v_hi);
|
||||||
|
}
|
||||||
|
if hi > cursor + MIN_SPAN {
|
||||||
|
rects.push((cursor, hi, z0, z1)); // Rest-Pfeiler nach der letzten Oeffnung
|
||||||
|
}
|
||||||
|
rects
|
||||||
|
}
|
||||||
@@ -107,11 +107,11 @@
|
|||||||
//! fuehren, dass eine Wand als "in eine andere eingebettet" verdeckt erscheint
|
//! fuehren, dass eine Wand als "in eine andere eingebettet" verdeckt erscheint
|
||||||
//! (siehe Test `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real,
|
//! (siehe Test `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real,
|
||||||
//! aber ein Artefakt der fehlenden Verschneidungslogik, kein Bug dieses Moduls.
|
//! aber ein Artefakt der fehlenden Verschneidungslogik, kein Bug dieses Moduls.
|
||||||
//! - Oeffnungen werden hier (Cut + Projektion) ausgewertet, aber NICHT von der
|
//! - Oeffnungen werden hier (Cut + Projektion) UND von der Vollkoerper-
|
||||||
//! Vollkoerper-Extrusion in `mesh.rs` — die 3D-Solid-Mesh-Erzeugung extrudiert
|
//! Extrusion in `mesh.rs` ausgewertet (`mesh::wall_solid_segments`) — beide
|
||||||
//! weiterhin die volle Wandflaeche (siehe `mesh.rs`-Moduldoc). Cut-Ansicht und
|
//! nutzen dieselbe Bruestungs-/Sturz-Zerlegung (`crate::openings::
|
||||||
//! 3D-Solid-Ansicht sind bis zu diesem Nachtrag also bewusst inkonsistent;
|
//! split_range_by_voids`), damit Cut-Ansicht und 3D-Solid-Ansicht
|
||||||
//! das Nachziehen von `mesh.rs` ist eine bekannte, separate Luecke.
|
//! konsistent bleiben.
|
||||||
//! - Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung
|
//! - Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung
|
||||||
//! wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
|
//! wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
|
||||||
//! gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
|
//! gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
|
||||||
@@ -508,28 +508,11 @@ fn wall_cut_rectangles(
|
|||||||
if voids.is_empty() {
|
if voids.is_empty() {
|
||||||
return vec![(u_lo, u_hi, prism.z0, prism.z1)];
|
return vec![(u_lo, u_hi, prism.z0, prism.z1)];
|
||||||
}
|
}
|
||||||
voids.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
// Bruestungs-/Sturz-Zerlegung ist ab hier koordinatenraum-neutral (nur noch
|
||||||
|
// u-Intervall + Hoehe + geclippte Voids) -> gemeinsame Basis mit
|
||||||
let mut rects = Vec::new();
|
// `mesh::wall_solid_segments` (siehe `crate::openings`-Moduldoc), damit
|
||||||
let mut cursor = u_lo;
|
// Cut-Ansicht und 3D-Solid-Mesh garantiert dasselbe Oeffnungsmodell nutzen.
|
||||||
for (v_lo, v_hi, op) in &voids {
|
crate::openings::split_range_by_voids(u_lo, u_hi, prism.z0, prism.z1, voids)
|
||||||
if *v_lo > cursor + MIN_SPAN {
|
|
||||||
rects.push((cursor, *v_lo, prism.z0, prism.z1));
|
|
||||||
}
|
|
||||||
let sill_abs = prism.z0 + op.sill.max(0.0);
|
|
||||||
let head_abs = (sill_abs + op.height).min(prism.z1);
|
|
||||||
if sill_abs - prism.z0 > MIN_SPAN {
|
|
||||||
rects.push((*v_lo, *v_hi, prism.z0, sill_abs)); // Bruestung
|
|
||||||
}
|
|
||||||
if prism.z1 - head_abs > MIN_SPAN {
|
|
||||||
rects.push((*v_lo, *v_hi, head_abs, prism.z1)); // Sturz
|
|
||||||
}
|
|
||||||
cursor = cursor.max(*v_hi);
|
|
||||||
}
|
|
||||||
if u_hi > cursor + MIN_SPAN {
|
|
||||||
rects.push((cursor, u_hi, prism.z0, prism.z1));
|
|
||||||
}
|
|
||||||
rects
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user