Fenster-Grundriss: Sims/Stulp folgen der eingezogenen Rahmenkante, mittel ohne Glaslinie

Sims (Fensterbank) und Anschlag-Kerben nutzten pauschal die volle Wandfläche
statt der tatsächlichen (ggf. per insetFromFace eingezogenen) Rahmen-
Aussenkante — bei rückversetzten Rahmen sass die Fensterbank dadurch sichtbar
falsch. Stulp-Marken nutzen jetzt die bereits korrekt (tiefenbewusst) in
windowSymbol berechneten meetingMarks statt einer zweiten, abweichenden
Neuberechnung. Ausserdem: SIA fig. 37 (1:50) zeigt noch keine Glaslinie, nur
Rahmen + Stulp-Quadrat — die kommt gemäss fig. 38 erst bei 1:20 dazu.
This commit is contained in:
2026-07-12 17:58:27 +02:00
parent 6ac054ea71
commit e65a6b7de1
3 changed files with 51 additions and 44 deletions
+9 -8
View File
@@ -446,11 +446,16 @@ export function windowSymbol(
const clearTo = seg.to - fw;
if (clearTo - clearFrom <= 1e-6) continue;
const operable = !!seg.sash.opening && seg.sash.opening !== "fest";
// „mittel" (SIA fig. 37) zeigt NUR Blendrahmen + Pfosten/Stulp — noch KEINE
// Glaslinie/Flügelrahmen (Nutzer-Vorgabe: „im 1:50 muss man die Fenster-
// linie noch nicht sehen, nur die Vierecke aussen/innen"). Das kommt erst
// bei „fein" (SIA fig. 38) dazu.
if (!fein) continue;
const gOuterN = frameOuterN - (operable ? reveal : 0);
const gInnerN = frameInnerN + (operable ? reveal : 0);
// Verschachtelter Flügelrahmen NUR bei „fein" (SIA fig. 38); „mittel" bleibt
// beim reinen Blendrahmen-Band (SIA fig. 37).
if (fein && operable) {
if (operable) {
const sFrom = along(jambStart, jambEnd, clearFrom);
const sTo = along(jambStart, jambEnd, clearTo);
sashFrames.push([
@@ -460,23 +465,19 @@ export function windowSymbol(
add(sFrom, scale(n, gInnerN)),
]);
}
const gInsetU = fein && operable ? fw * 0.6 : 0;
const gInsetU = 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 a = along(jambStart, jambEnd, gFrom);
const b = along(jambStart, jambEnd, gTo);
// fein: Glas als Doppellinie (Isolierverglasung, ~15 mm Abstand); mittel:
// eine Glaslinie mittig.
if (fein) {
// Glas als Doppellinie (Isolierverglasung, ~15 mm Abstand) — nur „fein"
// erreicht diesen Punkt (mittel bricht oben früh ab, s. o.).
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: [], meetingMarks };
}
+10 -30
View File
@@ -2271,26 +2271,7 @@ function addOpeningSymbol(
// 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 }))
: [];
const { n: wn } = wallAxisFrame(wall);
if (detail === "grob") {
// SIA 1:100 — nur eine dünne Glaslinie mittig über die weisse Öffnung.
@@ -2324,13 +2305,11 @@ function addOpeningSymbol(
}
}
// 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);
// fein = ZWEI anliegende Rechtecke (die beiden Stulp-Stäbe der Flügel)
// bereits tiefenkorrekt (inset-/insetFace-bewusst) in windowSymbol berechnet.
for (const mark of sym.meetingMarks) {
for (let i = 0; i < mark.length; i++) {
out.push({ kind: "line", a: mark[i], b: mark[(i + 1) % mark.length], cls: "window-stulp", weightMm: SYMBOL_HAIRLINE_MM, color: strokeColor, greyed, openingId: o.id });
}
}
// Glaslinien (Anzahl je Detailgrad/Verglasung, s. glassCount).
@@ -2345,9 +2324,10 @@ function addOpeningSymbol(
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;
// Tatsächliche Rahmen-Aussenkante (NICHT pauschal die volle Wandfläche —
// ein eingezogener Rahmen, s. {@link WindowType.insetFromFace}, hat seine
// Aussenkante weiter innen; sym.frame[0]/[1] tragen das bereits korrekt).
const outerN = dot(sub(sym.frame[0], jambStart), wn);
// Fensterbank: schmales Rechteck vor der Aussenfläche, seitlich ~3 cm
// überstehend, ~4 cm auskragend (SIA-Sims-Andeutung im Grundriss).
const ov = 0.03;
+29 -3
View File
@@ -178,13 +178,13 @@ describe("generatePlan — Verglasung glazingPanes (Item 2)", () => {
const glassLineCount = (p: Project, detail: "grob" | "mittel" | "fein" = "mittel") =>
linesOfClass(p, "window-glass", detail).length;
it("glazingPanes beeinflusst die 2D-Glaslinienzahl NICHT mehr — SIA-Detailgrad entscheidet (Doppellinie/IV nur bei 'fein', unabhängig von der Scheibenzahl)", () => {
it("glazingPanes beeinflusst die 2D-Glaslinienzahl NICHT mehr — SIA-Detailgrad entscheidet (mittel zeigt noch KEINE Glaslinie, SIA fig. 37; Doppellinie/IV erst 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, "mittel")).toBe(1);
expect(glassLineCount(p1, "mittel")).toBe(1);
expect(glassLineCount(p3, "mittel")).toBe(0);
expect(glassLineCount(p1, "mittel")).toBe(0);
expect(glassLineCount(p3, "fein")).toBe(2);
expect(glassLineCount(p1, "fein")).toBe(2);
});
@@ -242,6 +242,32 @@ describe("generatePlan — Stulp-/Flügelstoss-Marken (SIA fig. 37/38, window-st
});
});
describe("generatePlan — Fensterbank (window-sill) folgt der eingezogenen Rahmenkante", () => {
// Wand 0.3 m dick, Achse mittig (refOff=0) -> volle Wandfläche aussen wäre bei
// n=+0.15. Ein um 0.05 m eingezogener Rahmen (insetFromFace) sitzt aber bei
// n=+0.10 (halfT inset) — die Fensterbank muss DORT ansetzen, nicht an der
// vollen Wandfläche (Regression für den Nutzer-Report "Sims sitzt falsch").
it("Fensterbank sitzt an der eingezogenen Rahmen-Aussenkante, nicht an der vollen Wandfläche", () => {
const wt: WindowType = {
...baseWindowType,
sashes: [{ kind: "fluegel", autoWidth: true, opening: "fest" }],
insetFromFace: 0.05,
insetFace: "aussen",
frameDepth: 0.07,
};
const p = project({ ...baseWindow, typeId: "wt1" }, [wt]);
const sill = linesOfClass(p, "window-sill", "fein");
expect(sill.length).toBe(4);
const ys = sill
.flatMap((pr) => (pr.kind === "line" ? [pr.a.y, pr.b.y] : []))
.map((y) => Math.round(y * 1000) / 1000);
// Näher Rand der Fensterbank = Rahmen-Aussenkante (0.15 0.05 = 0.10),
// NICHT die volle Wandfläche (0.15 — der alte, fehlerhafte Wert).
expect(Math.min(...ys)).toBeCloseTo(0.1, 6);
expect(Math.min(...ys)).not.toBeCloseTo(0.15, 2);
});
});
describe("generatePlan — Rollladenkasten shading (Item 3)", () => {
const shadingLineCount = (p: Project) => linesOfClass(p, "window-shading").length;