Files
DOSSIER-STANDALONE/docs/design/engine-section-pipeline.md
karim f95df75ea1 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.
2026-07-03 08:36:35 +02:00

11 KiB
Raw Permalink Blame History

Schnitt/Ansicht-Pipeline: analytische Prismen-Projektion (render3d::section)

Gegenstueck zum OCCT-WASM-HLR-Spike (docs/welle-c-hlr-spike/FEASIBILITY.md): statt eines generischen CAD-Kernels nutzt dieser Ansatz eine Invariante des Modells, um Schnitt/Ansicht rein analytisch (kein Hidden-Line-Removal-Solver) zu berechnen. Implementiert in src-tauri/render3d/src/section.rs.

Ansatz

Jedes Bauteil in diesem Modell ist ein Prisma: ein 2D-Grundriss-Fussabdruck- Polygon (Wand-Band bzw. Decken-Umriss, siehe mesh.rs), konstant extrudiert ueber ein Hoehenintervall [z0, z1]. Diese Einschraenkung — konstanter Querschnitt ueber die gesamte Hoehe — macht die Schnittgeometrie trivial im Vergleich zu generischem HLR:

  • Schnitt einer vertikalen Ebene mit einem Prisma = 2D-Geraden/Polygon- Clipping IM GRUNDRISS (die Schnittebene projiziert im Grundriss auf eine Linie) → ein oder mehrere u-Intervalle, in denen die Linie das Fussabdruck- Polygon durchquert. Jedes Intervall × [z0, z1] ist das Cut-Rechteck. Da die Hoehe unabhaengig von der Grundriss-Position ist, ist das Ergebnis immer ein Rechteck, nie ein Trapez — auch bei diagonalen Wandachsen.
  • Projektion/Ansicht (Kanten hinter der Ebene, in Blickrichtung) reduziert sich auf ein 2D-Sichtbarkeitsproblem im Grundriss kombiniert mit einem Hoehen-Ueberlappungstest: da alle Seitenflaechen der Prismen vertikal sind, genuegt ein Sichtstrahl-Test im Grundriss (verdeckt ein naeheres Prisma- Fussabdruckpolygon die Sichtlinie?) plus Ueberlappung der Hoehenintervalle.

Das ersetzt einen 66-MB-WASM-CAD-Kernel durch closed-form Arithmetik in reinem Rust, ohne zusaetzliches Laufzeitgewicht ueber render3d hinaus.

SectionOutput-Format

Modul: render3d::section. Alle Groessen in Metern.

pub struct SectionPlane { pub point: [f32; 3], pub normal: [f32; 3] }

pub struct SectionOutput {
    pub cut_polygons: Vec<CutPolygon>,   // JSON: "cutPolygons"
    pub visible_edges: Vec<SectionEdge>, // JSON: "visibleEdges"
    pub hidden_edges: Vec<SectionEdge>,  // JSON: "hiddenEdges"
}

pub struct CutPolygon { pub component: ComponentRef, pub color: Rgb, pub pts: Vec<[f32; 2]> }
pub struct SectionEdge { pub component: ComponentRef, pub a: [f32; 2], pub b: [f32; 2] }
pub struct ComponentRef { pub kind: ComponentKind /* Wall | Slab */, pub index: usize }

