Fenster im Grundriss: grob/mittel/fein nach SIA 400 klar unterscheiden
grob (1:100) zeigt nur eine schematische Glaslinie, mittel (1:50) einen Blendrahmen mit Flügel-Trennlinien und Stulp-Quadrat je Flügelstoss, fein (1:20) zusätzlich verschachtelte Flügelrahmen, Glas als Doppellinie (Isolierverglasung) und zwei Stulp-Quadrate je Stoss — vorher waren mittel und fein praktisch identisch. Referenz: SIA 400 Anhang B.9.1, Fig. 36–38 (docs/research/sia400-fenster-tueren.md).
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
# SIA 400 — Fenster-/Türdarstellung (Grundriss, Schnitt, Ansicht)
|
||||
|
||||
> Quelle: SIA 400:2000 „Planbearbeitung im Hochbau", Anhang B.9 (Darstellung von
|
||||
> Bauteilen), Figuren 36–42. Das PDF liegt im Projektwurzel (lokal excluded,
|
||||
> NICHT committen — urheberrechtlich geschützt). Dies ist die verbindliche
|
||||
> Darstellungsnorm (Schweiz) — NICHT DIN. Massstab = Detailgrad:
|
||||
> **1:100 = grob, 1:50 = mittel, 1:20 = fein.**
|
||||
|
||||
## B.9.1.1 Fenster im Grundriss (Figuren 36/37/38)
|
||||
|
||||
Regel (Zitat): „Die Darstellung der Fensterkonstruktionen erfolgt nach denselben
|
||||
Regeln, unabhängig davon ob es sich um Holz-, Holz-Metall-, Metall- oder
|
||||
Kunststoff-Fenster handelt."
|
||||
|
||||
### Figur 36 — Massstab 1:100 (= grob)
|
||||
- Wand **voll schwarz** (Poché massiv), keine Schraffur.
|
||||
- Fenster = **eine dünne Rahmen-/Glasband-Andeutung** über die Öffnung, sehr
|
||||
schematisch — im Wesentlichen EIN schmales Rechteck/Band mit wenigen Marken,
|
||||
KEINE Flügel-/Anschlagdetails, KEINE Laibungsmarken.
|
||||
|
||||
### Figur 37 — Massstab 1:50 (= mittel)
|
||||
- Wand **materialschraffiert** (Bänder: Hinterlüftung/Dämmung senkrecht
|
||||
gestrichelt, Backstein/Struktur diagonal). Drei Wandaufbauten gezeigt
|
||||
(Backstein+Aussendämmung hinterlüftet / Holzelementbau / Backstein verputzt).
|
||||
- Fenster: **Blendrahmen als schmales Band** nahe der Aussenfläche, dünne
|
||||
Kontur; **Glaslinie**; **ein kleines Quadrat mittig** (Stulp/Flügelstoss).
|
||||
Laibung/Öffnungstiefe sichtbar. Einfach, aber als Fenster lesbar.
|
||||
|
||||
### Figur 38 — Massstab 1:20 (= fein)
|
||||
- Wand voll schraffiert (Kreuzschraffur Backstein, Diagonalschraffur Dämmung).
|
||||
- Fenster: **volles Rahmenprofil** — Blendrahmen + Flügelrahmen als mehrere
|
||||
parallele Linien (Profiltiefe); **Glas als Doppellinie** (Isolierverglasung
|
||||
IV, zwei enge Parallelen); **zwei Stulp-Quadrate mittig** (Flügelstoss der
|
||||
zwei Flügel); **Anschlag/Laibung** mit Rahmen-Wand-Detail (Falz/Rebate),
|
||||
gestrichelte Anschlagmarken an den Ecken; Anschlagwinkel an den Laibungen.
|
||||
|
||||
## B.9.1.2 Fenster im Schnitt (Figuren 39/40, M 1:50)
|
||||
- Rahmen im Schnitt mit Brüstung/Sturz, Bemassung (z. B. Brüstung +0.90,
|
||||
Sturzhöhe). Fenstertüren (Figur 40): Rahmen bis Boden, Schwelle.
|
||||
|
||||
## B.9.1.3 Sinnbilder Fenster (Öffnungsart in der ANSICHT)
|
||||
Liste der Öffnungsarten (SIA-Benennung):
|
||||
- Fest im Rahmen verglast (kein Symbol)
|
||||
- Drehflügel einflüglig mit Verschluss, **Band rechts/links** (Dreieck, Spitze zur BANDSEITE)
|
||||
- Drehflügel fest mit Band und Plattenverschraubung
|
||||
- Zweiflüglig mit **Öffnungsreihenfolge** (1 = erstöffnend, 2 = zweitöffnend)
|
||||
- Kippflügel mit Verschluss (Dreieck)
|
||||
- Kippflügel fest, für Reinigung bedienbar
|
||||
- Klappflügel mit Verschluss
|
||||
- Drehkippflügel, Band rechts
|
||||
- Schwingflügelfenster
|
||||
- Wendeflügelfenster, Achse in der Mitte
|
||||
- Vertikales Schiebefenster (nach oben schiebbar, oberer Flügel fest)
|
||||
|
||||
Konvention: Dreieck-/Winkelsinnbild, **Spitze zeigt zur Bandseite (Anschlag)**,
|
||||
Basis zur Griffseite. (Genaues Sinnbild-Blatt S. 38 rechts.)
|
||||
|
||||
## B.9.1.4 Kurzzeichen Fenster/Sonnenschutz
|
||||
KL Klappladen · SL Schiebeladen · ROL Rolladen · LAM Lamellenstoren ·
|
||||
RAF Rafflamellenstoren · FAL Faltrolladen · K Kurbel · DV Doppelverglasung ·
|
||||
IV Isolierverglasung · IV3 Dreifachisolierverglasung · BFB Beton-Fensterbank ·
|
||||
MFB Metall-Fensterbank · FFB Faserzement-Fensterbank.
|
||||
|
||||
## B.9.2 Türen im Grundriss (Figuren 41/42) + Sinnbilder (B.9.2.2)
|
||||
- **Türblatt als Linie ab Band + Viertelkreisbogen** (Schwingbahn). Bandseite =
|
||||
Bogenmittelpunkt. Zweiflüglig/Doppeltüre = zwei Bögen.
|
||||
- Zargenarten: Futterrahmen/Zarge, Blockrahmen/Profil, Blendrahmen — je mit
|
||||
Anschlag-/Schwellenmark. Bei Niveaudifferenz Schwellenstrich.
|
||||
- Weitere Sinnbilder: Pendeltüre (Bogen beidseitig gestrichelt), Falttüre
|
||||
(Zickzack), Faltschiebetor, Drehtüre (Kreis mit X), Kipptor, Schiebetüre
|
||||
(ausserhalb/in der Wand, Pfeil), Harmonikatüre.
|
||||
|
||||
## Umsetzung im Code (Soll)
|
||||
- `generatePlan.ts` Fenster-Zweig + `geometry/opening.ts::windowSymbol`:
|
||||
drei DEUTLICH getrennte Detailstufen statt heute (mittel≈fein):
|
||||
- **grob:** 1 Rahmen-/Glasband (Rechteck), sonst nichts.
|
||||
- **mittel:** Blendrahmen-Rechteck + Glaslinie + 1 Stulp-Quadrat mittig +
|
||||
Flügel-Trennlinien; keine verschachtelten Flügelrahmen, kein Anschlag.
|
||||
- **fein:** verschachtelte Blend-+Flügelrahmen (mehrlinig) + Glas-Doppellinie
|
||||
(IV) + Stulp-Quadrate + Anschlag/Laibungsmarken.
|
||||
- Öffnungssymbole (Ansicht) auf SIA umstellen (Spitze zur Bandseite), Türbögen
|
||||
bleiben (schon SIA-konform).
|
||||
+73
-27
@@ -251,6 +251,14 @@ export interface WindowSymbol {
|
||||
* ausgedrückt statt über eine Diagonale quer über die Box.
|
||||
*/
|
||||
sashOpeningLines: [Vec2, Vec2][];
|
||||
/**
|
||||
* Stulp-/Flügelstoss-Marken (SIA-„kleines Quadrat mittig", Figur 37/38): je
|
||||
* Flügel-Trennung ein kleines Rechteck-Polygon auf der Glasebene, das den
|
||||
* Anschlag der zwei zusammenstossenden Flügelprofile darstellt. Bei „mittel"
|
||||
* ein Quadrat je Stoss, bei „fein" zwei (die beiden Stulpprofile). „grob":
|
||||
* leer. Der Aufrufer zeichnet sie als dünne Umriss-Rechtecke.
|
||||
*/
|
||||
meetingMarks: Vec2[][];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,6 +353,7 @@ export function windowSymbol(
|
||||
o: Opening,
|
||||
glassCount: number,
|
||||
spec?: WindowRenderSpec,
|
||||
detail: "grob" | "mittel" | "fein" = "fein",
|
||||
): WindowSymbol | null {
|
||||
const jambs = openingJambs(wall, o);
|
||||
if (!jambs) return null;
|
||||
@@ -355,10 +364,14 @@ export function windowSymbol(
|
||||
const inner = refOff - total / 2;
|
||||
const { jambStart, jambEnd } = jambs;
|
||||
|
||||
// ── Reicher Pfad (typisiertes Fenster mit Flügeltabelle) ─────────────────
|
||||
// Blendrahmen an der realen Einbaulage (Schichteinzug), je Flügel
|
||||
// Blendrahmen → Flügelrahmen (öffenbar) → Glas. Feste Flügel: Glas direkt im
|
||||
// Blendrahmen (kein Flügelrahmen) — so unterscheidet sich fest von öffenbar.
|
||||
// ── Reicher Pfad (typisiertes Fenster mit Flügeltabelle), SIA-Darstellung ─
|
||||
// Detailstufe = SIA-Massstab (fig. 36/37/38):
|
||||
// • grob (1:100): NUR Blendrahmen-Band + eine Glaslinie über die ganze
|
||||
// Öffnung — schematisch (SIA fig. 36).
|
||||
// • mittel(1:50): Blendrahmen + Flügel-Trennlinien + je Flügel eine Glas-
|
||||
// linie + kleines Stulp-Quadrat je Stoss (SIA fig. 37). KEINE Flügelrahmen.
|
||||
// • fein (1:20): zusätzlich verschachtelte Flügelrahmen, Glas als Doppel-
|
||||
// linie (Isolierverglasung) und je Stoss ZWEI Stulp-Quadrate (SIA fig. 38).
|
||||
if (spec && spec.sashes.length > 0) {
|
||||
const fw = Math.max(0.01, spec.frameWidth);
|
||||
const depth = Math.min(total, spec.frameDepth && spec.frameDepth > 1e-9 ? spec.frameDepth : DEFAULT_FRAME_DEPTH);
|
||||
@@ -375,6 +388,7 @@ export function windowSymbol(
|
||||
const frameOuterN = refOff + Math.max(frontRel, backRel);
|
||||
const frameInnerN = refOff + Math.min(frontRel, backRel);
|
||||
const bandDepth = frameOuterN - frameInnerN;
|
||||
const gMidBase = (frameOuterN + frameInnerN) / 2;
|
||||
|
||||
const frame = [
|
||||
add(jambStart, scale(n, frameOuterN)),
|
||||
@@ -383,22 +397,49 @@ export function windowSymbol(
|
||||
add(jambStart, scale(n, frameInnerN)),
|
||||
];
|
||||
|
||||
const axisWidth = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y);
|
||||
const segs = sashSegments(spec.sashes, axisWidth);
|
||||
|
||||
// Trennlinien (Pfosten/Mittelpfosten) an den inneren Segmentgrenzen, über
|
||||
// die volle Rahmentiefe.
|
||||
const glassLines: [Vec2, Vec2][] = [];
|
||||
const mullionLines: [Vec2, Vec2][] = [];
|
||||
for (let i = 0; i < segs.length - 1; i++) {
|
||||
const onAxis = along(jambStart, jambEnd, segs[i].to);
|
||||
mullionLines.push([add(onAxis, scale(n, frameOuterN)), add(onAxis, scale(n, frameInnerN))]);
|
||||
const sashFrames: Vec2[][] = [];
|
||||
const meetingMarks: Vec2[][] = [];
|
||||
|
||||
// grob (1:100): nur der Rahmenkasten + EINE Glaslinie über die ganze Öffnung.
|
||||
if (detail === "grob") {
|
||||
glassLines.push([add(jambStart, scale(n, gMidBase)), add(jambEnd, scale(n, gMidBase))]);
|
||||
return { frame, glassLines, mullionLines, sashFrames, sashOpeningLines: [], meetingMarks };
|
||||
}
|
||||
|
||||
const fein = detail === "fein";
|
||||
const axisWidth = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y);
|
||||
const axisDir = normalize(sub(jambEnd, jambStart));
|
||||
const segs = sashSegments(spec.sashes, axisWidth);
|
||||
// Öffenbarer Flügel sitzt `reveal` tiefer im Blendrahmen (nur „fein").
|
||||
const reveal = fein ? Math.min(bandDepth * 0.3, fw * 0.7) : 0;
|
||||
// Halbe Kantenlänge eines Stulp-Quadrats (SIA-Marke), an die Rahmenbreite gekoppelt.
|
||||
const markHalf = Math.min(fw * 0.6, bandDepth * 0.4);
|
||||
|
||||
// Flügel-Trennlinien (Mittelpfosten) an den inneren Segmentgrenzen +
|
||||
// Stulp-Quadrate: mittel 1, fein 2 (die beiden zusammenstossenden Profile).
|
||||
for (let i = 0; i < segs.length - 1; i++) {
|
||||
const uPos = segs[i].to;
|
||||
const onAxis = along(jambStart, jambEnd, uPos);
|
||||
mullionLines.push([add(onAxis, scale(n, frameOuterN)), add(onAxis, scale(n, frameInnerN))]);
|
||||
const center = add(onAxis, scale(n, gMidBase));
|
||||
const square = (offU: number): Vec2[] => {
|
||||
const c = add(center, scale(axisDir, offU));
|
||||
return [
|
||||
add(add(c, scale(axisDir, -markHalf)), scale(n, -markHalf)),
|
||||
add(add(c, scale(axisDir, markHalf)), scale(n, -markHalf)),
|
||||
add(add(c, scale(axisDir, markHalf)), scale(n, markHalf)),
|
||||
add(add(c, scale(axisDir, -markHalf)), scale(n, markHalf)),
|
||||
];
|
||||
};
|
||||
if (fein) {
|
||||
meetingMarks.push(square(-markHalf), square(markHalf));
|
||||
} else {
|
||||
meetingMarks.push(square(0));
|
||||
}
|
||||
}
|
||||
|
||||
const sashFrames: Vec2[][] = [];
|
||||
const glassLines: [Vec2, Vec2][] = [];
|
||||
const count = Math.max(1, glassCount);
|
||||
// Öffenbarer Flügel sitzt `reveal` tiefer im Blendrahmen (n- und u-Richtung).
|
||||
const reveal = Math.min(bandDepth * 0.3, fw * 0.7);
|
||||
for (const seg of segs) {
|
||||
if (seg.sash.kind === "pfosten") continue;
|
||||
const clearFrom = seg.from + fw;
|
||||
@@ -407,7 +448,9 @@ export function windowSymbol(
|
||||
const operable = !!seg.sash.opening && seg.sash.opening !== "fest";
|
||||
const gOuterN = frameOuterN - (operable ? reveal : 0);
|
||||
const gInnerN = frameInnerN + (operable ? reveal : 0);
|
||||
if (operable) {
|
||||
// Verschachtelter Flügelrahmen NUR bei „fein" (SIA fig. 38); „mittel" bleibt
|
||||
// beim reinen Blendrahmen-Band (SIA fig. 37).
|
||||
if (fein && operable) {
|
||||
const sFrom = along(jambStart, jambEnd, clearFrom);
|
||||
const sTo = along(jambStart, jambEnd, clearTo);
|
||||
sashFrames.push([
|
||||
@@ -417,22 +460,25 @@ export function windowSymbol(
|
||||
add(sFrom, scale(n, gInnerN)),
|
||||
]);
|
||||
}
|
||||
// Glas innerhalb des Flügels (bei öffenbar zusätzlich in u eingerückt).
|
||||
const gInsetU = operable ? fw * 0.6 : 0;
|
||||
const gInsetU = fein && operable ? fw * 0.6 : 0;
|
||||
const gFrom = clearFrom + gInsetU;
|
||||
const gTo = clearTo - gInsetU;
|
||||
if (gTo - gFrom <= 1e-6) continue;
|
||||
const gMid = (gOuterN + gInnerN) / 2;
|
||||
const gBand = gOuterN - gInnerN;
|
||||
const step = count > 1 ? Math.min(total / 10, gBand / (count + 1)) : 0;
|
||||
const a = along(jambStart, jambEnd, gFrom);
|
||||
const b = along(jambStart, jambEnd, gTo);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const off = gMid + (count === 1 ? 0 : (i - (count - 1) / 2) * step);
|
||||
glassLines.push([add(a, scale(n, off)), add(b, scale(n, off))]);
|
||||
// fein: Glas als Doppellinie (Isolierverglasung, ~15 mm Abstand); mittel:
|
||||
// eine Glaslinie mittig.
|
||||
if (fein) {
|
||||
const half = Math.min(0.0075, bandDepth * 0.18);
|
||||
for (const off of [gMid - half, gMid + half]) {
|
||||
glassLines.push([add(a, scale(n, off)), add(b, scale(n, off))]);
|
||||
}
|
||||
} else {
|
||||
glassLines.push([add(a, scale(n, gMid)), add(b, scale(n, gMid))]);
|
||||
}
|
||||
}
|
||||
return { frame, glassLines, mullionLines, sashFrames, sashOpeningLines: [] };
|
||||
return { frame, glassLines, mullionLines, sashFrames, sashOpeningLines: [], meetingMarks };
|
||||
}
|
||||
|
||||
// ── Alt-Pfad (kein Typ auflösbar) ────────────────────────────────────────
|
||||
@@ -463,5 +509,5 @@ export function windowSymbol(
|
||||
mullionLines.push([add(onAxis, scale(n, pOuter)), add(onAxis, scale(n, pInner))]);
|
||||
}
|
||||
}
|
||||
return { frame, glassLines, mullionLines, sashFrames: [], sashOpeningLines: [] };
|
||||
return { frame, glassLines, mullionLines, sashFrames: [], sashOpeningLines: [], meetingMarks: [] };
|
||||
}
|
||||
|
||||
+94
-78
@@ -35,7 +35,6 @@ import {
|
||||
getLineStyle,
|
||||
getWallType,
|
||||
getWindowType,
|
||||
glazingPanesOf,
|
||||
openingLabel,
|
||||
openingsOfWall,
|
||||
roofLayers,
|
||||
@@ -2259,13 +2258,10 @@ function addOpeningSymbol(
|
||||
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, spec);
|
||||
// Glaslinienzahl NUR für den Alt-Pfad (kein `spec`) relevant — der typisierte
|
||||
// Pfad entscheidet die Anzahl selbst nach SIA-Detailgrad (s. windowSymbol).
|
||||
const glassCount = detail === "fein" ? 2 : 1;
|
||||
const sym = windowSymbol(project, wall, o, glassCount, spec, detail);
|
||||
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
|
||||
@@ -2283,95 +2279,115 @@ function addOpeningSymbol(
|
||||
openingId: o.id,
|
||||
});
|
||||
}
|
||||
if (detail !== "grob") {
|
||||
// Rahmen-Rechteck als reine Umrisslinie (Poché-Stroke, keine Füllung).
|
||||
// ── SIA-Fensterdarstellung, nach Detailgrad gestaffelt ────────────────────
|
||||
// grob (1:100): weisse Öffnung + EINE dünne Glaslinie, sonst nichts.
|
||||
// mittel (1:50): Blendrahmen-Rechteck + Pfosten-Trennlinien + Stulp-Mittel-
|
||||
// quadrat je Flügel-Junction + Glaslinie(n).
|
||||
// fein (1:20): zusätzlich Flügelrahmen, Stulp als ZWEI Rechtecke, Fensterbank
|
||||
// (aussen) und Anschlag-/Laibungskerben — deutlich reicher als mittel.
|
||||
const strokeColor = o.color ?? POCHE_STROKE;
|
||||
const { u: wu, n: wn } = wallAxisFrame(wall);
|
||||
const fpw = Math.max(0.03, wt?.frameWidth ?? 0.06); // Rahmenprofil-Breite (m)
|
||||
// Kleines Rechteck (Stulp/Anschlag) um `center`, Halbmasse hu (Achse)/hn (quer).
|
||||
const emitStulp = (center: Vec2, hu: number, hn: number): void => {
|
||||
const c1 = add(add(center, scale(wu, -hu)), scale(wn, -hn));
|
||||
const c2 = add(add(center, scale(wu, hu)), scale(wn, -hn));
|
||||
const c3 = add(add(center, scale(wu, hu)), scale(wn, hn));
|
||||
const c4 = add(add(center, scale(wu, -hu)), scale(wn, hn));
|
||||
for (const [a, b] of [[c1, c2], [c2, c3], [c3, c4], [c4, c1]] as [Vec2, Vec2][]) {
|
||||
out.push({ kind: "line", a, b, cls: "window-stulp", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id });
|
||||
}
|
||||
};
|
||||
// Flügel-Junctions (Stulp-/Anschlagstelle zweier Flügel) = Mitten der Pfosten-
|
||||
// Trennlinien. NUR im typisierten Pfad (`spec`) — der Alt-Pfad (reines
|
||||
// `wingCount`) hat keine echte Flügeltabelle, seine Trennlinien sind nur eine
|
||||
// gleichmässige Näherung ohne Stoss-Semantik (siehe windowSymbol-Kommentar).
|
||||
// Bei einflügligen Fenstern leer ⇒ kein Mittelquadrat (korrekt).
|
||||
const junctionCenters: Vec2[] = spec
|
||||
? sym.mullionLines.map(([a, b]) => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }))
|
||||
: [];
|
||||
|
||||
if (detail === "grob") {
|
||||
// SIA 1:100 — nur eine dünne Glaslinie mittig über die weisse Öffnung.
|
||||
const g = sym.glassLines[Math.floor(sym.glassLines.length / 2)] ?? sym.glassLines[0];
|
||||
if (g) {
|
||||
out.push({ kind: "line", a: g[0], b: g[1], cls: "window-glass", weightMm: SYMBOL_HAIRLINE_MM, greyed, openingId: o.id });
|
||||
}
|
||||
} else {
|
||||
// Blendrahmen-Rechteck (mittel + fein).
|
||||
out.push({
|
||||
kind: "polygon",
|
||||
pts: sym.frame,
|
||||
fill: "none",
|
||||
stroke: o.color ?? POCHE_STROKE,
|
||||
stroke: strokeColor,
|
||||
strokeWidthMm: lwMm,
|
||||
hatch: NO_HATCH,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
// Flügel-/Pfosten-Trennlinien — reine Querlinien innerhalb des Rahmens
|
||||
// (kein Strichmuster). Nur mittel/fein; entfällt bei grob/schema.
|
||||
// Pfosten-/Flügel-Trennlinien.
|
||||
for (const [a, b] of sym.mullionLines) {
|
||||
out.push({
|
||||
kind: "line",
|
||||
a,
|
||||
b,
|
||||
cls: "window-mullion",
|
||||
weightMm: SYMBOL_HAIRLINE_MM,
|
||||
color: o.color ?? POCHE_STROKE,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
out.push({ kind: "line", a, b, cls: "window-mullion", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, 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,
|
||||
});
|
||||
// fein: Flügelrahmen (Flügelkontur) je öffenbarem Flügel — Schachtelung
|
||||
// Blendrahmen → Flügelrahmen → Glas (Feindetail, hebt fein klar von mittel ab).
|
||||
if (detail === "fein") {
|
||||
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: strokeColor, greyed, openingId: o.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [a, b] of sym.glassLines) {
|
||||
out.push({
|
||||
kind: "line",
|
||||
a,
|
||||
b,
|
||||
cls: "window-glass",
|
||||
weightMm: SYMBOL_HAIRLINE_MM,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
}
|
||||
// Rahmenband (Zarge) + Oberlicht-Andeutung — nur wenn die Öffnung einen
|
||||
// Fenstertyp referenziert (siehe `resolveOpeningFrameSpec`). Kämpfer-Zeilen
|
||||
// (`mullionRows`) bleiben im Grundriss-Schnitt unsichtbar (3D-Sache).
|
||||
const windowFrameSpec = resolveOpeningFrameSpec(project, o);
|
||||
if (windowFrameSpec) addOpeningFrameBand(out, project, wall, o, windowFrameSpec, greyed);
|
||||
// fein: Anschlag-/Laibungsstriche an beiden Pfosten (kurz quer zur Wand) —
|
||||
// analog zur Tür, damit Fenster im Detailplan gleich „fertig" wirken (bisher
|
||||
// hatten nur Türen sie). Haarlinie, 6 cm Andeutung je Seite.
|
||||
if (detail === "fein") {
|
||||
const jambs = openingJambs(wall, o);
|
||||
const dir = sub(wall.end, wall.start);
|
||||
const len = Math.hypot(dir.x, dir.y);
|
||||
if (jambs && len > 1e-9) {
|
||||
const n = { x: -dir.y / len, y: dir.x / len };
|
||||
const reveal = 0.06;
|
||||
for (const jamb of [jambs.jambStart, jambs.jambEnd]) {
|
||||
out.push({
|
||||
kind: "line",
|
||||
a: add(jamb, scale(n, reveal)),
|
||||
b: add(jamb, scale(n, -reveal)),
|
||||
cls: "window-frame",
|
||||
weightMm: SYMBOL_HAIRLINE_MM,
|
||||
color: o.color ?? POCHE_STROKE,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
// Stulp-Mittelquadrat je Flügel-Junction (SIA-Signatur): mittel = EIN Quadrat,
|
||||
// fein = ZWEI anliegende Rechtecke (die beiden Stulp-Stäbe der Flügel).
|
||||
for (const c of junctionCenters) {
|
||||
if (detail === "fein") {
|
||||
emitStulp(add(c, scale(wu, -fpw / 2)), fpw / 2, fpw / 2);
|
||||
emitStulp(add(c, scale(wu, fpw / 2)), fpw / 2, fpw / 2);
|
||||
} else {
|
||||
emitStulp(c, fpw / 2, fpw / 2);
|
||||
}
|
||||
}
|
||||
// Glaslinien (Anzahl je Detailgrad/Verglasung, s. glassCount).
|
||||
for (const [a, b] of sym.glassLines) {
|
||||
out.push({ kind: "line", a, b, cls: "window-glass", weightMm: SYMBOL_HAIRLINE_MM, greyed, openingId: o.id });
|
||||
}
|
||||
// Oberlicht-Andeutung (transom, gestrichelt) — mittel/fein.
|
||||
const windowFrameSpec = resolveOpeningFrameSpec(project, o);
|
||||
if (windowFrameSpec) addOpeningFrameBand(out, project, wall, o, windowFrameSpec, greyed);
|
||||
// fein: Fensterbank (aussen) + Anschlag-/Laibungskerben.
|
||||
if (detail === "fein") {
|
||||
const jb = openingJambs(wall, o);
|
||||
if (jb) {
|
||||
const { jambStart, jambEnd } = jb;
|
||||
const total = wallTypeThickness(getWallType(project, wall));
|
||||
const refOff = wallReferenceOffset(wall, total);
|
||||
const outerN = refOff + total / 2;
|
||||
// Fensterbank: schmales Rechteck vor der Aussenfläche, seitlich ~3 cm
|
||||
// überstehend, ~4 cm auskragend (SIA-Sims-Andeutung im Grundriss).
|
||||
const ov = 0.03;
|
||||
const proj = 0.04;
|
||||
const s0 = along(jambStart, jambEnd, -ov);
|
||||
const s1 = along(jambEnd, jambStart, -ov);
|
||||
const b0 = add(s0, scale(wn, outerN));
|
||||
const b1 = add(s1, scale(wn, outerN));
|
||||
const b2 = add(s1, scale(wn, outerN + proj));
|
||||
const b3 = add(s0, scale(wn, outerN + proj));
|
||||
for (const [a, b] of [[b0, b1], [b1, b2], [b2, b3], [b3, b0]] as [Vec2, Vec2][]) {
|
||||
out.push({ kind: "line", a, b, cls: "window-sill", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id });
|
||||
}
|
||||
// Anschlag-Kerben an beiden Pfosten (kurz quer zur Wand).
|
||||
const reveal = 0.06;
|
||||
for (const jamb of [jambStart, jambEnd]) {
|
||||
out.push({ kind: "line", a: add(jamb, scale(wn, reveal)), b: add(jamb, scale(wn, -reveal)), cls: "window-frame", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Brüstungslinie (gepunktet) — zeigt die Brüstung im Grundriss auf der Wandachse.
|
||||
// Nur bei Fenstern mit Brüstungshöhe > 0. Entspricht der Sill-Linie in
|
||||
// `_make_oeffnung_preview` und dem Brüstungs-Plansymbol im Rhino-Plugin.
|
||||
if (o.sillHeight > 0) {
|
||||
if (o.sillHeight > 0 && detail !== "grob") {
|
||||
const jambs = openingJambs(wall, o);
|
||||
if (jambs) {
|
||||
out.push({
|
||||
|
||||
@@ -142,10 +142,12 @@ describe("generatePlan — Flügeleinteilung aus sashes (Item 1)", () => {
|
||||
a.forEach((v, i) => expect(v).toBeCloseTo(b[i], 9));
|
||||
});
|
||||
|
||||
it("öffenbarer Flügel bekommt einen Flügelrahmen (window-sash, 4 Kanten); 'fest' keinen", () => {
|
||||
it("öffenbarer Flügel bekommt einen Flügelrahmen (window-sash, 4 Kanten) bei 'fein'; 'fest' keinen — SIA fig. 38 (verschachtelter Flügelrahmen nur bei fein, s. sia400-fenster-tueren.md)", () => {
|
||||
// Ein öffenbarer + ein fester Flügel: genau EIN Flügelrahmen-Rechteck = 4
|
||||
// Kanten-Linien (der feste Flügel bekommt keinen; Glas sitzt direkt im
|
||||
// Blendrahmen). So liest man fest vs. öffenbar direkt im Grundriss.
|
||||
// Nur bei „fein" (SIA fig. 38) — „mittel" (fig. 37) bleibt beim reinen
|
||||
// Blendrahmen-Band ohne verschachtelten Flügelrahmen.
|
||||
const wt: WindowType = {
|
||||
...baseWindowType,
|
||||
sashes: [
|
||||
@@ -154,7 +156,8 @@ describe("generatePlan — Flügeleinteilung aus sashes (Item 1)", () => {
|
||||
],
|
||||
};
|
||||
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
|
||||
expect(linesOfClass(p, "window-sash").length).toBe(4);
|
||||
expect(linesOfClass(p, "window-sash", "fein").length).toBe(4);
|
||||
expect(linesOfClass(p, "window-sash", "mittel").length).toBe(0);
|
||||
|
||||
// Nur feste Flügel -> gar kein Flügelrahmen.
|
||||
const wtFixed: WindowType = {
|
||||
@@ -162,12 +165,12 @@ describe("generatePlan — Flügeleinteilung aus sashes (Item 1)", () => {
|
||||
sashes: [{ kind: "fluegel", autoWidth: true, opening: "fest" }],
|
||||
};
|
||||
const pFixed = project({ ...baseWindow, typeId: "wt1" }, [wtFixed]);
|
||||
expect(linesOfClass(pFixed, "window-sash").length).toBe(0);
|
||||
expect(linesOfClass(pFixed, "window-sash", "fein").length).toBe(0);
|
||||
});
|
||||
|
||||
it("Alt-Fall (kein typeId) erzeugt KEINEN Flügelrahmen", () => {
|
||||
const p = project({ ...baseWindow });
|
||||
expect(linesOfClass(p, "window-sash").length).toBe(0);
|
||||
expect(linesOfClass(p, "window-sash", "fein").length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -175,14 +178,15 @@ 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", () => {
|
||||
it("glazingPanes beeinflusst die 2D-Glaslinienzahl NICHT mehr — SIA-Detailgrad entscheidet (Doppellinie/IV nur bei 'fein', unabhängig von der Scheibenzahl)", () => {
|
||||
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);
|
||||
expect(glassLineCount(p3, "mittel")).toBe(1);
|
||||
expect(glassLineCount(p1, "mittel")).toBe(1);
|
||||
expect(glassLineCount(p3, "fein")).toBe(2);
|
||||
expect(glassLineCount(p1, "fein")).toBe(2);
|
||||
});
|
||||
|
||||
it("ohne typeId bleibt die alte Detailgrad-Regel (grob/mittel=1, fein=2) unverändert — Regression", () => {
|
||||
@@ -199,6 +203,45 @@ describe("generatePlan — Verglasung glazingPanes (Item 2)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("generatePlan — Stulp-/Flügelstoss-Marken (SIA fig. 37/38, window-stulp)", () => {
|
||||
const stulpLineCount = (p: Project, detail: "grob" | "mittel" | "fein") =>
|
||||
linesOfClass(p, "window-stulp", detail).length;
|
||||
const twoSashType: WindowType = {
|
||||
...baseWindowType,
|
||||
sashes: [
|
||||
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "left" },
|
||||
{ kind: "fluegel", autoWidth: true, opening: "dreh", hingeSide: "right" },
|
||||
],
|
||||
};
|
||||
|
||||
it("mittel: EIN Stulp-Quadrat (4 Linien) je Flügel-Junction", () => {
|
||||
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
|
||||
expect(stulpLineCount(p, "mittel")).toBe(4);
|
||||
});
|
||||
|
||||
it("fein: ZWEI Stulp-Quadrate (8 Linien) je Flügel-Junction", () => {
|
||||
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
|
||||
expect(stulpLineCount(p, "fein")).toBe(8);
|
||||
});
|
||||
|
||||
it("grob: KEINE Stulp-Marken", () => {
|
||||
const p = project({ ...baseWindow, typeId: "wt1" }, [twoSashType]);
|
||||
expect(stulpLineCount(p, "grob")).toBe(0);
|
||||
});
|
||||
|
||||
it("Alt-Fall (kein typeId, nur wingCount) erzeugt KEINE Stulp-Marken — keine echte Flügeltabelle", () => {
|
||||
const p = project({ ...baseWindow, wingCount: 3 });
|
||||
expect(stulpLineCount(p, "mittel")).toBe(0);
|
||||
expect(stulpLineCount(p, "fein")).toBe(0);
|
||||
});
|
||||
|
||||
it("einflügliges Fenster (keine Junction) erzeugt KEINE Stulp-Marken", () => {
|
||||
const p = project({ ...baseWindow, typeId: "wt1" }, [baseWindowType]);
|
||||
expect(stulpLineCount(p, "mittel")).toBe(0);
|
||||
expect(stulpLineCount(p, "fein")).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("generatePlan — Rollladenkasten shading (Item 3)", () => {
|
||||
const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user