render3d: Wandverschneidung an Ecken + sichtbare Schichten
mesh.rs berechnet die Ecken-Verschneidung jetzt selbst (portiert aus dem
Web-Kern computeJoins/clippedBand): Wandenden werden per Rundungs-Key
gruppiert, ueber Hoehen-Ueberlappung geclustert (gestapelte Geschosse mit
gleichem Grundriss verschneiden sich NICHT), und Zweier-Cluster ueber die
gemeinsame Gehrungslinie geschnitten. T-/X-Stoesse und freie Enden bleiben
stumpf, wie im Web-Kern. Endkappen-Normalen geometrisch aus dem Kreuzprodukt.
WallInput bekommt optional layers (WallLayer{thickness,color}, serde-default,
rueckwaertskompatibel): jede Schicht wird als eigenes Band ueber dieselbe
gehrte Grundflaeche extrudiert und materialgefaerbt. toWalls3d.ts loest die
WallType-Schichten je Wand auf (hexToRgb) und reicht sie durch.
39/39 Rust-Tests (7 neue: Gehrung, T-Stoss, freies Ende, Schichten, Kombi).
This commit is contained in:
@@ -31,6 +31,7 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
|||||||
sill: 0.8,
|
sill: 0.8,
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
}],
|
}],
|
||||||
|
layers: None,
|
||||||
},
|
},
|
||||||
WallInput {
|
WallInput {
|
||||||
start: [0.0, 3.0],
|
start: [0.0, 3.0],
|
||||||
@@ -40,6 +41,7 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
},
|
},
|
||||||
// "Spaeh"-Wand: kurzer Stummel HINTER Schenkel A (groesseres Modell-X,
|
// "Spaeh"-Wand: kurzer Stummel HINTER Schenkel A (groesseres Modell-X,
|
||||||
// also groessere Tiefe von der Betrachter-Ebene aus), genau im
|
// also groessere Tiefe von der Betrachter-Ebene aus), genau im
|
||||||
@@ -54,6 +56,7 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let slabs = vec![SlabInput {
|
let slabs = vec![SlabInput {
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ fn demo_walls() -> Vec<WallInput> {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: grey,
|
color: grey,
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
};
|
};
|
||||||
// Raum 6 x 4 m.
|
// Raum 6 x 4 m.
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -47,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, Opening, WallInput, FLOATS_PER_VERTEX};
|
use super::types::{Mesh, Opening, WallInput, WallLayer, 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 {
|
||||||
@@ -59,6 +59,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +96,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX);
|
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX);
|
||||||
@@ -172,6 +174,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
};
|
};
|
||||||
let mut mesh = Mesh::default();
|
let mut mesh = Mesh::default();
|
||||||
extrude_wall(&mut mesh, &w);
|
extrude_wall(&mut mesh, &w);
|
||||||
@@ -192,6 +195,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
};
|
};
|
||||||
let mesh = build_walls_mesh(&[w]);
|
let mesh = build_walls_mesh(&[w]);
|
||||||
assert_eq!(mesh.vertex_count(), 0);
|
assert_eq!(mesh.vertex_count(), 0);
|
||||||
@@ -295,6 +299,228 @@ mod tests {
|
|||||||
assert_eq!(mesh.indices.len(), INDICES_PER_BOX);
|
assert_eq!(mesh.indices.len(), INDICES_PER_BOX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Ecken-Verschneidung (Gehrung) -----------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rechtwinklige_ecke_wird_gehrt_kein_ueberstand_kein_spalt() {
|
||||||
|
// Zwei Waende entlang +X bzw. +Y, die sich exakt im Ursprung treffen
|
||||||
|
// (L-Ecke, Dicke 0.4 -> Halbdicke 0.2). Ohne Gehrung ueberlappen sich die
|
||||||
|
// Baender im Eckbereich (jede Wand ragt um die Halbdicke der jeweils
|
||||||
|
// anderen ueber den Knoten hinaus); MIT Gehrung muessen alle Vertices IM
|
||||||
|
// Eckbereich exakt auf der 45-Grad-Gehrungslinie |x| == |z| liegen —
|
||||||
|
// weder darueber hinaus (Ueberstand) noch darunter (Spalt).
|
||||||
|
let a = WallInput {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [3.0, 0.0],
|
||||||
|
thickness: 0.4,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
|
};
|
||||||
|
let b = WallInput {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [0.0, 3.0],
|
||||||
|
thickness: 0.4,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
|
};
|
||||||
|
let mesh = build_walls_mesh(&[a, b]);
|
||||||
|
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX, "weiterhin zwei Quader");
|
||||||
|
|
||||||
|
// Jeder Vertex nahe dem Knoten (innerhalb der doppelten Halbdicke) muss
|
||||||
|
// auf der Gehrungslinie liegen: world.x und world.z (== Grundriss-y)
|
||||||
|
// betragsgleich (45-Grad-Winkelhalbierende der beiden Achsen).
|
||||||
|
let near_corner_margin = 0.5; // deutlich groesser als die Halbdicke 0.2
|
||||||
|
let n = mesh.vertex_count();
|
||||||
|
let mut checked_any = false;
|
||||||
|
for i in 0..n {
|
||||||
|
let (p, _) = vert(&mesh, i);
|
||||||
|
if p[0].abs() < near_corner_margin && p[2].abs() < near_corner_margin {
|
||||||
|
checked_any = true;
|
||||||
|
assert!(
|
||||||
|
(p[0].abs() - p[2].abs()).abs() < 1e-4,
|
||||||
|
"Eck-Vertex {i} bei {p:?} liegt nicht auf der 45-Grad-Gehrungslinie"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(checked_any, "Test hat keinen Eck-Vertex gefunden (Setup falsch)");
|
||||||
|
|
||||||
|
// Kein Vertex ragt ueber die Gehrungslinie hinaus: fuer Punkte mit x>0
|
||||||
|
// UND z>0 (im "inneren" Quadranten der Ecke) muss min(x,z) <= Halbdicke
|
||||||
|
// gelten -- sonst wuerde die Wand ueber die andere hinausragen.
|
||||||
|
for i in 0..n {
|
||||||
|
let (p, _) = vert(&mesh, i);
|
||||||
|
if p[0] > 1e-4 && p[2] > 1e-4 && p[0] < near_corner_margin && p[2] < near_corner_margin {
|
||||||
|
assert!(
|
||||||
|
p[0].min(p[2]) <= 0.2 + 1e-4,
|
||||||
|
"Vertex {i} bei {p:?} ragt ueber die Gehrung hinaus"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn freies_wandende_bleibt_rechtwinklig() {
|
||||||
|
// Eine einzelne Wand ohne Nachbarn: keine Gehrung, Enden bleiben exakt
|
||||||
|
// senkrecht zur Achse -> identisch zum Einzel-Wand-Regressionsfall.
|
||||||
|
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||||||
|
let (min, max) = mesh.bounds();
|
||||||
|
assert!((min[0]).abs() < 1e-5 && (max[0] - 3.0).abs() < 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn t_stoss_bleibt_rechtwinklig() {
|
||||||
|
// Drei Wandenden treffen sich im selben Knoten (T-Stoss) -> gemaess
|
||||||
|
// Moduldoc/`joins.ts` bewusst NICHT gehrt (mehrdeutig), alle drei bleiben
|
||||||
|
// rechtwinklig. Regressions-Absicherung: keine Panik, stabile Zaehlung.
|
||||||
|
let mk = |s: [f32; 2], e: [f32; 2]| WallInput {
|
||||||
|
start: s,
|
||||||
|
end: e,
|
||||||
|
thickness: 0.2,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
|
};
|
||||||
|
let walls = vec![
|
||||||
|
mk([0.0, 0.0], [3.0, 0.0]),
|
||||||
|
mk([0.0, 0.0], [0.0, 3.0]),
|
||||||
|
mk([0.0, 0.0], [-3.0, 0.0]),
|
||||||
|
];
|
||||||
|
let mesh = build_walls_mesh(&walls);
|
||||||
|
assert_eq!(mesh.vertex_count(), 3 * VERTS_PER_BOX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zwei_geschosse_mit_gleichem_grundriss_verschneiden_sich_nicht_ueber_geschosse() {
|
||||||
|
// EG (Hoehe 0..2.5) und OG (Basis 2.5, Hoehe 2.5..5.0) mit identischem
|
||||||
|
// L-Grundriss: die Hoehenbereiche BERUEHREN sich nur (bei 2.5), ueber-
|
||||||
|
// lappen aber nicht -> jedes Geschoss wird NUR gegen sein eigenes
|
||||||
|
// Pendant gehrt, nicht ueber die Geschossgrenze hinweg. Absicherung:
|
||||||
|
// beide Geschosse zusammen liefern exakt dieselbe Gehrung wie ein
|
||||||
|
// einzelnes Geschoss fuer sich (Vertex-Zaehlung + Eck-Vertices je
|
||||||
|
// Geschoss weiterhin auf der 45-Grad-Linie relativ zu SEINER Basis).
|
||||||
|
let mk = |s: [f32; 2], e: [f32; 2], base: f32| WallInput {
|
||||||
|
start: s,
|
||||||
|
end: e,
|
||||||
|
thickness: 0.4,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: base,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
|
};
|
||||||
|
let walls = vec![
|
||||||
|
mk([0.0, 0.0], [3.0, 0.0], 0.0),
|
||||||
|
mk([0.0, 0.0], [0.0, 3.0], 0.0),
|
||||||
|
mk([0.0, 0.0], [3.0, 0.0], 2.5),
|
||||||
|
mk([0.0, 0.0], [0.0, 3.0], 2.5),
|
||||||
|
];
|
||||||
|
let mesh = build_walls_mesh(&walls);
|
||||||
|
assert_eq!(mesh.vertex_count(), 4 * VERTS_PER_BOX);
|
||||||
|
let n = mesh.vertex_count();
|
||||||
|
for i in 0..n {
|
||||||
|
let (p, _) = vert(&mesh, i);
|
||||||
|
if p[0].abs() < 0.5 && p[2].abs() < 0.5 {
|
||||||
|
assert!(
|
||||||
|
(p[0].abs() - p[2].abs()).abs() < 1e-4,
|
||||||
|
"Eck-Vertex {i} bei {p:?} (Geschoss-Basis {}) nicht auf der Gehrung",
|
||||||
|
p[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Schichten (WallInput::layers) -----------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ohne_layers_entsteht_weiterhin_ein_vollkoerper() {
|
||||||
|
// Regressionsfall: `layers: None` extrudiert exakt wie zuvor.
|
||||||
|
let mesh = build_walls_mesh(&[wall_x(3.0, 0.2, 2.5)]);
|
||||||
|
assert_eq!(mesh.vertex_count(), VERTS_PER_BOX);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_wand_liefert_ein_prisma_je_schicht() {
|
||||||
|
// Drei Schichten (Putz/Daemmung/Mauerwerk) -> drei eigenstaendige
|
||||||
|
// Quader, je VERTS_PER_BOX Vertices, ohne Oeffnungen.
|
||||||
|
let mut w = wall_x(3.0, 0.3, 2.5);
|
||||||
|
w.layers = Some(vec![
|
||||||
|
WallLayer { thickness: 0.02, color: [1.0, 1.0, 1.0] },
|
||||||
|
WallLayer { thickness: 0.18, color: [0.9, 0.85, 0.7] },
|
||||||
|
WallLayer { thickness: 0.10, color: [0.6, 0.3, 0.2] },
|
||||||
|
]);
|
||||||
|
let mesh = build_walls_mesh(&[w]);
|
||||||
|
assert_eq!(mesh.vertex_count(), 3 * VERTS_PER_BOX);
|
||||||
|
assert_eq!(mesh.indices.len(), 3 * INDICES_PER_BOX);
|
||||||
|
|
||||||
|
// Die Schichten decken zusammen exakt die Gesamtdicke (0.3, Z in
|
||||||
|
// -0.15..0.15) lueckenlos und ueberlappungsfrei ab.
|
||||||
|
let (min, max) = mesh.bounds();
|
||||||
|
assert!((min[2] + 0.15).abs() < 1e-4, "min z == -0.15");
|
||||||
|
assert!((max[2] - 0.15).abs() < 1e-4, "max z == 0.15");
|
||||||
|
|
||||||
|
// Jede Schicht-Farbe taucht in den Vertex-Farben auf (Layer wurden
|
||||||
|
// tatsaechlich mit ihrer eigenen Farbe extrudiert, nicht der Wandfarbe).
|
||||||
|
let n = mesh.vertex_count();
|
||||||
|
let has_color = |target: [f32; 3]| {
|
||||||
|
(0..n).any(|i| {
|
||||||
|
let b = i * FLOATS_PER_VERTEX;
|
||||||
|
let c = [mesh.verts[b + 6], mesh.verts[b + 7], mesh.verts[b + 8]];
|
||||||
|
(c[0] - target[0]).abs() < 1e-6
|
||||||
|
&& (c[1] - target[1]).abs() < 1e-6
|
||||||
|
&& (c[2] - target[2]).abs() < 1e-6
|
||||||
|
})
|
||||||
|
};
|
||||||
|
assert!(has_color([1.0, 1.0, 1.0]), "Putz-Farbe fehlt");
|
||||||
|
assert!(has_color([0.9, 0.85, 0.7]), "Daemmungs-Farbe fehlt");
|
||||||
|
assert!(has_color([0.6, 0.3, 0.2]), "Mauerwerk-Farbe fehlt");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn layer_ecke_wird_ebenfalls_gehrt() {
|
||||||
|
// Zwei mehrschichtige Waende an einer Ecke: JEDE Schicht nutzt dasselbe
|
||||||
|
// gehrte Fussabdruck-Band -> auch hier landen alle Eck-Vertices exakt
|
||||||
|
// auf der 45-Grad-Gehrungslinie (analog zum Vollkoerper-Test oben).
|
||||||
|
let layers = || {
|
||||||
|
Some(vec![
|
||||||
|
WallLayer { thickness: 0.15, color: [0.9, 0.85, 0.7] },
|
||||||
|
WallLayer { thickness: 0.15, color: [0.6, 0.3, 0.2] },
|
||||||
|
])
|
||||||
|
};
|
||||||
|
let a = WallInput {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [3.0, 0.0],
|
||||||
|
thickness: 0.3,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
|
layers: layers(),
|
||||||
|
};
|
||||||
|
let mut b = a.clone();
|
||||||
|
b.end = [0.0, 3.0];
|
||||||
|
|
||||||
|
let mesh = build_walls_mesh(&[a, b]);
|
||||||
|
let n = mesh.vertex_count();
|
||||||
|
assert_eq!(n, 4 * VERTS_PER_BOX, "2 Waende x 2 Schichten");
|
||||||
|
for i in 0..n {
|
||||||
|
let (p, _) = vert(&mesh, i);
|
||||||
|
if p[0].abs() < 0.4 && p[2].abs() < 0.4 {
|
||||||
|
assert!(
|
||||||
|
(p[0].abs() - p[2].abs()).abs() < 1e-4,
|
||||||
|
"Layer-Eck-Vertex {i} bei {p:?} nicht auf der Gehrung"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Deckenplatten (extrudierte Polygone) ---------------------------------
|
// --- Deckenplatten (extrudierte Polygone) ---------------------------------
|
||||||
|
|
||||||
use super::mesh::{build_model_mesh, extrude_slab, triangulate};
|
use super::mesh::{build_model_mesh, extrude_slab, triangulate};
|
||||||
|
|||||||
+356
-53
@@ -8,8 +8,32 @@
|
|||||||
// Diese 2D-Band-Ecken werden nach world abgebildet (XZ-Ebene) und entlang +Y von
|
// Diese 2D-Band-Ecken werden nach world abgebildet (XZ-Ebene) und entlang +Y von
|
||||||
// `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`).
|
||||||
// (`computeJoins`/`clippedBand`) sind ein spaeterer Milestone.
|
//
|
||||||
|
// ECKEN-VERSCHNEIDUNG (Gehrung): `compute_wall_miters` berechnet — analog zum
|
||||||
|
// Web-Pendant `src/model/joins.ts::computeJoins` + `geometry.ts::clippedBand` —
|
||||||
|
// fuer jede Wand die Gehrungslinien an ihren ECHTEN Achsenenden, rein aus der
|
||||||
|
// Wandkonnektivitaet (Endpunkte, die sich bei genau ZWEI, nicht-parallelen
|
||||||
|
// Waenden treffen). Anders als der Web-Pfad braucht das HIER keine eigenen
|
||||||
|
// Eingabefelder: die Berechnung laeuft direkt auf `WallInput` (Start/Ende/
|
||||||
|
// Dicke), robust auch gegenueber der Teilquader-Zerlegung an Oeffnungen (siehe
|
||||||
|
// unten) — zwei Teilstuecke DERSELBEN Wand haben stets dieselbe (parallele)
|
||||||
|
// Achsrichtung, ihre "Gehrung" waere kollinear und wird automatisch verworfen
|
||||||
|
// (siehe `miter_line`). Mehrgeschossige Modelle mit deckungsgleichem Grundriss
|
||||||
|
// 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).
|
||||||
|
//
|
||||||
|
// SCHICHTEN (`WallInput::layers`): mehrschichtige Waende (Putz/Daemmung/
|
||||||
|
// Mauerwerk/...) werden nicht als ein Vollkoerper, sondern als je EIN Prisma
|
||||||
|
// pro Schicht extrudiert, quer zur Gesamtdicke gestapelt (gleiche Idee wie
|
||||||
|
// Viewport3D.tsx `addWallMeshes`: `off` laeuft ueber die Schichtdicken).
|
||||||
|
// Jede Schicht nutzt DASSELBE gehrte Fussabdruck-Band wie Gap 1 (nur mit ihrem
|
||||||
|
// eigenen Dicken-Versatz), sodass auch mehrschichtige Waende an Ecken saeuberlich
|
||||||
|
// stossen. Fehlt `layers` (None/leer), wird ein synthetisches Einzel-Layer aus
|
||||||
|
// `thickness`/`color` gebildet — bitgenau dasselbe Ergebnis wie zuvor.
|
||||||
//
|
//
|
||||||
// OEFFNUNGEN (Tueren/Fenster, `WallInput::openings`): eine Wand mit Oeffnungen
|
// OEFFNUNGEN (Tueren/Fenster, `WallInput::openings`): eine Wand mit Oeffnungen
|
||||||
// wird NICHT als ein Vollkoerper-Quader extrudiert, sondern in mehrere
|
// wird NICHT als ein Vollkoerper-Quader extrudiert, sondern in mehrere
|
||||||
@@ -19,14 +43,19 @@
|
|||||||
// fuer die Schnitt-Cut-Polygone (gemeinsame Basis: `crate::openings::
|
// fuer die Schnitt-Cut-Polygone (gemeinsame Basis: `crate::openings::
|
||||||
// split_range_by_voids`), nur direkt in Wandachsen-Metern statt in der
|
// split_range_by_voids`), nur direkt in Wandachsen-Metern statt in der
|
||||||
// Projektions-u-Achse einer Schnittebene — damit zeigt die 3D-Vollansicht
|
// Projektions-u-Achse einer Schnittebene — damit zeigt die 3D-Vollansicht
|
||||||
// (`?engine=wasm`) dieselbe Oeffnung wie der Schnitt.
|
// (`?engine=wasm`) dieselbe Oeffnung wie der Schnitt. Die Gehrung (siehe oben)
|
||||||
|
// greift dabei NUR an Teilstuecken, die tatsaechlich an ein echtes Wandende
|
||||||
|
// grenzen (Achsposition 0 bzw. Wandlaenge) — innere Teilstuecke (Laibungen an
|
||||||
|
// Oeffnungen) bleiben immer rechtwinklig.
|
||||||
//
|
//
|
||||||
// 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 std::collections::HashMap;
|
||||||
|
|
||||||
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
use crate::openings::{split_range_by_voids, MIN_SPAN};
|
||||||
use crate::types::{Mesh, Opening, Point2, Rgb, SlabInput, WallInput, FLOATS_PER_VERTEX};
|
use crate::types::{Mesh, Opening, Point2, Rgb, SlabInput, WallInput, WallLayer, 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,
|
||||||
@@ -46,13 +75,24 @@ fn left_normal(start: Point2, end: Point2) -> Point2 {
|
|||||||
[-dy / len, dx / len]
|
[-dy / len, dx / len]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrudiert EINE Wand zu einem oder mehreren Quader-Meshes und haengt sie an
|
/// Extrudiert EINE Wand (ohne Kenntnis von Nachbarn, also stets rechtwinklige
|
||||||
/// `mesh` an. Ohne Oeffnungen (Regressionsfall) entsteht wie zuvor GENAU ein
|
/// Enden) zu einem oder mehreren Quader-Meshes. Fuer Szenen mit mehreren
|
||||||
/// Quader (`VERTS_PER_BOX`/`INDICES_PER_BOX`); mit Oeffnungen wird die Wand
|
/// Waenden bitte `build_walls_mesh`/`build_model_mesh` nutzen — die berechnen
|
||||||
/// zuerst in Achsen-Teilstuecke (Pfeiler/Bruestung/Sturz) zerlegt und jedes
|
/// zuerst die Eck-Gehrung ueber alle Waende (siehe `compute_wall_miters`) und
|
||||||
/// Teilstueck einzeln als Quader extrudiert (siehe `wall_solid_segments` und
|
/// wenden sie hier mit an. Diese Funktion bleibt der einfache Einzel-Wand-
|
||||||
/// die Oeffnungen-Erklaerung im Moduldoc oben).
|
/// Einstieg (u. a. fuer bestehende Tests) und entspricht exakt dem Aufruf mit
|
||||||
|
/// leeren Gehrungen.
|
||||||
pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
||||||
|
extrude_wall_with_miters(mesh, wall, WallMiters::default());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wie `extrude_wall`, aber mit den (ggf. leeren) Gehrungslinien an den
|
||||||
|
/// echten Wandenden. Ohne Oeffnungen und ohne `layers` entsteht wie zuvor
|
||||||
|
/// GENAU ein Quader je Wand (`VERTS_PER_BOX`/`INDICES_PER_BOX`); mit
|
||||||
|
/// Oeffnungen wird die Wand zuerst in Achsen-Teilstuecke (Pfeiler/Bruestung/
|
||||||
|
/// Sturz) zerlegt, mit `layers` zusaetzlich in Schicht-Baender quer zur
|
||||||
|
/// Dicke — siehe Moduldoc.
|
||||||
|
fn extrude_wall_with_miters(mesh: &mut Mesh, wall: &WallInput, miters: WallMiters) {
|
||||||
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] {
|
||||||
@@ -64,8 +104,42 @@ pub fn extrude_wall(mesh: &mut Mesh, wall: &WallInput) {
|
|||||||
|
|
||||||
let z0 = wall.base_elevation;
|
let z0 = wall.base_elevation;
|
||||||
let z1 = wall.base_elevation + wall.height;
|
let z1 = wall.base_elevation + wall.height;
|
||||||
for (a0, a1, y0, y1) in wall_solid_segments(length, &wall.openings, z0, z1) {
|
let segments = wall_solid_segments(length, &wall.openings, z0, z1);
|
||||||
extrude_wall_segment(mesh, wall, n, length, a0, a1, y0, y1);
|
|
||||||
|
// Schicht-Stapel: ohne `layers` (oder leer) EIN synthetisches Layer aus
|
||||||
|
// der Gesamtdicke/-Farbe — bitgenau das bisherige Vollkoerper-Verhalten.
|
||||||
|
let synthetic;
|
||||||
|
let layers: &[WallLayer] = match &wall.layers {
|
||||||
|
Some(ls) if !ls.is_empty() => ls.as_slice(),
|
||||||
|
_ => {
|
||||||
|
synthetic = [WallLayer {
|
||||||
|
thickness: wall.thickness,
|
||||||
|
color: wall.color,
|
||||||
|
}];
|
||||||
|
&synthetic
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Schichten von der +n-Seite (offA = +Gesamtdicke/2) zur -n-Seite stapeln,
|
||||||
|
// damit offA stets > offB gilt — exakt die Konvention, unter der die
|
||||||
|
// bisherige Ecken-/Flankennormalen-Herleitung (siehe `extrude_layer_segment`)
|
||||||
|
// gilt. Fuer das synthetische Einzel-Layer ergibt sich offA=+h, offB=-h wie
|
||||||
|
// zuvor.
|
||||||
|
let total: f32 = layers.iter().map(|l| l.thickness).sum();
|
||||||
|
let mut off_a = total * 0.5;
|
||||||
|
for layer in layers {
|
||||||
|
let off_b = off_a - layer.thickness;
|
||||||
|
for &(a0, a1, y0, y1) in &segments {
|
||||||
|
// Gehrung nur an Teilstuecken, die tatsaechlich an ein echtes
|
||||||
|
// 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 };
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,16 +183,28 @@ fn axis_point(wall: &WallInput, length: f32, a: f32) -> Point2 {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extrudiert EIN Achsen-Teilstueck `[a0,a1]` (Hoehe `[y0,y1]`) einer Wand zu
|
/// Extrudiert EIN Schicht-Achsen-Teilstueck (Achse `[a0,a1]`, Hoehe `[y0,y1]`,
|
||||||
|
/// Dicken-Versatz `[off_a,off_b]` gegen die Achse entlang `n`) einer Wand zu
|
||||||
/// einem Quader-Mesh und haengt ihn an `mesh` an. Fuer `a0=0, a1=length,
|
/// 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
|
/// y0=base_elevation, y1=base_elevation+height, off_a=+thickness/2,
|
||||||
/// exakt die bisherige Vollkoerper-Extrusion.
|
/// off_b=-thickness/2` und ohne Gehrung (`start_cut`/`end_cut` beide `None`)
|
||||||
|
/// ist das exakt die bisherige Vollkoerper-Extrusion (Regressionsfall).
|
||||||
|
///
|
||||||
|
/// GEHRUNG: ist `start_cut`/`end_cut` gesetzt, werden die jeweiligen Band-Ecken
|
||||||
|
/// NICHT rechtwinklig bei `a0`/`a1` gekappt, sondern mit der Gehrungslinie
|
||||||
|
/// verschnitten (analog `clippedBand` im Web) — siehe `compute_wall_miters`.
|
||||||
|
/// Die beiden End-Kappenflaechen bleiben dabei planar (siehe Beweis im
|
||||||
|
/// Moduldoc von `compute_wall_miters`), ihre Normale wird deshalb geometrisch
|
||||||
|
/// aus den tatsaechlichen Kantenvektoren hergeleitet (`quad_normal`) statt aus
|
||||||
|
/// der festen Achsrichtung — das liefert im rechtwinkligen Fall exakt dieselbe
|
||||||
|
/// Normale wie zuvor UND bleibt bei einer schraegen Gehrung korrekt.
|
||||||
///
|
///
|
||||||
/// Aufbau: die vier Grundriss-Ecken des Bandes (CCW) bilden das Deckel-Rechteck.
|
/// 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
|
/// Boden = gleiche XZ auf `y0`, Deckel = auf `y1`. Jede der sechs Seiten bekommt
|
||||||
/// eigene Vertices mit ihrer (nach aussen zeigenden) Flaechen-Normale — daher
|
/// eigene Vertices mit ihrer (nach aussen zeigenden) Flaechen-Normale — daher
|
||||||
/// flaches Shading und korrektes Backface-Culling.
|
/// flaches Shading und korrektes Backface-Culling.
|
||||||
fn extrude_wall_segment(
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn extrude_layer_segment(
|
||||||
mesh: &mut Mesh,
|
mesh: &mut Mesh,
|
||||||
wall: &WallInput,
|
wall: &WallInput,
|
||||||
n: Point2,
|
n: Point2,
|
||||||
@@ -127,21 +213,35 @@ fn extrude_wall_segment(
|
|||||||
a1: f32,
|
a1: f32,
|
||||||
y0: f32,
|
y0: f32,
|
||||||
y1: f32,
|
y1: f32,
|
||||||
|
off_a: f32,
|
||||||
|
off_b: f32,
|
||||||
|
start_cut: Option<MiterLine>,
|
||||||
|
end_cut: Option<MiterLine>,
|
||||||
|
color: Rgb,
|
||||||
) {
|
) {
|
||||||
let h = wall.thickness * 0.5;
|
|
||||||
|
|
||||||
// Vier Band-Ecken im Grundriss (CCW, wie `wallCorners(start,end,thickness)`),
|
|
||||||
// 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 p0 = axis_point(wall, length, a0);
|
let p0 = axis_point(wall, length, a0);
|
||||||
let p1 = axis_point(wall, length, a1);
|
let p1 = axis_point(wall, length, a1);
|
||||||
let g0 = off(p0, 1.0);
|
|
||||||
let g1 = off(p1, 1.0);
|
|
||||||
let g2 = off(p1, -1.0);
|
|
||||||
let g3 = off(p0, -1.0);
|
|
||||||
|
|
||||||
let color = wall.color;
|
// Achsenrichtung der GESAMTEN Wand (fuer Gehrungs-Schnitt und Kappen) —
|
||||||
|
// konstant ueber alle Teilstuecke, unabhaengig von `a0`/`a1`.
|
||||||
|
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];
|
||||||
|
|
||||||
|
// Band-Eckpunkte, ggf. an der Gehrungslinie verschnitten statt rechtwinklig
|
||||||
|
// bei a0/a1 gekappt (siehe Moduldoc oben / `clippedBand` im Web).
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let g0 = clip(off(p0, off_a), start_cut);
|
||||||
|
let g1 = clip(off(p1, off_a), end_cut);
|
||||||
|
let g2 = clip(off(p1, off_b), end_cut);
|
||||||
|
let g3 = clip(off(p0, off_b), start_cut);
|
||||||
|
|
||||||
// world-Position aus Grundriss-XZ + Hoehe: (x, y, z=grundriss.y).
|
// world-Position aus Grundriss-XZ + Hoehe: (x, y, z=grundriss.y).
|
||||||
let w = |g: Point2, y: f32| -> [f32; 3] { [g[0], y, g[1]] };
|
let w = |g: Point2, y: f32| -> [f32; 3] { [g[0], y, g[1]] };
|
||||||
@@ -162,30 +262,230 @@ fn extrude_wall_segment(
|
|||||||
// Boden unten (-Y): b0,b3,b2,b1 (von unten gesehen CCW).
|
// Boden unten (-Y): b0,b3,b2,b1 (von unten gesehen CCW).
|
||||||
push_quad(mesh, b0, b3, b2, b1, [0.0, -1.0, 0.0], color);
|
push_quad(mesh, b0, b3, b2, b1, [0.0, -1.0, 0.0], color);
|
||||||
|
|
||||||
// Vier Mantelflaechen. Aussen-Normalen aus dem Grundriss:
|
// Zwei Mantelflaechen (+n-/−n-Seite): bleiben bei Gehrung EXAKT planar mit
|
||||||
// Kante g0->g1 liegt auf der +n-Seite -> Normale +n.
|
// fester Normale, weil sich die Ecken nur entlang `u` verschieben (siehe
|
||||||
// Kante g2->g3 liegt auf der -n-Seite -> Normale -n.
|
// Moduldoc-Beweis) — der `off_a`/`off_b`-Abstand zur Achse bleibt konstant.
|
||||||
// Kappen an a0 (g3->g0) und a1 (g1->g2) liegen an den Achsen-Enden des
|
let np = [n[0], 0.0, n[1]];
|
||||||
// Teilstuecks (bei einem inneren Teilstueck also am Rand der Oeffnung
|
let nn = [-n[0], 0.0, -n[1]];
|
||||||
// -> Leibungsflaeche/Reveal statt Wandende).
|
|
||||||
let np = [n[0], 0.0, n[1]]; // +n-Seite (world)
|
|
||||||
let nn = [-n[0], 0.0, -n[1]]; // -n-Seite
|
|
||||||
// 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 uz = wall.end[1] - wall.start[1];
|
|
||||||
let ul = (ux * ux + uz * uz).sqrt().max(1e-9);
|
|
||||||
let u = [ux / ul, 0.0, uz / ul];
|
|
||||||
let neg_u = [-u[0], 0.0, -u[2]];
|
|
||||||
|
|
||||||
// +n-Wand: aussen betrachtet CCW = b0, b1, t1, t0.
|
|
||||||
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.
|
|
||||||
push_quad(mesh, b2, b3, t3, t2, nn, color);
|
push_quad(mesh, b2, b3, t3, t2, nn, color);
|
||||||
// End-Kappe bei a1, Normale +u: b1, b2, t2, t1.
|
|
||||||
push_quad(mesh, b1, b2, t2, t1, u, color);
|
// End-Kappen bei a1/a0: Normale geometrisch hergeleitet (siehe Moduldoc) —
|
||||||
// End-Kappe bei a0, Normale -u: b3, b0, t0, t3.
|
// liefert im rechtwinkligen Fall exakt +u/-u, bei Gehrung die tatsaechliche
|
||||||
push_quad(mesh, b3, b0, t0, t3, neg_u, color);
|
// (schraege) Kappen-Normale.
|
||||||
|
let end_normal = quad_normal(b1, b2, t1);
|
||||||
|
let start_normal = quad_normal(b3, b0, t3);
|
||||||
|
push_quad(mesh, b1, b2, t2, t1, end_normal, color);
|
||||||
|
push_quad(mesh, b3, b0, t0, t3, start_normal, 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
|
||||||
|
/// Moduldoc von `compute_wall_miters`. Degenerierte (nulllange) Flaechen liefern
|
||||||
|
/// den Nullvektor statt NaN (Winkel spielt dort keine Rolle, siehe Aufrufer).
|
||||||
|
fn quad_normal(a: [f32; 3], b: [f32; 3], d: [f32; 3]) -> [f32; 3] {
|
||||||
|
let ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
||||||
|
let ad = [d[0] - a[0], d[1] - a[1], d[2] - a[2]];
|
||||||
|
let c = [
|
||||||
|
ab[1] * ad[2] - ab[2] * ad[1],
|
||||||
|
ab[2] * ad[0] - ab[0] * ad[2],
|
||||||
|
ab[0] * ad[1] - ab[1] * ad[0],
|
||||||
|
];
|
||||||
|
let len = (c[0] * c[0] + c[1] * c[1] + c[2] * c[2]).sqrt().max(1e-9);
|
||||||
|
[c[0] / len, c[1] / len, c[2] / len]
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
// Ecken-Verschneidung (Gehrung): Wandkonnektivitaet -> Gehrungslinien je Wandende
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Eine Gehrungslinie (Punkt + Richtung, Modell-Meter) an einem Wandende.
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
struct MiterLine {
|
||||||
|
point: Point2,
|
||||||
|
dir: Point2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Die (ggf. fehlenden) Gehrungslinien EINER Wand an Start/Ende.
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
struct WallMiters {
|
||||||
|
start: Option<MiterLine>,
|
||||||
|
end: Option<MiterLine>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schnittpunkt der Geraden `a + t*da` mit `b + s*db` (2D). `None` bei
|
||||||
|
/// (nahezu) parallelen Richtungen — 2D-Pendant zu `math::cross`/`lineIntersect`
|
||||||
|
/// im Web (`src/model/geometry.ts`).
|
||||||
|
fn line_intersect_2(a: Point2, da: Point2, b: Point2, db: Point2) -> Option<Point2> {
|
||||||
|
let denom = da[0] * db[1] - da[1] * db[0];
|
||||||
|
if denom.abs() < 1e-9 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let bx = b[0] - a[0];
|
||||||
|
let by = b[1] - a[1];
|
||||||
|
let t = (bx * db[1] - by * db[0]) / denom;
|
||||||
|
Some([a[0] + t * da[0], a[1] + t * da[1]])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Rundungsschluessel eines 2D-Punkts (0.1 mm), zur robusten Gruppierung von
|
||||||
|
/// Wandenden am selben Knoten — 1:1 das Muster von `joins.ts::roundKey`.
|
||||||
|
fn round_key(p: Point2) -> (i64, i64) {
|
||||||
|
(
|
||||||
|
(p[0] * 1e4).round() as i64,
|
||||||
|
(p[1] * 1e4).round() as i64,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ein Wandende, wie es fuer die Knoten-Gruppierung gebraucht wird.
|
||||||
|
struct EndRef {
|
||||||
|
wall: usize,
|
||||||
|
is_start: bool,
|
||||||
|
point: Point2,
|
||||||
|
/// Normalisierte Achsenrichtung Start->Ende der GESAMTEN Wand (konstant,
|
||||||
|
/// unabhaengig davon, ob es sich um das Start- oder Endende handelt).
|
||||||
|
dir: Point2,
|
||||||
|
z0: f32,
|
||||||
|
z1: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gruppiert `members` (Indizes in `ends`) nach echt ueberlappendem
|
||||||
|
/// Hoehenbereich (Union-Find) — trennt uebereinanderliegende Geschosse mit
|
||||||
|
/// deckungsgleichem Grundriss (deren Hoehenbereiche sich hoechstens BERUEHREN,
|
||||||
|
/// nicht ueberlappen) von echten Knoten INNERHALB eines Geschosses (deren
|
||||||
|
/// Hoehenbereiche sich ueberschneiden, auch wenn Oeffnungen ein Teilstueck auf
|
||||||
|
/// Bruestung/Sturz reduzieren).
|
||||||
|
fn cluster_by_height_overlap(ends: &[EndRef], members: &[usize]) -> Vec<Vec<usize>> {
|
||||||
|
let n = members.len();
|
||||||
|
let mut parent: Vec<usize> = (0..n).collect();
|
||||||
|
fn find(parent: &mut [usize], x: usize) -> usize {
|
||||||
|
if parent[x] != x {
|
||||||
|
let root = find(parent, parent[x]);
|
||||||
|
parent[x] = root;
|
||||||
|
}
|
||||||
|
parent[x]
|
||||||
|
}
|
||||||
|
const OVERLAP_EPS: f32 = 1e-4;
|
||||||
|
for i in 0..n {
|
||||||
|
for j in (i + 1)..n {
|
||||||
|
let a = &ends[members[i]];
|
||||||
|
let b = &ends[members[j]];
|
||||||
|
let lo = a.z0.max(b.z0);
|
||||||
|
let hi = a.z1.min(b.z1);
|
||||||
|
if hi - lo > OVERLAP_EPS {
|
||||||
|
let ri = find(&mut parent, i);
|
||||||
|
let rj = find(&mut parent, j);
|
||||||
|
if ri != rj {
|
||||||
|
parent[ri] = rj;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let mut clusters: HashMap<usize, Vec<usize>> = HashMap::new();
|
||||||
|
for i in 0..n {
|
||||||
|
let r = find(&mut parent, i);
|
||||||
|
clusters.entry(r).or_default().push(members[i]);
|
||||||
|
}
|
||||||
|
clusters.into_values().collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gemeinsame Gehrungslinie zweier Wandenden `a`, `b`, die sich im selben
|
||||||
|
/// Knoten treffen — 1:1-Port von `joins.ts::miterLine` (Referenzlinie "center",
|
||||||
|
/// da `WallInput` keine `referenceLine`-Information mitfuehrt): `a`s linke
|
||||||
|
/// Flaeche wird mit der naeher gelegenen Flaeche von `b` verschnitten, `a`s
|
||||||
|
/// rechte mit der jeweils anderen. Liefert `None` bei (nahezu) kollinearen
|
||||||
|
/// Richtungen — das deckt insbesondere zwei Teilstuecke DERSELBEN Wand ab
|
||||||
|
/// (identische Richtung, siehe Moduldoc).
|
||||||
|
fn miter_line(walls: &[WallInput], a: &EndRef, b: &EndRef) -> Option<MiterLine> {
|
||||||
|
let j = a.point;
|
||||||
|
let t_a = walls[a.wall].thickness;
|
||||||
|
let t_b = walls[b.wall].thickness;
|
||||||
|
let n_a: Point2 = [-a.dir[1], a.dir[0]];
|
||||||
|
let n_b: Point2 = [-b.dir[1], b.dir[0]];
|
||||||
|
|
||||||
|
let add_scaled = |p: Point2, v: Point2, s: f32| -> Point2 { [p[0] + v[0] * s, p[1] + v[1] * s] };
|
||||||
|
let p_la = add_scaled(j, n_a, t_a * 0.5);
|
||||||
|
let p_ra = add_scaled(j, n_a, -t_a * 0.5);
|
||||||
|
let p_lb = add_scaled(j, n_b, t_b * 0.5);
|
||||||
|
let p_rb = add_scaled(j, n_b, -t_b * 0.5);
|
||||||
|
|
||||||
|
let dist2 = |p: Point2, q: Point2| {
|
||||||
|
let dx = p[0] - q[0];
|
||||||
|
let dy = p[1] - q[1];
|
||||||
|
dx * dx + dy * dy
|
||||||
|
};
|
||||||
|
let lb_closer = dist2(p_la, p_lb) <= dist2(p_la, p_rb);
|
||||||
|
let b_for_left = if lb_closer { p_lb } else { p_rb };
|
||||||
|
let b_for_right = if lb_closer { p_rb } else { p_lb };
|
||||||
|
|
||||||
|
let c1 = line_intersect_2(p_la, a.dir, b_for_left, b.dir)?;
|
||||||
|
let c2 = line_intersect_2(p_ra, a.dir, b_for_right, b.dir)?;
|
||||||
|
let dir = [c2[0] - c1[0], c2[1] - c1[1]];
|
||||||
|
if (dir[0] * dir[0] + dir[1] * dir[1]).sqrt() < 1e-9 {
|
||||||
|
return None; // kollinear -> kein Schnitt
|
||||||
|
}
|
||||||
|
Some(MiterLine { point: c1, dir })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_cut(out: &mut [WallMiters], e: &EndRef, cut: MiterLine) {
|
||||||
|
if e.is_start {
|
||||||
|
out[e.wall].start = Some(cut);
|
||||||
|
} else {
|
||||||
|
out[e.wall].end = Some(cut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Berechnet fuer JEDE Wand in `walls` die Gehrungslinien an ihren echten
|
||||||
|
/// Achsenenden — reine Funktion der Wandkonnektivitaet (Endpunkte + Dicke +
|
||||||
|
/// Hoehenbereich), siehe Moduldoc "Ecken-Verschneidung". Nur Knoten, an denen
|
||||||
|
/// sich (innerhalb desselben Hoehenbereichs) GENAU zwei, nicht-kollineare
|
||||||
|
/// Wandenden treffen, werden gehrt; freie Enden, T-/X-Stoesse und mehrdeutige
|
||||||
|
/// Knoten (z. B. eine Oeffnung direkt an einer echten Ecke, die das Ende in
|
||||||
|
/// mehrere Hoehen-Teilstuecke zerlegt) bleiben konservativ rechtwinklig.
|
||||||
|
fn compute_wall_miters(walls: &[WallInput]) -> Vec<WallMiters> {
|
||||||
|
let mut out = vec![WallMiters::default(); walls.len()];
|
||||||
|
|
||||||
|
let mut ends: Vec<EndRef> = Vec::with_capacity(walls.len() * 2);
|
||||||
|
for (i, w) in walls.iter().enumerate() {
|
||||||
|
let n = left_normal(w.start, w.end);
|
||||||
|
if n == [0.0, 0.0] {
|
||||||
|
continue; // entartete Wand, siehe extrude_wall
|
||||||
|
}
|
||||||
|
// Achsenrichtung aus der Links-Normale zurueckgedreht (90 Grad):
|
||||||
|
// n = (-dir.y, dir.x) => dir = (n.y, -n.x). Siehe section.rs::wall_prism.
|
||||||
|
let dir: Point2 = [n[1], -n[0]];
|
||||||
|
let z0 = w.base_elevation;
|
||||||
|
let z1 = w.base_elevation + w.height;
|
||||||
|
ends.push(EndRef { wall: i, is_start: true, point: w.start, dir, z0, z1 });
|
||||||
|
ends.push(EndRef { wall: i, is_start: false, point: w.end, dir, z0, z1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut groups: HashMap<(i64, i64), Vec<usize>> = HashMap::new();
|
||||||
|
for (idx, e) in ends.iter().enumerate() {
|
||||||
|
groups.entry(round_key(e.point)).or_default().push(idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
for members in groups.into_values() {
|
||||||
|
if members.len() < 2 {
|
||||||
|
continue; // freies Ende
|
||||||
|
}
|
||||||
|
for cluster in cluster_by_height_overlap(&ends, &members) {
|
||||||
|
if cluster.len() != 2 {
|
||||||
|
continue; // T-/X-Stoss oder mehrdeutig -> rechtwinklig
|
||||||
|
}
|
||||||
|
let (ia, ib) = (cluster[0], cluster[1]);
|
||||||
|
let (a, b) = (&ends[ia], &ends[ib]);
|
||||||
|
if a.wall == b.wall {
|
||||||
|
continue; // eine Wand kann sich nicht selbst verschneiden
|
||||||
|
}
|
||||||
|
let Some(cut) = miter_line(walls, a, b) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
apply_cut(&mut out, a, cut);
|
||||||
|
apply_cut(&mut out, b, cut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
@@ -210,12 +510,15 @@ fn push_quad(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Baut ein einzelnes Mesh aus mehreren Waenden (alle in EINEN Puffer). Die
|
/// Baut ein einzelnes Mesh aus mehreren Waenden (alle in EINEN Puffer). Die
|
||||||
/// Reihenfolge der Vertices ist deterministisch (je Wand ein Quader), damit die
|
/// Reihenfolge der Vertices ist deterministisch (je Wand ein Quader, bzw. bei
|
||||||
/// Tests stabile Zaehlungen erwarten koennen.
|
/// `layers`/Oeffnungen mehrere), damit die Tests stabile Zaehlungen erwarten
|
||||||
|
/// koennen. Berechnet zuerst die Eck-Gehrung ueber ALLE Waende (siehe
|
||||||
|
/// `compute_wall_miters`) und extrudiert dann jede Wand mit ihren Gehrungen.
|
||||||
pub fn build_walls_mesh(walls: &[WallInput]) -> Mesh {
|
pub fn build_walls_mesh(walls: &[WallInput]) -> Mesh {
|
||||||
let mut mesh = Mesh::default();
|
let mut mesh = Mesh::default();
|
||||||
for w in walls {
|
let miters = compute_wall_miters(walls);
|
||||||
extrude_wall(&mut mesh, w);
|
for (w, m) in walls.iter().zip(miters.iter()) {
|
||||||
|
extrude_wall_with_miters(&mut mesh, w, *m);
|
||||||
}
|
}
|
||||||
mesh
|
mesh
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,12 +101,19 @@
|
|||||||
//! (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): Waende werden wie in
|
//! - Keine Wandknoten-Verschneidung (T-/X-Stoesse): DIESES Modul behandelt
|
||||||
//! `mesh.rs` als eigenstaendige, stumpf abgeschlossene Quader behandelt, die
|
//! Waende weiterhin als eigenstaendige, stumpf abgeschlossene Quader, die
|
||||||
//! sich an Ecken UEBERLAPPEN (kein Miter-Join). Das kann in Ecknaehe dazu
|
//! sich an Ecken UEBERLAPPEN (kein Miter-Join) — sowohl fuer die Cut-Polygone
|
||||||
//! fuehren, dass eine Wand als "in eine andere eingebettet" verdeckt erscheint
|
//! als auch fuer die Verdeckungs-Bounding-Box. `mesh.rs` (die Vollkoerper-
|
||||||
//! (siehe Test `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real,
|
//! Extrusion fuer die 3D-Ansicht) berechnet die Eck-Gehrung inzwischen SELBST
|
||||||
//! aber ein Artefakt der fehlenden Verschneidungslogik, kein Bug dieses Moduls.
|
//! (`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.
|
||||||
//! - 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::
|
||||||
@@ -984,6 +991,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
},
|
},
|
||||||
WallInput {
|
WallInput {
|
||||||
start: [0.0, 3.0],
|
start: [0.0, 3.0],
|
||||||
@@ -993,6 +1001,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let slabs = vec![SlabInput {
|
let slabs = vec![SlabInput {
|
||||||
@@ -1208,6 +1217,7 @@ mod tests {
|
|||||||
sill: 1.0,
|
sill: 1.0,
|
||||||
height: 1.0,
|
height: 1.0,
|
||||||
}],
|
}],
|
||||||
|
layers: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1292,6 +1302,7 @@ mod tests {
|
|||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
openings: vec![],
|
openings: vec![],
|
||||||
|
layers: None,
|
||||||
};
|
};
|
||||||
// Blick von y = -2 nach +Y: beide Waende (y=0 bzw. y=1) liegen dahinter.
|
// Blick von y = -2 nach +Y: beide Waende (y=0 bzw. y=1) liegen dahinter.
|
||||||
let plane = SectionPlane::looking_plus_y(-2.0);
|
let plane = SectionPlane::looking_plus_y(-2.0);
|
||||||
|
|||||||
@@ -25,13 +25,17 @@ pub type Rgb = [f32; 3];
|
|||||||
/// erzeugt (Band aus Achse+Dicke, hochgezogen auf `height` ab `base_elevation`).
|
/// erzeugt (Band aus Achse+Dicke, hochgezogen auf `height` ab `base_elevation`).
|
||||||
///
|
///
|
||||||
/// Vereinfachung des Web-`Wall` (siehe src/model/wall.ts): Ecken-Verschneidung
|
/// Vereinfachung des Web-`Wall` (siehe src/model/wall.ts): Ecken-Verschneidung
|
||||||
/// (`computeJoins`/`wallCorners`) kommt in einem spaeteren Milestone; M1 extrudiert
|
/// (Gehrung an echten Wandenden, analog `computeJoins`/`wallCorners` im Web)
|
||||||
/// jede Wand als eigenstaendigen, an den Enden stumpf abgeschlossenen Quader.
|
/// wird von `mesh.rs` SELBSTAENDIG aus der Wandkonnektivitaet berechnet (siehe
|
||||||
|
/// `mesh::compute_wall_miters`) — keine eigenen Eingabefelder noetig, robust
|
||||||
|
/// auch gegenueber der hier bereits in Teilquader zerlegten Darstellung (siehe
|
||||||
|
/// `openings` unten). Das Schnitt-Modul (`section.rs`) nutzt weiterhin die
|
||||||
|
/// (unveraenderte) achsparallele Bounding-Box-Naeherung ohne Gehrung — siehe
|
||||||
|
/// dessen Moduldoc "Bekannte Vereinfachungen".
|
||||||
/// `openings` (Tueren/Fenster, siehe `Opening`) werden vom Schnitt-Modul
|
/// `openings` (Tueren/Fenster, siehe `Opening`) werden vom Schnitt-Modul
|
||||||
/// (`section.rs`) ausgewertet; die Vollkoerper-Extrusion (`mesh.rs`) kennt sie
|
/// (`section.rs`) UND von der Vollkoerper-Extrusion (`mesh.rs`) ausgewertet
|
||||||
/// (noch) NICHT und extrudiert weiterhin die volle Wandflaeche — siehe
|
/// (siehe `mesh::wall_solid_segments`) — beide nutzen dieselbe Bruestungs-/
|
||||||
/// `mesh.rs`-Moduldoc und `docs/design/engine-section-pipeline.md` fuer diese
|
/// Sturz-Zerlegung (`crate::openings`).
|
||||||
/// bekannte Diskrepanz.
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct WallInput {
|
pub struct WallInput {
|
||||||
/// Achsen-Startpunkt im Grundriss (Modell-Meter).
|
/// Achsen-Startpunkt im Grundriss (Modell-Meter).
|
||||||
@@ -39,6 +43,9 @@ pub struct WallInput {
|
|||||||
/// Achsen-Endpunkt im Grundriss (Modell-Meter).
|
/// Achsen-Endpunkt im Grundriss (Modell-Meter).
|
||||||
pub end: Point2,
|
pub end: Point2,
|
||||||
/// Wanddicke in Metern (das Band wird um `thickness/2` je Seite versetzt).
|
/// Wanddicke in Metern (das Band wird um `thickness/2` je Seite versetzt).
|
||||||
|
/// Bleibt auch bei gesetztem `layers` die GESAMT-Dicke (Summe der
|
||||||
|
/// Schichtdicken) — `section.rs`/`web::model_bounds` rechnen weiterhin
|
||||||
|
/// allein mit diesem Feld, kennen `layers` nicht.
|
||||||
pub thickness: f32,
|
pub thickness: f32,
|
||||||
/// Wandhoehe in Metern (Extrusion entlang +Y).
|
/// Wandhoehe in Metern (Extrusion entlang +Y).
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
@@ -46,11 +53,28 @@ pub struct WallInput {
|
|||||||
#[serde(default, rename = "baseElevation")]
|
#[serde(default, rename = "baseElevation")]
|
||||||
pub base_elevation: f32,
|
pub base_elevation: f32,
|
||||||
/// Albedo-Farbe der Wand (RGB 0..1). Default helles Grau wie die three.js-Sicht.
|
/// Albedo-Farbe der Wand (RGB 0..1). Default helles Grau wie die three.js-Sicht.
|
||||||
|
/// Nur wirksam, wenn `layers` fehlt/leer ist (Vollkoerper-Fallback).
|
||||||
#[serde(default = "default_wall_color")]
|
#[serde(default = "default_wall_color")]
|
||||||
pub color: Rgb,
|
pub color: Rgb,
|
||||||
/// Oeffnungen (Tueren/Fenster) entlang der Wandachse. Default leer (volle Wand).
|
/// Oeffnungen (Tueren/Fenster) entlang der Wandachse. Default leer (volle Wand).
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub openings: Vec<Opening>,
|
pub openings: Vec<Opening>,
|
||||||
|
/// Optionaler Schichtaufbau (z. B. Putz/Daemmung/Mauerwerk), quer zur
|
||||||
|
/// Gesamtdicke gestapelt (siehe `mesh::extrude_wall`). Fehlt dieses Feld
|
||||||
|
/// (`None`) oder ist es leer, extrudiert `mesh.rs` weiterhin EINEN
|
||||||
|
/// Vollkoerper aus `thickness`/`color` (Rueckwaertskompatibilitaet, M1-
|
||||||
|
/// Verhalten unveraendert).
|
||||||
|
#[serde(default)]
|
||||||
|
pub layers: Option<Vec<WallLayer>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine Wandschicht: Dicke + eigene Albedo-Farbe. Siehe `WallInput::layers`.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct WallLayer {
|
||||||
|
/// Schichtdicke in Metern.
|
||||||
|
pub thickness: f32,
|
||||||
|
/// Albedo-Farbe dieser Schicht (RGB 0..1).
|
||||||
|
pub color: Rgb,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Eine Oeffnung (Tuer/Fenster) in einer Wand: Intervall entlang der Wandachse
|
/// Eine Oeffnung (Tuer/Fenster) in einer Wand: Intervall entlang der Wandachse
|
||||||
|
|||||||
+47
-7
@@ -17,15 +17,29 @@
|
|||||||
// DECKEN (Slabs): geschossgebundene Flächenbauteile werden als extrudierte
|
// DECKEN (Slabs): geschossgebundene Flächenbauteile werden als extrudierte
|
||||||
// Polygon-Platten (SlabInput) mitgegeben — render3d trianguliert den Umriss und
|
// Polygon-Platten (SlabInput) mitgegeben — render3d trianguliert den Umriss und
|
||||||
// zieht ihn über die Deckendicke hoch.
|
// zieht ihn über die Deckendicke hoch.
|
||||||
|
//
|
||||||
|
// SCHICHTEN (Putz/Dämmung/Mauerwerk/…): zusätzlich zur Gesamtdicke wird der
|
||||||
|
// aufgelöste Schichtaufbau des WallType mitgegeben (`layers`) — je Schicht
|
||||||
|
// Dicke + 3D-Farbe des zugehörigen Bauteil-Materials (Component). render3d
|
||||||
|
// extrudiert daraus je Schicht ein eigenes Band quer zur Wanddicke (siehe
|
||||||
|
// render3d::mesh); die Eck-Gehrung (Wandknoten-Verschneidung) berechnet
|
||||||
|
// render3d selbständig aus der Wandkonnektivität, braucht also keine
|
||||||
|
// zusätzlichen Felder von hier.
|
||||||
|
|
||||||
import type { Project, Wall } from "../model/types";
|
import type { Project, Wall } from "../model/types";
|
||||||
import { getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
|
import { getComponent, getWallType, wallTypeThickness, openingsOfWall } from "../model/types";
|
||||||
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
|
import { wallVerticalExtent, ceilingVerticalExtent } from "../model/wall";
|
||||||
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
import { openingInterval, openingVerticalExtent } from "../geometry/opening";
|
||||||
|
|
||||||
export type RVec2 = [number, number];
|
export type RVec2 = [number, number];
|
||||||
export type RRgb = [number, number, number];
|
export type RRgb = [number, number, number];
|
||||||
|
|
||||||
|
/** Eine aufgelöste Wandschicht: Dicke + 3D-Albedo-Farbe (aus dem Component). */
|
||||||
|
export interface RLayer {
|
||||||
|
thickness: number;
|
||||||
|
color: RRgb;
|
||||||
|
}
|
||||||
|
|
||||||
export interface RWall {
|
export interface RWall {
|
||||||
start: RVec2;
|
start: RVec2;
|
||||||
end: RVec2;
|
end: RVec2;
|
||||||
@@ -33,6 +47,8 @@ export interface RWall {
|
|||||||
height: number;
|
height: number;
|
||||||
baseElevation: number;
|
baseElevation: number;
|
||||||
color: RRgb;
|
color: RRgb;
|
||||||
|
/** Aufgelöster Schichtaufbau (leer = kein WallType auflösbar, siehe emitWall). */
|
||||||
|
layers: RLayer[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Eine extrudierte Deckenplatte: geschlossener Grundriss-Umriss + Z-Ausdehnung. */
|
/** Eine extrudierte Deckenplatte: geschlossener Grundriss-Umriss + Z-Ausdehnung. */
|
||||||
@@ -57,6 +73,18 @@ const SLAB_RGB: RRgb = [0.86, 0.86, 0.88];
|
|||||||
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
/** Ignoriere entartete vertikale Ausschnitte (Rundungsrauschen). */
|
||||||
const EPS = 1e-4;
|
const EPS = 1e-4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wandelt eine Hex-Farbe ("#rrggbb", Component-Farbe im Plan) in float-RGB
|
||||||
|
* (0..1, render3d-Konvention) um. Ungültiges/fehlendes Format fällt auf den
|
||||||
|
* neutralen Wand-Grundton zurück (nie ein hartes Fehlbild).
|
||||||
|
*/
|
||||||
|
function hexToRgb(hex: string): RRgb {
|
||||||
|
const m = /^#?([0-9a-fA-F]{6})$/.exec(hex);
|
||||||
|
if (!m) return WALL_RGB;
|
||||||
|
const v = parseInt(m[1], 16);
|
||||||
|
return [((v >> 16) & 0xff) / 255, ((v >> 8) & 0xff) / 255, (v & 0xff) / 255];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Hängt einen Wand-Teilquader an: das Achsenstück [from..to] (Meter ab
|
* Hängt einen Wand-Teilquader an: das Achsenstück [from..to] (Meter ab
|
||||||
* Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke
|
* Wand-Startpunkt) über den Höhenbereich [zBottom..zTop]. Entartete Stücke
|
||||||
@@ -71,6 +99,7 @@ function pushSegment(
|
|||||||
zBottom: number,
|
zBottom: number,
|
||||||
zTop: number,
|
zTop: number,
|
||||||
thickness: number,
|
thickness: number,
|
||||||
|
layers: RLayer[],
|
||||||
): void {
|
): void {
|
||||||
if (to - from <= EPS) return;
|
if (to - from <= EPS) return;
|
||||||
if (zTop - zBottom <= EPS) return;
|
if (zTop - zBottom <= EPS) return;
|
||||||
@@ -89,16 +118,27 @@ function pushSegment(
|
|||||||
height: zTop - zBottom,
|
height: zTop - zBottom,
|
||||||
baseElevation: zBottom,
|
baseElevation: zBottom,
|
||||||
color: WALL_RGB,
|
color: WALL_RGB,
|
||||||
|
layers,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */
|
/** Emittiert die Teilquader EINER Wand (mit oder ohne Öffnungen). */
|
||||||
function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
||||||
let thickness = 0.2;
|
let thickness = 0.2;
|
||||||
|
// Aufgelöster Schichtaufbau (Dicke + 3D-Farbe je Schicht des WallType) —
|
||||||
|
// leer, wenn kein WallType auflösbar ist (render3d extrudiert dann wie
|
||||||
|
// bisher einen Vollkörper aus `thickness`/`color`, siehe RWall-Moduldoc).
|
||||||
|
let layers: RLayer[] = [];
|
||||||
try {
|
try {
|
||||||
thickness = wallTypeThickness(getWallType(project, wall));
|
const wt = getWallType(project, wall);
|
||||||
|
thickness = wallTypeThickness(wt);
|
||||||
|
layers = wt.layers.map((l) => ({
|
||||||
|
thickness: l.thickness,
|
||||||
|
color: hexToRgb(getComponent(project, l.componentId).color),
|
||||||
|
}));
|
||||||
} catch {
|
} catch {
|
||||||
thickness = 0.2;
|
thickness = 0.2;
|
||||||
|
layers = [];
|
||||||
}
|
}
|
||||||
const { zBottom, zTop } = wallVerticalExtent(project, wall);
|
const { zBottom, zTop } = wallVerticalExtent(project, wall);
|
||||||
const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
|
const axisLen = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y);
|
||||||
@@ -128,7 +168,7 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
|||||||
|
|
||||||
// Ohne Aussparungen: ein durchgehender Quader (wie bisher).
|
// Ohne Aussparungen: ein durchgehender Quader (wie bisher).
|
||||||
if (cutouts.length === 0) {
|
if (cutouts.length === 0) {
|
||||||
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness);
|
pushSegment(out, wall, 0, axisLen, zBottom, zTop, thickness, layers);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,23 +180,23 @@ function emitWall(out: RWall[], project: Project, wall: Wall): void {
|
|||||||
const segFrom = Math.max(cursor, from);
|
const segFrom = Math.max(cursor, from);
|
||||||
if (from > cursor) {
|
if (from > cursor) {
|
||||||
// Voller Wandpfeiler bis zur Aussparung.
|
// Voller Wandpfeiler bis zur Aussparung.
|
||||||
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness);
|
pushSegment(out, wall, cursor, from, zBottom, zTop, thickness, layers);
|
||||||
}
|
}
|
||||||
if (to > segFrom) {
|
if (to > segFrom) {
|
||||||
// Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom).
|
// Brüstung unter der Öffnung (bei Türen entfällt sie, da oBottom == zBottom).
|
||||||
if (oBottom > zBottom + EPS) {
|
if (oBottom > zBottom + EPS) {
|
||||||
pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness);
|
pushSegment(out, wall, segFrom, to, zBottom, oBottom, thickness, layers);
|
||||||
}
|
}
|
||||||
// Sturz über der Öffnung (bis zum Wandkopf).
|
// Sturz über der Öffnung (bis zum Wandkopf).
|
||||||
if (oTop < zTop - EPS) {
|
if (oTop < zTop - EPS) {
|
||||||
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness);
|
pushSegment(out, wall, segFrom, to, oTop, zTop, thickness, layers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cursor = Math.max(cursor, to);
|
cursor = Math.max(cursor, to);
|
||||||
}
|
}
|
||||||
// Restlicher Wandpfeiler bis zum Achsenende.
|
// Restlicher Wandpfeiler bis zum Achsenende.
|
||||||
if (cursor < axisLen) {
|
if (cursor < axisLen) {
|
||||||
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness);
|
pushSegment(out, wall, cursor, axisLen, zBottom, zTop, thickness, layers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user