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
+111 -20
View File
@@ -11,7 +11,7 @@
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import type { Opening, Project, Vec2, Wall } from "../model/types";
import type { Opening, Project, SashDef, Vec2, Wall } from "../model/types";
import { getWallType, wallTypeThickness } from "../model/types";
import { add, along, leftNormal, normalize, scale, sub } from "../model/geometry";
import { wallReferenceOffset, wallVerticalExtent } from "../model/wall";
@@ -168,29 +168,93 @@ export function doorSymbol(wall: Wall, o: Opening): DoorSymbol | null {
export interface WindowSymbol {
/** Rahmen-Rechteck (volle Dicke) als Ecken. */
frame: Vec2[];
/** Glaslinien-Segmente quer über die Lücke (12 parallele Linien). */
/** Glaslinien-Segmente quer über die Lücke (13 parallele Linien). */
glassLines: [Vec2, Vec2][];
/**
* Flügel-Mittelpfosten (`wingCount 1` Querlinien), jeweils von einer
* Laibungsseite zur anderen (parallel zu den Laibungslinien, quer zur
* Öffnungsrichtung), gleichmäßig zwischen den beiden Pfosten verteilt. Bei
* `wingCount ≤ 1` leer. Entspricht den Mittelpfosten in `_make_oeffnung_pieces`
* des Rhino-Plugins (`oeff_fluegel`).
* Flügel-/Pfosten-Trennlinien, jeweils von einer Laibungsseite zur anderen
* (parallel zu den Laibungslinien, quer zur Öffnungsrichtung). Ist `sashes`
* (dritter Parameter von {@link windowSymbol}) gesetzt, ergeben sich die
* Positionen aus den Flügel-/Pfostenbreiten (siehe `sashSegments`); sonst
* (Alt-Fall) `wingCount 1` gleichmäßig verteilte Querlinien. Entspricht den
* Mittelpfosten in `_make_oeffnung_pieces` des Rhino-Plugins (`oeff_fluegel`).
*/
mullionLines: [Vec2, Vec2][];
/**
* Öffnungsandeutung je NICHT-festem Flügel (Dreh/Kipp/Drehkipp/Schiebe): eine
* einzelne Diagonale über die Flügelfläche, von der äusseren Ecke der
* Bandseite GEGENüberliegenden Seite (aussen) zur Bandseite (innen) — eine
* schlichte Andeutung der Öffnungsrichtung, analog zur Schwenkbogen-Andeutung
* bei Türen, aber ohne Bogen (P0-Minimalfassung). Nur befüllt, wenn `sashes`
* übergeben wird; leer im Alt-Fall (kein `sashes`).
*/
sashOpeningLines: [Vec2, Vec2][];
}
/** Ein Segment (Flügel/Pfosten) der Flügeleinteilung, mit aufgelöster Breite. */
interface SashSegment {
sash: SashDef;
/** Abstand vom Laibungsstart in Metern (0 = jambStart). */
from: number;
/** Abstand vom Laibungsstart in Metern. */
to: number;
}
/** Default-Pfostenbreite (Meter), wenn ein `kind:"pfosten"`-Eintrag keine `postWidth` angibt. */
const DEFAULT_POST_WIDTH = 0.06;
/**
* Löst eine Flügeleinteilung ({@link SashDef}-Liste) zu konkreten Segment-
* Intervallen (Meter, gemessen ab Laibungsstart) über die gegebene
* Gesamtbreite auf: `kind:"pfosten"` und Flügel mit `autoWidth === false`
* erhalten ihre feste Breite (`postWidth` bzw. `width`), die restlichen
* (`autoWidth`, Default) teilen sich die verbleibende Breite gleichmäßig.
* Degeneriert bei GLEICHMÄSSIGEN Flügeln (alle `autoWidth`, keine Pfosten) zur
* bisherigen `wingCount`-Gleichverteilung (Regressionssicherheit).
*/
function sashSegments(sashes: SashDef[], totalWidth: number): SashSegment[] {
if (sashes.length === 0) return [];
let fixedSum = 0;
let autoCount = 0;
for (const s of sashes) {
if (s.kind === "pfosten") fixedSum += Math.max(0, s.postWidth ?? DEFAULT_POST_WIDTH);
else if (s.autoWidth === false) fixedSum += Math.max(0, s.width ?? 0);
else autoCount++;
}
const remaining = Math.max(0, totalWidth - fixedSum);
const autoWidth = autoCount > 0 ? remaining / autoCount : 0;
const segs: SashSegment[] = [];
let cursor = 0;
for (const s of sashes) {
const w =
s.kind === "pfosten"
? Math.max(0, s.postWidth ?? DEFAULT_POST_WIDTH)
: s.autoWidth === false
? Math.max(0, s.width ?? 0)
: autoWidth;
segs.push({ sash: s, from: cursor, to: cursor + w });
cursor += w;
}
return segs;
}
/**
* Löst die Fenster-Symbolik auf: ein Rahmen-Rechteck über die Lücke (volle
* Wanddicke) plus 12 Glaslinien (dünne parallele Linien in der Wandmitte,
* `count` steuert die Anzahl je Detailgrad) plus optionale Flügel-Mittelpfosten
* (`wingCount 1` Querlinien).
* Wanddicke) plus 13 Glaslinien (dünne parallele Linien in der Wandmitte,
* `glassCount` steuert die Anzahl) plus optionale Flügel-/Pfosten-Trennlinien
* und Öffnungsandeutungen je Flügel.
*
* `sashes` (optional) ist die aufgelöste Flügeltabelle (siehe
* `sashesOfWindowType`) EINES referenzierten Fenstertyps. Fehlt sie (kein Typ
* referenziert/auflösbar), bleibt die Mittelpfosten-Berechnung beim
* BISHERIGEN Verhalten — `o.wingCount` gleichmäßig verteilt, KEINE
* Öffnungsandeutungen — exakt wie vor dieser Erweiterung (Rückwärtskompatibilität).
*/
export function windowSymbol(
project: Project,
wall: Wall,
o: Opening,
glassCount: number,
sashes?: SashDef[],
): WindowSymbol | null {
const jambs = openingJambs(wall, o);
if (!jambs) return null;
@@ -214,21 +278,48 @@ export function windowSymbol(
const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2);
glassLines.push([add(jambStart, scale(n, off)), add(jambEnd, scale(n, off))]);
}
// Flügel-Mittelpfosten: (wingCount 1) Querlinien gleichmäßig zwischen den
// Pfosten. Jede Linie läuft quer über die Öffnung (n-Richtung), auf ~60 % der
// Mauerdicke symmetrisch um die Wandmitte (Referenz-Offset) — analog Rhino, wo
// die Pfosten x_mid = inner_l + span·(i/fluegel) über die Rahmentiefe sitzen.
const mullionLines: [Vec2, Vec2][] = [];
const wings = Math.max(1, Math.min(4, Math.round(o.wingCount ?? 1)));
if (wings > 1) {
const sashOpeningLines: [Vec2, Vec2][] = [];
if (sashes && sashes.length > 0) {
// Neuer Weg: Trennlinien + Öffnungsandeutungen aus der Flügeltabelle.
const axisWidth = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y);
const segs = sashSegments(sashes, axisWidth);
const postDepth = total * 0.6;
const pOuter = refOff + postDepth / 2;
const pInner = refOff - postDepth / 2;
for (let i = 1; i < wings; i++) {
const t = i / wings; // gleichmäßige Teilung entlang der Achse
const onAxis = add(jambStart, scale(sub(jambEnd, jambStart), t));
for (let i = 0; i < segs.length - 1; i++) {
const onAxis = along(jambStart, jambEnd, segs[i].to);
mullionLines.push([add(onAxis, scale(n, pOuter)), add(onAxis, scale(n, pInner))]);
}
for (const seg of segs) {
if (seg.sash.kind !== "fluegel") continue;
const kind = seg.sash.opening;
if (!kind || kind === "fest") continue;
// Bandseite "left" = Scharnier am Laibungsstart (jambStart-Seite), "right"
// = am Laibungsende. Die Diagonale läuft von der GEGENüberliegenden
// Aussenecke zur Bandecke (innen) — schlichte Andeutung, kein Schwenkbogen.
const hingeAtStart = (seg.sash.hingeSide ?? "left") === "left";
const hingeAxis = along(jambStart, jambEnd, hingeAtStart ? seg.from : seg.to);
const farAxis = along(jambStart, jambEnd, hingeAtStart ? seg.to : seg.from);
sashOpeningLines.push([add(farAxis, scale(n, outer)), add(hingeAxis, scale(n, inner))]);
}
} else {
// Alt-Fall (kein sashes): (wingCount 1) Querlinien gleichmäßig zwischen
// den Pfosten. Jede Linie läuft quer über die Öffnung (n-Richtung), auf
// ~60 % der Mauerdicke symmetrisch um die Wandmitte (Referenz-Offset) —
// analog Rhino, wo die Pfosten x_mid = inner_l + span·(i/fluegel) über die
// Rahmentiefe sitzen. UNVERÄNDERT gegenüber dem bisherigen Verhalten.
const wings = Math.max(1, Math.min(4, Math.round(o.wingCount ?? 1)));
if (wings > 1) {
const postDepth = total * 0.6;
const pOuter = refOff + postDepth / 2;
const pInner = refOff - postDepth / 2;
for (let i = 1; i < wings; i++) {
const t = i / wings; // gleichmäßige Teilung entlang der Achse
const onAxis = add(jambStart, scale(sub(jambEnd, jambStart), t));
mullionLines.push([add(onAxis, scale(n, pOuter)), add(onAxis, scale(n, pInner))]);
}
}
}
return { frame, glassLines, mullionLines };
return { frame, glassLines, mullionLines, sashOpeningLines };
}