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:
@@ -32,6 +32,7 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
||||
height: 1.0,
|
||||
}],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
},
|
||||
WallInput {
|
||||
start: [0.0, 3.0],
|
||||
@@ -42,6 +43,7 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
||||
color,
|
||||
openings: vec![],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
},
|
||||
// "Spaeh"-Wand: kurzer Stummel HINTER Schenkel A (groesseres Modell-X,
|
||||
// also groessere Tiefe von der Betrachter-Ebene aus), genau im
|
||||
@@ -57,6 +59,7 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
||||
color,
|
||||
openings: vec![],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
},
|
||||
];
|
||||
let slabs = vec![SlabInput {
|
||||
|
||||
@@ -38,6 +38,7 @@ fn demo_walls() -> Vec<WallInput> {
|
||||
color: grey,
|
||||
openings: vec![],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
};
|
||||
// Raum 6 x 4 m.
|
||||
vec![
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -59,7 +59,8 @@ use std::collections::HashMap;
|
||||
|
||||
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
||||
use crate::types::{
|
||||
Mesh, MeshInput, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer, FLOATS_PER_VERTEX,
|
||||
Hole, Mesh, MeshInput, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer,
|
||||
FLOATS_PER_VERTEX,
|
||||
};
|
||||
|
||||
/// Ein Quader-Mesh besteht aus 6 Seiten (Boden, Deckel, 4 Waende) zu je 2
|
||||
@@ -109,7 +110,16 @@ fn extrude_wall_with_miters(mesh: &mut Mesh, wall: &WallInput, miters: WallMiter
|
||||
|
||||
let z0 = wall.base_elevation;
|
||||
let z1 = wall.base_elevation + wall.height;
|
||||
let segments = wall_solid_segments(length, &wall.openings, z0, z1);
|
||||
// Mit echten Loechern (`holes`) wird die Wand NICHT an Oeffnungen segmentiert,
|
||||
// sondern als EIN voller Achsen-Abschnitt extrudiert; die Loecher stanzt
|
||||
// `extrude_layer_segment_with_holes` in die Langseiten (holes/openings
|
||||
// schliessen sich aus, siehe `types::WallInput::holes`).
|
||||
let has_holes = !wall.holes.is_empty();
|
||||
let segments = if has_holes {
|
||||
vec![(0.0, length, z0, z1)]
|
||||
} else {
|
||||
wall_solid_segments(length, &wall.openings, z0, z1)
|
||||
};
|
||||
|
||||
// Schicht-Stapel: ohne `layers` (oder leer) EIN synthetisches Layer aus
|
||||
// der Gesamtdicke/-Farbe — bitgenau das bisherige Vollkoerper-Verhalten.
|
||||
@@ -139,11 +149,21 @@ fn extrude_wall_with_miters(mesh: &mut Mesh, wall: &WallInput, miters: WallMiter
|
||||
// Wandende grenzen (innere Laibungs-Teilstuecke bleiben eckig).
|
||||
let start_cut = if a0 <= 1e-6 { miters.start } else { None };
|
||||
let end_cut = if a1 >= length - 1e-6 { miters.end } else { None };
|
||||
if has_holes {
|
||||
// Voller Achsen-Abschnitt (a0=0, a1=length) mit echten Loechern
|
||||
// in den Langseiten — pro Schicht-Band dieselben Loecher (quer
|
||||
// gestapelt bilden ihre Laibungen die volle Loch-Tiefe).
|
||||
extrude_layer_segment_with_holes(
|
||||
mesh, wall, n, length, y0, y1, off_a, off_b, start_cut, end_cut, layer.color,
|
||||
&wall.holes,
|
||||
);
|
||||
} else {
|
||||
extrude_layer_segment(
|
||||
mesh, wall, n, length, a0, a1, y0, y1, off_a, off_b, start_cut, end_cut,
|
||||
layer.color,
|
||||
);
|
||||
}
|
||||
}
|
||||
off_a = off_b;
|
||||
}
|
||||
}
|
||||
@@ -284,6 +304,328 @@ fn extrude_layer_segment(
|
||||
push_quad(mesh, b3, b0, t0, t3, start_normal, color);
|
||||
}
|
||||
|
||||
/// Haengt ein Quad (vier Ecken als Ring, beliebige Reihenfolge) als zwei
|
||||
/// orientierte Dreiecke an — Winding via `push_tri_oriented` an `want_normal`
|
||||
/// ausgerichtet (robust gegen die Eck-Reihenfolge, anders als `push_quad`).
|
||||
/// Genutzt fuer die Loch-Zerlegung (Langseiten-Teilrechtecke, Deckel-/Boden-
|
||||
/// Streifen, Laibungen), wo die Ecken aus Gitter-Koordinaten kommen.
|
||||
fn push_quad_oriented(
|
||||
mesh: &mut Mesh,
|
||||
a: [f32; 3],
|
||||
b: [f32; 3],
|
||||
c: [f32; 3],
|
||||
d: [f32; 3],
|
||||
want_normal: [f32; 3],
|
||||
color: Rgb,
|
||||
) {
|
||||
push_tri_oriented(mesh, a, b, c, want_normal, color);
|
||||
push_tri_oriented(mesh, a, c, d, want_normal, color);
|
||||
}
|
||||
|
||||
/// Zerlegt das Rechteck `[u_lo,u_hi] x [z_lo,z_hi]` MINUS der `holes`
|
||||
/// (`(u0,u1,z0,z1)`) in achsparallele, paarweise disjunkte Teilrechtecke (die
|
||||
/// SOLIDEN Bereiche). Verfahren: Koordinaten-Kompression — alle Loch-Kanten in u
|
||||
/// und z bilden ein Gitter; jede Gitterzelle, deren Mittelpunkt in KEINEM Loch
|
||||
/// liegt, ist ein solides Teilrechteck. Dadurch koennen sich Loecher in u
|
||||
/// ueberlappen und auf unterschiedlicher Hoehe liegen (Kernfall zweier versetzt
|
||||
/// uebereinanderliegender Fenster), ohne dass degenerierte oder ueberlappende
|
||||
/// Teilrechtecke entstehen.
|
||||
fn solid_subrects(
|
||||
u_lo: f32,
|
||||
u_hi: f32,
|
||||
z_lo: f32,
|
||||
z_hi: f32,
|
||||
holes: &[(f32, f32, f32, f32)],
|
||||
) -> Vec<(f32, f32, f32, f32)> {
|
||||
let sort_dedup = |mut v: Vec<f32>| -> Vec<f32> {
|
||||
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
|
||||
v.dedup_by(|a, b| (*a - *b).abs() <= MIN_SPAN);
|
||||
v
|
||||
};
|
||||
let mut us = vec![u_lo, u_hi];
|
||||
let mut zs = vec![z_lo, z_hi];
|
||||
for (a0, a1, hb, ht) in holes {
|
||||
us.push(*a0);
|
||||
us.push(*a1);
|
||||
zs.push(*hb);
|
||||
zs.push(*ht);
|
||||
}
|
||||
let us = sort_dedup(us);
|
||||
let zs = sort_dedup(zs);
|
||||
let mut out = Vec::new();
|
||||
for i in 0..us.len().saturating_sub(1) {
|
||||
let (ua, ub) = (us[i], us[i + 1]);
|
||||
if ub - ua <= MIN_SPAN {
|
||||
continue;
|
||||
}
|
||||
for j in 0..zs.len().saturating_sub(1) {
|
||||
let (za, zb) = (zs[j], zs[j + 1]);
|
||||
if zb - za <= MIN_SPAN {
|
||||
continue;
|
||||
}
|
||||
let cu = (ua + ub) * 0.5;
|
||||
let cz = (za + zb) * 0.5;
|
||||
let in_hole = holes
|
||||
.iter()
|
||||
.any(|(a0, a1, hb, ht)| cu > *a0 && cu < *a1 && cz > *hb && cz < *ht);
|
||||
if !in_hole {
|
||||
out.push((ua, ub, za, zb));
|
||||
}
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Zieht die (ggf. ueberlappenden) `gaps` vom Intervall `[lo,hi]` ab und liefert
|
||||
/// die verbleibenden Teilintervalle (sortiert, disjunkt). Fuer die Deckel-/
|
||||
/// Boden-/Kappen-Flaechen, in denen ein randstaendiges Loch eine Luecke reisst.
|
||||
fn subtract_intervals(lo: f32, hi: f32, gaps: &[(f32, f32)]) -> Vec<(f32, f32)> {
|
||||
let mut gs: Vec<(f32, f32)> = gaps
|
||||
.iter()
|
||||
.map(|(a, b)| (a.max(lo), b.min(hi)))
|
||||
.filter(|(a, b)| b - a > MIN_SPAN)
|
||||
.collect();
|
||||
gs.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||
let mut out = Vec::new();
|
||||
let mut cursor = lo;
|
||||
for (a, b) in gs {
|
||||
if a > cursor + MIN_SPAN {
|
||||
out.push((cursor, a));
|
||||
}
|
||||
cursor = cursor.max(b);
|
||||
}
|
||||
if hi > cursor + MIN_SPAN {
|
||||
out.push((cursor, hi));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Extrudiert EIN Schicht-Band (volle Achse `0..length`, Hoehe `[y0,y1]`,
|
||||
/// Dicken-Versatz `[off_a,off_b]`) mit ECHTEN rechteckigen LOECHERN (`holes`,
|
||||
/// siehe `types::Hole`) statt der Oeffnungs-Segmentierung. Aufbau:
|
||||
/// • Langseiten (+n bei `off_a`, −n bei `off_b`): das Flaechen-Rechteck MINUS
|
||||
/// der Loecher, per `solid_subrects` in Teilrechtecke zerlegt (je 2 Dreiecke).
|
||||
/// • Deckel (`y1`) / Boden (`y0`): voller Streifen minus der u-Intervalle der
|
||||
/// Loecher, die die jeweilige Kante beruehren (z. B. eine Tuer am Boden).
|
||||
/// • End-Kappen (`a=0`/`a=length`, gemitert): volle Hoehe minus der z-Intervalle
|
||||
/// randstaendiger Loecher (Loecher liegen normalerweise im Inneren → volle
|
||||
/// Kappe, unveraendert gegenueber der Gehrung).
|
||||
/// • Laibungen je Loch: vier Quads (links/rechts/unten/oben) — aber nur fuer die
|
||||
/// Seiten, die im Wand-INNEREN liegen (die randstaendige Seite hat stattdessen
|
||||
/// die Luecke in Deckel/Boden/Kappe, keine Laibung).
|
||||
/// Winding/Normalen konsistent ueber `push_quad_oriented`/`push_tri_oriented`
|
||||
/// (Backface-Culling + Beleuchtung korrekt).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn extrude_layer_segment_with_holes(
|
||||
mesh: &mut Mesh,
|
||||
wall: &WallInput,
|
||||
n: Point2,
|
||||
length: f32,
|
||||
y0: f32,
|
||||
y1: f32,
|
||||
off_a: f32,
|
||||
off_b: f32,
|
||||
start_cut: Option<MiterLine>,
|
||||
end_cut: Option<MiterLine>,
|
||||
color: Rgb,
|
||||
holes: &[Hole],
|
||||
) {
|
||||
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, p[1] + n[1] * s] };
|
||||
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),
|
||||
}
|
||||
};
|
||||
// Plan-Punkt bei Achsen-Position `a`, Dicken-Versatz `offv`; NUR an den echten
|
||||
// Wandenden (a≈0 / a≈length) gegen die Gehrungslinie verschnitten — innere
|
||||
// Loch-Kanten bleiben rechtwinklig (unabhaengig von der Gehrung).
|
||||
let g_at = |a: f32, offv: f32| -> Point2 {
|
||||
let base = off(axis_point(wall, length, a), offv);
|
||||
if a <= 1e-6 {
|
||||
clip(base, start_cut)
|
||||
} else if a >= length - 1e-6 {
|
||||
clip(base, end_cut)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
};
|
||||
let w = |g: Point2, y: f32| -> [f32; 3] { [g[0], y, g[1]] };
|
||||
|
||||
// Loecher auf [0,length] x [y0,y1] klemmen, entartete verwerfen.
|
||||
let mut hs: Vec<(f32, f32, f32, f32)> = Vec::new();
|
||||
for h in holes {
|
||||
let a0 = h.from.max(0.0);
|
||||
let a1 = h.to.min(length);
|
||||
let zb = h.z_bottom.max(y0);
|
||||
let zt = h.z_top.min(y1);
|
||||
if a1 - a0 > MIN_SPAN && zt - zb > MIN_SPAN {
|
||||
hs.push((a0, a1, zb, zt));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Langseiten (+n / −n): Rechteck-Gitter minus Loecher. ──────────────────
|
||||
let np = [n[0], 0.0, n[1]];
|
||||
let nn = [-n[0], 0.0, -n[1]];
|
||||
for (ua, ub, za, zb) in solid_subrects(0.0, length, y0, y1, &hs) {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(ua, off_a), za),
|
||||
w(g_at(ub, off_a), za),
|
||||
w(g_at(ub, off_a), zb),
|
||||
w(g_at(ua, off_a), zb),
|
||||
np,
|
||||
color,
|
||||
);
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(ua, off_b), za),
|
||||
w(g_at(ub, off_b), za),
|
||||
w(g_at(ub, off_b), zb),
|
||||
w(g_at(ua, off_b), zb),
|
||||
nn,
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Deckel (y1) und Boden (y0): voller Streifen minus randstaendiger Loecher.
|
||||
let top_gaps: Vec<(f32, f32)> = hs
|
||||
.iter()
|
||||
.filter(|(_, _, _, zt)| *zt >= y1 - MIN_SPAN)
|
||||
.map(|(a0, a1, _, _)| (*a0, *a1))
|
||||
.collect();
|
||||
for (ua, ub) in subtract_intervals(0.0, length, &top_gaps) {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(ua, off_a), y1),
|
||||
w(g_at(ub, off_a), y1),
|
||||
w(g_at(ub, off_b), y1),
|
||||
w(g_at(ua, off_b), y1),
|
||||
[0.0, 1.0, 0.0],
|
||||
color,
|
||||
);
|
||||
}
|
||||
let bot_gaps: Vec<(f32, f32)> = hs
|
||||
.iter()
|
||||
.filter(|(_, _, zb, _)| *zb <= y0 + MIN_SPAN)
|
||||
.map(|(a0, a1, _, _)| (*a0, *a1))
|
||||
.collect();
|
||||
for (ua, ub) in subtract_intervals(0.0, length, &bot_gaps) {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(ua, off_a), y0),
|
||||
w(g_at(ub, off_a), y0),
|
||||
w(g_at(ub, off_b), y0),
|
||||
w(g_at(ua, off_b), y0),
|
||||
[0.0, -1.0, 0.0],
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
// ── End-Kappen (a=0 / a=length): volle Hoehe minus randstaendiger Loecher. ─
|
||||
// Aussen-Normale je Kappe geometrisch aus den vollen Kappen-Ecken (wie im
|
||||
// rechtwinkligen/gehrten Fall in `extrude_layer_segment`).
|
||||
let start_normal = quad_normal(
|
||||
w(g_at(0.0, off_b), y0),
|
||||
w(g_at(0.0, off_a), y0),
|
||||
w(g_at(0.0, off_b), y1),
|
||||
);
|
||||
let start_gaps: Vec<(f32, f32)> = hs
|
||||
.iter()
|
||||
.filter(|(a0, _, _, _)| *a0 <= MIN_SPAN)
|
||||
.map(|(_, _, zb, zt)| (*zb, *zt))
|
||||
.collect();
|
||||
for (za, zb) in subtract_intervals(y0, y1, &start_gaps) {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(0.0, off_b), za),
|
||||
w(g_at(0.0, off_a), za),
|
||||
w(g_at(0.0, off_a), zb),
|
||||
w(g_at(0.0, off_b), zb),
|
||||
start_normal,
|
||||
color,
|
||||
);
|
||||
}
|
||||
let end_normal = quad_normal(
|
||||
w(g_at(length, off_a), y0),
|
||||
w(g_at(length, off_b), y0),
|
||||
w(g_at(length, off_a), y1),
|
||||
);
|
||||
let end_gaps: Vec<(f32, f32)> = hs
|
||||
.iter()
|
||||
.filter(|(_, a1, _, _)| *a1 >= length - MIN_SPAN)
|
||||
.map(|(_, _, zb, zt)| (*zb, *zt))
|
||||
.collect();
|
||||
for (za, zb) in subtract_intervals(y0, y1, &end_gaps) {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(length, off_a), za),
|
||||
w(g_at(length, off_b), za),
|
||||
w(g_at(length, off_b), zb),
|
||||
w(g_at(length, off_a), zb),
|
||||
end_normal,
|
||||
color,
|
||||
);
|
||||
}
|
||||
|
||||
// ── Laibungen je Loch: nur die im Wand-Inneren liegenden Seiten. ──────────
|
||||
for &(a0, a1, zb, zt) in &hs {
|
||||
// Linke Laibung (bei a0), Normale +u (in die Oeffnung).
|
||||
if a0 > MIN_SPAN {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(a0, off_a), zb),
|
||||
w(g_at(a0, off_b), zb),
|
||||
w(g_at(a0, off_b), zt),
|
||||
w(g_at(a0, off_a), zt),
|
||||
[u[0], 0.0, u[1]],
|
||||
color,
|
||||
);
|
||||
}
|
||||
// Rechte Laibung (bei a1), Normale −u.
|
||||
if a1 < length - MIN_SPAN {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(a1, off_a), zb),
|
||||
w(g_at(a1, off_b), zb),
|
||||
w(g_at(a1, off_b), zt),
|
||||
w(g_at(a1, off_a), zt),
|
||||
[-u[0], 0.0, -u[1]],
|
||||
color,
|
||||
);
|
||||
}
|
||||
// Untere Laibung (bei zb = Bruestungsoberkante), Normale +Y.
|
||||
if zb > y0 + MIN_SPAN {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(a0, off_a), zb),
|
||||
w(g_at(a1, off_a), zb),
|
||||
w(g_at(a1, off_b), zb),
|
||||
w(g_at(a0, off_b), zb),
|
||||
[0.0, 1.0, 0.0],
|
||||
color,
|
||||
);
|
||||
}
|
||||
// Obere Laibung (bei zt = Sturzunterkante), Normale −Y.
|
||||
if zt < y1 - MIN_SPAN {
|
||||
push_quad_oriented(
|
||||
mesh,
|
||||
w(g_at(a0, off_a), zt),
|
||||
w(g_at(a1, off_a), zt),
|
||||
w(g_at(a1, off_b), zt),
|
||||
w(g_at(a0, off_b), zt),
|
||||
[0.0, -1.0, 0.0],
|
||||
color,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Aussen-Normale eines planaren, CCW-von-aussen georderten Quads `(a,b,_,d)`
|
||||
/// (nur die drei ersten Ecken werden gebraucht: `d` ist die vierte, ueber `a`
|
||||
/// benachbarte Ecke). `cross(b-a, d-a)` — Herleitung/Verifikation siehe
|
||||
|
||||
@@ -1016,6 +1016,7 @@ mod tests {
|
||||
color,
|
||||
openings: vec![],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
},
|
||||
WallInput {
|
||||
start: [0.0, 3.0],
|
||||
@@ -1026,6 +1027,7 @@ mod tests {
|
||||
color,
|
||||
openings: vec![],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
},
|
||||
];
|
||||
let slabs = vec![SlabInput {
|
||||
@@ -1282,6 +1284,7 @@ mod tests {
|
||||
height: 1.0,
|
||||
}],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1367,6 +1370,7 @@ mod tests {
|
||||
color: [0.8, 0.8, 0.8],
|
||||
openings: vec![],
|
||||
layers: None,
|
||||
holes: vec![],
|
||||
};
|
||||
// Blick von y = -2 nach +Y: beide Waende (y=0 bzw. y=1) liegen dahinter.
|
||||
let plane = SectionPlane::looking_plus_y(-2.0);
|
||||
|
||||
@@ -66,6 +66,46 @@ pub struct WallInput {
|
||||
/// Verhalten unveraendert).
|
||||
#[serde(default)]
|
||||
pub layers: Option<Vec<WallLayer>>,
|
||||
/// Rechteckige LOECHER (Fenster/Tueren) als ECHTE Aussparungen in EINEM
|
||||
/// Wandkoerper (siehe `Hole` + `mesh::extrude_wall*`). Default leer.
|
||||
///
|
||||
/// Anders als `openings` zerlegt das die Wand NICHT in Pfeiler/Bruestung/
|
||||
/// Sturz-Teilquader, sondern stanzt die Loecher in die beiden Langseiten
|
||||
/// (Rechteck-Gitter-Zerlegung + Laibungsflaechen). So bildet EIN Koerper
|
||||
/// beliebig (auch versetzt uebereinander) angeordnete Oeffnungen ab — das
|
||||
/// kann die Segment-Zerlegung nicht. Genutzt vom 3D-Viewer-Pfad der
|
||||
/// TS-Ableitung (`toWalls3d`, `layeredWalls:true`); der Schnitt-Pfad
|
||||
/// (`layeredWalls:false`) bleibt bei `openings`/Segmentierung.
|
||||
///
|
||||
/// `holes` und `openings` schliessen sich im TS-Emitter gegenseitig aus; ist
|
||||
/// `holes` gesetzt, ueberspringt `mesh.rs` die Oeffnungs-Segmentierung und
|
||||
/// extrudiert den vollen Achsen-Abschnitt mit Loechern.
|
||||
#[serde(default)]
|
||||
pub holes: Vec<Hole>,
|
||||
}
|
||||
|
||||
/// Ein rechteckiges Loch (Fenster/Tuer) in EINEM Wandkoerper. Siehe
|
||||
/// `WallInput::holes`.
|
||||
///
|
||||
/// KONVENTION: `from`/`to` sind Achsen-Meter ab `WallInput::start` (0..Wandlaenge,
|
||||
/// dieselbe Achse wie `Opening::from`/`to`); `z_bottom`/`z_top` sind ABSOLUTE
|
||||
/// Weltmeter (NICHT relativ zu `base_elevation` — anders als `Opening::sill`).
|
||||
/// Eine Tuer ist der Sonderfall `z_bottom == base_elevation` (Loch bis zur
|
||||
/// Wand-Unterkante). Loecher liegen definitionsgemaess im Wand-INNEREN und
|
||||
/// beruehren die gemiterten Enden nicht — die Gehrung (`compute_wall_miters`)
|
||||
/// bleibt davon unberuehrt (siehe `mesh`-Moduldoc).
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
||||
pub struct Hole {
|
||||
/// Start des Loch-Intervalls entlang der Wandachse (Meter ab `start`).
|
||||
pub from: f32,
|
||||
/// Ende des Loch-Intervalls entlang der Wandachse (Meter ab `start`).
|
||||
pub to: f32,
|
||||
/// Absolute Unterkante des Lochs (Weltmeter).
|
||||
#[serde(default, rename = "zBottom")]
|
||||
pub z_bottom: f32,
|
||||
/// Absolute Oberkante des Lochs (Weltmeter).
|
||||
#[serde(default, rename = "zTop")]
|
||||
pub z_top: f32,
|
||||
}
|
||||
|
||||
/// Eine Wandschicht: Dicke + eigene Albedo-Farbe. Siehe `WallInput::layers`.
|
||||
|
||||
@@ -63,12 +63,96 @@ describe("selectionHighlightLines", () => {
|
||||
expect(two.length).toBe(one.length * 2);
|
||||
});
|
||||
|
||||
it("eine Wand MIT Öffnungen -> mehrere Teilquader (mehr als 12 Kanten)", () => {
|
||||
// W1 trägt Tür D1 + Fenster O1 -> Pfeiler/Brüstung/Sturz-Segmente, also
|
||||
// ein Vielfaches von 12 Kanten (mehr als ein einzelner Quader).
|
||||
const lines = selectionHighlightLines(sampleProject, ["W1"], [], RGB);
|
||||
expect(lines.length).toBeGreaterThan(12 * VERTICES_PER_EDGE * FLOATS_PER_VERTEX);
|
||||
expect(lines.length % (12 * VERTICES_PER_EDGE * FLOATS_PER_VERTEX)).toBe(0);
|
||||
it("eine Wand MIT Öffnungen -> EIN Körper je Schicht-Band mit Löchern (kein Segment-Vielfaches mehr)", () => {
|
||||
// NEUES Verhalten (3D-Pfad, layered=true): W1 (Tür D1 + Fenster O1) wird
|
||||
// NICHT mehr in Pfeiler/Brüstung/Sturz zerlegt, sondern als EIN Körper pro
|
||||
// Schicht-Band mit echten Löchern emittiert. Das Highlight umrandet daher
|
||||
// genau die Schicht-Bänder (Wandtyp "aw" = 4 Schichten) — exakt so viele
|
||||
// Kanten wie eine öffnungslose Wand desselben Typs (W2), NICHT ein Vielfaches
|
||||
// durch Segmente. (Die Löcher selbst fügen im Highlight keine Kanten hinzu.)
|
||||
const w1 = selectionHighlightLines(sampleProject, ["W1"], [], RGB);
|
||||
const w2 = selectionHighlightLines(sampleProject, ["W2"], [], RGB);
|
||||
expect(w1.length).toBe(4 * 12 * VERTICES_PER_EDGE * FLOATS_PER_VERTEX);
|
||||
expect(w1.length).toBe(w2.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Öffnungen als echte Löcher in EINEM Körper (3D-Pfad, layeredWalls:true)", () => {
|
||||
// Wand W1 (EG, Wandtyp "aw" = 4 Schichten) mit ZWEI Fenstern, deren Achsen-
|
||||
// Intervalle sich ÜBERLAPPEN, aber auf UNTERSCHIEDLICHER Höhe liegen:
|
||||
// Fenster A: u=[1.0,2.0] (position 1.0, width 1.0), z=[0.9,2.1] (sill 0.9, h 1.2)
|
||||
// Fenster B: u=[1.5,2.5] (position 1.5, width 1.0), z=[0.3,0.7] (sill 0.3, h 0.4)
|
||||
// Die alte Segment-Zerlegung kann diesen versetzt-überlappenden Fall nicht
|
||||
// abbilden; als Löcher in EINEM Körper müssen beide sauber als `holes`
|
||||
// erscheinen. W1: start (0,0)->(5,0), floor "eg" (baseElevation 0).
|
||||
const twoOffsetWindows: Project = {
|
||||
...sampleProject,
|
||||
doors: [],
|
||||
openings: [
|
||||
{
|
||||
id: "WA",
|
||||
type: "opening",
|
||||
hostWallId: "W1",
|
||||
categoryCode: "21",
|
||||
kind: "window",
|
||||
position: 1.0,
|
||||
width: 1.0,
|
||||
height: 1.2,
|
||||
sillHeight: 0.9,
|
||||
},
|
||||
{
|
||||
id: "WB",
|
||||
type: "opening",
|
||||
hostWallId: "W1",
|
||||
categoryCode: "21",
|
||||
kind: "window",
|
||||
position: 1.5,
|
||||
width: 1.0,
|
||||
height: 0.4,
|
||||
sillHeight: 0.3,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it("layered=true: 1 RWall je Schicht-Band mit BEIDEN Fenstern als holes", () => {
|
||||
const w1 = projectToWalls3d(twoOffsetWindows, true).filter((w) => w.wallId === "W1");
|
||||
// Genau ein Körper je Schicht-Band (aw = 4 Schichten), KEINE Segment-Boxen.
|
||||
expect(w1.length).toBe(4);
|
||||
for (const body of w1) {
|
||||
// Jeder Band-Körper trägt BEIDE Löcher (in EINEM Körper).
|
||||
expect(body.holes).toBeDefined();
|
||||
expect(body.holes!.length).toBe(2);
|
||||
const byFrom = [...body.holes!].sort((a, b) => a.from - b.from);
|
||||
// Fenster A: u=[1,2], z=[0.9,2.1].
|
||||
expect(byFrom[0].from).toBeCloseTo(1.0, 6);
|
||||
expect(byFrom[0].to).toBeCloseTo(2.0, 6);
|
||||
expect(byFrom[0].zBottom).toBeCloseTo(0.9, 6);
|
||||
expect(byFrom[0].zTop).toBeCloseTo(2.1, 6);
|
||||
// Fenster B: u=[1.5,2.5], z=[0.3,0.7] — überlappt A in u, liegt tiefer.
|
||||
expect(byFrom[1].from).toBeCloseTo(1.5, 6);
|
||||
expect(byFrom[1].to).toBeCloseTo(2.5, 6);
|
||||
expect(byFrom[1].zBottom).toBeCloseTo(0.3, 6);
|
||||
expect(byFrom[1].zTop).toBeCloseTo(0.7, 6);
|
||||
}
|
||||
});
|
||||
|
||||
it("layered=false (Schnitt-Pfad): unverändert segmentiert, KEINE holes", () => {
|
||||
const w1 = projectToWalls3d(twoOffsetWindows, false).filter((w) => w.wallId === "W1");
|
||||
// Der Schnitt-Pfad zerlegt weiter in Pfeiler/Brüstung/Sturz-Segmente (mehr
|
||||
// Boxen als die 4 Schicht-Bänder des 3D-Pfads) und setzt NIE `holes`.
|
||||
expect(w1.length).toBeGreaterThan(4);
|
||||
for (const seg of w1) expect(seg.holes).toBeUndefined();
|
||||
});
|
||||
|
||||
it("Tür = Loch bis zur Wand-UK (zBottom == baseElevation)", () => {
|
||||
// Eine Tür (D1 auf W1) muss im 3D-Pfad als Loch bis zum Boden erscheinen:
|
||||
// zBottom des Lochs == Wand-UK (0), NICHT ab einer Brüstung.
|
||||
const p: Project = { ...sampleProject, openings: [] };
|
||||
const w1 = projectToWalls3d(p, true).filter((w) => w.wallId === "W1");
|
||||
expect(w1.length).toBe(4);
|
||||
const door = w1[0].holes?.find((h) => h.zBottom < 1e-6);
|
||||
expect(door).toBeDefined();
|
||||
expect(door!.zBottom).toBeCloseTo(0, 6); // Loch bis zur Wand-UK.
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+83
-4
@@ -59,6 +59,24 @@ export interface RLayer {
|
||||
color: RRgb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein rechteckiges LOCH (Fenster/Tür) in EINEM Wandkörper — matcht 1:1
|
||||
* render3d::types::Hole. KONVENTION: `from`/`to` sind Achsen-Meter ab
|
||||
* `RWall.start` (0..Achslänge, dieselbe Achse wie die Öffnungs-Intervalle);
|
||||
* `zBottom`/`zTop` sind ABSOLUTE Weltmeter (NICHT relativ zur Wand-UK — anders
|
||||
* als `Opening.sillHeight`). Eine Tür ist der Sonderfall
|
||||
* `zBottom == RWall.baseElevation` (Loch bis zur Wand-UK). render3d stanzt die
|
||||
* Löcher als echte Aussparungen in die beiden Langseiten (Rechteck-Gitter-
|
||||
* Zerlegung + Laibungsflächen), statt die Wand in Pfeiler/Brüstung/Sturz zu
|
||||
* zerlegen.
|
||||
*/
|
||||
export interface RHole {
|
||||
from: number;
|
||||
to: number;
|
||||
zBottom: number;
|
||||
zTop: number;
|
||||
}
|
||||
|
||||
export interface RWall {
|
||||
start: RVec2;
|
||||
end: RVec2;
|
||||
@@ -79,6 +97,19 @@ export interface RWall {
|
||||
* serde verwirft unbekannte Felder).
|
||||
*/
|
||||
wallId: string;
|
||||
/**
|
||||
* Echte rechteckige Löcher (Fenster/Türen) in DIESEM Wandkörper (siehe
|
||||
* {@link RHole}). Nur im 3D-Viewer-Pfad ({@link projectToModel3d} mit
|
||||
* `layeredWalls:true`) gesetzt: dort emittiert {@link emitWall} EINEN RWall pro
|
||||
* Schicht-Band über die volle Achse mit ALLEN Öffnungen als `holes` — statt der
|
||||
* Pfeiler/Brüstung/Sturz-Segment-Boxen. So bildet EIN Körper auch versetzt
|
||||
* übereinanderliegende Fenster ab (das kann die Segment-Zerlegung nicht). Der
|
||||
* Schnitt-Pfad (`layeredWalls:false`) setzt `holes` NICHT und segmentiert
|
||||
* unverändert weiter (Owner-Kompatibilität mit `toSection.ts`, s. u.). Fehlt/
|
||||
* leer ⇒ Vollkörper ohne Aussparung (render3d: `#[serde(default)]`). `holes`
|
||||
* und die Segment-Zerlegung schliessen sich damit gegenseitig aus.
|
||||
*/
|
||||
holes?: RHole[];
|
||||
}
|
||||
|
||||
/** Eine extrudierte Deckenplatte: geschlossener Grundriss-Umriss + Z-Ausdehnung. */
|
||||
@@ -224,6 +255,13 @@ function resolveWallBands(project: Project, wall: Wall, layered = true): WallBan
|
||||
* mit Schicht-`thickness`/`color`. Entartete Stücke (Länge ≤ 0 oder Höhe ≤ 0)
|
||||
* werden übersprungen — so überleben Öffnungen dicht an den Wandenden ohne
|
||||
* Nullquader.
|
||||
*
|
||||
* Optionale `holes`: echte rechteckige Aussparungen in DIESER Box (siehe
|
||||
* {@link RHole}). Ihre `from`/`to` sind Achsen-Meter ab Wand-Start (dieselbe
|
||||
* Achse wie `from`/`to` dieser Box), `zBottom`/`zTop` absolute Weltmeter — beides
|
||||
* unverändert an render3d durchgereicht (der Querversatz `normalOffset` wirkt nur
|
||||
* senkrecht zur Achse und lässt die Loch-Parametrisierung unberührt). Werden nur
|
||||
* im 3D-Pfad (voller Achsen-Quader) gesetzt.
|
||||
*/
|
||||
function pushSegment(
|
||||
out: RWall[],
|
||||
@@ -236,6 +274,7 @@ function pushSegment(
|
||||
normalOffset: number,
|
||||
color: RRgb,
|
||||
wallId: string,
|
||||
holes?: RHole[],
|
||||
): void {
|
||||
if (to - from <= EPS) return;
|
||||
if (zTop - zBottom <= EPS) return;
|
||||
@@ -269,6 +308,7 @@ function pushSegment(
|
||||
color,
|
||||
layers: [],
|
||||
wallId,
|
||||
...(holes && holes.length > 0 ? { holes } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -422,11 +462,31 @@ function trimWallTopForCeilings(
|
||||
return Math.max(zBottom, top);
|
||||
}
|
||||
|
||||
/** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */
|
||||
/**
|
||||
* Emittiert die Wandkörper EINER Wand (mit oder ohne Öffnungen).
|
||||
*
|
||||
* ZWEI PFADE je `layered`:
|
||||
* • layered=true (3D-Viewer, Default): EIN Körper pro Schicht-Band über die
|
||||
* VOLLE Achse mit allen Öffnungen als echte `holes` (render3d stanzt sie in
|
||||
* die Langseiten). Ermöglicht auch versetzt übereinanderliegende Fenster in
|
||||
* EINEM Körper (keine Segment-Nähte).
|
||||
* • layered=false (Schnitt-Pfad, `computeSection`): UNVERÄNDERTE Pfeiler/
|
||||
* Brüstung/Sturz-Segmentierung (EIN Voll-Band je Segment), damit die
|
||||
* `ComponentRef.index`-Reihenfolge exakt zu `toSection.ts::wallSegmentOwners`
|
||||
* passt (Owner-Kompatibilität; toSection.ts bleibt unangetastet). Löcher
|
||||
* werden hier NICHT emittiert.
|
||||
*/
|
||||
function emitWall(out: RWall[], project: Project, wall: Wall, layered = true): void {
|
||||
const bands = resolveWallBands(project, wall, layered);
|
||||
const { zBottom, zTop: rawZTop } = wallVerticalExtent(project, wall);
|
||||
const zTop = trimWallTopForCeilings(project, wall, zBottom, rawZTop);
|
||||
// Decken-Trim (kappt die Wand an der Unterkante einer dominanteren Decke) NUR
|
||||
// im 3D-Viewer-Pfad (layered=true): dort vermeidet das Kappen das Z-Fighting
|
||||
// Wand/Decke am bündigen Anschluss. Der Schnitt-Pfad (layered=false) will die
|
||||
// UNGEKAPPTE volle Wandhöhe, weil er seine EIGENE prioritätsbasierte Schicht-
|
||||
// Dominanz (toSection.ts::subtractDominantBands) auf Rohgeometrie anwendet —
|
||||
// ein vorab gekürzter Wandkopf würde dort Schichteinzug/Bodenaufbau am Wand-
|
||||
// Decken-Anschluss verfälschen.
|
||||
const zTop = layered ? trimWallTopForCeilings(project, wall, zBottom, rawZTop) : rawZTop;
|
||||
const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
|
||||
if (axisLen < 1e-9 || zTop - zBottom <= EPS) return;
|
||||
|
||||
@@ -461,8 +521,27 @@ function emitWall(out: RWall[], project: Project, wall: Wall, layered = true): v
|
||||
}
|
||||
cutouts.sort((a, b) => a.from - b.from);
|
||||
|
||||
// Ohne Aussparungen: ein durchgehender Quader je Schicht (wie bisher, aber
|
||||
// in Lagen aufgeteilt).
|
||||
// ── 3D-Viewer-Pfad (layered=true): EIN Körper je Schicht-Band über die volle
|
||||
// Achse mit allen Öffnungen als echte Löcher. Die Segment-Zerlegung entfällt
|
||||
// komplett — render3d stanzt die Löcher (Rechteck-Gitter + Laibungen). Türen
|
||||
// sind Löcher bis zur Wand-UK (`oBottom == zBottom`), Fenster ab Brüstung. So
|
||||
// bildet EIN Körper auch versetzt übereinanderliegende Fenster ab.
|
||||
if (layered) {
|
||||
const holes: RHole[] = cutouts.map((c) => ({
|
||||
from: c.from,
|
||||
to: c.to,
|
||||
zBottom: c.oBottom,
|
||||
zTop: c.oTop,
|
||||
}));
|
||||
for (const b of bands) {
|
||||
pushSegment(out, wall, 0, axisLen, zBottom, zTop, b.thickness, b.offset, b.color, wall.id, holes);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// ── Schnitt-Pfad (layered=false): unveränderte Pfeiler/Brüstung/Sturz-
|
||||
// Segmentierung (Owner-Kompatibilität mit toSection.ts::wallSegmentOwners).
|
||||
// Ohne Aussparungen: ein durchgehender Quader je Schicht.
|
||||
if (cutouts.length === 0) {
|
||||
emitSeg(0, axisLen, zBottom, zTop);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user