Schnitt: Wandquerschnitte an den gemitterten Fussabdruck koppeln

Der Schnitt-Extraktor schnitt die Wand-Cut-Polygone bisher gegen
achsparallele Einzelwand-Rechtecke ohne Eckverschneidung — an einer
Wandecke ueberlappten dadurch zwei Querschnitte. mesh.rs exponiert jetzt
den gemitterten Fussabdruck (wall_mitered_footprints, dieselbe Gehrung wie
das 3D-Mesh) als geteilte Wahrheit; section.rs bildet die Cut-Polygone
gegen diesen Fussabdruck. Die achsparallele Naeherung fuer Verdeckung/
Projektionskanten bleibt bewusst unveraendert.
This commit is contained in:
2026-07-03 23:52:33 +02:00
parent 56bbcec2a8
commit 0c82d01cd9
2 changed files with 129 additions and 17 deletions
+52 -2
View File
@@ -23,8 +23,11 @@
// werden durch einen Hoehen-Ueberlappungstest (`cluster_by_height_overlap`) // werden durch einen Hoehen-Ueberlappungstest (`cluster_by_height_overlap`)
// sauber getrennt (kein Verschneiden ueber Geschosse hinweg). T-/X-Stoesse // sauber getrennt (kein Verschneiden ueber Geschosse hinweg). T-/X-Stoesse
// (mehr als zwei Enden an einem Knoten) bleiben — wie im Web-Pendant — // (mehr als zwei Enden an einem Knoten) bleiben — wie im Web-Pendant —
// rechtwinklig. Das Schnitt-Modul (`section.rs`) bleibt UNVERAENDERT bei der // rechtwinklig. Das Schnitt-Modul (`section.rs`) nutzt fuer die WAND-CUT-Polygone
// achsparallelen Bounding-Box-Naeherung (siehe dessen Moduldoc). // denselben gehrten Fussabdruck (`wall_mitered_footprints`), damit ein Schnitt an
// einer Wandecke genau EINEN verschnittenen Querschnitt liefert statt zweier
// ueberlappender Rechtecke; seine Verdeckungs-Naeherung bleibt achsparallel
// (siehe dessen Moduldoc).
// //
// SCHICHTEN (`WallInput::layers`): mehrschichtige Waende (Putz/Daemmung/ // SCHICHTEN (`WallInput::layers`): mehrschichtige Waende (Putz/Daemmung/
// Mauerwerk/...) werden nicht als ein Vollkoerper, sondern als je EIN Prisma // Mauerwerk/...) werden nicht als ein Vollkoerper, sondern als je EIN Prisma
@@ -488,6 +491,53 @@ fn compute_wall_miters(walls: &[WallInput]) -> Vec<WallMiters> {
out out
} }
/// Grundriss-Fussabdruck EINER Wand (die vier Band-Eckpunkte g0..g3 in CCW),
/// an ihren echten Achsenenden ggf. gehrt — bitgenau dieselbe Ecken-Konstruktion
/// wie `extrude_layer_segment` fuer das Vollstueck (Einzel-Layer, keine
/// Oeffnung): `off(start/end, +/-thickness/2)`, an Start/Ende gegen die jeweilige
/// Gehrungslinie verschnitten. `None` fuer entartete Waende (Start == Ende, wie
/// `extrude_wall`). Reihenfolge: start+n, end+n, end-n, start-n.
fn wall_mitered_footprint(wall: &WallInput, miters: WallMiters) -> Option<[Point2; 4]> {
let n = left_normal(wall.start, wall.end);
if n == [0.0, 0.0] {
return None;
}
let h = wall.thickness * 0.5;
let ux = wall.end[0] - wall.start[0];
let uz = wall.end[1] - wall.start[1];
let ul = (ux * ux + uz * uz).sqrt().max(1e-9);
let u: Point2 = [ux / ul, uz / ul];
let off = |p: Point2, s: f32| -> Point2 { [p[0] + n[0] * s * h, p[1] + n[1] * s * h] };
let clip = |base: Point2, cut: Option<MiterLine>| -> Point2 {
match cut {
None => base,
Some(c) => line_intersect_2(base, u, c.point, c.dir).unwrap_or(base),
}
};
Some([
clip(off(wall.start, 1.0), miters.start),
clip(off(wall.end, 1.0), miters.end),
clip(off(wall.end, -1.0), miters.end),
clip(off(wall.start, -1.0), miters.start),
])
}
/// Grundriss-Fussabdruecke ALLER Waende, an ihren echten Ecken verschnitten
/// (siehe `wall_mitered_footprint`). Berechnet die Gehrung einmalig ueber die
/// gesamte Wandkonnektivitaet (`compute_wall_miters`) und teilt so die
/// Ecken-Verschneidungs-Wahrheit mit `section::cut_section` — damit der Schnitt
/// an Wandecken denselben verschnittenen Wand-Querschnitt liefert wie die
/// 3D-Extrusion, ohne die Gehrungslogik zu duplizieren. Reihenfolge/Anzahl
/// deckungsgleich mit `walls` (`None` bei entarteter Wand).
pub(crate) fn wall_mitered_footprints(walls: &[WallInput]) -> Vec<Option<[Point2; 4]>> {
let miters = compute_wall_miters(walls);
walls
.iter()
.zip(miters)
.map(|(w, m)| wall_mitered_footprint(w, m))
.collect()
}
/// Haengt ein Quad (vier Ecken in CCW von aussen) als zwei Dreiecke an, mit fester /// Haengt ein Quad (vier Ecken in CCW von aussen) als zwei Dreiecke an, mit fester
/// Flaechen-Normale und Farbe je Vertex. Reihenfolge (0,1,2) + (0,2,3). /// Flaechen-Normale und Farbe je Vertex. Reihenfolge (0,1,2) + (0,2,3).
fn push_quad( fn push_quad(
+77 -15
View File
@@ -101,19 +101,22 @@
//! (Rechteck-Baender) und Decken (i.d.R. konvexe Rechtecke) ist das exakt; //! (Rechteck-Baender) und Decken (i.d.R. konvexe Rechtecke) ist das exakt;
//! fuer stark diagonale oder konkave Grundrisse kann es zu Ueberverdeckung //! fuer stark diagonale oder konkave Grundrisse kann es zu Ueberverdeckung
//! fuehren. //! fuehren.
//! - Keine Wandknoten-Verschneidung (T-/X-Stoesse): DIESES Modul behandelt //! - Wandknoten-Verschneidung (Gehrung) — zwei Wege, je nach Zweck:
//! Waende weiterhin als eigenstaendige, stumpf abgeschlossene Quader, die //! * CUT-Polygone (der eigentliche Schnitt-Querschnitt) nutzen den an den
//! sich an Ecken UEBERLAPPEN (kein Miter-Join) — sowohl fuer die Cut-Polygone //! echten Wandecken GEHRTEN Fussabdruck aus `mesh::wall_mitered_footprints`
//! als auch fuer die Verdeckungs-Bounding-Box. `mesh.rs` (die Vollkoerper- //! — DIESELBE Ecken-Verschneidung wie die 3D-Vollkoerper-Extrusion
//! Extrusion fuer die 3D-Ansicht) berechnet die Eck-Gehrung inzwischen SELBST //! (`mesh::compute_wall_miters`). Dadurch liefert ein Schnitt an einer
//! (`mesh::compute_wall_miters`, aus der Wandkonnektivitaet); dieses Modul //! Wandecke genau EINEN sauberen, verschnittenen Querschnitt statt zweier
//! zieht davon bewusst KEINEN Nutzen, weil seine Verdeckungs-Bounding-Box //! ueberlappender Rechtecke (siehe `Prism::cut_footprint`, `cut_section` und
//! ohnehin nur die achsparallele Grundflaeche kennt (siehe oben) — eine //! Test `schnitt_an_wandecke_liefert_verschnittene_querschnitte`).
//! gehrte, nicht-achsparallele Kante wuerde dort keinen Praezisionsgewinn //! * Verdeckung/Projektion (Bounding-Box + Ansichtskanten) bleiben bei der
//! bringen. Das kann in Ecknaehe dazu fuehren, dass eine Wand als "in eine //! ACHSPARALLELEN, ungehrten Grundflaeche (`Prism::footprint`): die
//! andere eingebettet" verdeckt erscheint (siehe Test //! Verdeckungs-Bounding-Box kennt ohnehin nur die achsparallele Ausdehnung
//! `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real, aber ein //! (siehe naechster Punkt), eine gehrte Kante brächte dort keinen
//! Artefakt der fehlenden Verschneidungslogik DIESES Moduls, kein Bug. //! Praezisionsgewinn. In Ecknaehe kann eine Wand deshalb als "in eine
//! andere eingebettet" verdeckt erscheinen (siehe Test
//! `ansicht_vor_dem_modell_hat_keine_cut_polygone_aber_verdeckung` unten) —
//! real, aber ein Artefakt der achsparallelen Verdeckungs-Naeherung, kein Bug.
//! - Oeffnungen werden hier (Cut + Projektion) UND von der Vollkoerper- //! - Oeffnungen werden hier (Cut + Projektion) UND von der Vollkoerper-
//! Extrusion in `mesh.rs` ausgewertet (`mesh::wall_solid_segments`) — beide //! Extrusion in `mesh.rs` ausgewertet (`mesh::wall_solid_segments`) — beide
//! nutzen dieselbe Bruestungs-/Sturz-Zerlegung (`crate::openings:: //! nutzen dieselbe Bruestungs-/Sturz-Zerlegung (`crate::openings::
@@ -296,7 +299,16 @@ pub struct SectionOutput {
struct Prism { struct Prism {
kind: ComponentKind, kind: ComponentKind,
index: usize, index: usize,
/// Achsparalleles Band-/Umriss-Polygon (Modell-Meter). Basis fuer die
/// Verdeckungs-Bounding-Box und die Projektions-Kanten (siehe Moduldoc
/// "Bekannte Vereinfachungen").
footprint: Vec<Point2>, footprint: Vec<Point2>,
/// Grundriss-Polygon fuer die CUT-Polygone. Fuer Decken identisch zu
/// `footprint`; fuer Waende der an den Ecken GEHRTE Fussabdruck aus
/// `mesh::wall_mitered_footprints` (dieselbe Verschneidung wie das 3D-Mesh),
/// damit ein Schnitt an einer Wandecke genau EINEN verschnittenen Querschnitt
/// liefert statt zweier ueberlappender Rechtecke. Siehe `cut_section`.
cut_footprint: Vec<Point2>,
z0: f32, z0: f32,
z1: f32, z1: f32,
color: Rgb, color: Rgb,
@@ -378,6 +390,9 @@ fn wall_prism(index: usize, wall: &WallInput) -> Option<Prism> {
Some(Prism { Some(Prism {
kind: ComponentKind::Wall, kind: ComponentKind::Wall,
index, index,
// Standardmaessig ungehrt; `cut_section` ersetzt ihn durch den gehrten
// Fussabdruck aus `mesh::wall_mitered_footprints`.
cut_footprint: footprint.clone(),
footprint, footprint,
z0: wall.base_elevation, z0: wall.base_elevation,
z1: wall.base_elevation + wall.height, z1: wall.base_elevation + wall.height,
@@ -403,6 +418,7 @@ fn slab_prism(index: usize, slab: &SlabInput) -> Option<Prism> {
Some(Prism { Some(Prism {
kind: ComponentKind::Slab, kind: ComponentKind::Slab,
index, index,
cut_footprint: slab.outline.clone(),
footprint: slab.outline.clone(), footprint: slab.outline.clone(),
z0, z0,
z1, z1,
@@ -928,8 +944,14 @@ fn compute_view_edges(
/// Decken) aus. Siehe Moduldoc fuer Format/Konventionen. /// Decken) aus. Siehe Moduldoc fuer Format/Konventionen.
pub fn cut_section(plane: &SectionPlane, walls: &[WallInput], slabs: &[SlabInput]) -> SectionOutput { pub fn cut_section(plane: &SectionPlane, walls: &[WallInput], slabs: &[SlabInput]) -> SectionOutput {
let mut prisms: Vec<Prism> = Vec::new(); let mut prisms: Vec<Prism> = Vec::new();
// Gehrte Wand-Fussabdruecke einmalig ueber die gesamte Wandkonnektivitaet
// (dieselbe Verschneidung wie das 3D-Mesh, siehe `mesh::wall_mitered_footprints`).
let mitered = crate::mesh::wall_mitered_footprints(walls);
for (i, w) in walls.iter().enumerate() { for (i, w) in walls.iter().enumerate() {
if let Some(p) = wall_prism(i, w) { if let Some(mut p) = wall_prism(i, w) {
if let Some(fp) = mitered[i] {
p.cut_footprint = fp.to_vec();
}
prisms.push(p); prisms.push(p);
} }
} }
@@ -941,7 +963,7 @@ pub fn cut_section(plane: &SectionPlane, walls: &[WallInput], slabs: &[SlabInput
let mut cut_polygons = Vec::new(); let mut cut_polygons = Vec::new();
for prism in &prisms { for prism in &prisms {
for (u0, u1) in footprint_u_intervals(plane, &prism.footprint) { for (u0, u1) in footprint_u_intervals(plane, &prism.cut_footprint) {
let (lo, hi) = (u0.min(u1), u0.max(u1)); let (lo, hi) = (u0.min(u1), u0.max(u1));
for (rlo, rhi, rz0, rz1) in wall_cut_rectangles(plane, prism, lo, hi) { for (rlo, rhi, rz0, rz1) in wall_cut_rectangles(plane, prism, lo, hi) {
cut_polygons.push(CutPolygon { cut_polygons.push(CutPolygon {
@@ -1198,6 +1220,46 @@ mod tests {
} }
} }
// --- Ecken-Gehrung im Schnitt --------------------------------------------
#[test]
fn schnitt_an_wandecke_liefert_verschnittene_querschnitte() {
// L-Ecke bei (0,3): Schenkel A (Achse (0,0)->(0,3)) und Schenkel B
// (Achse (0,3)->(3,3)), beide Dicke 0.2. Schnittebene bei Grundriss-X =
// 0.05 (knapp in der Ecke). Ohne Gehrung ueberlappten die beiden
// Wand-Cut-Rechtecke im u-Bereich [2.9, 3.0]; mit dem gehrten Fussabdruck
// (wie im 3D-Mesh) stossen sie exakt an der Gehrungslinie (u ~ 2.95)
// sauber aneinander, ohne Ueberlappung.
let (walls, _) = l_wall_scene();
let plane = SectionPlane::looking_plus_x(0.05);
let out = cut_section(&plane, &walls, &[]);
let wall_cuts: Vec<&CutPolygon> = out
.cut_polygons
.iter()
.filter(|c| c.component.kind == ComponentKind::Wall)
.collect();
assert_eq!(wall_cuts.len(), 2, "je ein Querschnitt fuer Schenkel A und B");
let u_bounds = |c: &CutPolygon| -> (f32, f32) {
let us: Vec<f32> = c.pts.iter().map(|p| p[0]).collect();
(
us.iter().cloned().fold(f32::INFINITY, f32::min),
us.iter().cloned().fold(f32::NEG_INFINITY, f32::max),
)
};
let a = wall_cuts.iter().find(|c| c.component.index == 0).expect("Schenkel A");
let b = wall_cuts.iter().find(|c| c.component.index == 1).expect("Schenkel B");
let (_a_lo, a_hi) = u_bounds(a);
let (b_lo, _b_hi) = u_bounds(b);
// Beide enden/beginnen an der Gehrungslinie (u ~ 2.95) statt sich zu
// ueberlappen (Schenkel A reichte ungehrt bis u=3.0, Schenkel B ab 2.9).
assert!((a_hi - 2.95).abs() < 1e-3, "Schenkel-A-Querschnitt endet an der Gehrung, u_hi={a_hi}");
assert!((b_lo - 2.95).abs() < 1e-3, "Schenkel-B-Querschnitt beginnt an der Gehrung, u_lo={b_lo}");
assert!(a_hi <= b_lo + 1e-4, "keine u-Ueberlappung der beiden Querschnitte");
}
// --- Oeffnungen (Tueren/Fenster) ----------------------------------------- // --- Oeffnungen (Tueren/Fenster) -----------------------------------------
/// Eine Wand entlang +X (Achse (0,0)->(4,0)) mit einem Fenster: Intervall /// Eine Wand entlang +X (Achse (0,0)->(4,0)) mit einem Fenster: Intervall