Decken-Aussparungen: Treppenauge/Schacht in 2D und 3D (Brücken-Trick)

Ceiling.openings (e469a93) wird jetzt gerendert. Kern: mergeHoles() fügt
Löcher über schmale Brücken (1e-4) in den Aussenring ein → EIN einfacher
Polygonring, paritätskorrekt für SVG-Fill, Scanline-Hatch, Ear-Clipping
(render3d) und Punkt-in-Polygon-Pick — kein Loch-Support in den Primitiven
nötig. Loch-Orientierung gegenläufig (signedArea), Sichtbarkeitstest für die
Brücke, mehrere Löcher iterativ.

- 2D (addCeilingPoche): Poché-Ring mit Löchern; Brückenkanten via
  noStrokeEdges unsichtbar, Lochränder werden als Ringkanten gestrichen
  (wand-footprint-geclippt wie der Aussenumriss)
- 3D (emitSlabs): derselbe Ring für alle Schichten; Klick ins Loch trifft
  die Decke nicht (Pick paritätskorrekt)
- Ohne openings byte-identischer Codepfad (Regressionstests)
+11 Tests (Ring-Einfachheit, Shoelace-Flächen, 2-Loch-Fall, Regressionen).
684/684 grün. 3D visuell in Tauri zu prüfen.
This commit is contained in:
2026-07-10 18:37:12 +02:00
parent 1bba149f72
commit b290120195
6 changed files with 508 additions and 9 deletions
+22 -8
View File
@@ -45,6 +45,7 @@ import {
wallTypeThickness,
} from "../model/types";
import { columnFootprint } from "../geometry/column";
import { mergeHoles } from "../geometry/polygonHoles";
import { roofGeometry } from "../geometry/roof";
import { effectiveOverrides, hasOverrides } from "../overrides/engine";
import { evaluateRoom, siaLabel } from "../geometry/roomArea";
@@ -2466,6 +2467,13 @@ function addCeilingPoche(
): void {
const pts = ceiling.outline;
if (pts.length < 3) return;
// Aussparungen (Treppenauge/Schacht) über den Brücken-Trick in EINEN
// einfachen Poché-Ring einfügen: kein `openings` ⇒ `ring === pts` und
// `bridgeEdges` leer (exakt heutiger Codepfad, siehe {@link mergeHoles}).
const ceilingHoles = ceiling.openings ?? [];
const { ring, bridgeEdges } = ceilingHoles.length
? mergeHoles(pts, ceilingHoles)
: { ring: pts, bridgeEdges: [] as number[] };
const wt = getCeilingType(project, ceiling);
const comp = wt.layers.length > 0 ? getComponent(project, wt.layers[0].componentId) : null;
const stroke = ceiling.color ?? POCHE_STROKE;
@@ -2500,23 +2508,29 @@ function addCeilingPoche(
// Trägt weiterhin die `ceilingId` für die Links-Klick-Auswahl der Fläche.
out.push({
kind: "polygon",
pts,
pts: ring,
fill: comp ? pocheFill(hatch.pattern) : "none",
stroke: "none",
strokeWidthMm: 0,
hatch,
greyed,
ceilingId,
...(bridgeEdges.length ? { noStrokeEdges: bridgeEdges } : {}),
});
// Kräftige Umrisslinie NUR dort, wo keine Wand die Decke verdeckt: jede
// Umriss-Kante gegen die Wand-Footprints clippen und die freien Teilstücke als
// Linien ausgeben. Liegt die Decke deckungsgleich unter den Wänden (Regelfall:
// Deckenumriss = Gebäude-/Wand-Mittellinien), entfällt der Umriss ganz; nur
// Überstände (Vordach/Balkon jenseits der Wand) bleiben sichtbar.
const n = pts.length;
// Umriss-Kante (inkl. Lochränder, die als Teil des Rings automatisch
// mitgestrichen werden) gegen die Wand-Footprints clippen und die freien
// Teilstücke als Linien ausgeben. Liegt die Decke deckungsgleich unter den
// Wänden (Regelfall: Deckenumriss = Gebäude-/Wand-Mittellinien), entfällt
// der Umriss ganz; nur Überstände (Vordach/Balkon jenseits der Wand)
// bleiben sichtbar. Brückenkanten (künstliche Verbindung Aussenring↔Loch)
// werden NICHT gestrichen.
const n = ring.length;
const bridgeSet = new Set(bridgeEdges);
for (let i = 0; i < n; i++) {
const a = pts[i];
const b = pts[(i + 1) % n];
if (bridgeSet.has(i)) continue;
const a = ring[i];
const b = ring[(i + 1) % n];
const holes: Array<[number, number]> = [];
for (const fp of wallFootprints) {
const iv = segmentInsideWallBand(a, b, fp);