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:
@@ -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<WallMiters> {
|
||||
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
|
||||
/// Flaechen-Normale und Farbe je Vertex. Reihenfolge (0,1,2) + (0,2,3).
|
||||
fn push_quad(
|
||||
|
||||
@@ -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<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,
|
||||
z1: f32,
|
||||
color: Rgb,
|
||||
@@ -378,6 +390,9 @@ fn wall_prism(index: usize, wall: &WallInput) -> Option<Prism> {
|
||||
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<Prism> {
|
||||
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<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() {
|
||||
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<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) -----------------------------------------
|
||||
|
||||
/// Eine Wand entlang +X (Achse (0,0)->(4,0)) mit einem Fenster: Intervall
|
||||
|
||||
Reference in New Issue
Block a user