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:
+111
-20
@@ -11,7 +11,7 @@
|
|||||||
//
|
//
|
||||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
// 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 { getWallType, wallTypeThickness } from "../model/types";
|
||||||
import { add, along, leftNormal, normalize, scale, sub } from "../model/geometry";
|
import { add, along, leftNormal, normalize, scale, sub } from "../model/geometry";
|
||||||
import { wallReferenceOffset, wallVerticalExtent } from "../model/wall";
|
import { wallReferenceOffset, wallVerticalExtent } from "../model/wall";
|
||||||
@@ -168,29 +168,93 @@ export function doorSymbol(wall: Wall, o: Opening): DoorSymbol | null {
|
|||||||
export interface WindowSymbol {
|
export interface WindowSymbol {
|
||||||
/** Rahmen-Rechteck (volle Dicke) als Ecken. */
|
/** Rahmen-Rechteck (volle Dicke) als Ecken. */
|
||||||
frame: Vec2[];
|
frame: Vec2[];
|
||||||
/** Glaslinien-Segmente quer über die Lücke (1–2 parallele Linien). */
|
/** Glaslinien-Segmente quer über die Lücke (1–3 parallele Linien). */
|
||||||
glassLines: [Vec2, Vec2][];
|
glassLines: [Vec2, Vec2][];
|
||||||
/**
|
/**
|
||||||
* Flügel-Mittelpfosten (`wingCount − 1` Querlinien), jeweils von einer
|
* Flügel-/Pfosten-Trennlinien, jeweils von einer Laibungsseite zur anderen
|
||||||
* Laibungsseite zur anderen (parallel zu den Laibungslinien, quer zur
|
* (parallel zu den Laibungslinien, quer zur Öffnungsrichtung). Ist `sashes`
|
||||||
* Öffnungsrichtung), gleichmäßig zwischen den beiden Pfosten verteilt. Bei
|
* (dritter Parameter von {@link windowSymbol}) gesetzt, ergeben sich die
|
||||||
* `wingCount ≤ 1` leer. Entspricht den Mittelpfosten in `_make_oeffnung_pieces`
|
* Positionen aus den Flügel-/Pfostenbreiten (siehe `sashSegments`); sonst
|
||||||
* des Rhino-Plugins (`oeff_fluegel`).
|
* (Alt-Fall) `wingCount − 1` gleichmäßig verteilte Querlinien. Entspricht den
|
||||||
|
* Mittelpfosten in `_make_oeffnung_pieces` des Rhino-Plugins (`oeff_fluegel`).
|
||||||
*/
|
*/
|
||||||
mullionLines: [Vec2, Vec2][];
|
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
|
* Löst die Fenster-Symbolik auf: ein Rahmen-Rechteck über die Lücke (volle
|
||||||
* Wanddicke) plus 1–2 Glaslinien (dünne parallele Linien in der Wandmitte,
|
* Wanddicke) plus 1–3 Glaslinien (dünne parallele Linien in der Wandmitte,
|
||||||
* `count` steuert die Anzahl je Detailgrad) plus optionale Flügel-Mittelpfosten
|
* `glassCount` steuert die Anzahl) plus optionale Flügel-/Pfosten-Trennlinien
|
||||||
* (`wingCount − 1` Querlinien).
|
* 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(
|
export function windowSymbol(
|
||||||
project: Project,
|
project: Project,
|
||||||
wall: Wall,
|
wall: Wall,
|
||||||
o: Opening,
|
o: Opening,
|
||||||
glassCount: number,
|
glassCount: number,
|
||||||
|
sashes?: SashDef[],
|
||||||
): WindowSymbol | null {
|
): WindowSymbol | null {
|
||||||
const jambs = openingJambs(wall, o);
|
const jambs = openingJambs(wall, o);
|
||||||
if (!jambs) return null;
|
if (!jambs) return null;
|
||||||
@@ -214,21 +278,48 @@ export function windowSymbol(
|
|||||||
const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2);
|
const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2);
|
||||||
glassLines.push([add(jambStart, scale(n, off)), add(jambEnd, scale(n, off))]);
|
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 mullionLines: [Vec2, Vec2][] = [];
|
||||||
const wings = Math.max(1, Math.min(4, Math.round(o.wingCount ?? 1)));
|
const sashOpeningLines: [Vec2, Vec2][] = [];
|
||||||
if (wings > 1) {
|
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 postDepth = total * 0.6;
|
||||||
const pOuter = refOff + postDepth / 2;
|
const pOuter = refOff + postDepth / 2;
|
||||||
const pInner = refOff - postDepth / 2;
|
const pInner = refOff - postDepth / 2;
|
||||||
for (let i = 1; i < wings; i++) {
|
for (let i = 0; i < segs.length - 1; i++) {
|
||||||
const t = i / wings; // gleichmäßige Teilung entlang der Achse
|
const onAxis = along(jambStart, jambEnd, segs[i].to);
|
||||||
const onAxis = add(jambStart, scale(sub(jambEnd, jambStart), t));
|
|
||||||
mullionLines.push([add(onAxis, scale(n, pOuter)), add(onAxis, scale(n, pInner))]);
|
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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,11 @@ import {
|
|||||||
getLineStyle,
|
getLineStyle,
|
||||||
getWallType,
|
getWallType,
|
||||||
getWindowType,
|
getWindowType,
|
||||||
|
glazingPanesOf,
|
||||||
openingLabel,
|
openingLabel,
|
||||||
openingsOfWall,
|
openingsOfWall,
|
||||||
roomsOfFloor,
|
roomsOfFloor,
|
||||||
|
sashesOfWindowType,
|
||||||
stairLabel,
|
stairLabel,
|
||||||
stairsOfFloor,
|
stairsOfFloor,
|
||||||
wallTypeThickness,
|
wallTypeThickness,
|
||||||
@@ -2211,9 +2213,19 @@ function addOpeningSymbol(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fenster: Rahmen (mittel/fein) + Glaslinie(n) je Detailgrad.
|
// Fenster: Rahmen (mittel/fein) + Glaslinie(n) je Detailgrad + Fenstertyp.
|
||||||
const glassCount = detail === "fein" ? 2 : 1;
|
const wt = getWindowType(project, o);
|
||||||
const sym = windowSymbol(project, wall, o, glassCount);
|
// 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;
|
if (!sym) return;
|
||||||
// Unsichtbares Pick-Polygon über der Öffnungslücke — macht das Fenster über
|
// Unsichtbares Pick-Polygon über der Öffnungslücke — macht das Fenster über
|
||||||
// die ganze Fläche selektierbar, auch bei „grob" (nur eine Glaslinie, sonst
|
// die ganze Fläche selektierbar, auch bei „grob" (nur eine Glaslinie, sonst
|
||||||
@@ -2243,8 +2255,8 @@ function addOpeningSymbol(
|
|||||||
greyed,
|
greyed,
|
||||||
openingId: o.id,
|
openingId: o.id,
|
||||||
});
|
});
|
||||||
// Flügel-Mittelpfosten (wingCount − 1) — reine Querlinien innerhalb des
|
// Flügel-/Pfosten-Trennlinien — reine Querlinien innerhalb des Rahmens
|
||||||
// Rahmens (kein Strichmuster). Nur mittel/fein; entfällt bei grob/schema.
|
// (kein Strichmuster). Nur mittel/fein; entfällt bei grob/schema.
|
||||||
for (const [a, b] of sym.mullionLines) {
|
for (const [a, b] of sym.mullionLines) {
|
||||||
out.push({
|
out.push({
|
||||||
kind: "line",
|
kind: "line",
|
||||||
@@ -2257,6 +2269,21 @@ function addOpeningSymbol(
|
|||||||
openingId: o.id,
|
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) {
|
for (const [a, b] of sym.glassLines) {
|
||||||
out.push({
|
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). */
|
/** Schraffur-Platzhalter „ohne" (für reine Umriss-/Sammelflächen). */
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Unit-Tests für die NEUEN Fenster-Modellfelder im Grundriss (generatePlan +
|
||||||
|
* geometry/opening.windowSymbol):
|
||||||
|
* • `WindowType.sashes` (Flügeltabelle) — ersetzt die gleichmäßige
|
||||||
|
* `wingCount`-Ableitung durch echte Flügel-/Pfostenbreiten, sobald ein
|
||||||
|
* Fenstertyp referenziert ist. Alt-Fall (keine `sashes`/kein `typeId`)
|
||||||
|
* bleibt UNVERÄNDERT (reines `Opening.wingCount`, siehe
|
||||||
|
* generatePlan.openings.test.ts).
|
||||||
|
* • `WindowType.glazingPanes` — steuert die Anzahl paralleler Glaslinien.
|
||||||
|
* • `WindowType.shading` — zeichnet eine zusätzliche, gestrichelte
|
||||||
|
* Kastenkontur über der Öffnung.
|
||||||
|
* • Öffnungsandeutung je Flügel (`SashDef.opening`/`hingeSide`).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import { generatePlan } from "./generatePlan";
|
||||||
|
import type { Opening, Project, Wall, WindowType } from "../model/types";
|
||||||
|
|
||||||
|
/** Minimalprojekt: eine Wand (Dicke 0.3 m, Achse mittig, horizontal entlang x)
|
||||||
|
* + eine Fenster-Öffnung + optionale Fenstertyp-Bibliothek. */
|
||||||
|
function project(opening: Opening, windowTypes?: WindowType[]): Project {
|
||||||
|
const wall: Wall = {
|
||||||
|
id: "W1",
|
||||||
|
type: "wall",
|
||||||
|
floorId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
start: { x: 0, y: 0 },
|
||||||
|
end: { x: 4, y: 0 },
|
||||||
|
wallTypeId: "aw",
|
||||||
|
height: 2.6,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
id: "t",
|
||||||
|
name: "T",
|
||||||
|
lineStyles: [{ id: "thin", name: "d", weight: 0.13, color: "#111", dash: null }],
|
||||||
|
hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }],
|
||||||
|
components: [{ id: "a", name: "A", color: "#d8d2c7", hatchId: "none", joinPriority: 10 }],
|
||||||
|
wallTypes: [{ id: "aw", name: "AW", layers: [{ componentId: "a", thickness: 0.3 }] }],
|
||||||
|
windowTypes,
|
||||||
|
drawingLevels: [
|
||||||
|
{
|
||||||
|
id: "eg",
|
||||||
|
name: "EG",
|
||||||
|
kind: "floor",
|
||||||
|
visible: true,
|
||||||
|
locked: false,
|
||||||
|
floorHeight: 2.6,
|
||||||
|
cutHeight: 1.0,
|
||||||
|
baseElevation: 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
layers: [
|
||||||
|
{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false },
|
||||||
|
{ code: "21", name: "Öffnungen", color: "#0a0a0a", lw: 0.3, visible: true, locked: false },
|
||||||
|
],
|
||||||
|
walls: [wall],
|
||||||
|
doors: [],
|
||||||
|
openings: [opening],
|
||||||
|
ceilings: [],
|
||||||
|
stairs: [],
|
||||||
|
rooms: [],
|
||||||
|
drawings2d: [],
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const visible = new Set(["20", "21"]);
|
||||||
|
|
||||||
|
/** Basis-Fenster (Brüstung 0, damit keine Sill-Linie mitzählt), position 1.0, width 1.2. */
|
||||||
|
const baseWindow: Opening = {
|
||||||
|
id: "F1",
|
||||||
|
type: "opening",
|
||||||
|
hostWallId: "W1",
|
||||||
|
categoryCode: "21",
|
||||||
|
kind: "window",
|
||||||
|
position: 1.0,
|
||||||
|
width: 1.2,
|
||||||
|
height: 1.2,
|
||||||
|
sillHeight: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Minimaler Fenstertyp (Grunddaten, Felder je Test überschrieben/ergänzt). */
|
||||||
|
const baseWindowType: WindowType = {
|
||||||
|
id: "wt1",
|
||||||
|
name: "Test-Fenster",
|
||||||
|
kind: "dreh",
|
||||||
|
wingCount: 1,
|
||||||
|
glazing: "einfach",
|
||||||
|
frameThickness: 0.06,
|
||||||
|
defaultSillHeight: 0.8,
|
||||||
|
defaultWidth: 1.2,
|
||||||
|
defaultHeight: 1.2,
|
||||||
|
};
|
||||||
|
|
||||||
|
function linesOfClass(p: Project, cls: string, detail: "grob" | "mittel" | "fein" = "mittel") {
|
||||||
|
return generatePlan(p, "eg", visible, undefined, detail).primitives.filter(
|
||||||
|
(pr) => pr.kind === "line" && pr.cls === cls,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("generatePlan — Flügeleinteilung aus sashes (Item 1)", () => {
|
||||||
|
it("Alt-Fall (kein typeId/sashes, nur wingCount=3) erzeugt genau 2 Pfostenlinien — Regression", () => {
|
||||||
|
const p = project({ ...baseWindow, wingCount: 3 });
|
||||||
|
expect(linesOfClass(p, "window-mullion").length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("WindowType MIT sashes (3 feste Flügelbreiten 0.4 m) erzeugt Pfostenlinien exakt an den Flügelgrenzen", () => {
|
||||||
|
// Öffnung 1.2 m breit, jambStart bei x=1.0 (position 1.0) -> jambEnd x=2.2.
|
||||||
|
// 3 Flügel à 0.4 m (autoWidth:false) -> Grenzen bei x=1.4 und x=1.8.
|
||||||
|
const wt: WindowType = {
|
||||||
|
...baseWindowType,
|
||||||
|
sashes: [
|
||||||
|
{ kind: "fluegel", autoWidth: false, width: 0.4, opening: "dreh", hingeSide: "left" },
|
||||||
|
{ kind: "fluegel", autoWidth: false, width: 0.4, opening: "kipp" },
|
||||||
|
{ kind: "fluegel", autoWidth: false, width: 0.4, opening: "dreh", hingeSide: "right" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||||
|
const mullions = linesOfClass(p, "window-mullion");
|
||||||
|
expect(mullions.length).toBe(2);
|
||||||
|
const xs = mullions
|
||||||
|
.map((pr) => (pr.kind === "line" ? pr.a.x : NaN))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
expect(xs[0]).toBeCloseTo(1.4, 6);
|
||||||
|
expect(xs[1]).toBeCloseTo(1.8, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("WindowType MIT sashes, aber alle autoWidth (kein fixes width/pfosten) -> identische Grenzen wie die alte wingCount-Gleichverteilung", () => {
|
||||||
|
// 3 gleichmässige Flügel über sashesOfWindowType (kein `sashes` am Typ,
|
||||||
|
// nur `wingCount: 3`) müssen exakt dieselben Pfosten liefern wie der
|
||||||
|
// Alt-Fall ohne Typ (Regressionssicherheit der Ableitung).
|
||||||
|
const wt: WindowType = { ...baseWindowType, wingCount: 3 };
|
||||||
|
const withType = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||||
|
const withoutType = project({ ...baseWindow, wingCount: 3 });
|
||||||
|
const linesOf = (p: Project) =>
|
||||||
|
linesOfClass(p, "window-mullion")
|
||||||
|
.map((pr) => (pr.kind === "line" ? pr.a.x : NaN))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
const a = linesOf(withType);
|
||||||
|
const b = linesOf(withoutType);
|
||||||
|
expect(a.length).toBe(b.length);
|
||||||
|
a.forEach((v, i) => expect(v).toBeCloseTo(b[i], 9));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Flügel mit opening !== 'fest' erzeugt eine Öffnungsandeutung (window-opening); 'fest' keine", () => {
|
||||||
|
const wt: WindowType = {
|
||||||
|
...baseWindowType,
|
||||||
|
sashes: [
|
||||||
|
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "left" },
|
||||||
|
{ kind: "fluegel", autoWidth: true, opening: "fest" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||||
|
expect(linesOfClass(p, "window-opening").length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Alt-Fall (kein typeId) erzeugt KEINE Öffnungsandeutung", () => {
|
||||||
|
const p = project({ ...baseWindow });
|
||||||
|
expect(linesOfClass(p, "window-opening").length).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generatePlan — Verglasung glazingPanes (Item 2)", () => {
|
||||||
|
const glassLineCount = (p: Project, detail: "grob" | "mittel" | "fein" = "mittel") =>
|
||||||
|
linesOfClass(p, "window-glass", detail).length;
|
||||||
|
|
||||||
|
it("glazingPanes: 3 erzeugt mehr Glaslinien als glazingPanes: 1", () => {
|
||||||
|
const wt3: WindowType = { ...baseWindowType, id: "wt3", glazingPanes: 3 };
|
||||||
|
const wt1: WindowType = { ...baseWindowType, id: "wt1", glazingPanes: 1 };
|
||||||
|
const p3 = project({ ...baseWindow, typeId: "wt3" }, [wt3]);
|
||||||
|
const p1 = project({ ...baseWindow, typeId: "wt1" }, [wt1]);
|
||||||
|
expect(glassLineCount(p3)).toBeGreaterThan(glassLineCount(p1));
|
||||||
|
expect(glassLineCount(p3)).toBe(3);
|
||||||
|
expect(glassLineCount(p1)).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ohne typeId bleibt die alte Detailgrad-Regel (grob/mittel=1, fein=2) unverändert — Regression", () => {
|
||||||
|
const p = project({ ...baseWindow });
|
||||||
|
expect(glassLineCount(p, "grob")).toBe(1);
|
||||||
|
expect(glassLineCount(p, "mittel")).toBe(1);
|
||||||
|
expect(glassLineCount(p, "fein")).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("glazingPanes wirkt bei 'grob' NICHT (bleibt bei 1 Glaslinie)", () => {
|
||||||
|
const wt: WindowType = { ...baseWindowType, glazingPanes: 3 };
|
||||||
|
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||||
|
expect(glassLineCount(p, "grob")).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generatePlan — Rollladenkasten shading (Item 3)", () => {
|
||||||
|
const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length;
|
||||||
|
|
||||||
|
it("shading.create: true erzeugt eine zusätzliche Kastenkontur (4 Linien)", () => {
|
||||||
|
const wt: WindowType = {
|
||||||
|
...baseWindowType,
|
||||||
|
shading: { create: true, kind: "rollladen", box: { width: 1.2, height: 0.2, depth: 0.15 } },
|
||||||
|
};
|
||||||
|
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||||
|
expect(shadingLineCount(p)).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shading.create: false erzeugt KEINE Kastenkontur", () => {
|
||||||
|
const wt: WindowType = {
|
||||||
|
...baseWindowType,
|
||||||
|
shading: { create: false, kind: "rollladen", box: { width: 1.2, height: 0.2, depth: 0.15 } },
|
||||||
|
};
|
||||||
|
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||||
|
expect(shadingLineCount(p)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kein shading-Feld am Typ erzeugt KEINE Kastenkontur", () => {
|
||||||
|
const p = project({ ...baseWindow, typeId: "wt1" }, [baseWindowType]);
|
||||||
|
expect(shadingLineCount(p)).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ohne typeId (Alt-Fall) erzeugt KEINE Kastenkontur", () => {
|
||||||
|
const p = project({ ...baseWindow });
|
||||||
|
expect(shadingLineCount(p)).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
+86
-23
@@ -260,10 +260,16 @@ describe("Bauteil-/Materialfarbe statt Einheits-Grau (Wände + Decken)", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("Fenster-Glasscheiben (emitOpeningGlass)", () => {
|
describe("Fenster-Glasscheiben (emitOpeningGlass)", () => {
|
||||||
it("1 Fenster + 1 Tür -> genau 1 Glas-Mesh mit korrekter vertikaler Ausdehnung", () => {
|
it("1 Fenster + 1 Tür -> 2 Glas-Meshes (Typ 'window-standard' = Zweifachverglasung) mit korrekter vertikaler Ausdehnung", () => {
|
||||||
// Sample-Projekt auf EINE Öffnung (Fenster O1) + EINE Tür (D1) reduzieren,
|
// Sample-Projekt auf EINE Öffnung (Fenster O1) + EINE Tür (D1) reduzieren,
|
||||||
// beide auf W1 (EG, baseElevation 0). Kontext ist leer -> `meshes` enthält
|
// beide auf W1 (EG, baseElevation 0). Kontext ist leer -> `meshes` enthält
|
||||||
// ausschliesslich die Glasscheiben.
|
// ausschliesslich die Glasscheiben.
|
||||||
|
//
|
||||||
|
// ANGEPASST (glazingPanes-Feature): O1 referenziert den Typ "window-standard"
|
||||||
|
// (`glazing: "zweifach"` -> `glazingPanesOf` = 2). `glassPanesForOpening`
|
||||||
|
// zeichnet jetzt render-wirksam so viele Scheiben, statt wie bisher immer
|
||||||
|
// fix EINE — bewusste, korrekte Verhaltensänderung (Aufgabe: 2D/3D-Renderer
|
||||||
|
// sollen die neuen Fenster-Verglasungsfelder konsumieren).
|
||||||
const p: Project = {
|
const p: Project = {
|
||||||
...sampleProject,
|
...sampleProject,
|
||||||
openings: (sampleProject.openings ?? []).filter((o) => o.id === "O1"),
|
openings: (sampleProject.openings ?? []).filter((o) => o.id === "O1"),
|
||||||
@@ -276,34 +282,91 @@ describe("Fenster-Glasscheiben (emitOpeningGlass)", () => {
|
|||||||
const glassMeshes = meshes.filter(
|
const glassMeshes = meshes.filter(
|
||||||
(m) => m.color[0] === 0.62 && m.color[1] === 0.76 && m.color[2] === 0.85,
|
(m) => m.color[0] === 0.62 && m.color[1] === 0.76 && m.color[2] === 0.85,
|
||||||
);
|
);
|
||||||
// Genau EINE Scheibe: das Fenster O1; die Tür D1 bleibt offen (keine Scheibe).
|
expect(glassMeshes.length).toBe(2);
|
||||||
expect(glassMeshes.length).toBe(1);
|
// Vertikale Ausdehnung (z = 3. Komponente jeder Ecke, MODELL-Höhe) — BEIDE
|
||||||
const glass = glassMeshes[0];
|
// Scheiben sitzen INNERHALB desselben Rahmens (Schwelle/Sturz ziehen je
|
||||||
// Quader: 8 Ecken (24 Floats) + 12 Dreiecke (36 Indizes), leicht bläuliches Glas.
|
// frameWidth ab): O1 sillHeight 0.9, height 1.2, Wand-UK 0 -> Öffnung UK
|
||||||
expect(glass.positions.length).toBe(24);
|
// 0.9 .. OK 2.1, Rahmen 0.06 -> Scheiben 0.96 .. 2.04.
|
||||||
expect(glass.indices.length).toBe(36);
|
for (const glass of glassMeshes) {
|
||||||
expect(glass.color).toEqual([0.62, 0.76, 0.85]);
|
// Quader: 8 Ecken (24 Floats) + 12 Dreiecke (36 Indizes), leicht bläuliches Glas.
|
||||||
|
expect(glass.positions.length).toBe(24);
|
||||||
// Vertikale Ausdehnung (z = 3. Komponente jeder Ecke, MODELL-Höhe):
|
expect(glass.indices.length).toBe(36);
|
||||||
// O1 sillHeight 0.9, height 1.2, Wand-UK 0 -> Öffnung UK 0.9 .. OK 2.1.
|
expect(glass.color).toEqual([0.62, 0.76, 0.85]);
|
||||||
// O1 referenziert den Typ "window-standard" (frameWidth 0.06, kein Oberlicht)
|
const zs: number[] = [];
|
||||||
// -> die Scheibe sitzt jetzt INNERHALB des Rahmens (Schwelle/Sturz ziehen je
|
for (let i = 2; i < glass.positions.length; i += 3) zs.push(glass.positions[i]);
|
||||||
// frameWidth ab), s. emitOpeningFrames/glassPanesForOpening: 0.96 .. 2.04.
|
expect(Math.min(...zs)).toBeCloseTo(0.96, 6);
|
||||||
const zs: number[] = [];
|
expect(Math.max(...zs)).toBeCloseTo(2.04, 6);
|
||||||
for (let i = 2; i < glass.positions.length; i += 3) zs.push(glass.positions[i]);
|
}
|
||||||
expect(Math.min(...zs)).toBeCloseTo(0.96, 6);
|
// Quer zur Wand (y-Achse, da W1 entlang x läuft) sitzen die beiden Scheiben
|
||||||
expect(Math.max(...zs)).toBeCloseTo(2.04, 6);
|
// MIT Luftraum versetzt (Isolierverglasung) — sie überlappen sich nicht.
|
||||||
|
const yRange = (m: { positions: number[] }): { min: number; max: number } => {
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
for (let i = 1; i < m.positions.length; i += 3) {
|
||||||
|
min = Math.min(min, m.positions[i]);
|
||||||
|
max = Math.max(max, m.positions[i]);
|
||||||
|
}
|
||||||
|
return { min, max };
|
||||||
|
};
|
||||||
|
const [a, b] = glassMeshes.map(yRange).sort((r1, r2) => r1.min - r2.min);
|
||||||
|
expect(a.max).toBeLessThanOrEqual(b.min + 1e-9);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Türen bekommen keine Scheibe; je Fenster genau eine", () => {
|
it("Türen bekommen keine Scheibe; je Fenster genau 2 (window-standard = zweifach verglast)", () => {
|
||||||
// Volles Sample-Projekt: 2 Fenster (O1, O2) + 1 Tür (D1), Kontext leer.
|
// Volles Sample-Projekt: 2 Fenster (O1, O2, beide Typ "window-standard" =
|
||||||
// Neben den Glasscheiben liegt jetzt auch das Betontreppen-Mesh in `meshes`
|
// Zweifachverglasung) + 1 Tür (D1), Kontext leer. Neben den Glasscheiben
|
||||||
// → nach Glasfarbe filtern: genau 2 Scheiben (O1, O2), Tür ohne Scheibe.
|
// liegt jetzt auch das Betontreppen-Mesh in `meshes` → nach Glasfarbe
|
||||||
|
// filtern: 2 Fenster × 2 Scheiben = 4, Tür ohne Scheibe.
|
||||||
const { meshes } = projectToModel3d(sampleProject);
|
const { meshes } = projectToModel3d(sampleProject);
|
||||||
const glassMeshes = meshes.filter(
|
const glassMeshes = meshes.filter(
|
||||||
(m) => m.color[0] === 0.62 && m.color[1] === 0.76 && m.color[2] === 0.85,
|
(m) => m.color[0] === 0.62 && m.color[1] === 0.76 && m.color[2] === 0.85,
|
||||||
);
|
);
|
||||||
expect(glassMeshes.length).toBe(2);
|
expect(glassMeshes.length).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("glazingPanes: 3 erzeugt mehr Glas-Meshes als glazingPanes: 1 (glazingPanesOf)", () => {
|
||||||
|
const glassMeshCount = (glazingPanes: 1 | 2 | 3): number => {
|
||||||
|
const p: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
windowTypes: [
|
||||||
|
{
|
||||||
|
id: "win-panes-test",
|
||||||
|
name: "Test Verglasung",
|
||||||
|
kind: "fest",
|
||||||
|
wingCount: 1,
|
||||||
|
glazing: "einfach",
|
||||||
|
glazingPanes,
|
||||||
|
frameThickness: 0.06,
|
||||||
|
frameWidth: 0.06,
|
||||||
|
defaultSillHeight: 0.9,
|
||||||
|
defaultWidth: 1.0,
|
||||||
|
defaultHeight: 1.2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
openings: [
|
||||||
|
{
|
||||||
|
id: "OG",
|
||||||
|
type: "opening",
|
||||||
|
hostWallId: "W1",
|
||||||
|
categoryCode: "21",
|
||||||
|
kind: "window",
|
||||||
|
typeId: "win-panes-test",
|
||||||
|
position: 3.0,
|
||||||
|
width: 1.0,
|
||||||
|
height: 1.2,
|
||||||
|
sillHeight: 0.9,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
doors: [],
|
||||||
|
context: [],
|
||||||
|
};
|
||||||
|
const { meshes } = projectToModel3d(p);
|
||||||
|
return meshes.filter((m) => m.color[0] === 0.62 && m.color[1] === 0.76 && m.color[2] === 0.85)
|
||||||
|
.length;
|
||||||
|
};
|
||||||
|
expect(glassMeshCount(1)).toBe(1);
|
||||||
|
expect(glassMeshCount(3)).toBe(3);
|
||||||
|
expect(glassMeshCount(3)).toBeGreaterThan(glassMeshCount(1));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+38
-13
@@ -44,6 +44,7 @@ import {
|
|||||||
flattenCategories,
|
flattenCategories,
|
||||||
getDoorType,
|
getDoorType,
|
||||||
getWindowType,
|
getWindowType,
|
||||||
|
glazingPanesOf,
|
||||||
} from "../model/types";
|
} from "../model/types";
|
||||||
import { wallVerticalExtent, ceilingVerticalExtent, columnVerticalExtent, stairVerticalExtent } from "../model/wall";
|
import { wallVerticalExtent, ceilingVerticalExtent, columnVerticalExtent, stairVerticalExtent } from "../model/wall";
|
||||||
import { columnFootprint } from "../geometry/column";
|
import { columnFootprint } from "../geometry/column";
|
||||||
@@ -1840,6 +1841,13 @@ interface OpeningFrameParams {
|
|||||||
mullionRows: number;
|
mullionRows: number;
|
||||||
/** Ob eine Glasscheibe gezeichnet wird (Fenster immer, Tür nur `leafStyle: "glas"`). */
|
/** Ob eine Glasscheibe gezeichnet wird (Fenster immer, Tür nur `leafStyle: "glas"`). */
|
||||||
glazed: boolean;
|
glazed: boolean;
|
||||||
|
/**
|
||||||
|
* Verglasungs-Scheibenzahl (1–3), render-wirksam ({@link glazingPanesOf}).
|
||||||
|
* Türen: immer 1 (keine typisierte Mehrfachverglasung). Steuert die Anzahl
|
||||||
|
* dünner, in Wandrichtung leicht versetzter Glasflächen in
|
||||||
|
* {@link glassPanesForOpening}.
|
||||||
|
*/
|
||||||
|
glazingPanes: 1 | 2 | 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): OpeningFrameParams | null {
|
function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): OpeningFrameParams | null {
|
||||||
@@ -1865,6 +1873,7 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
wingCount: 1,
|
wingCount: 1,
|
||||||
mullionRows: 1,
|
mullionRows: 1,
|
||||||
glazed: dt.leafStyle === "glas",
|
glazed: dt.leafStyle === "glas",
|
||||||
|
glazingPanes: 1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const wt = getWindowType(project, op);
|
const wt = getWindowType(project, op);
|
||||||
@@ -1888,6 +1897,7 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening
|
|||||||
wingCount: Math.max(1, Math.round(op.wingCount ?? wt.wingCount ?? 1)),
|
wingCount: Math.max(1, Math.round(op.wingCount ?? wt.wingCount ?? 1)),
|
||||||
mullionRows: Math.max(1, Math.round(wt.mullionRows ?? 1)),
|
mullionRows: Math.max(1, Math.round(wt.mullionRows ?? 1)),
|
||||||
glazed: true,
|
glazed: true,
|
||||||
|
glazingPanes: glazingPanesOf(wt),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1954,13 +1964,20 @@ function frameMeshesForOpening(
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Luftraum zwischen zwei Scheiben einer Mehrfachverglasung (Meter, informativ). */
|
||||||
|
const GLASS_PANE_GAP = 0.01;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Glasscheibe(n) EINER typisierten Öffnung (Fenster immer, Tür nur bei
|
* Glasscheibe(n) EINER typisierten Öffnung (Fenster immer, Tür nur bei
|
||||||
* `params.glazed`): eine Hauptscheibe im Flügel-Bereich (zwischen Schwelle und
|
* `params.glazed`): je Z-Band (Hauptfeld zwischen Schwelle und Sturz bzw.
|
||||||
* Sturz bzw. Kämpfer), plus — bei `transomHeight>0` — eine ZWEITE Scheibe im
|
* Kämpfer, plus — bei `transomHeight>0` — ein zweites Oberlicht-Band) werden
|
||||||
* Oberlicht-Band oberhalb des Kämpfers. Die Sprossen/Mittelpfosten aus
|
* `params.glazingPanes` dünne, in Wandrichtung (quer zur Wand, n-Achse) leicht
|
||||||
* {@link frameMeshesForOpening} liegen VOR dieser einen Hauptscheibe (keine
|
* versetzte Scheiben mit Luftraum dazwischen erzeugt (Isolierverglasung:
|
||||||
* Pro-Feld-Aufteilung — bewusst einfach gehalten, s. Datei-weite Erläuterung).
|
* 1 = Einfach-, 2 = Zweifach-, 3 = Dreifachverglasung). Bei `glazingPanes: 1`
|
||||||
|
* (Alt-Verhalten/Default) entsteht exakt EINE Scheibe an derselben Position wie
|
||||||
|
* bisher. Die Sprossen/Mittelpfosten aus {@link frameMeshesForOpening} liegen
|
||||||
|
* VOR diesen Scheiben (keine Pro-Feld-Aufteilung — bewusst einfach gehalten,
|
||||||
|
* s. Datei-weite Erläuterung).
|
||||||
*/
|
*/
|
||||||
function glassPanesForOpening(
|
function glassPanesForOpening(
|
||||||
wall: Wall,
|
wall: Wall,
|
||||||
@@ -1974,16 +1991,24 @@ function glassPanesForOpening(
|
|||||||
const sashTo = iv.to - fw;
|
const sashTo = iv.to - fw;
|
||||||
const sashBottom = vert.zBottom + (params.hasSill ? fw : 0);
|
const sashBottom = vert.zBottom + (params.hasSill ? fw : 0);
|
||||||
const sashTopFull = vert.zTop - fw;
|
const sashTopFull = vert.zTop - fw;
|
||||||
// Die Scheibe ist eine DÜNNE Tafel NAHE der Aussenfläche (nMax = +n-Seite, s.
|
// Die Scheibe(n) sitzen als DÜNNE Tafeln NAHE der Aussenfläche (nMax = +n-
|
||||||
// resolveFrameNormalRange) — mittig tief im Reveal wirkte sie schwach/kaum
|
// Seite, s. resolveFrameNormalRange) — mittig tief im Reveal wirkte das
|
||||||
// sichtbar. Direkt hinter der Aussenkante (3 cm zurück) liest sich das Fenster
|
// schwach/kaum sichtbar. Direkt hinter der Aussenkante (3 cm zurück) liest
|
||||||
// klar, die Laibung/Reveal-Tiefe zeigt nach innen.
|
// sich das Fenster klar, die Laibung/Reveal-Tiefe zeigt nach innen. Bei
|
||||||
|
// mehreren Scheiben rücken die weiteren mit `GLASS_PANE_GAP` Luftraum
|
||||||
|
// dazwischen weiter nach innen (Richtung `nMin`).
|
||||||
const glassThick = GLASS_HALF_THICK * 2;
|
const glassThick = GLASS_HALF_THICK * 2;
|
||||||
const gN1 = params.nMax - 0.03;
|
const panes = Math.max(1, Math.min(3, Math.round(params.glazingPanes)));
|
||||||
const gN0 = Math.max(params.nMin, gN1 - glassThick);
|
|
||||||
const push = (zBottom: number, zTop: number): void => {
|
const push = (zBottom: number, zTop: number): void => {
|
||||||
const mesh = openingAxisBox(wall, sashFrom, sashTo, zBottom, zTop, gN0, gN1, GLASS_RGB);
|
let n1 = params.nMax - 0.03;
|
||||||
if (mesh) out.push(mesh);
|
for (let p = 0; p < panes; p++) {
|
||||||
|
const n0 = Math.max(params.nMin, n1 - glassThick);
|
||||||
|
if (n1 - n0 <= EPS) break;
|
||||||
|
const mesh = openingAxisBox(wall, sashFrom, sashTo, zBottom, zTop, n0, n1, GLASS_RGB);
|
||||||
|
if (mesh) out.push(mesh);
|
||||||
|
n1 = n0 - GLASS_PANE_GAP;
|
||||||
|
if (n1 <= params.nMin) break;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
if (params.transomHeight > EPS) {
|
if (params.transomHeight > EPS) {
|
||||||
const splitZ = clampRange(vert.zTop - params.transomHeight, sashBottom, sashTopFull);
|
const splitZ = clampRange(vert.zTop - params.transomHeight, sashBottom, sashTopFull);
|
||||||
|
|||||||
Reference in New Issue
Block a user