Fenster: echte Schachtelung Blendrahmen → Flügelrahmen → Glas (2D+3D)

Öffenbare vs. feste Flügel sind jetzt unterscheidbar, und die Einbaulage
(Schichteinzug) wirkt auch im 2D-Grundriss.

2D (windowSymbol/generatePlan):
- Der reiche Pfad (typisiertes Fenster) zeichnet den Blendrahmen an der REALEN
  Einbaulage (insetFace/insetFromFace, vorher ignoriert), je Flügel einen
  Flügelrahmen (window-sash) nur wenn ÖFFENBAR, und das Glas darin. Feste
  Flügel: Glas direkt im Blendrahmen.
- Die alte Diagonale quer über die Box (sah aus wie durchgestrichen) entfernt —
  Öffenbarkeit wird jetzt über den Flügelrahmen ausgedrückt.
- Alt-Pfad (kein Typ) byte-identisch → kernel2d-Parität unberührt.

3D (frameMeshesForOpening/resolveOpeningFrame):
- Je öffenbarem Flügel ein Flügelrahmen-Ring (4 Riegel) im Blendrahmen (fein).
- Flügeltabelle konsistent aus explizitem sashes bzw. effektivem wingCount
  (Instanz-Override wirkt jetzt auch auf Flügelrahmen); wingCount daraus abgeleitet.

Tests angepasst/ergänzt (fest vs. öffenbar, Zählungen). 663/663 grün.
This commit is contained in:
2026-07-10 01:47:27 +02:00
parent d131683304
commit 9a38636bf2
5 changed files with 332 additions and 77 deletions
+27 -16
View File
@@ -2218,14 +2218,22 @@ function addOpeningSymbol(
// Flügeltabelle NUR übergeben, wenn ein Fenstertyp auflösbar ist — fehlt er
// (kein `typeId`/unbekannt), bleibt `windowSymbol` beim reinen
// `o.wingCount`-Alt-Verhalten (siehe dortiger Kommentar, Regressionssicherheit).
const sashes = wt ? sashesOfWindowType(wt) : undefined;
const spec = wt
? {
sashes: sashesOfWindowType(wt),
frameWidth: wt.frameWidth ?? 0.06,
frameDepth: wt.frameDepth,
insetFromFace: wt.insetFromFace,
insetFace: wt.insetFace,
}
: undefined;
const detailGlassCount = detail === "fein" ? 2 : 1;
// Verglasungs-Scheibenzahl des Typs (glazingPanesOf) hebt die Detailgrad-
// Vorgabe bei mittel/fein an (mind. so viele Glaslinien); "grob" bleibt bei
// einer Glaslinie, unabhängig vom Typ (bleibt die schlichteste Stufe).
const glassCount =
detail === "grob" ? detailGlassCount : Math.max(detailGlassCount, wt ? glazingPanesOf(wt) : 0);
const sym = windowSymbol(project, wall, o, glassCount, sashes);
const sym = windowSymbol(project, wall, o, glassCount, spec);
if (!sym) return;
// Unsichtbares Pick-Polygon über der Öffnungslücke — macht das Fenster über
// die ganze Fläche selektierbar, auch bei „grob" (nur eine Glaslinie, sonst
@@ -2269,20 +2277,23 @@ function addOpeningSymbol(
openingId: o.id,
});
}
// Öffnungsandeutung je NICHT-festem Flügel (Dreh/Kipp/Drehkipp/Schiebe) —
// schlichte Diagonale zur Bandseite, nur wenn ein Fenstertyp mit
// Flügeltabelle referenziert ist (siehe windowSymbol/sashOpeningLines).
for (const [a, b] of sym.sashOpeningLines) {
out.push({
kind: "line",
a,
b,
cls: "window-opening",
weightMm: SYMBOL_HAIRLINE_MM,
color: o.color ?? POCHE_STROKE,
greyed,
openingId: o.id,
});
// Flügelrahmen (Flügelkontur) je ÖFFENBAREM Flügel — Rechteck (4 Kanten)
// innerhalb des Blendrahmens (Schachtelung Blendrahmen → Flügelrahmen →
// Glas). Feste Flügel bekommen keinen; so liest man fest vs. öffenbar
// direkt im Plan.
for (const pts of sym.sashFrames) {
for (let i = 0; i < pts.length; i++) {
out.push({
kind: "line",
a: pts[i],
b: pts[(i + 1) % pts.length],
cls: "window-sash",
weightMm: SYMBOL_HAIRLINE_MM,
color: o.color ?? POCHE_STROKE,
greyed,
openingId: o.id,
});
}
}
}
for (const [a, b] of sym.glassLines) {