From 4319e12e35bfaf78adad3f0f404b48fc396cbf1a Mon Sep 17 00:00:00 2001 From: Karim Date: Fri, 10 Jul 2026 02:06:37 +0200 Subject: [PATCH] =?UTF-8?q?T=C3=BCr:=20zweifl=C3=BCgelige=20T=C3=BCren=20w?= =?UTF-8?q?irklich=20rendern=20(leafCount=202D+3D)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DoorType.leafCount wurde bisher von keinem Renderer gelesen — eine zweiflügelige Tür sah aus wie eine einflügelige (Designdoc-Prio #3). - opening.ts: additive doorLeaves(wall, o, leafCount) — einflüglig ein Blatt voller Breite, zweiflüglig zwei halbbreite Blätter an gegenüberliegenden Pfosten, beide zur selben Seite (symmetrische Doppeltür). doorSymbol bleibt unverändert (Parität). - generatePlan: Tür-Zweig zeichnet je Blatt Linie + Schwenkbogen (leafCount aus DoorType). - toWalls3d: Tür-wingCount = leafCount → mittiger Anschlagpfosten (Stulp) im 3D. - +1 Test (2 Blätter/Bögen, halber Radius). 669/669 grün. --- src/geometry/opening.ts | 57 ++++++++++++++++++++++++ src/plan/generatePlan.doorwindow.test.ts | 24 ++++++++++ src/plan/generatePlan.ts | 41 +++++++++-------- src/plan/toWalls3d.ts | 3 +- 4 files changed, 106 insertions(+), 19 deletions(-) diff --git a/src/geometry/opening.ts b/src/geometry/opening.ts index b4e21f5..c2a6a9f 100644 --- a/src/geometry/opening.ts +++ b/src/geometry/opening.ts @@ -162,6 +162,63 @@ export function doorSymbol(wall: Wall, o: Opening): DoorSymbol | null { }; } +/** Ein einzelnes Türblatt (Grundriss): Scharnier + geschlossenes/offenes Ende + Radius. */ +export interface DoorLeaf { + hinge: Vec2; + openEnd: Vec2; + closedEnd: Vec2; + radius: number; +} + +/** Blatt-Geometrie aus Scharnier, geschlossener Richtung, Schwenkrichtung, Winkel, Breite. */ +function computeLeaf( + hinge: Vec2, + closedDir: Vec2, + swingDir: Vec2, + angle: number, + width: number, +): DoorLeaf { + const openDir = { + x: closedDir.x * Math.cos(angle) + swingDir.x * Math.sin(angle), + y: closedDir.y * Math.cos(angle) + swingDir.y * Math.sin(angle), + }; + return { + hinge, + openEnd: add(hinge, scale(openDir, width)), + closedEnd: add(hinge, scale(closedDir, width)), + radius: width, + }; +} + +/** + * Türblätter im Grundriss (1 oder 2 Flügel). Einflüglig: ein Blatt über die + * volle Breite (Scharnier je `o.hinge`). Zweiflüglig (`leafCount === 2`): zwei + * halbbreite Blätter, an den GEGENüberliegenden Pfosten angeschlagen, beide zur + * selben Seite schwenkend (symmetrische Doppeltür / Stulptür). `doorSymbol` + * bleibt unverändert (einflügelig) für Bestandspfade/Parität; dies ist additiv. + */ +export function doorLeaves(wall: Wall, o: Opening, leafCount: number): DoorLeaf[] { + const jambs = openingJambs(wall, o); + if (!jambs) return []; + const { u, n } = wallAxisFrame(wall); + const { jambStart, jambEnd } = jambs; + const width = Math.hypot(jambEnd.x - jambStart.x, jambEnd.y - jambStart.y); + const swingSign = (o.swing ?? "left") === "left" ? 1 : -1; + const dirSign = (o.openingDir ?? "in") === "in" ? 1 : -1; + const swingDir = scale(n, swingSign * dirSign); + const angle = (o.swingAngle ?? 90) * DEG; + if (leafCount >= 2) { + const hw = width / 2; + return [ + computeLeaf(jambStart, u, swingDir, angle, hw), + computeLeaf(jambEnd, scale(u, -1), swingDir, angle, hw), + ]; + } + const hinge = (o.hinge ?? "start") === "start" ? jambStart : jambEnd; + const closedDir = (o.hinge ?? "start") === "start" ? u : scale(u, -1); + return [computeLeaf(hinge, closedDir, swingDir, angle, width)]; +} + // ── Fenster-Symbol (Plan) ──────────────────────────────────────────────────── /** Aufgelöste Fenster-Linien im Grundriss (Weltkoordinaten). */ diff --git a/src/plan/generatePlan.doorwindow.test.ts b/src/plan/generatePlan.doorwindow.test.ts index 9fe90d9..80c7c8e 100644 --- a/src/plan/generatePlan.doorwindow.test.ts +++ b/src/plan/generatePlan.doorwindow.test.ts @@ -164,6 +164,30 @@ describe("generatePlan — Tür-/Fenstertyp-Rahmenband (opening-frame)", () => { }); }); +describe("generatePlan — zweiflügelige Tür (leafCount)", () => { + it("leafCount 2 -> zwei Türblätter + zwei Schwenkbögen; leafCount 1 -> je eines", () => { + const oneLeaf = project([{ ...baseDoor, typeId: "dt1" }], { doorTypes: [doorType] }); + expect(linesOfClass(oneLeaf, "door-leaf", "T1").length).toBe(1); + + const twoLeafType: DoorType = { ...doorType, id: "dt2", leafCount: 2 }; + const twoLeaf = project([{ ...baseDoor, typeId: "dt2" }], { doorTypes: [twoLeafType] }); + expect(linesOfClass(twoLeaf, "door-leaf", "T1").length).toBe(2); + // Zwei Schwenkbögen (arc-Primitive) bei mittel/fein. + const arcs = generatePlan(twoLeaf, "eg", visible, undefined, "mittel").primitives.filter( + (pr) => pr.kind === "arc" && (pr as { cls?: string }).cls === "door-swing", + ); + expect(arcs.length).toBe(2); + // Die beiden Blätter sind je halb so breit (Radius) wie das einflügelige. + const oneRadius = ( + generatePlan(oneLeaf, "eg", visible, undefined, "mittel").primitives.find( + (pr) => pr.kind === "arc" && (pr as { cls?: string }).cls === "door-swing", + ) as { r: number } + ).r; + const twoRadius = (arcs[0] as { r: number }).r; + expect(twoRadius).toBeCloseTo(oneRadius / 2, 6); + }); +}); + describe("generatePlan — Oberlicht (opening-transom)", () => { it("erscheint genau dann, wenn transomHeight > 0", () => { const withTransom = project([{ ...baseWindow, typeId: "wt1" }], { windowTypes: [windowType] }); diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index 5839c0c..57826d1 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -57,6 +57,7 @@ import { stairGeometry, stairCut, stairOutline } from "../geometry/stair"; import type { SpiralOutlineSegments } from "../geometry/stair"; import { doorSymbol, + doorLeaves, openingGapQuad, openingInterval, openingJambs, @@ -2115,29 +2116,33 @@ function addOpeningSymbol( // Entspricht `oeff_tuer_typ == "wandoeffnung"` im Rhino-Plugin. const isWandoeffnung = (o.doorType ?? "normal") === "wandoeffnung"; if (!isWandoeffnung) { - // Türblatt (alle Stufen). - out.push({ - kind: "line", - a: sym.hinge, - b: sym.openEnd, - cls: "door-leaf", - weightMm: SYMBOL_HAIRLINE_MM, - greyed, - openingId: o.id, - }); - // mittel + fein: Schwenkbogen von geschlossen → offen. - if (detail !== "grob") { + // Türblätter (1 oder 2 Flügel, je DoorType.leafCount). Zweiflüglig ⇒ zwei + // halbbreite Blätter an gegenüberliegenden Pfosten (symmetrische Doppeltür). + const leafCount = Math.max(1, Math.round(getDoorType(project, o)?.leafCount ?? 1)); + for (const leaf of doorLeaves(wall, o, leafCount)) { out.push({ - kind: "arc", - center: sym.hinge, - from: sym.closedEnd, - to: sym.openEnd, - r: sym.radius, - cls: "door-swing", + kind: "line", + a: leaf.hinge, + b: leaf.openEnd, + cls: "door-leaf", weightMm: SYMBOL_HAIRLINE_MM, greyed, openingId: o.id, }); + // mittel + fein: Schwenkbogen von geschlossen → offen je Blatt. + if (detail !== "grob") { + out.push({ + kind: "arc", + center: leaf.hinge, + from: leaf.closedEnd, + to: leaf.openEnd, + r: leaf.radius, + cls: "door-swing", + weightMm: SYMBOL_HAIRLINE_MM, + greyed, + openingId: o.id, + }); + } } } // fein: Anschlag-Striche an beiden Pfosten (quer zur Wand). diff --git a/src/plan/toWalls3d.ts b/src/plan/toWalls3d.ts index e47ef92..09eb9ee 100644 --- a/src/plan/toWalls3d.ts +++ b/src/plan/toWalls3d.ts @@ -1970,7 +1970,8 @@ function resolveOpeningFrame(project: Project, wall: Wall, op: Opening): Opening nMax, transomHeight: Math.max(0, dt.transomHeight ?? 0), hasSill: dt.threshold === true, - wingCount: 1, + // Zweiflüglige Tür ⇒ 2 Flügel = ein mittiger Anschlagpfosten (Stulp) im 3D. + wingCount: Math.max(1, Math.round(dt.leafCount ?? 1)), mullionRows: 1, glazed: dt.leafStyle === "glas", glazingPanes: 1,