render3d-Schnitt: Öffnungen — Brüstung/Sturz-Teilpolygone und Durchblick
WallInput bekommt openings (from/to/sill/height, serde-default). Der Cut durch eine Öffnung liefert mehrere einfache Rechtecke (Brüstung, Sturz, Leibungen) statt Lochpolygon — kompatibel zu render2d::FillPolygon. Der Verdeckungstest behandelt Öffnungen als Durchblick (u-Zuordnung exakt im Elevationsfall, konservativ undurchsichtig bei kantennaher Ausrichtung); Fensterrahmen-Kanten werden als projizierte Kanten ausgegeben. Tests 26 auf 29, Beweis-SVG mit Fenster + dahinterliegender Wand neu generiert (sichtbares Band exakt im Fensterausschnitt). mesh.rs (3D-Vollkoerper) beruecksichtigt Öffnungen weiterhin nicht — dokumentierte Luecke.
This commit is contained in:
@@ -67,6 +67,65 @@ generischer Punktering abgelegt — kompatibel zu `render2d::types::FillPolygon`
|
|||||||
(`pts: Vec<Point>`), dem vorgesehenen Zielformat fuer die spaetere 1:1-
|
(`pts: Vec<Point>`), dem vorgesehenen Zielformat fuer die spaetere 1:1-
|
||||||
Uebersetzung in die Plan-/Schnitt-Ansicht.
|
Uebersetzung in die Plan-/Schnitt-Ansicht.
|
||||||
|
|
||||||
|
## Oeffnungen (Tueren/Fenster)
|
||||||
|
|
||||||
|
`WallInput` hat seit diesem Nachtrag ein Feld `openings: Vec<Opening>`
|
||||||
|
(`types.rs`), unabhaengig von der reinen Achse/Dicke/Hoehe: je Oeffnung ein
|
||||||
|
Intervall entlang der Wandachse (`from`/`to`, Meter ab `start`) plus Bruestungs-
|
||||||
|
und Kopfhoehe (`sill`/`height`, relativ zu `base_elevation`). Eine Tuer ist der
|
||||||
|
Sonderfall `sill == 0.0`.
|
||||||
|
|
||||||
|
**Cut-Polygone.** Trifft die Schnittlinie eine Wand exakt im Bereich einer
|
||||||
|
Oeffnung, splittet `wall_cut_rectangles` das sonst einzelne Vollrechteck
|
||||||
|
`[z0,z1]` in bis zu ZWEI Teil-Rechtecke: Bruestung `[z0, z0+sill]` und Sturz
|
||||||
|
`[z0+sill+height, z1]`. Gewaehlt wurde diese Mehrfach-Rechteck-Darstellung
|
||||||
|
BEWUSST gegenueber einem Loch-Polygon (Ring mit Aussparung): mehrere einfache,
|
||||||
|
konvexe Ringe sind direkt kompatibel zum Zielformat `render2d::types::
|
||||||
|
FillPolygon` (nur einfache Ringe, kein Loch-Format), waehrend ein
|
||||||
|
Loch-Polygon eine zusaetzliche Datenstruktur (Ring-mit-Loch) noetig gemacht
|
||||||
|
haette, die die Zielstruktur (noch) nicht kennt. Ausserhalb einer Oeffnung
|
||||||
|
bleibt es beim einzelnen Vollrechteck (Regressionsfall).
|
||||||
|
|
||||||
|
Die Zuordnung "welche Wandachsen-Position entspricht dieser Schnittposition"
|
||||||
|
ist fuer den STANDARDFALL (Schnittebene senkrecht zur Wandachse — der uebliche
|
||||||
|
architektonische Wandquerschnitt) EXAKT konstant ueber das Cut-Intervall. Fuer
|
||||||
|
schraege Wand/Ebene-Kombinationen wird sie als AFFINE Funktion (`axis_map`)
|
||||||
|
exakt mitgefuehrt (keine Naeherung noetig, da die Abbildung linear ist).
|
||||||
|
|
||||||
|
**Projektion/Ansicht.** Eine Wand mit Oeffnungen bekommt zusaetzlich zur
|
||||||
|
Boxen-Drahtsilhouette die vier Rahmenkanten jeder Oeffnung (zwei Leibungen,
|
||||||
|
Sturz, Bruestung), berechnet auf der Wandachsen-Mittellinie (dieselbe
|
||||||
|
Vereinfachung wie die Bounding-Box-Verdeckung generell — keine eigene
|
||||||
|
Dicken-Aufloesung der Leibungsflaeche). Fuer die Verdeckung wird jede
|
||||||
|
Wand-Bounding-Box um ihre Oeffnungen als "Loch" reduziert
|
||||||
|
(`PrismBounds::opening_voids`): ein anderes (oder dasselbe) Bauteil hinter der
|
||||||
|
Wand wird in genau dem (u, Hoehe)-Rechteck der Oeffnung NICHT von dieser Wand
|
||||||
|
verdeckt — "Durchblick". Die eigenen Rahmenkanten einer Oeffnung werden dabei
|
||||||
|
NICHT gegen das eigene Bauteil auf Verdeckung geprueft (ein Loch kann sich
|
||||||
|
nicht selbst verdecken); gegen alle anderen Bauteile gilt die normale
|
||||||
|
Verdeckungslogik unveraendert (inkl. der bereits dokumentierten
|
||||||
|
Selbstverdeckung der Rueckseite eines ANDEREN Bauteils durch dessen eigene
|
||||||
|
Vorderseite).
|
||||||
|
|
||||||
|
GENAUIGKEIT (bewusste Vereinfachung): die u-Zuordnung fuer den Durchblick ist
|
||||||
|
nur DANN aussagekraeftig, wenn die Wandachse hinreichend parallel zur u-Achse
|
||||||
|
der Schnittebene steht — das ist GENAU der Fall, in dem man die Wand als
|
||||||
|
Elevation/Ansicht von vorne sieht (und ein Fenster ueberhaupt als Durchblick
|
||||||
|
sichtbar waere). Steht die Wand naeher an "senkrecht zur u-Achse" (Wand auf
|
||||||
|
Kante gesehen bzw. der reine Cut-Fall), wird die Durchblick-Berechnung
|
||||||
|
uebersprungen und die Wand bleibt fuer die Verdeckung VOLL UNDURCHSICHTIG
|
||||||
|
(konservativ hidden) — Schwelle `AXIS_ALIGN_EPS = 1e-3` in `section.rs`. Ein
|
||||||
|
Kante-auf-Kante gesehenes Fenster liefert ohnehin keine sinnvolle
|
||||||
|
Durchblick-Flaeche in der Projektion.
|
||||||
|
|
||||||
|
Getestet in `section::tests` (u. a. `schnitt_durch_fenster_liefert_bruestung_
|
||||||
|
und_sturz`, `schnitt_neben_dem_fenster_liefert_volles_rechteck`,
|
||||||
|
`ansicht_zeigt_fensterrahmen_sichtbar_und_durchblick_bei_dahinterliegender_
|
||||||
|
kante`) und im Beweis-SVG (`examples/section_svg.rs`): Schenkel A der L-Wand
|
||||||
|
hat dort ein Fenster, eine kurze zusaetzliche Wand steht dahinter genau im
|
||||||
|
Fensterband und erscheint im SVG als durchgezogene (sichtbare) Linie zwischen
|
||||||
|
Bruestungs- und Sturzhoehe, gestrichelt (verdeckt) darueber/darunter.
|
||||||
|
|
||||||
## Bekannte Luecken
|
## Bekannte Luecken
|
||||||
|
|
||||||
- **Keine Wandknoten-Verschneidung (T-/X-Stoesse).** Wie in `mesh.rs`
|
- **Keine Wandknoten-Verschneidung (T-/X-Stoesse).** Wie in `mesh.rs`
|
||||||
@@ -77,11 +136,19 @@ Uebersetzung in die Plan-/Schnitt-Ansicht.
|
|||||||
`section::tests` bewusst als reales, erwartetes Verhalten dokumentiert und
|
`section::tests` bewusst als reales, erwartetes Verhalten dokumentiert und
|
||||||
geprueft — kein Bug dieses Moduls, sondern ein Artefakt der fehlenden
|
geprueft — kein Bug dieses Moduls, sondern ein Artefakt der fehlenden
|
||||||
Verschneidungslogik weiter oben in der Pipeline).
|
Verschneidungslogik weiter oben in der Pipeline).
|
||||||
- **Keine Oeffnungs-Aussparungen.** `WallInput`/`SlabInput` kennen (noch) keine
|
- **Oeffnungen sind NICHT in der Vollkoerper-Extrusion (`mesh.rs`)
|
||||||
Tueren/Fenster; der Schnitt schneidet daher immer die volle Wandflaeche.
|
nachgezogen.** `wall_prism`/`wall_cut_rectangles`/die Verdeckung in
|
||||||
Sobald Oeffnungen im Modell ankommen, muss `wall_prism`/die Cut-Polygon-
|
`section.rs` werten `WallInput::openings` vollstaendig aus (siehe Abschnitt
|
||||||
Bildung sie als Aussparungen (Lochpolygone bzw. mehrere Teil-Rechtecke pro
|
"Oeffnungen" oben), aber `mesh::extrude_wall` extrudiert weiterhin die volle
|
||||||
Hoehenband) beruecksichtigen.
|
Wandflaeche ohne Aussparung (`mesh.rs`-Moduldoc: „Oeffnungen kommen in
|
||||||
|
spaeteren Milestones"). Cut-/Ansichts-Pipeline und 3D-Solid-Mesh sind bis zum
|
||||||
|
Nachziehen von `mesh.rs` also bewusst inkonsistent — eine bekannte, separate
|
||||||
|
Luecke (nicht Gegenstand dieses Nachtrags).
|
||||||
|
- **Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung**
|
||||||
|
wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
|
||||||
|
gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
|
||||||
|
"senkrecht zur u-Achse" konservativ auf "kein Durchblick" zurueckgestuft
|
||||||
|
(siehe Abschnitt "Oeffnungen" oben, `AXIS_ALIGN_EPS`).
|
||||||
- **Keine echte Component-/Material-Id.** `WallInput`/`SlabInput` haben aktuell
|
- **Keine echte Component-/Material-Id.** `WallInput`/`SlabInput` haben aktuell
|
||||||
keine eigene Id; `ComponentRef` referenziert daher nur `(Art, Index im
|
keine eigene Id; `ComponentRef` referenziert daher nur `(Art, Index im
|
||||||
Eingabe-Array)` + die rohe Albedo-Farbe als Material-Platzhalter. Sobald ein
|
Eingabe-Array)` + die rohe Albedo-Farbe als Material-Platzhalter. Sobald ein
|
||||||
|
|||||||
@@ -5,9 +5,15 @@
|
|||||||
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
<line x1="108.00" y1="348.00" x2="468.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
|
<line x1="108.00" y1="348.00" x2="468.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
<line x1="108.00" y1="48.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
<line x1="108.00" y1="48.00" x2="468.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
|
<line x1="228.00" y1="252.00" x2="228.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
|
<line x1="348.00" y1="252.00" x2="348.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
|
<line x1="228.00" y1="252.00" x2="348.00" y2="252.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
|
<line x1="228.00" y1="132.00" x2="348.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
<line x1="480.00" y1="348.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
<line x1="480.00" y1="348.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
<line x1="468.00" y1="348.00" x2="480.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
|
<line x1="468.00" y1="348.00" x2="480.00" y2="348.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
<line x1="468.00" y1="48.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
<line x1="468.00" y1="48.00" x2="480.00" y2="48.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
|
<line x1="252.00" y1="252.00" x2="252.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
|
<line x1="324.00" y1="252.00" x2="324.00" y2="132.00" stroke="#111111" stroke-width="1.6"/>
|
||||||
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="468.00" y1="348.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="108.00" y1="348.00" x2="108.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="108.00" y1="348.00" x2="108.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="468.00" y1="348.00" x2="108.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="468.00" y1="348.00" x2="108.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
@@ -19,6 +25,16 @@
|
|||||||
<line x1="480.00" y1="48.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="480.00" y1="48.00" x2="456.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="456.00" y1="348.00" x2="468.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="456.00" y1="348.00" x2="468.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="456.00" y1="48.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="456.00" y1="48.00" x2="468.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="252.00" y1="348.00" x2="252.00" y2="252.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="252.00" y1="132.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="324.00" y1="348.00" x2="324.00" y2="252.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="324.00" y1="132.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="324.00" y1="348.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="252.00" y1="348.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="252.00" y1="348.00" x2="324.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="252.00" y1="48.00" x2="324.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="324.00" y1="348.00" x2="252.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
<line x1="324.00" y1="48.00" x2="252.00" y2="48.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="48.00" y1="372.00" x2="48.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="48.00" y1="372.00" x2="48.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="528.00" y1="372.00" x2="528.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="528.00" y1="372.00" x2="528.00" y2="348.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
<line x1="48.00" y1="372.00" x2="528.00" y2="372.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
<line x1="48.00" y1="372.00" x2="528.00" y2="372.00" stroke="#888888" stroke-width="1.0" stroke-dasharray="5,4"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 4.2 KiB |
@@ -1,16 +1,23 @@
|
|||||||
//! Beweis-Artefakt fuer den analytischen Schnitt/Ansicht-Extraktor (`section.rs`):
|
//! Beweis-Artefakt fuer den analytischen Schnitt/Ansicht-Extraktor (`section.rs`):
|
||||||
//! baut die L-Wand + Bodenplatte-Testszene, schneidet sie mit einer Ebene, die
|
//! baut die L-Wand + Bodenplatte-Testszene (Schenkel A bekommt ein FENSTER) plus
|
||||||
//! sowohl Material schneidet ALS AUCH auf dahinterliegende Kanten blickt, und
|
//! eine kurze "Spaeh"-Wand hinter dem Fenster, schneidet/betrachtet sie mit einer
|
||||||
//! schreibt das Ergebnis als SVG nach `docs/welle-c-hlr-spike/section-engine-proof.svg`.
|
//! Ebene, die sowohl Material schneidet ALS AUCH auf dahinterliegende Kanten
|
||||||
|
//! blickt, und schreibt das Ergebnis als SVG nach
|
||||||
|
//! `docs/welle-c-hlr-spike/section-engine-proof.svg`.
|
||||||
//!
|
//!
|
||||||
//! Aufruf: `cargo run --example section_svg` (im Verzeichnis `src-tauri/render3d`).
|
//! Aufruf: `cargo run --example section_svg` (im Verzeichnis `src-tauri/render3d`).
|
||||||
|
|
||||||
use render3d::section::{cut_section, ComponentKind, SectionOutput, SectionPlane};
|
use render3d::section::{cut_section, ComponentKind, SectionOutput, SectionPlane};
|
||||||
|
use render3d::types::Opening;
|
||||||
use render3d::{SlabInput, WallInput};
|
use render3d::{SlabInput, WallInput};
|
||||||
|
|
||||||
fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
||||||
let color = [0.8, 0.8, 0.8];
|
let color = [0.8, 0.8, 0.8];
|
||||||
let walls = vec![
|
let walls = vec![
|
||||||
|
// Schenkel A: Achse (0,0)->(0,3) -- bekommt ein Fenster (u = Modell-Y
|
||||||
|
// liegt fuer diese Blickrichtung parallel zur Achse -> Elevations-Fall,
|
||||||
|
// siehe section.rs-Moduldoc "Oeffnungen"). Fenster von Y=1.0 bis 2.0,
|
||||||
|
// Bruestung 0.8 m, Fensterhoehe 1.0 m (Sturz ab Y-Hoehe 1.8).
|
||||||
WallInput {
|
WallInput {
|
||||||
start: [0.0, 0.0],
|
start: [0.0, 0.0],
|
||||||
end: [0.0, 3.0],
|
end: [0.0, 3.0],
|
||||||
@@ -18,6 +25,12 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
|
openings: vec![Opening {
|
||||||
|
from: 1.0,
|
||||||
|
to: 2.0,
|
||||||
|
sill: 0.8,
|
||||||
|
height: 1.0,
|
||||||
|
}],
|
||||||
},
|
},
|
||||||
WallInput {
|
WallInput {
|
||||||
start: [0.0, 3.0],
|
start: [0.0, 3.0],
|
||||||
@@ -26,6 +39,21 @@ fn l_wall_scene() -> (Vec<WallInput>, Vec<SlabInput>) {
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
|
openings: vec![],
|
||||||
|
},
|
||||||
|
// "Spaeh"-Wand: kurzer Stummel HINTER Schenkel A (groesseres Modell-X,
|
||||||
|
// also groessere Tiefe von der Betrachter-Ebene aus), genau im
|
||||||
|
// Grundriss-Y-Bereich des Fensters (1.2..1.8, sicher innerhalb 1.0..2.0)
|
||||||
|
// -- demonstriert den Durchblick: sichtbar im Fensterband (Hoehe
|
||||||
|
// 0.8..1.8), verdeckt darueber/darunter.
|
||||||
|
WallInput {
|
||||||
|
start: [1.5, 1.2],
|
||||||
|
end: [1.5, 1.8],
|
||||||
|
thickness: 0.2,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color,
|
||||||
|
openings: vec![],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let slabs = vec![SlabInput {
|
let slabs = vec![SlabInput {
|
||||||
@@ -123,12 +151,13 @@ fn render_svg(out: &SectionOutput) -> String {
|
|||||||
fn main() {
|
fn main() {
|
||||||
let (walls, slabs) = l_wall_scene();
|
let (walls, slabs) = l_wall_scene();
|
||||||
// Ebene bei Grundriss-X = -0.3: schneidet NUR die Bodenplatte (Umriss x in
|
// Ebene bei Grundriss-X = -0.3: schneidet NUR die Bodenplatte (Umriss x in
|
||||||
// [-0.5,3.5]) -- liefert deren Poche als Cut-Polygon -- waehrend BEIDE
|
// [-0.5,3.5]) -- liefert deren Poche als Cut-Polygon -- waehrend ALLE DREI
|
||||||
// Wandschenkel (x in [-0.1,0.1] bzw. [0,3]) vollstaendig HINTER der Ebene
|
// Waende (Schenkel A samt Fenster, Schenkel B, die Spaeh-Wand) vollstaendig
|
||||||
// liegen und daher als Draht-Silhouette projiziert werden (inkl. der in
|
// HINTER der Ebene liegen und daher als Draht-Silhouette projiziert werden
|
||||||
// den Tests verifizierten Eck-Ueberlappungs-Verdeckung). Zeigt also
|
// (inkl. der in den Tests verifizierten Eck-Ueberlappungs-Verdeckung UND
|
||||||
// geschnittenes Material UND eine gemischte sichtbar/verdeckt-Ansicht in
|
// des Fensterrahmens + Durchblicks auf die Spaeh-Wand, siehe
|
||||||
// einem Bild.
|
// section.rs-Tests). Zeigt also geschnittenes Material UND eine gemischte
|
||||||
|
// sichtbar/verdeckt-Ansicht MIT Oeffnung in einem Bild.
|
||||||
let plane = SectionPlane::looking_plus_x(-0.3);
|
let plane = SectionPlane::looking_plus_x(-0.3);
|
||||||
let out = cut_section(&plane, &walls, &slabs);
|
let out = cut_section(&plane, &walls, &slabs);
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ fn demo_walls() -> Vec<WallInput> {
|
|||||||
height: ht,
|
height: ht,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: grey,
|
color: grey,
|
||||||
|
openings: vec![],
|
||||||
};
|
};
|
||||||
// Raum 6 x 4 m.
|
// Raum 6 x 4 m.
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ mod tests {
|
|||||||
height,
|
height,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +93,7 @@ mod tests {
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX);
|
assert_eq!(mesh.vertex_count(), 2 * VERTS_PER_BOX);
|
||||||
@@ -168,6 +170,7 @@ mod tests {
|
|||||||
height: 3.0,
|
height: 3.0,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
};
|
};
|
||||||
let mut mesh = Mesh::default();
|
let mut mesh = Mesh::default();
|
||||||
extrude_wall(&mut mesh, &w);
|
extrude_wall(&mut mesh, &w);
|
||||||
@@ -187,6 +190,7 @@ mod tests {
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color: [0.8, 0.8, 0.8],
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
};
|
};
|
||||||
let mesh = build_walls_mesh(&[w]);
|
let mesh = build_walls_mesh(&[w]);
|
||||||
assert_eq!(mesh.vertex_count(), 0);
|
assert_eq!(mesh.vertex_count(), 0);
|
||||||
|
|||||||
@@ -53,6 +53,47 @@
|
|||||||
//! Punkt. Kanten werden dafuer in Sichtbarkeits-Teilstuecke zerlegt (echtes
|
//! Punkt. Kanten werden dafuer in Sichtbarkeits-Teilstuecke zerlegt (echtes
|
||||||
//! 1D-Intervall-Clipping in ihrem Parameterbereich, kein Nur-Mittelpunkt-Test).
|
//! 1D-Intervall-Clipping in ihrem Parameterbereich, kein Nur-Mittelpunkt-Test).
|
||||||
//!
|
//!
|
||||||
|
//! ## Oeffnungen (Tueren/Fenster)
|
||||||
|
//!
|
||||||
|
//! `WallInput::openings` (siehe `types.rs`) gibt je Wand ein Intervall entlang
|
||||||
|
//! der Wandachse (`from`/`to`, Meter ab `start`) plus eine vertikale Bruestungs-/
|
||||||
|
//! Kopfhoehe (`sill`/`height`, relativ zu `base_elevation`) an. Dieses Modul
|
||||||
|
//! nutzt das fuer zwei getrennte Faelle:
|
||||||
|
//!
|
||||||
|
//! - **Cut-Polygone:** trifft die Schnittlinie eine Wand exakt im Bereich einer
|
||||||
|
//! Oeffnung, wird die volle Rechteck-Hoehe `[z0,z1]` in bis zu ZWEI Rechtecke
|
||||||
|
//! gesplittet (Bruestung `[z0, z0+sill]`, Sturz `[z0+sill+height, z1]`) statt
|
||||||
|
//! eines einzelnen Vollrechtecks — bevorzugt gegenueber einem Loch-Polygon
|
||||||
|
//! (Ring mit Aussparung): mehrere einfache, konvexe Rechtecke sind fuer die
|
||||||
|
//! spaetere Schraffur/Fuellung (Ziel: `render2d::types::FillPolygon`, das
|
||||||
|
//! NUR einfache Ringe kennt) direkt verwendbar, waehrend ein Loch-Polygon
|
||||||
|
//! ein Ring-mit-Loch-Format braeuchte, das die Zielstruktur (noch) nicht hat.
|
||||||
|
//! Liegt die Oeffnung ausserhalb der aktuellen Schnittposition, entsteht wie
|
||||||
|
//! zuvor ein einzelnes Vollrechteck (Regressionsfall, siehe Tests).
|
||||||
|
//! - **Projektion/Ansicht:** eine Wand mit Oeffnungen bekommt zusaetzlich zur
|
||||||
|
//! Boxen-Drahtsilhouette die vier Rahmenkanten jeder Oeffnung (zwei Leibungen,
|
||||||
|
//! Sturz, Bruestung) als eigene projizierte Kanten. Fuer die Verdeckung wird
|
||||||
|
//! jede Wand-Bounding-Box um ihre Oeffnungen als "Loch" reduziert (siehe
|
||||||
|
//! `PrismBounds::opening_voids`): ein anderes (oder das gleiche) Bauteil
|
||||||
|
//! hinter der Wand wird in exakt dem (u, Hoehe)-Rechteck der Oeffnung NICHT
|
||||||
|
//! von dieser Wand verdeckt — "Durchblick".
|
||||||
|
//!
|
||||||
|
//! GENAUIGKEIT (bewusste Vereinfachung, siehe auch unten "Bekannte
|
||||||
|
//! Vereinfachungen"): die Zuordnung "welche Wandachsen-Position entspricht
|
||||||
|
//! dieser u-Projektionsposition" ist nur DANN exakt/aussagekraeftig, wenn die
|
||||||
|
//! Wandachse hinreichend parallel zur u-Achse der Schnittebene steht — das ist
|
||||||
|
//! GENAU der Fall, in dem man die Wand als Ansicht/Elevation von vorne sieht
|
||||||
|
//! (und ein Fenster darin ueberhaupt als Durchblick sichtbar waere). Steht die
|
||||||
|
//! Wand naeher an "senkrecht zur u-Achse" (Wand auf Kante gesehen bzw. der
|
||||||
|
//! Cut-Fall), wird die Durchblick-Berechnung uebersprungen und die Wand bleibt
|
||||||
|
//! fuer die Verdeckung VOLL UNDURCHSICHTIG (konservativ hidden) — ein
|
||||||
|
//! Kante-auf-Kante gesehenes Fenster liefert ohnehin keine sinnvolle
|
||||||
|
//! Durchblick-Flaeche in der Projektion. Schwelle: siehe `AXIS_ALIGN_EPS`.
|
||||||
|
//!
|
||||||
|
//! Die eigenen Rahmenkanten einer Oeffnung werden NICHT gegen das eigene
|
||||||
|
//! Bauteil auf Verdeckung geprueft (ein Loch kann sich nicht selbst verdecken);
|
||||||
|
//! gegen ALLE ANDEREN Bauteile gilt die normale Verdeckungslogik.
|
||||||
|
//!
|
||||||
//! ## Bekannte Vereinfachungen (siehe auch docs/design/engine-section-pipeline.md)
|
//! ## Bekannte Vereinfachungen (siehe auch docs/design/engine-section-pipeline.md)
|
||||||
//!
|
//!
|
||||||
//! - Verdeckung nutzt die ACHSPARALLELE (u, Tiefe)-Bounding-Box jedes Prismas,
|
//! - Verdeckung nutzt die ACHSPARALLELE (u, Tiefe)-Bounding-Box jedes Prismas,
|
||||||
@@ -66,15 +107,23 @@
|
|||||||
//! fuehren, dass eine Wand als "in eine andere eingebettet" verdeckt erscheint
|
//! fuehren, dass eine Wand als "in eine andere eingebettet" verdeckt erscheint
|
||||||
//! (siehe Test `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real,
|
//! (siehe Test `eck_ueberlappung_verdeckt_kante_am_wandknoten` unten) — real,
|
||||||
//! aber ein Artefakt der fehlenden Verschneidungslogik, kein Bug dieses Moduls.
|
//! aber ein Artefakt der fehlenden Verschneidungslogik, kein Bug dieses Moduls.
|
||||||
//! - Oeffnungen (Tueren/Fenster) sind in `WallInput` noch nicht modelliert und
|
//! - Oeffnungen werden hier (Cut + Projektion) ausgewertet, aber NICHT von der
|
||||||
//! werden daher hier auch nicht ausgespart.
|
//! Vollkoerper-Extrusion in `mesh.rs` — die 3D-Solid-Mesh-Erzeugung extrudiert
|
||||||
|
//! weiterhin die volle Wandflaeche (siehe `mesh.rs`-Moduldoc). Cut-Ansicht und
|
||||||
|
//! 3D-Solid-Ansicht sind bis zu diesem Nachtrag also bewusst inkonsistent;
|
||||||
|
//! das Nachziehen von `mesh.rs` ist eine bekannte, separate Luecke.
|
||||||
|
//! - Oeffnungs-Durchblick nutzt dieselbe achsparallele Bounding-Box-Naeherung
|
||||||
|
//! wie die allgemeine Verdeckung (kein exaktes Polygon-Clipping der Lochflaeche
|
||||||
|
//! gegen dahinterliegende Kanten) und wird bei Wand-Orientierungen nahe
|
||||||
|
//! "senkrecht zur u-Achse" konservativ auf "kein Durchblick" zurueckgestuft
|
||||||
|
//! (siehe Abschnitt "Oeffnungen" oben).
|
||||||
//! - Vertices exakt AUF der Schnittlinie werden nicht robust (kein Tie-Breaking/
|
//! - Vertices exakt AUF der Schnittlinie werden nicht robust (kein Tie-Breaking/
|
||||||
//! Pertubation); die Testszenarien vermeiden diesen Sonderfall bewusst.
|
//! Pertubation); die Testszenarien vermeiden diesen Sonderfall bewusst.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::math;
|
use crate::math;
|
||||||
use crate::types::{Point2, Rgb, SlabInput, WallInput};
|
use crate::types::{Opening, Point2, Rgb, SlabInput, WallInput};
|
||||||
|
|
||||||
/// Toleranz fuer "naeher an der Ebene" / Grenzwert-Vergleiche (Meter, Tiefe/Hoehe).
|
/// Toleranz fuer "naeher an der Ebene" / Grenzwert-Vergleiche (Meter, Tiefe/Hoehe).
|
||||||
const EPS: f32 = 1e-4;
|
const EPS: f32 = 1e-4;
|
||||||
@@ -244,6 +293,45 @@ struct Prism {
|
|||||||
z0: f32,
|
z0: f32,
|
||||||
z1: f32,
|
z1: f32,
|
||||||
color: Rgb,
|
color: Rgb,
|
||||||
|
/// Nur fuer Waende: Achse + Oeffnungen (siehe `WallAxis`). `None` fuer
|
||||||
|
/// Decken (Slabs kennen keine Oeffnungen).
|
||||||
|
wall_axis: Option<WallAxis>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Wandachse + Oeffnungen, wie sie fuer die Oeffnungs-Zuordnung bei Cut-
|
||||||
|
/// Polygonen/Projektion gebraucht werden (siehe Moduldoc "Oeffnungen").
|
||||||
|
struct WallAxis {
|
||||||
|
/// Achsen-Startpunkt (== `WallInput::start`).
|
||||||
|
start: Point2,
|
||||||
|
/// Normalisierte Achsenrichtung (Start -> Ende).
|
||||||
|
dir: Point2,
|
||||||
|
/// Achsenlaenge (Meter).
|
||||||
|
length: f32,
|
||||||
|
/// Oeffnungen entlang der Achse (unveraendert aus `WallInput::openings`).
|
||||||
|
openings: Vec<Opening>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Schwelle fuer `|dot(u_axis, wall_dir)|`, ab der eine Wandachse als
|
||||||
|
/// "hinreichend parallel zur u-Achse" gilt (siehe Moduldoc "Oeffnungen" ->
|
||||||
|
/// Genauigkeit). Darunter wird die Oeffnungs-Durchblick-Berechnung fuer diese
|
||||||
|
/// Wand konservativ uebersprungen (Wand bleibt fuer die Verdeckung blickdicht).
|
||||||
|
const AXIS_ALIGN_EPS: f32 = 1e-3;
|
||||||
|
|
||||||
|
/// Affine Abbildung `u -> Wandachsen-Position` fuer eine Wand in einer
|
||||||
|
/// gegebenen Schnittebene: `along_wall(u) = c + d*u`. Herleitung: ein Punkt bei
|
||||||
|
/// Projektions-Parameter `u` auf der Schnittlinie liegt bei
|
||||||
|
/// `plane.plan_point() + u * plane.plan_u_axis()` (siehe `project`/`u_of` in
|
||||||
|
/// `footprint_u_intervals`); seine Position entlang der Wandachse ist das
|
||||||
|
/// Skalarprodukt aus (Punkt - Wandstart) und der (normalisierten) Wandrichtung.
|
||||||
|
/// Ausmultipliziert ergibt das die affine Form unten. `d == 0` heisst: die
|
||||||
|
/// Wandachse steht senkrecht auf der u-Achse (Wand auf Kante gesehen / reiner
|
||||||
|
/// Cut-Fall) — dort ist `along_wall` konstant ueber das ganze u-Intervall.
|
||||||
|
fn axis_map(plane: &SectionPlane, axis: &WallAxis) -> (f32, f32) {
|
||||||
|
let p0 = plane.plan_point();
|
||||||
|
let ua = plane.plan_u_axis();
|
||||||
|
let c = (p0[0] - axis.start[0]) * axis.dir[0] + (p0[1] - axis.start[1]) * axis.dir[1];
|
||||||
|
let d = ua[0] * axis.dir[0] + ua[1] * axis.dir[1];
|
||||||
|
(c, d)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 2D-Links-Normale (90 Grad CCW) — 1:1 gespiegelt aus `mesh::left_normal`
|
/// 2D-Links-Normale (90 Grad CCW) — 1:1 gespiegelt aus `mesh::left_normal`
|
||||||
@@ -274,6 +362,12 @@ fn wall_prism(index: usize, wall: &WallInput) -> Option<Prism> {
|
|||||||
off(wall.end, -1.0),
|
off(wall.end, -1.0),
|
||||||
off(wall.start, -1.0),
|
off(wall.start, -1.0),
|
||||||
];
|
];
|
||||||
|
// Achsenrichtung aus der bereits berechneten Links-Normale ableiten (90 Grad
|
||||||
|
// zurueckgedreht): n = (-dir.y, dir.x) => dir = (n.y, -n.x).
|
||||||
|
let dir = [n[1], -n[0]];
|
||||||
|
let dx = wall.end[0] - wall.start[0];
|
||||||
|
let dy = wall.end[1] - wall.start[1];
|
||||||
|
let length = (dx * dx + dy * dy).sqrt();
|
||||||
Some(Prism {
|
Some(Prism {
|
||||||
kind: ComponentKind::Wall,
|
kind: ComponentKind::Wall,
|
||||||
index,
|
index,
|
||||||
@@ -281,6 +375,12 @@ fn wall_prism(index: usize, wall: &WallInput) -> Option<Prism> {
|
|||||||
z0: wall.base_elevation,
|
z0: wall.base_elevation,
|
||||||
z1: wall.base_elevation + wall.height,
|
z1: wall.base_elevation + wall.height,
|
||||||
color: wall.color,
|
color: wall.color,
|
||||||
|
wall_axis: Some(WallAxis {
|
||||||
|
start: wall.start,
|
||||||
|
dir,
|
||||||
|
length,
|
||||||
|
openings: wall.openings.clone(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,6 +400,7 @@ fn slab_prism(index: usize, slab: &SlabInput) -> Option<Prism> {
|
|||||||
z0,
|
z0,
|
||||||
z1,
|
z1,
|
||||||
color: slab.color,
|
color: slab.color,
|
||||||
|
wall_axis: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -356,18 +457,100 @@ fn footprint_u_intervals(plane: &SectionPlane, footprint: &[Point2]) -> Vec<(f32
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Zerlegt EIN Cut-u-Intervall `[u_lo, u_hi]` (volle Rechteck-Hoehe `[prism.z0,
|
||||||
|
/// prism.z1]`) an den Oeffnungen der Wand in mehrere Rechtecke `(u0, u1, v0,
|
||||||
|
/// v1)`. Ohne Oeffnungen (oder fuer Decken, `wall_axis == None`) liefert das
|
||||||
|
/// exakt das eine Vollrechteck von vorher (Regressionsfall). Siehe Moduldoc
|
||||||
|
/// "Oeffnungen" fuer die Herleitung von `axis_map`.
|
||||||
|
fn wall_cut_rectangles(
|
||||||
|
plane: &SectionPlane,
|
||||||
|
prism: &Prism,
|
||||||
|
u_lo: f32,
|
||||||
|
u_hi: f32,
|
||||||
|
) -> Vec<(f32, f32, f32, f32)> {
|
||||||
|
let Some(axis) = &prism.wall_axis else {
|
||||||
|
return vec![(u_lo, u_hi, prism.z0, prism.z1)];
|
||||||
|
};
|
||||||
|
if axis.openings.is_empty() {
|
||||||
|
return vec![(u_lo, u_hi, prism.z0, prism.z1)];
|
||||||
|
}
|
||||||
|
let (c, d) = axis_map(plane, axis);
|
||||||
|
|
||||||
|
// Oeffnungs-u-Teilintervalle sammeln (auf [u_lo,u_hi] geclippt), sortiert.
|
||||||
|
let mut voids: Vec<(f32, f32, &Opening)> = Vec::new();
|
||||||
|
for op in &axis.openings {
|
||||||
|
let from = op.from.max(0.0);
|
||||||
|
let to = op.to.min(axis.length);
|
||||||
|
if to <= from {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (open_lo, open_hi) = if d.abs() < 1e-6 {
|
||||||
|
// Wandachse (nahezu) senkrecht zur u-Achse: die Wandachsen-Position
|
||||||
|
// ist ueber das gesamte Cut-Intervall praktisch konstant (reiner
|
||||||
|
// Quer-Schnitt, wie in der Moduldoc beschrieben) -> ganz drin oder
|
||||||
|
// ganz draussen.
|
||||||
|
if c >= from - EPS && c <= to + EPS {
|
||||||
|
(u_lo, u_hi)
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let t0 = (from - c) / d;
|
||||||
|
let t1 = (to - c) / d;
|
||||||
|
(t0.min(t1), t0.max(t1))
|
||||||
|
};
|
||||||
|
let lo = open_lo.max(u_lo);
|
||||||
|
let hi = open_hi.min(u_hi);
|
||||||
|
if hi - lo > MIN_SPAN {
|
||||||
|
voids.push((lo, hi, op));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if voids.is_empty() {
|
||||||
|
return vec![(u_lo, u_hi, prism.z0, prism.z1)];
|
||||||
|
}
|
||||||
|
voids.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
|
||||||
|
|
||||||
|
let mut rects = Vec::new();
|
||||||
|
let mut cursor = u_lo;
|
||||||
|
for (v_lo, v_hi, op) in &voids {
|
||||||
|
if *v_lo > cursor + MIN_SPAN {
|
||||||
|
rects.push((cursor, *v_lo, prism.z0, prism.z1));
|
||||||
|
}
|
||||||
|
let sill_abs = prism.z0 + op.sill.max(0.0);
|
||||||
|
let head_abs = (sill_abs + op.height).min(prism.z1);
|
||||||
|
if sill_abs - prism.z0 > MIN_SPAN {
|
||||||
|
rects.push((*v_lo, *v_hi, prism.z0, sill_abs)); // Bruestung
|
||||||
|
}
|
||||||
|
if prism.z1 - head_abs > MIN_SPAN {
|
||||||
|
rects.push((*v_lo, *v_hi, head_abs, prism.z1)); // Sturz
|
||||||
|
}
|
||||||
|
cursor = cursor.max(*v_hi);
|
||||||
|
}
|
||||||
|
if u_hi > cursor + MIN_SPAN {
|
||||||
|
rects.push((cursor, u_hi, prism.z0, prism.z1));
|
||||||
|
}
|
||||||
|
rects
|
||||||
|
}
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Projektions-Kanten + Verdeckung
|
// Projektions-Kanten + Verdeckung
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Achsparallele Bounding-Box eines Prismas in (u, Tiefe, Hoehe) — genutzt als
|
/// Achsparallele Bounding-Box eines Prismas in (u, Tiefe, Hoehe) — genutzt als
|
||||||
/// Verdeckungs-Naeherung (siehe Moduldoc "Bekannte Vereinfachungen").
|
/// Verdeckungs-Naeherung (siehe Moduldoc "Bekannte Vereinfachungen"). `kind`/
|
||||||
|
/// `index` erlauben es, das Bauteil bei der Verdeckungspruefung gezielt
|
||||||
|
/// auszuschliessen (fuer die eigenen Oeffnungs-Rahmenkanten, siehe Moduldoc
|
||||||
|
/// "Oeffnungen"). `opening_voids` sind (u0,u1,v0,v1)-Rechtecke, in denen diese
|
||||||
|
/// Bounding-Box NICHT verdeckt (Oeffnungs-"Loch", siehe `axis_map`/Moduldoc).
|
||||||
struct PrismBounds {
|
struct PrismBounds {
|
||||||
|
kind: ComponentKind,
|
||||||
|
index: usize,
|
||||||
u_min: f32,
|
u_min: f32,
|
||||||
u_max: f32,
|
u_max: f32,
|
||||||
depth_min: f32,
|
depth_min: f32,
|
||||||
z0: f32,
|
z0: f32,
|
||||||
z1: f32,
|
z1: f32,
|
||||||
|
opening_voids: Vec<(f32, f32, f32, f32)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prism_bounds(plane: &SectionPlane, prism: &Prism) -> PrismBounds {
|
fn prism_bounds(plane: &SectionPlane, prism: &Prism) -> PrismBounds {
|
||||||
@@ -382,12 +565,42 @@ fn prism_bounds(plane: &SectionPlane, prism: &Prism) -> PrismBounds {
|
|||||||
u_max = u_max.max(u);
|
u_max = u_max.max(u);
|
||||||
depth_min = depth_min.min(d);
|
depth_min = depth_min.min(d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut opening_voids = Vec::new();
|
||||||
|
if let Some(axis) = &prism.wall_axis {
|
||||||
|
if !axis.openings.is_empty() {
|
||||||
|
let (c, d) = axis_map(plane, axis);
|
||||||
|
// Nur bei hinreichend u-paralleler Wandachse: siehe Moduldoc
|
||||||
|
// "Oeffnungen" -> Genauigkeit. Sonst bleibt die Wand blickdicht.
|
||||||
|
if d.abs() >= AXIS_ALIGN_EPS {
|
||||||
|
for op in &axis.openings {
|
||||||
|
let from = op.from.max(0.0);
|
||||||
|
let to = op.to.min(axis.length);
|
||||||
|
if to <= from {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ua = (from - c) / d;
|
||||||
|
let ub = (to - c) / d;
|
||||||
|
let (open_u0, open_u1) = (ua.min(ub), ua.max(ub));
|
||||||
|
let sill_abs = prism.z0 + op.sill.max(0.0);
|
||||||
|
let head_abs = (sill_abs + op.height).min(prism.z1);
|
||||||
|
if head_abs > sill_abs + MIN_SPAN {
|
||||||
|
opening_voids.push((open_u0, open_u1, sill_abs, head_abs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PrismBounds {
|
PrismBounds {
|
||||||
|
kind: prism.kind,
|
||||||
|
index: prism.index,
|
||||||
u_min,
|
u_min,
|
||||||
u_max,
|
u_max,
|
||||||
depth_min,
|
depth_min,
|
||||||
z0: prism.z0,
|
z0: prism.z0,
|
||||||
z1: prism.z1,
|
z1: prism.z1,
|
||||||
|
opening_voids,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,6 +706,28 @@ fn merge_intervals(mut ivs: Vec<(f32, f32)>) -> Vec<(f32, f32)> {
|
|||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Entfernt `cut` aus jedem Intervall in `pieces` (kann ein Intervall in ZWEI
|
||||||
|
/// Stuecke spalten, wenn `cut` es mittig trifft). Genutzt, um ein Oeffnungs-
|
||||||
|
/// "Loch" (`PrismBounds::opening_voids`) aus einem sonst blockierten
|
||||||
|
/// t-Teilstueck herauszuschneiden (siehe Moduldoc "Oeffnungen").
|
||||||
|
fn subtract_range(pieces: Vec<(f32, f32)>, cut: (f32, f32)) -> Vec<(f32, f32)> {
|
||||||
|
let (cs, ce) = cut;
|
||||||
|
let mut out = Vec::with_capacity(pieces.len());
|
||||||
|
for (s, e) in pieces {
|
||||||
|
if ce <= s + MIN_SPAN || cs >= e - MIN_SPAN {
|
||||||
|
out.push((s, e)); // keine nennenswerte Ueberlappung
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if cs > s + MIN_SPAN {
|
||||||
|
out.push((s, cs));
|
||||||
|
}
|
||||||
|
if ce < e - MIN_SPAN {
|
||||||
|
out.push((ce, e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Komplement der (sortierten, disjunkten) Intervalle `blocked` in [0, 1].
|
/// Komplement der (sortierten, disjunkten) Intervalle `blocked` in [0, 1].
|
||||||
fn subtract_from_unit(blocked: &[(f32, f32)]) -> Vec<(f32, f32)> {
|
fn subtract_from_unit(blocked: &[(f32, f32)]) -> Vec<(f32, f32)> {
|
||||||
let mut visible = Vec::new();
|
let mut visible = Vec::new();
|
||||||
@@ -511,7 +746,11 @@ fn subtract_from_unit(blocked: &[(f32, f32)]) -> Vec<(f32, f32)> {
|
|||||||
|
|
||||||
/// Fuer eine projizierte Kante (u0,v0)->(u1,v1) bei Tiefen (d0,d1): die
|
/// Fuer eine projizierte Kante (u0,v0)->(u1,v1) bei Tiefen (d0,d1): die
|
||||||
/// Teilbereiche (t in [0,1]), die durch `bounds` verdeckt werden (Vereinigung
|
/// Teilbereiche (t in [0,1]), die durch `bounds` verdeckt werden (Vereinigung
|
||||||
/// ueber alle Blocker).
|
/// ueber alle Blocker). `exclude` (falls gesetzt) nimmt EIN Bauteil von der
|
||||||
|
/// Verdeckungspruefung aus — fuer die eigenen Oeffnungs-Rahmenkanten eines
|
||||||
|
/// Bauteils, die sich nicht selbst verdecken koennen (siehe Moduldoc
|
||||||
|
/// "Oeffnungen"). Jeder Blocker mit `opening_voids` (Fenster/Tueren) gibt an
|
||||||
|
/// diesen Stellen NICHTS zur Verdeckung bei ("Durchblick").
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn blocked_intervals(
|
fn blocked_intervals(
|
||||||
u0: f32,
|
u0: f32,
|
||||||
@@ -522,16 +761,31 @@ fn blocked_intervals(
|
|||||||
d1: f32,
|
d1: f32,
|
||||||
bias: HeightBias,
|
bias: HeightBias,
|
||||||
bounds: &[PrismBounds],
|
bounds: &[PrismBounds],
|
||||||
|
exclude: Option<ComponentRef>,
|
||||||
) -> Vec<(f32, f32)> {
|
) -> Vec<(f32, f32)> {
|
||||||
let mut ivs = Vec::new();
|
let mut ivs = Vec::new();
|
||||||
for b in bounds {
|
for b in bounds {
|
||||||
|
if let Some(ex) = exclude {
|
||||||
|
if ex.kind == b.kind && ex.index == b.index {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
let ru = height_in_range(u0, u1, b.u_min, b.u_max, HeightBias::Vertical);
|
let ru = height_in_range(u0, u1, b.u_min, b.u_max, HeightBias::Vertical);
|
||||||
let rv = height_in_range(v0, v1, b.z0, b.z1, bias);
|
let rv = height_in_range(v0, v1, b.z0, b.z1, bias);
|
||||||
let rd = linear_greater_than(d0, d1, b.depth_min + EPS);
|
let rd = linear_greater_than(d0, d1, b.depth_min + EPS);
|
||||||
if let Some(r) = intersect_ranges(intersect_ranges(ru, rv), rd) {
|
let Some(solid) = intersect_ranges(intersect_ranges(ru, rv), rd) else {
|
||||||
ivs.push(r);
|
continue;
|
||||||
|
};
|
||||||
|
let mut pieces = vec![solid];
|
||||||
|
for &(vu0, vu1, vz0, vz1) in &b.opening_voids {
|
||||||
|
let vru = height_in_range(u0, u1, vu0, vu1, HeightBias::Vertical);
|
||||||
|
let vrv = height_in_range(v0, v1, vz0, vz1, HeightBias::Vertical);
|
||||||
|
if let Some(void_range) = intersect_ranges(vru, vrv) {
|
||||||
|
pieces = subtract_range(pieces, void_range);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ivs.extend(pieces);
|
||||||
|
}
|
||||||
merge_intervals(ivs)
|
merge_intervals(ivs)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -561,6 +815,46 @@ fn prism_wireframe_edges(prism: &Prism) -> Vec<([f32; 3], [f32; 3], HeightBias)>
|
|||||||
edges
|
edges
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Baut die vier Rahmenkanten JEDER Oeffnung einer Wand (zwei Leibungen, Sturz,
|
||||||
|
/// Bruestung) als Weltpunkt-Paare — analog zu `prism_wireframe_edges`, aber
|
||||||
|
/// fuer die Loch-Geometrie statt die Aussenhuelle. Die horizontale Position
|
||||||
|
/// nutzt die Wandachsen-Mittellinie (siehe Moduldoc "Oeffnungen": dieselbe
|
||||||
|
/// Vereinfachung wie die Bounding-Box-Verdeckung — keine eigene Dicken-
|
||||||
|
/// Aufloesung der Leibungsflaeche). `HeightBias::Vertical` fuer alle vier
|
||||||
|
/// Kanten: weder Leibungen (echte Hoehenvariation) noch Sturz/Bruestung
|
||||||
|
/// (konstante Hoehe, aber keine Prisma-Deckel-/Bodenkante) brauchen die
|
||||||
|
/// asymmetrische Cap-Randbehandlung (siehe `HeightBias`-Doku).
|
||||||
|
fn opening_wire_edges(prism: &Prism) -> Vec<([f32; 3], [f32; 3], HeightBias)> {
|
||||||
|
let Some(axis) = &prism.wall_axis else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let mut edges = Vec::new();
|
||||||
|
for op in &axis.openings {
|
||||||
|
let from = op.from.max(0.0);
|
||||||
|
let to = op.to.min(axis.length);
|
||||||
|
if to <= from {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let sill_abs = prism.z0 + op.sill.max(0.0);
|
||||||
|
let head_abs = (sill_abs + op.height).min(prism.z1);
|
||||||
|
if head_abs <= sill_abs + MIN_SPAN {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let at = |t: f32| -> Point2 { [axis.start[0] + t * axis.dir[0], axis.start[1] + t * axis.dir[1]] };
|
||||||
|
let p_from = at(from);
|
||||||
|
let p_to = at(to);
|
||||||
|
let jamb_from = (to_world(p_from, sill_abs), to_world(p_from, head_abs));
|
||||||
|
let jamb_to = (to_world(p_to, sill_abs), to_world(p_to, head_abs));
|
||||||
|
let sill_edge = (to_world(p_from, sill_abs), to_world(p_to, sill_abs));
|
||||||
|
let head_edge = (to_world(p_from, head_abs), to_world(p_to, head_abs));
|
||||||
|
edges.push((jamb_from.0, jamb_from.1, HeightBias::Vertical));
|
||||||
|
edges.push((jamb_to.0, jamb_to.1, HeightBias::Vertical));
|
||||||
|
edges.push((sill_edge.0, sill_edge.1, HeightBias::Vertical));
|
||||||
|
edges.push((head_edge.0, head_edge.1, HeightBias::Vertical));
|
||||||
|
}
|
||||||
|
edges
|
||||||
|
}
|
||||||
|
|
||||||
fn compute_view_edges(
|
fn compute_view_edges(
|
||||||
plane: &SectionPlane,
|
plane: &SectionPlane,
|
||||||
prisms: &[Prism],
|
prisms: &[Prism],
|
||||||
@@ -574,7 +868,15 @@ fn compute_view_edges(
|
|||||||
kind: prism.kind,
|
kind: prism.kind,
|
||||||
index: prism.index,
|
index: prism.index,
|
||||||
};
|
};
|
||||||
for (pa, pb, bias) in prism_wireframe_edges(prism) {
|
// Box-Silhouette (Selbstverdeckung gewollt, siehe Moduldoc) + eigene
|
||||||
|
// Oeffnungs-Rahmenkanten (NICHT gegen das eigene Bauteil verdeckt).
|
||||||
|
let mut all_edges: Vec<(([f32; 3], [f32; 3], HeightBias), bool)> = prism_wireframe_edges(prism)
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| (e, false))
|
||||||
|
.collect();
|
||||||
|
all_edges.extend(opening_wire_edges(prism).into_iter().map(|e| (e, true)));
|
||||||
|
|
||||||
|
for ((pa, pb, bias), self_exclude) in all_edges {
|
||||||
let da = plane.depth(pa);
|
let da = plane.depth(pa);
|
||||||
let db = plane.depth(pb);
|
let db = plane.depth(pb);
|
||||||
if da <= EPS && db <= EPS {
|
if da <= EPS && db <= EPS {
|
||||||
@@ -607,7 +909,8 @@ fn compute_view_edges(
|
|||||||
let d0 = plane.depth(ca);
|
let d0 = plane.depth(ca);
|
||||||
let d1 = plane.depth(cb);
|
let d1 = plane.depth(cb);
|
||||||
|
|
||||||
let blocked = blocked_intervals(u0, u1, v0, v1, d0, d1, bias, &bounds);
|
let exclude = if self_exclude { Some(cref) } else { None };
|
||||||
|
let blocked = blocked_intervals(u0, u1, v0, v1, d0, d1, bias, &bounds, exclude);
|
||||||
for &(s, e) in &blocked {
|
for &(s, e) in &blocked {
|
||||||
hidden.push(SectionEdge {
|
hidden.push(SectionEdge {
|
||||||
component: cref,
|
component: cref,
|
||||||
@@ -650,16 +953,18 @@ pub fn cut_section(plane: &SectionPlane, walls: &[WallInput], slabs: &[SlabInput
|
|||||||
for prism in &prisms {
|
for prism in &prisms {
|
||||||
for (u0, u1) in footprint_u_intervals(plane, &prism.footprint) {
|
for (u0, u1) in footprint_u_intervals(plane, &prism.footprint) {
|
||||||
let (lo, hi) = (u0.min(u1), u0.max(u1));
|
let (lo, hi) = (u0.min(u1), u0.max(u1));
|
||||||
|
for (rlo, rhi, rz0, rz1) in wall_cut_rectangles(plane, prism, lo, hi) {
|
||||||
cut_polygons.push(CutPolygon {
|
cut_polygons.push(CutPolygon {
|
||||||
component: ComponentRef {
|
component: ComponentRef {
|
||||||
kind: prism.kind,
|
kind: prism.kind,
|
||||||
index: prism.index,
|
index: prism.index,
|
||||||
},
|
},
|
||||||
color: prism.color,
|
color: prism.color,
|
||||||
pts: vec![[lo, prism.z0], [hi, prism.z0], [hi, prism.z1], [lo, prism.z1]],
|
pts: vec![[rlo, rz0], [rhi, rz0], [rhi, rz1], [rlo, rz1]],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (visible_edges, hidden_edges) = compute_view_edges(plane, &prisms);
|
let (visible_edges, hidden_edges) = compute_view_edges(plane, &prisms);
|
||||||
|
|
||||||
@@ -695,6 +1000,7 @@ mod tests {
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
|
openings: vec![],
|
||||||
},
|
},
|
||||||
WallInput {
|
WallInput {
|
||||||
start: [0.0, 3.0],
|
start: [0.0, 3.0],
|
||||||
@@ -703,6 +1009,7 @@ mod tests {
|
|||||||
height: 2.5,
|
height: 2.5,
|
||||||
base_elevation: 0.0,
|
base_elevation: 0.0,
|
||||||
color,
|
color,
|
||||||
|
openings: vec![],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
let slabs = vec![SlabInput {
|
let slabs = vec![SlabInput {
|
||||||
@@ -898,4 +1205,205 @@ mod tests {
|
|||||||
assert!(e.a[1] >= -1e-4 && e.a[1] <= 2.5 + 1e-4);
|
assert!(e.a[1] >= -1e-4 && e.a[1] <= 2.5 + 1e-4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Oeffnungen (Tueren/Fenster) -----------------------------------------
|
||||||
|
|
||||||
|
/// Eine Wand entlang +X (Achse (0,0)->(4,0)) mit einem Fenster: Intervall
|
||||||
|
/// entlang der Achse [1.5, 2.5] (1 m breit), Bruestung 1.0 m, Fensterhoehe
|
||||||
|
/// 1.0 m (Sturz beginnt also bei 2.0, Wand-Oberkante bei 2.5 -> 0.5 m Sturz).
|
||||||
|
fn wall_with_window() -> WallInput {
|
||||||
|
WallInput {
|
||||||
|
start: [0.0, 0.0],
|
||||||
|
end: [4.0, 0.0],
|
||||||
|
thickness: 0.2,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![Opening {
|
||||||
|
from: 1.5,
|
||||||
|
to: 2.5,
|
||||||
|
sill: 1.0,
|
||||||
|
height: 1.0,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schnitt_durch_fenster_liefert_bruestung_und_sturz() {
|
||||||
|
// Schnittebene bei Grundriss-X = 2.0: liegt MITTEN im Fenster (1.5..2.5).
|
||||||
|
// Statt eines Vollrechtecks (0..2.5) muessen zwei Teil-Rechtecke
|
||||||
|
// entstehen: Bruestung (0..1.0) und Sturz (2.0..2.5).
|
||||||
|
let wall = wall_with_window();
|
||||||
|
let plane = SectionPlane::looking_plus_x(2.0);
|
||||||
|
let out = cut_section(&plane, &[wall], &[]);
|
||||||
|
|
||||||
|
assert_eq!(out.cut_polygons.len(), 2, "Bruestung + Sturz statt Vollrechteck");
|
||||||
|
|
||||||
|
let bounds_of = |c: &CutPolygon| -> (f32, f32, f32, f32) {
|
||||||
|
let us: Vec<f32> = c.pts.iter().map(|p| p[0]).collect();
|
||||||
|
let vs: Vec<f32> = c.pts.iter().map(|p| p[1]).collect();
|
||||||
|
(
|
||||||
|
us.iter().cloned().fold(f32::INFINITY, f32::min),
|
||||||
|
us.iter().cloned().fold(f32::NEG_INFINITY, f32::max),
|
||||||
|
vs.iter().cloned().fold(f32::INFINITY, f32::min),
|
||||||
|
vs.iter().cloned().fold(f32::NEG_INFINITY, f32::max),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
let bruestung = out
|
||||||
|
.cut_polygons
|
||||||
|
.iter()
|
||||||
|
.find(|c| bounds_of(c).2.abs() < 1e-3)
|
||||||
|
.expect("Bruestungs-Rechteck (v ab 0)");
|
||||||
|
let (u_lo, u_hi, v_lo, v_hi) = bounds_of(bruestung);
|
||||||
|
assert!((u_lo + 0.1).abs() < 1e-4, "u_lo={u_lo}");
|
||||||
|
assert!((u_hi - 0.1).abs() < 1e-4, "u_hi={u_hi}");
|
||||||
|
assert!((v_lo - 0.0).abs() < 1e-4, "v_lo={v_lo}");
|
||||||
|
assert!((v_hi - 1.0).abs() < 1e-4, "v_hi={v_hi} (Bruestungshoehe)");
|
||||||
|
|
||||||
|
let sturz = out
|
||||||
|
.cut_polygons
|
||||||
|
.iter()
|
||||||
|
.find(|c| (bounds_of(c).3 - 2.5).abs() < 1e-3)
|
||||||
|
.expect("Sturz-Rechteck (v bis 2.5, Wand-Oberkante)");
|
||||||
|
let (u_lo, u_hi, v_lo, v_hi) = bounds_of(sturz);
|
||||||
|
assert!((u_lo + 0.1).abs() < 1e-4, "u_lo={u_lo}");
|
||||||
|
assert!((u_hi - 0.1).abs() < 1e-4, "u_hi={u_hi}");
|
||||||
|
assert!((v_lo - 2.0).abs() < 1e-4, "v_lo={v_lo} (Sturz beginnt bei sill+height=2.0)");
|
||||||
|
assert!((v_hi - 2.5).abs() < 1e-4, "v_hi={v_hi}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn schnitt_neben_dem_fenster_liefert_volles_rechteck() {
|
||||||
|
// Schnittebene bei Grundriss-X = 0.5: liegt ausserhalb des Fensters
|
||||||
|
// (1.5..2.5) -> Regressionsfall, unveraendert ein Vollrechteck.
|
||||||
|
let wall = wall_with_window();
|
||||||
|
let plane = SectionPlane::looking_plus_x(0.5);
|
||||||
|
let out = cut_section(&plane, &[wall], &[]);
|
||||||
|
|
||||||
|
assert_eq!(out.cut_polygons.len(), 1, "kein Fenster an dieser Stelle -> ein Vollrechteck");
|
||||||
|
let c = &out.cut_polygons[0];
|
||||||
|
let us: Vec<f32> = c.pts.iter().map(|p| p[0]).collect();
|
||||||
|
let vs: Vec<f32> = c.pts.iter().map(|p| p[1]).collect();
|
||||||
|
let (u_lo, u_hi) = (us.iter().cloned().fold(f32::INFINITY, f32::min), us.iter().cloned().fold(f32::NEG_INFINITY, f32::max));
|
||||||
|
let (v_lo, v_hi) = (vs.iter().cloned().fold(f32::INFINITY, f32::min), vs.iter().cloned().fold(f32::NEG_INFINITY, f32::max));
|
||||||
|
assert!((u_lo + 0.1).abs() < 1e-4, "u_lo={u_lo}");
|
||||||
|
assert!((u_hi - 0.1).abs() < 1e-4, "u_hi={u_hi}");
|
||||||
|
assert!((v_lo - 0.0).abs() < 1e-4, "v_lo={v_lo}");
|
||||||
|
assert!((v_hi - 2.5).abs() < 1e-4, "v_hi={v_hi} (volle Wandhoehe)");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn ansicht_zeigt_fensterrahmen_sichtbar_und_durchblick_bei_dahinterliegender_kante() {
|
||||||
|
// Wand mit Fenster (Index 0) + eine zweite Wand DAHINTER (Index 1,
|
||||||
|
// ohne Oeffnung), die teils hinter dem Fenster, teils hinter der
|
||||||
|
// geschlossenen Wandflaeche steht. Ansicht von vorne (Blick +Y, Wand-
|
||||||
|
// achse (0,0)->(4,0) steht dabei u-parallel -> Elevations-Fall, siehe
|
||||||
|
// Moduldoc "Oeffnungen").
|
||||||
|
let w0 = wall_with_window();
|
||||||
|
let w1 = WallInput {
|
||||||
|
start: [2.0, 1.0],
|
||||||
|
end: [4.0, 1.0],
|
||||||
|
thickness: 0.2,
|
||||||
|
height: 2.5,
|
||||||
|
base_elevation: 0.0,
|
||||||
|
color: [0.8, 0.8, 0.8],
|
||||||
|
openings: vec![],
|
||||||
|
};
|
||||||
|
// Blick von y = -2 nach +Y: beide Waende (y=0 bzw. y=1) liegen dahinter.
|
||||||
|
let plane = SectionPlane::looking_plus_y(-2.0);
|
||||||
|
let out = cut_section(&plane, &[w0, w1], &[]);
|
||||||
|
|
||||||
|
// -- Fensterrahmen (Wand 0) ist vollstaendig sichtbar (nichts davor). --
|
||||||
|
// Leibungen bei u=-1.5 und u=-2.5 (Fenster-Enden), v von 1.0 (Bruestung)
|
||||||
|
// bis 2.0 (Sturzunterkante).
|
||||||
|
let jamb_visible = |u: f32| {
|
||||||
|
out.visible_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 0
|
||||||
|
&& (e.a[0] - u).abs() < 1e-3
|
||||||
|
&& (e.b[0] - u).abs() < 1e-3
|
||||||
|
&& (e.a[1].min(e.b[1]) - 1.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].max(e.b[1]) - 2.0).abs() < 1e-3
|
||||||
|
})
|
||||||
|
};
|
||||||
|
assert!(jamb_visible(-1.5), "linke Leibung (u=-1.5) muss sichtbar sein");
|
||||||
|
assert!(jamb_visible(-2.5), "rechte Leibung (u=-2.5) muss sichtbar sein");
|
||||||
|
// Bruestungs- und Sturzkante (horizontale Rahmenkanten bei v=1.0/2.0).
|
||||||
|
let horizontal_edge_visible = |v: f32| {
|
||||||
|
out.visible_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 0
|
||||||
|
&& (e.a[1] - v).abs() < 1e-3
|
||||||
|
&& (e.b[1] - v).abs() < 1e-3
|
||||||
|
&& (e.a[0].min(e.b[0]) + 2.5).abs() < 1e-3
|
||||||
|
&& (e.a[0].max(e.b[0]) + 1.5).abs() < 1e-3
|
||||||
|
})
|
||||||
|
};
|
||||||
|
assert!(horizontal_edge_visible(1.0), "Bruestungskante (v=1.0) muss sichtbar sein");
|
||||||
|
assert!(horizontal_edge_visible(2.0), "Sturzkante (v=2.0) muss sichtbar sein");
|
||||||
|
// Keine dieser vier Rahmenkanten darf (auch nur teilweise) hidden sein
|
||||||
|
// (Wand 0 steht als naechstes Bauteil vorne, nichts verdeckt sie).
|
||||||
|
let frame_hidden = out.hidden_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 0
|
||||||
|
&& ((e.a[0] + 1.5).abs() < 1e-3 || (e.a[0] + 2.5).abs() < 1e-3)
|
||||||
|
&& e.a[1] >= 1.0 - 1e-3
|
||||||
|
&& e.a[1] <= 2.0 + 1e-3
|
||||||
|
});
|
||||||
|
assert!(!frame_hidden, "Fensterrahmen darf nicht (teilweise) hidden sein");
|
||||||
|
|
||||||
|
// -- Durchblick: die (naeherliegende) Vorderkante von Wand 1 bei
|
||||||
|
// u=-2.0 (Wandstart x=2, liegt IM Fenster-u-Bereich -2.5..-1.5) muss
|
||||||
|
// GENAU im Fensterband (v: 1.0..2.0) sichtbar sein, darueber/darunter
|
||||||
|
// (verdeckt durch Bruestung/Sturz der Wand 0) hidden. --
|
||||||
|
let w1_visible_window = out.visible_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 1
|
||||||
|
&& (e.a[0] + 2.0).abs() < 1e-3
|
||||||
|
&& (e.b[0] + 2.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].min(e.b[1]) - 1.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].max(e.b[1]) - 2.0).abs() < 1e-3
|
||||||
|
});
|
||||||
|
assert!(w1_visible_window, "Durchblick auf Wand 1 durchs Fenster (v 1.0..2.0) fehlt");
|
||||||
|
|
||||||
|
let w1_hidden_below = out.hidden_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 1
|
||||||
|
&& (e.a[0] + 2.0).abs() < 1e-3
|
||||||
|
&& (e.b[0] + 2.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].min(e.b[1]) - 0.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].max(e.b[1]) - 1.0).abs() < 1e-3
|
||||||
|
});
|
||||||
|
assert!(w1_hidden_below, "Wand 1 unterhalb der Bruestung (v 0..1.0) muss hidden sein");
|
||||||
|
|
||||||
|
let w1_hidden_above = out.hidden_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 1
|
||||||
|
&& (e.a[0] + 2.0).abs() < 1e-3
|
||||||
|
&& (e.b[0] + 2.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].min(e.b[1]) - 2.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].max(e.b[1]) - 2.5).abs() < 1e-3
|
||||||
|
});
|
||||||
|
assert!(w1_hidden_above, "Wand 1 oberhalb des Sturzes (v 2.0..2.5) muss hidden sein");
|
||||||
|
|
||||||
|
// Kante hinter der GESCHLOSSENEN Wandflaeche (u=-4.0, ausserhalb des
|
||||||
|
// Fensters, x=4-Ende von Wand 1) bleibt vollstaendig verdeckt.
|
||||||
|
let w1_closed_wall_hidden = out.hidden_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall
|
||||||
|
&& e.component.index == 1
|
||||||
|
&& (e.a[0] + 4.0).abs() < 1e-3
|
||||||
|
&& (e.b[0] + 4.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].min(e.b[1]) - 0.0).abs() < 1e-3
|
||||||
|
&& (e.a[1].max(e.b[1]) - 2.5).abs() < 1e-3
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
w1_closed_wall_hidden,
|
||||||
|
"Kante hinter geschlossener Wandflaeche (u=-4.0) muss vollstaendig hidden sein"
|
||||||
|
);
|
||||||
|
let w1_closed_wall_visible = out.visible_edges.iter().any(|e| {
|
||||||
|
e.component.kind == ComponentKind::Wall && e.component.index == 1 && (e.a[0] + 4.0).abs() < 1e-3
|
||||||
|
});
|
||||||
|
assert!(!w1_closed_wall_visible, "u=-4.0 (hinter geschlossener Wand) darf nicht sichtbar sein");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,13 @@ 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`) und Oeffnungen kommen in spaeteren Milestones;
|
/// (`computeJoins`/`wallCorners`) kommt in einem spaeteren Milestone; M1 extrudiert
|
||||||
/// M1 extrudiert jede Wand als eigenstaendigen, an den Enden stumpf abgeschlossenen
|
/// jede Wand als eigenstaendigen, an den Enden stumpf abgeschlossenen Quader.
|
||||||
/// Quader.
|
/// `openings` (Tueren/Fenster, siehe `Opening`) werden vom Schnitt-Modul
|
||||||
|
/// (`section.rs`) ausgewertet; die Vollkoerper-Extrusion (`mesh.rs`) kennt sie
|
||||||
|
/// (noch) NICHT und extrudiert weiterhin die volle Wandflaeche — siehe
|
||||||
|
/// `mesh.rs`-Moduldoc und `docs/design/engine-section-pipeline.md` fuer diese
|
||||||
|
/// 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).
|
||||||
@@ -44,6 +48,30 @@ pub struct WallInput {
|
|||||||
/// 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.
|
||||||
#[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).
|
||||||
|
#[serde(default)]
|
||||||
|
pub openings: Vec<Opening>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Eine Oeffnung (Tuer/Fenster) in einer Wand: Intervall entlang der Wandachse
|
||||||
|
/// (Meter ab `WallInput::start`, 0..Wandlaenge) plus vertikale Ausdehnung relativ
|
||||||
|
/// zur Wand-Unterkante (`base_elevation`).
|
||||||
|
///
|
||||||
|
/// Vereinfachung des Web-`Opening` (siehe src/model/types.ts): keine eigene Id,
|
||||||
|
/// kein Oeffnungstyp (Tuer/Fenster) — nur die reine Geometrie, die Schnitt/Ansicht
|
||||||
|
/// brauchen. Eine Tuer ist der Sonderfall `sill == 0.0` (keine Bruestungsmauer).
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub struct Opening {
|
||||||
|
/// Start des Oeffnungs-Intervalls entlang der Wandachse (Meter ab `start`).
|
||||||
|
pub from: f32,
|
||||||
|
/// Ende des Oeffnungs-Intervalls entlang der Wandachse (Meter ab `start`).
|
||||||
|
pub to: f32,
|
||||||
|
/// Bruestungshoehe: Unterkante der Oeffnung relativ zu `base_elevation`
|
||||||
|
/// (Meter). 0 = Tuer (keine Bruestungsmauer darunter).
|
||||||
|
#[serde(default)]
|
||||||
|
pub sill: f32,
|
||||||
|
/// Hoehe der Oeffnung (vertikale Ausdehnung ab `sill`), Meter.
|
||||||
|
pub height: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_wall_color() -> Rgb {
|
fn default_wall_color() -> Rgb {
|
||||||
|
|||||||
Reference in New Issue
Block a user