From 89112eff9b40d2cda19ed23248a64d8bd6d0a366 Mon Sep 17 00:00:00 2001 From: Karim Date: Thu, 9 Jul 2026 22:03:39 +0200 Subject: [PATCH] =?UTF-8?q?Fenster:=202D-Doppelrahmen=20entfernt=20+=203D-?= =?UTF-8?q?Scheibe=20an=20die=20Aussenfl=C3=A4che?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nutzer-Feedback „Fenster im 2D nicht korrekt / im 3D zu schwach": - 2D: das Rahmenband (opening-frame) wurde beim Fenster ZUSÄTZLICH zum windowSymbol-Rahmen gezeichnet → doppeltes, verschachteltes Rechteck. Das Band gilt jetzt nur noch für Türen (deren Symbol nur Blatt+Bogen hat); Fenster nutzen allein windowSymbol (Rahmen + Glaslinien). Oberlicht-Andeutung (opening-transom) bleibt für beide. - 3D: die Scheibe sass mittig tief im Reveal und wirkte schwach; sie sitzt jetzt knapp (3 cm) hinter der Aussenfläche → klar sichtbar, Laibung zeigt nach innen. Tests angepasst (Band jetzt tür-spezifisch, Einzug am Tür-Zargenband geprüft). tsc + vitest 640 grün. --- src/plan/generatePlan.doorwindow.test.ts | 41 +++++---- src/plan/generatePlan.ts | 111 ++++++++++++----------- src/plan/toWalls3d.ts | 14 +-- 3 files changed, 91 insertions(+), 75 deletions(-) diff --git a/src/plan/generatePlan.doorwindow.test.ts b/src/plan/generatePlan.doorwindow.test.ts index 0ec2b15..9fe90d9 100644 --- a/src/plan/generatePlan.doorwindow.test.ts +++ b/src/plan/generatePlan.doorwindow.test.ts @@ -135,7 +135,7 @@ function linesOfClass(p: Project, cls: string, openingId?: string): LinePrim[] { } describe("generatePlan — Tür-/Fenstertyp-Rahmenband (opening-frame)", () => { - it("erzeugt Rahmenband-Primitive für Tür UND Fenster mit referenziertem Typ", () => { + it("erzeugt das Rahmenband NUR für die Tür (Fenster: windowSymbol zeichnet den Rahmen)", () => { const p = project( [ { ...baseWindow, typeId: "wt1" }, @@ -143,23 +143,24 @@ describe("generatePlan — Tür-/Fenstertyp-Rahmenband (opening-frame)", () => { ], { doorTypes: [doorType], windowTypes: [windowType] }, ); - expect(linesOfClass(p, "opening-frame").length).toBeGreaterThan(0); - // Je Öffnung ein geschlossenes Rechteck aus 4 Kanten. - expect(linesOfClass(p, "opening-frame", "F1").length).toBe(4); + // Tür: ein geschlossenes Rechteck aus 4 Kanten. Fenster: KEIN Band (sonst + // doppeltes, verschachteltes Rechteck über dem windowSymbol-Rahmen). expect(linesOfClass(p, "opening-frame", "T1").length).toBe(4); + expect(linesOfClass(p, "opening-frame", "F1").length).toBe(0); }); - it("Blockrahmen (Tür) zeichnet eine kräftigere Linie als die Zargen-Optik (Fenster)", () => { + it("Blockrahmen-Tür zeichnet eine kräftigere Linie als eine Zargen-Tür", () => { + const zargeDoor: DoorType = { ...doorType, id: "dt-zarge", frameKind: "zarge" }; const p = project( [ - { ...baseWindow, typeId: "wt1" }, - { ...baseDoor, typeId: "dt1" }, + { ...baseDoor, id: "T1", typeId: "dt1" }, + { ...baseDoor, id: "T2", position: 3.0, typeId: "dt-zarge" }, ], - { doorTypes: [doorType], windowTypes: [windowType] }, + { doorTypes: [doorType, zargeDoor] }, ); - const doorWeight = linesOfClass(p, "opening-frame", "T1")[0].weightMm; - const windowWeight = linesOfClass(p, "opening-frame", "F1")[0].weightMm; - expect(doorWeight).toBeGreaterThan(windowWeight); + const blockWeight = linesOfClass(p, "opening-frame", "T1")[0].weightMm; + const zargeWeight = linesOfClass(p, "opening-frame", "T2")[0].weightMm; + expect(blockWeight).toBeGreaterThan(zargeWeight); }); }); @@ -178,17 +179,25 @@ describe("generatePlan — Oberlicht (opening-transom)", () => { describe("generatePlan — Schichteinzug (insetFromFace/insetFace)", () => { it("verschiebt die Rahmen-Nahfläche um den Einzugsbetrag, die Gegenfläche bleibt unverändert", () => { - const flushType: WindowType = { ...windowType, id: "wt-flush", insetFromFace: 0 }; - const inset = project([{ ...baseWindow, typeId: "wt1" }], { windowTypes: [windowType] }); - const flush = project([{ ...baseWindow, typeId: "wt-flush" }], { windowTypes: [flushType] }); + // Am Tür-Zargenband geprüft (das Fenster nutzt jetzt windowSymbol statt Band). + const insetDoor: DoorType = { + ...doorType, + id: "dt-inset", + frameKind: "zarge", + insetFromFace: 0.05, + insetFace: "aussen", + }; + const flushDoor: DoorType = { ...insetDoor, id: "dt-flush", insetFromFace: 0 }; + const inset = project([{ ...baseDoor, typeId: "dt-inset" }], { doorTypes: [insetDoor] }); + const flush = project([{ ...baseDoor, typeId: "dt-flush" }], { doorTypes: [flushDoor] }); // Die Wandachse ist horizontal (u=(1,0)) ⇒ Wandnormale n=(0,1); die // y-Koordinate der Rahmenband-Punkte entspricht direkt dem Offset quer zur // Wand. Die "aussen"-Fläche liegt bei +n — ihr Rahmenrand muss um genau // `insetFromFace` (0.05 m) nach innen (kleineres y) rücken; die // gegenüberliegende ("innen"-)Fläche bleibt unverändert. - const insetYs = linesOfClass(inset, "opening-frame", "F1").flatMap((l) => [l.a.y, l.b.y]); - const flushYs = linesOfClass(flush, "opening-frame", "F1").flatMap((l) => [l.a.y, l.b.y]); + const insetYs = linesOfClass(inset, "opening-frame", "T1").flatMap((l) => [l.a.y, l.b.y]); + const flushYs = linesOfClass(flush, "opening-frame", "T1").flatMap((l) => [l.a.y, l.b.y]); expect(Math.max(...flushYs) - Math.max(...insetYs)).toBeCloseTo(0.05, 9); expect(Math.min(...insetYs)).toBeCloseTo(Math.min(...flushYs), 9); diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index f495d4a..09a17a1 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -1980,61 +1980,68 @@ function addOpeningFrameBand( const hi = Math.max(faceNear, faceFar); const { jambStart, jambEnd } = jambs; - const frameWidth = Math.max(0, spec.frameWidth); - const isBlock = o.kind === "door" && spec.frameKind === "blockrahmen"; const color = o.color ?? POCHE_STROKE; - let bandLo: number; - let bandHi: number; - let jStart: Vec2; - let jEnd: Vec2; - let weightMm: number; - if (isBlock) { - // Blockrahmen: volle Öffnungsbreite (kein Pfosten-Einzug), nur eine - // Tiefenscheibe von frameWidth ab der Nah-Fläche — sitzt proud VOR/auf der - // Laibung, statt schmal darin zu verschwinden (Zargen-Optik). - const depth = Math.min(frameWidth, hi - lo); - bandLo = nearIsAussen ? hi - depth : lo; - bandHi = nearIsAussen ? hi : lo + depth; - jStart = jambStart; - jEnd = jambEnd; - weightMm = SYMBOL_HAIRLINE_MM * 2.2; - } else { - // Zarge: ringsum um frameWidth von der vollen Öffnung eingezogenes, - // schmales Rahmenband (Laibungs-Anschlaglinie). - const jambSpan = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y); - const jambInset = Math.min(frameWidth, jambSpan / 2); - jStart = along(jambStart, jambEnd, jambInset); - jEnd = along(jambEnd, jambStart, jambInset); - const depthInset = Math.min(frameWidth, (hi - lo) / 2); - bandLo = lo + depthInset; - bandHi = hi - depthInset; - weightMm = SYMBOL_HAIRLINE_MM; - } + // Rahmen-Rechteck NUR bei Türen zeichnen. Beim Fenster stellt bereits + // `windowSymbol` den Rahmen dar (voller Öffnungsumriss + Glaslinien); ein + // zusätzliches Band ergäbe ein doppeltes, verschachteltes Rechteck — genau + // das wirkte im 2D „nicht korrekt". Die Tür hat dort nur Blatt+Bogen, daher + // ist ihr Rahmenband die einzige Rahmenkontur und bleibt. + if (o.kind === "door") { + const frameWidth = Math.max(0, spec.frameWidth); + const isBlock = spec.frameKind === "blockrahmen"; + let bandLo: number; + let bandHi: number; + let jStart: Vec2; + let jEnd: Vec2; + let weightMm: number; + if (isBlock) { + // Blockrahmen: volle Öffnungsbreite (kein Pfosten-Einzug), nur eine + // Tiefenscheibe von frameWidth ab der Nah-Fläche — sitzt proud VOR/auf der + // Laibung, statt schmal darin zu verschwinden (Zargen-Optik). + const depth = Math.min(frameWidth, hi - lo); + bandLo = nearIsAussen ? hi - depth : lo; + bandHi = nearIsAussen ? hi : lo + depth; + jStart = jambStart; + jEnd = jambEnd; + weightMm = SYMBOL_HAIRLINE_MM * 2.2; + } else { + // Zarge: ringsum um frameWidth von der vollen Öffnung eingezogenes, + // schmales Rahmenband (Laibungs-Anschlaglinie). + const jambSpan = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y); + const jambInset = Math.min(frameWidth, jambSpan / 2); + jStart = along(jambStart, jambEnd, jambInset); + jEnd = along(jambEnd, jambStart, jambInset); + const depthInset = Math.min(frameWidth, (hi - lo) / 2); + bandLo = lo + depthInset; + bandHi = hi - depthInset; + weightMm = SYMBOL_HAIRLINE_MM; + } - // Geschlossenes Rechteck aus 4 Kanten (Primitive "line", nicht "polygon" — - // so bleibt das Band per `cls` selektierbar/testbar wie die übrigen Symbole). - const p1 = add(jStart, scale(n, bandLo)); - const p2 = add(jEnd, scale(n, bandLo)); - const p3 = add(jEnd, scale(n, bandHi)); - const p4 = add(jStart, scale(n, bandHi)); - 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: "opening-frame", - weightMm, - color, - greyed, - openingId: o.id, - }); + // Geschlossenes Rechteck aus 4 Kanten (Primitive "line", nicht "polygon" — + // so bleibt das Band per `cls` selektierbar/testbar wie die übrigen Symbole). + const p1 = add(jStart, scale(n, bandLo)); + const p2 = add(jEnd, scale(n, bandLo)); + const p3 = add(jEnd, scale(n, bandHi)); + const p4 = add(jStart, scale(n, bandHi)); + 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: "opening-frame", + weightMm, + color, + greyed, + openingId: o.id, + }); + } } // Oberlicht (Überkopf-Andeutung): eine gestrichelte Linie quer über die diff --git a/src/plan/toWalls3d.ts b/src/plan/toWalls3d.ts index 64fb8ba..b5239fa 100644 --- a/src/plan/toWalls3d.ts +++ b/src/plan/toWalls3d.ts @@ -1974,13 +1974,13 @@ function glassPanesForOpening( const sashTo = iv.to - fw; const sashBottom = vert.zBottom + (params.hasSill ? fw : 0); const sashTopFull = vert.zTop - fw; - // Die Scheibe ist eine DÜNNE Tafel mittig in der (nun tiefen) Rahmen-Laibung — - // nicht der volle Rahmen-Querschnitt, sonst wäre das „Glas" ein dicker Block - // und die Laibung/Reveal-Tiefe unsichtbar. So liest sich das Fenster plastisch: - // Rahmen zieht in die Wand, die Scheibe sitzt zurückgesetzt in der Mitte. - const glassMid = (params.nMin + params.nMax) / 2; - const gN0 = glassMid - GLASS_HALF_THICK; - const gN1 = glassMid + GLASS_HALF_THICK; + // Die Scheibe ist eine DÜNNE Tafel NAHE der Aussenfläche (nMax = +n-Seite, s. + // resolveFrameNormalRange) — mittig tief im Reveal wirkte sie schwach/kaum + // sichtbar. Direkt hinter der Aussenkante (3 cm zurück) liest sich das Fenster + // klar, die Laibung/Reveal-Tiefe zeigt nach innen. + const glassThick = GLASS_HALF_THICK * 2; + const gN1 = params.nMax - 0.03; + const gN0 = Math.max(params.nMin, gN1 - glassThick); const push = (zBottom: number, zTop: number): void => { const mesh = openingAxisBox(wall, sashFrom, sashTo, zBottom, zTop, gN0, gN1, GLASS_RGB); if (mesh) out.push(mesh);