Koordinatensystem der Ausgabe (u, v):

  • Ursprung: SectionPlane::point, projiziert.
  • u (horizontal): Strecke entlang der Schnittebene, senkrecht zur Blickrichtung, berechnet als normalize(cross(normal, world_up)) — dieselbe rechtshaendige Konvention wie math::look_ats right-Vektor. Bei den vier Standard-Konstruktoren (looking_plus_x/minus_x/plus_y/minus_y) ist u direkt die jeweils andere Grundriss-Achse.
  • v (vertikal, "Hoehe"): v = world.y, ABSOLUT. Diese Codebasis ist durchgaengig Y-up (types.rs/mesh.rs/math.rs: „world.y = Hoehe"); v folgt bewusst dieser etablierten Konvention statt einer wortwoertlichen „world.z"-Lesart, um modulübergreifend konsistent zu bleiben.
  • Nur vertikale Schnittebenen (Normale ohne Hoehen-Komponente) sind unterstuetzt — Grundriss-/Horizontalschnitte bleiben Sache der bestehenden 2D-Plan-Pipeline.

cut_polygons sind bei diesem Modell immer Rechtecke (siehe oben), aber als generischer Punktering abgelegt — kompatibel zu render2d::types::FillPolygon (pts: Vec<Point>), dem vorgesehenen Zielformat fuer die spaetere 1:1- 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

  • Keine Wandknoten-Verschneidung (T-/X-Stoesse). Wie in mesh.rs (M1-Stand) werden Waende als eigenstaendige, stumpf abgeschlossene Quader behandelt, die sich an Ecken UEBERLAPPEN statt sich zu vereinen (kein Miter-Join). Der Schnitt-Extraktor erbt das: an einem L-/T-Knoten kann eine Wand als „in die andere eingebettet" verdeckt erscheinen (im Testmodul section::tests bewusst als reales, erwartetes Verhalten dokumentiert und geprueft — kein Bug dieses Moduls, sondern ein Artefakt der fehlenden Verschneidungslogik weiter oben in der Pipeline).
  • Oeffnungen sind NICHT in der Vollkoerper-Extrusion (mesh.rs) nachgezogen. wall_prism/wall_cut_rectangles/die Verdeckung in section.rs werten WallInput::openings vollstaendig aus (siehe Abschnitt "Oeffnungen" oben), aber mesh::extrude_wall extrudiert weiterhin die volle 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 eigene Id; ComponentRef referenziert daher nur (Art, Index im Eingabe-Array) + die rohe Albedo-Farbe als Material-Platzhalter. Sobald ein echtes Ressourcen-/Material-System existiert, sollte ComponentRef auf eine stabile Id umgestellt werden (Indizes sind nicht stabil ueber Modell-Edits).
  • Verdeckungstest nutzt Bounding-Boxen, nicht die exakte Fussabdruckform. Fuer Wand-Baender und (i. d. R. konvexe) Deckenumrisse ist das exakt; bei stark konkaven oder diagonalen Grundrissen kann es zu Ueberverdeckung fuehren (ein Prisma blockiert dann auch Bereiche seiner eigenen „Nischen").
  • Kein generisches HLR. Der Ansatz funktioniert NUR, weil alle Bauteile Prismen mit konstantem Querschnitt sind. Fuer echte gekrümmte oder nicht- prismatische Geometrie (Bogenwaende, Freiformdaecher, …) waere er nicht anwendbar — dafuer bliebe der OCCT-Weg (oder eine eigene, generischere HLR-Implementierung) die Referenz.
  • Keine robuste Sonderfallbehandlung fuer Vertices exakt auf der Schnittlinie (Toleranz-basiert, kein Tie-Breaking/Pertubation) — die Testszenarien vermeiden diesen Fall bewusst.

Vergleich zum OCCT-WASM-Spike

OCCT-WASM (docs/welle-c-hlr-spike) Dieser Ansatz (render3d::section)
Verfahren Generisches HLR (HLRAppli_ReflectLines) Analytische Prismen-Projektion
Anwendbarkeit Beliebige BREP-Geometrie Nur Prismen (konstanter Querschnitt über Höhe)
Zusaetzliches Gewicht ~62,8 MB WASM (~19,6 MB gzip), separater Lazy-Chunk Keins — reines Rust in render3d, kein weiteres WASM-Asset
Modul-Init ~450 ms (Browser) / ~680 ms (Node) Kein Initialisierungsschritt (kein Fremd-Modul zu laden)
Rechenzeit (L-Wand + Platte) ~21 ms (reiner HLR-Lauf, gemessen im Browser) Nicht separat gemessen (kein Millisekunden-Timer im Test); die Operationen sind reine Vektor-/Intervall-Arithmetik über wenige Kanten (O(Anzahl-Prismen × Kanten-pro-Prisma) mit kleinen Konstanten) und liegen der Groessenordnung nach klar unter 1 ms fuer Szenen dieser Groesse — eine belastbare Messung steht noch aus
Sichtbarkeit (verdeckte Kanten) Exakt (echter HLR-Solver) Naeherung über Bounding-Boxen im Grundriss + Hoehenintervall-Ueberlappung; exakt fuer achsparallele/konvexe Fussabdruecke
Reifegrad Isolierter Spike, nicht verdrahtet Isolierter Spike (dieses Modul), nicht in render2d/die Plan-Ansicht verdrahtet

Fazit: Fuer den ueberwiegenden Regelfall dieses Projekts (Waende, Decken — alles Prismen) ist die analytische Loesung der pragmatischere Weg: kein zusaetzliches WASM-Gewicht, keine Fremd-Bibliothek, headless testbar wie der Rest von render3d. Der OCCT-Weg bleibt die Referenz, falls/sobald echte generische Volumenkoerper (Booleans, gekrümmte Flaechen) ins Modell kommen.