From 0c82d01cd96df9dc22cd7117f5214dad876dab0e Mon Sep 17 00:00:00 2001 From: Karim Date: Fri, 3 Jul 2026 23:52:33 +0200 Subject: [PATCH] Schnitt: Wandquerschnitte an den gemitterten Fussabdruck koppeln MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src-tauri/render3d/src/mesh.rs | 54 +++++++++++++++++- src-tauri/render3d/src/section.rs | 92 ++++++++++++++++++++++++++----- 2 files changed, 129 insertions(+), 17 deletions(-) diff --git a/src-tauri/render3d/src/mesh.rs b/src-tauri/render3d/src/mesh.rs index de0fba2..29aca5c 100644 --- a/src-tauri/render3d/src/mesh.rs +++ b/src-tauri/render3d/src/mesh.rs @@ -23,8 +23,11 @@ // werden durch einen Hoehen-Ueberlappungstest (`cluster_by_height_overlap`) // sauber getrennt (kein Verschneiden ueber Geschosse hinweg). T-/X-Stoesse // (mehr als zwei Enden an einem Knoten) bleiben — wie im Web-Pendant — -// rechtwinklig. Das Schnitt-Modul (`section.rs`) bleibt UNVERAENDERT bei der -// achsparallelen Bounding-Box-Naeherung (siehe dessen Moduldoc). +// rechtwinklig. Das Schnitt-Modul (`section.rs`) nutzt fuer die WAND-CUT-Polygone +// 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/ // Mauerwerk/...) werden nicht als ein Vollkoerper, sondern als je EIN Prisma @@ -488,6 +491,53 @@ fn compute_wall_miters(walls: &[WallInput]) -> Vec { 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| -> 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> { + 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 /// Flaechen-Normale und Farbe je Vertex. Reihenfolge (0,1,2) + (0,2,3). fn push_quad( diff --git a/src-tauri/render3d/src/section.rs b/src-tauri/render3d/src/section.rs index 0ee9cc6..a0a063c 100644 --- a/src-tauri/render3d/src/section.rs +++ b/src-tauri/render3d/src/section.rs @@ -101,19 +101,22 @@ //! (Rechteck-Baender) und Decken (i.d.R. konvexe Rechtecke) ist das exakt; //! fuer stark diagonale oder konkave Grundrisse kann es zu Ueberverdeckung //! fuehren. -//! - Keine Wandknoten-Verschneidung (T-/X-Stoesse): DIESES Modul behandelt -//! Waende weiterhin als eigenstaendige, stumpf abgeschlossene Quader, die -//! sich an Ecken UEBERLAPPEN (kein Miter-Join) — sowohl fuer die Cut-Polygone -//! als auch fuer die Verdeckungs-Bounding-Box. `mesh.rs` (die Vollkoerper- -//! Extrusion fuer die 3D-Ansicht) berechnet die Eck-Gehrung inzwischen SELBST -//! (`mesh::compute_wall_miters`, aus der Wandkonnektivitaet); dieses Modul -//! zieht davon bewusst KEINEN Nutzen, weil seine Verdeckungs-Bounding-Box -//! ohnehin nur die achsparallele Grundflaeche kennt (siehe oben) — eine -//! gehrte, nicht-achsparallele Kante wuerde dort keinen Praezisionsgewinn -//! bringen. Das kann in Ecknaehe dazu fuehren, dass eine Wand als "in eine -//! andere eingebettet" verdeckt erscheint (siehe Test -//! `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real, aber ein -//! Artefakt der fehlenden Verschneidungslogik DIESES Moduls, kein Bug. +//! - Wandknoten-Verschneidung (Gehrung) — zwei Wege, je nach Zweck: +//! * CUT-Polygone (der eigentliche Schnitt-Querschnitt) nutzen den an den +//! echten Wandecken GEHRTEN Fussabdruck aus `mesh::wall_mitered_footprints` +//! — DIESELBE Ecken-Verschneidung wie die 3D-Vollkoerper-Extrusion +//! (`mesh::compute_wall_miters`). Dadurch liefert ein Schnitt an einer +//! Wandecke genau EINEN sauberen, verschnittenen Querschnitt statt zweier +//! ueberlappender Rechtecke (siehe `Prism::cut_footprint`, `cut_section` und +//! Test `schnitt_an_wandecke_liefert_verschnittene_querschnitte`). +//! * Verdeckung/Projektion (Bounding-Box + Ansichtskanten) bleiben bei der +//! ACHSPARALLELEN, ungehrten Grundflaeche (`Prism::footprint`): die +//! Verdeckungs-Bounding-Box kennt ohnehin nur die achsparallele Ausdehnung +//! (siehe naechster Punkt), eine gehrte Kante brächte dort keinen +//! 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- //! Extrusion in `mesh.rs` ausgewertet (`mesh::wall_solid_segments`) — beide //! nutzen dieselbe Bruestungs-/Sturz-Zerlegung (`crate::openings:: @@ -296,7 +299,16 @@ pub struct SectionOutput { struct Prism { kind: ComponentKind, index: usize, + /// Achsparalleles Band-/Umriss-Polygon (Modell-Meter). Basis fuer die + /// Verdeckungs-Bounding-Box und die Projektions-Kanten (siehe Moduldoc + /// "Bekannte Vereinfachungen"). footprint: Vec, + /// 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, z0: f32, z1: f32, color: Rgb, @@ -378,6 +390,9 @@ fn wall_prism(index: usize, wall: &WallInput) -> Option { Some(Prism { kind: ComponentKind::Wall, index, + // Standardmaessig ungehrt; `cut_section` ersetzt ihn durch den gehrten + // Fussabdruck aus `mesh::wall_mitered_footprints`. + cut_footprint: footprint.clone(), footprint, z0: wall.base_elevation, z1: wall.base_elevation + wall.height, @@ -403,6 +418,7 @@ fn slab_prism(index: usize, slab: &SlabInput) -> Option { Some(Prism { kind: ComponentKind::Slab, index, + cut_footprint: slab.outline.clone(), footprint: slab.outline.clone(), z0, z1, @@ -928,8 +944,14 @@ fn compute_view_edges( /// Decken) aus. Siehe Moduldoc fuer Format/Konventionen. pub fn cut_section(plane: &SectionPlane, walls: &[WallInput], slabs: &[SlabInput]) -> SectionOutput { let mut prisms: Vec = 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() { - 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); } } @@ -941,7 +963,7 @@ pub fn cut_section(plane: &SectionPlane, walls: &[WallInput], slabs: &[SlabInput let mut cut_polygons = Vec::new(); 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)); for (rlo, rhi, rz0, rz1) in wall_cut_rectangles(plane, prism, lo, hi) { 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 = 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) ----------------------------------------- /// Eine Wand entlang +X (Achse (0,0)->(4,0)) mit einem Fenster: Intervall