Fenster-Renderer: Flügeleinteilung, Öffnungslinien, Verglasung, Rollladenkasten

Der 2D/3D-Renderer konsumiert die neuen Fenster-Modellfelder (VW-Studie §6):

- opening.ts: windowSymbol nimmt die Flügeltabelle (sashes); Trennlinien aus
  Flügel-/Pfostenbreiten statt gleichmässiger wingCount-Teilung + Öffnungs-
  andeutung (Diagonale zur Bandseite) je nicht-festem Flügel. Ohne sashes
  exakt das bisherige Verhalten (Regressionssicherheit).
- generatePlan: glazingPanesOf steuert die Glaslinien-Anzahl (mittel/fein),
  grob bleibt bei einer; gestrichelte Rollladenkasten-Kontur über der Öffnung.
- toWalls3d: glassPanesForOpening zeichnet glazingPanes dünne, in Wandrichtung
  versetzte Scheiben mit Luftraum (Ein-/Zwei-/Dreifachverglasung); panes=1 =
  exakt die alte Einzelscheibe.

Verhaltensänderung: window-standard (zweifach) zeigt 3D nun 2 Scheiben statt
einer — 2 Bestandstests entsprechend angepasst (+ Nicht-Überlappungs-Check).
+13 Primitiv-Tests (generatePlan.windowFields, toWalls3d). 659/659 grün.
This commit is contained in:
2026-07-10 01:17:35 +02:00
parent c734802555
commit 2e13ec3097
5 changed files with 537 additions and 61 deletions
+81 -5
View File
@@ -35,9 +35,11 @@ import {
getLineStyle,
getWallType,
getWindowType,
glazingPanesOf,
openingLabel,
openingsOfWall,
roomsOfFloor,
sashesOfWindowType,
stairLabel,
stairsOfFloor,
wallTypeThickness,
@@ -2211,9 +2213,19 @@ function addOpeningSymbol(
return;
}
// Fenster: Rahmen (mittel/fein) + Glaslinie(n) je Detailgrad.
const glassCount = detail === "fein" ? 2 : 1;
const sym = windowSymbol(project, wall, o, glassCount);
// Fenster: Rahmen (mittel/fein) + Glaslinie(n) je Detailgrad + Fenstertyp.
const wt = getWindowType(project, o);
// 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 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);
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
@@ -2243,8 +2255,8 @@ function addOpeningSymbol(
greyed,
openingId: o.id,
});
// Flügel-Mittelpfosten (wingCount 1) — reine Querlinien innerhalb des
// Rahmens (kein Strichmuster). Nur mittel/fein; entfällt bei grob/schema.
// Flügel-/Pfosten-Trennlinien — reine Querlinien innerhalb des Rahmens
// (kein Strichmuster). Nur mittel/fein; entfällt bei grob/schema.
for (const [a, b] of sym.mullionLines) {
out.push({
kind: "line",
@@ -2257,6 +2269,21 @@ 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,
});
}
}
for (const [a, b] of sym.glassLines) {
out.push({
@@ -2318,6 +2345,55 @@ function addOpeningSymbol(
});
}
}
// Rollladen-/Sonnenschutzkasten (P0-Minimalfassung, reine 2D-Kontur): ein
// gestricheltes Rechteck ÜBER der Öffnung, ausserhalb der Wand-Aussenfläche —
// Breite = `shading.box.width` (0/fehlt ⇒ Öffnungsbreite), die zweite
// Rechteckseite = `shading.box.height`. Die 3D-Tiefe (`box.depth`) bleibt für
// eine spätere 3D-Box reserviert (Fokus hier bewusst 2D, s. Aufgabenstellung).
// Gestrichelt wie die übrigen Überkopf-Projektionen (Sturz/Oberlicht) — der
// Kasten liegt oberhalb der Schnittebene und wird nur angedeutet.
if (wt?.shading?.create) {
const jambs = openingJambs(wall, o);
if (jambs) {
const { u, n: nrm } = wallAxisFrame(wall);
const total = wallTypeThickness(getWallType(project, wall));
const refOff = wallReferenceOffset(wall, total);
const outerFace = refOff + total / 2;
const axisWidth = Math.hypot(
jambs.jambEnd.x - jambs.jambStart.x,
jambs.jambEnd.y - jambs.jambStart.y,
);
const boxWidth = wt.shading.box.width > 0 ? wt.shading.box.width : axisWidth;
const boxHeight = Math.max(0, wt.shading.box.height);
const center = add(jambs.jambStart, scale(u, axisWidth / 2));
const half = boxWidth / 2;
const c1 = add(center, scale(u, -half));
const c2 = add(center, scale(u, half));
const p1 = add(c1, scale(nrm, outerFace));
const p2 = add(c2, scale(nrm, outerFace));
const p3 = add(c2, scale(nrm, outerFace + boxHeight));
const p4 = add(c1, scale(nrm, outerFace + boxHeight));
const edges: [Vec2, Vec2][] = [
[p1, p2],
[p2, p3],
[p3, p4],
[p4, p1],
];
for (const [a, b] of edges) {
out.push({
kind: "line",
a,
b,
cls: "window-shading",
weightMm: SYMBOL_HAIRLINE_MM,
dash: OVERHEAD_DASH,
color: o.color ?? POCHE_STROKE,
greyed,
openingId: o.id,
});
}
}
}
}
/** Schraffur-Platzhalter „ohne" (für reine Umriss-/Sammelflächen). */