From e65a6b7de110ec6bc488fd2df029773605cc39cc Mon Sep 17 00:00:00 2001 From: Karim Date: Sun, 12 Jul 2026 17:58:27 +0200 Subject: [PATCH] Fenster-Grundriss: Sims/Stulp folgen der eingezogenen Rahmenkante, mittel ohne Glaslinie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/geometry/opening.ts | 23 +++++++------ src/plan/generatePlan.ts | 40 ++++++---------------- src/plan/generatePlan.windowFields.test.ts | 32 +++++++++++++++-- 3 files changed, 51 insertions(+), 44 deletions(-) diff --git a/src/geometry/opening.ts b/src/geometry/opening.ts index 5c006f3..22a623e 100644 --- a/src/geometry/opening.ts +++ b/src/geometry/opening.ts @@ -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,22 +465,18 @@ 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) { - 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))]); + // 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))]); } } return { frame, glassLines, mullionLines, sashFrames, sashOpeningLines: [], meetingMarks }; diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index fc881bb..038af23 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -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; diff --git a/src/plan/generatePlan.windowFields.test.ts b/src/plan/generatePlan.windowFields.test.ts index 9f9bb8c..05db541 100644 --- a/src/plan/generatePlan.windowFields.test.ts +++ b/src/plan/generatePlan.windowFields.test.ts @@ -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;