Bauteile Gruppe A (4-6): Treppe-Referenz, Fenster-Fluegel, Tuer-wandoeffnung

- Treppe referenz links/mitte/rechts (Stair.referenz, Default mitte): Perp-Versatz
  referenzMidShift auf start/center, Lauflinie/Pfeil bleiben visuell zentriert.
- Fenster Fluegel-Mittelpfosten (Opening.wingCount 1-4, Default 1): wingCount-1
  Querlinien im Rahmen (window-mullion), nur mittel/fein.
- Tuer-Typ wandoeffnung (Opening.doorType, Default normal): ueberspringt
  Tuerblatt + Schwenkbogen, Laibung/Sturz bleiben.
- Alle Modellfelder optional/rueckwaertskompatibel. 17 neue Tests.
  vitest 292/292, tsc sauber.
This commit is contained in:
2026-07-05 02:33:48 +02:00
parent 6e5998ce72
commit 26fbd11b73
6 changed files with 361 additions and 31 deletions
+27 -2
View File
@@ -170,12 +170,21 @@ export interface WindowSymbol {
frame: Vec2[]; frame: Vec2[];
/** Glaslinien-Segmente quer über die Lücke (12 parallele Linien). */ /** Glaslinien-Segmente quer über die Lücke (12 parallele Linien). */
glassLines: [Vec2, Vec2][]; glassLines: [Vec2, Vec2][];
/**
* Flügel-Mittelpfosten (`wingCount 1` Querlinien), jeweils von einer
* Laibungsseite zur anderen (parallel zu den Laibungslinien, quer zur
* Öffnungsrichtung), gleichmäßig zwischen den beiden Pfosten verteilt. Bei
* `wingCount ≤ 1` leer. Entspricht den Mittelpfosten in `_make_oeffnung_pieces`
* des Rhino-Plugins (`oeff_fluegel`).
*/
mullionLines: [Vec2, Vec2][];
} }
/** /**
* Löst die Fenster-Symbolik auf: ein Rahmen-Rechteck über die Lücke (volle * Löst die Fenster-Symbolik auf: ein Rahmen-Rechteck über die Lücke (volle
* Wanddicke) plus 12 Glaslinien (dünne parallele Linien in der Wandmitte, * Wanddicke) plus 12 Glaslinien (dünne parallele Linien in der Wandmitte,
* `count` steuert die Anzahl je Detailgrad). * `count` steuert die Anzahl je Detailgrad) plus optionale Flügel-Mittelpfosten
* (`wingCount 1` Querlinien).
*/ */
export function windowSymbol( export function windowSymbol(
project: Project, project: Project,
@@ -205,5 +214,21 @@ export function windowSymbol(
const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2); const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2);
glassLines.push([add(jambStart, scale(n, off)), add(jambEnd, scale(n, off))]); glassLines.push([add(jambStart, scale(n, off)), add(jambEnd, scale(n, off))]);
} }
return { frame, glassLines }; // Flügel-Mittelpfosten: (wingCount 1) Querlinien gleichmäßig zwischen den
// Pfosten. Jede Linie läuft quer über die Öffnung (n-Richtung), auf ~60 % der
// Mauerdicke symmetrisch um die Wandmitte (Referenz-Offset) — analog Rhino, wo
// die Pfosten x_mid = inner_l + span·(i/fluegel) über die Rahmentiefe sitzen.
const mullionLines: [Vec2, Vec2][] = [];
const wings = Math.max(1, Math.min(4, Math.round(o.wingCount ?? 1)));
if (wings > 1) {
const postDepth = total * 0.6;
const pOuter = refOff + postDepth / 2;
const pInner = refOff - postDepth / 2;
for (let i = 1; i < wings; i++) {
const t = i / wings; // gleichmäßige Teilung entlang der Achse
const onAxis = add(jambStart, scale(sub(jambEnd, jambStart), t));
mullionLines.push([add(onAxis, scale(n, pOuter)), add(onAxis, scale(n, pInner))]);
}
}
return { frame, glassLines, mullionLines };
} }
+53 -10
View File
@@ -27,6 +27,32 @@ const norm = (a: Vec2): Vec2 => {
/** Linke Normale (n = (u.y, u.x)) — quer zur Laufrichtung. */ /** Linke Normale (n = (u.y, u.x)) — quer zur Laufrichtung. */
const leftN = (u: Vec2): Vec2 => ({ x: -u.y, y: u.x }); const leftN = (u: Vec2): Vec2 => ({ x: -u.y, y: u.x });
/**
* Perp-Versatz von der Referenzkante zur visuellen Treppen-MITTE, entlang der
* linken Normale `n = leftN(u)`. Entspricht `mid_off` aus `_treppe_2d_side_offsets`
* im Rhino-Plugin (Konvention: `perp` = CCW-Normale = linke Normale):
* • referenz "links" → Achse ist die LINKE Kante; die Mitte liegt eine
* halbe Breite in n-Richtung (rechts) → mid_off = width/2.
* • referenz "rechts" → Achse ist die RECHTE Kante; die Mitte liegt eine
* halbe Breite in +n-Richtung (links) → mid_off = +width/2.
* • referenz "mitte"/undefined → Achse ist bereits die Mitte → 0.
*
* ANNAHME (selbstkritisch): „links" heisst „linke Kante in Laufrichtung", und die
* linke Kante liegt in +n (n = leftN(u), 90° links der Laufrichtung). Von der
* linken Kante zur Mitte geht es also nach n, daher mid_off = width/2. Das
* deckt sich mit `_treppe_2d_side_offsets` (referenz="links" → off_a=0, off_b=b,
* Mitte = b/2·perp). Wird die Richtung im UI als seitenverkehrt empfunden, ist
* hier das Vorzeichen zu drehen — die Geometrie bleibt in sich konsistent.
*/
function referenzMidShift(stair: Stair, u: Vec2): Vec2 {
const ref = stair.referenz ?? "mitte";
if (ref === "mitte") return { x: 0, y: 0 };
const n = leftN(u);
const halfW = Math.max(0.05, stair.width / 2);
const midOff = ref === "links" ? -halfW : halfW;
return scale(n, midOff);
}
/** SIA-nahe Schrittregel: gute Steigungshöhe ≈ 0.17 m, Auftritt ≈ 0.29 m. */ /** SIA-nahe Schrittregel: gute Steigungshöhe ≈ 0.17 m, Auftritt ≈ 0.29 m. */
export const IDEAL_RISER = 0.17; export const IDEAL_RISER = 0.17;
export const IDEAL_TREAD = 0.29; export const IDEAL_TREAD = 0.29;
@@ -105,13 +131,22 @@ export function stairGeometry(stair: Stair, totalRise: number): StairGeometry {
const u = norm(stair.dir); const u = norm(stair.dir);
const n = leftN(u); const n = leftN(u);
if (stair.shape === "spiral") { // Referenz (links/mitte/rechts): die gespeicherte Achse wird auf die visuelle
return spiralGeometry(stair, totalRise, steps, riserHeight, halfW); // Treppen-MITTE versetzt, danach greift die bestehende, um die Achse zentrierte
// Tritt-/Lauflinien-Logik unverändert. So bleibt die Lauflinie samt Pfeil stets
// auf der Mitte (nicht auf der Referenzkante). "mitte"/undefined ⇒ Nullversatz.
const c = referenzMidShift(stair, u);
const s = stair.referenz && stair.referenz !== "mitte"
? { ...stair, start: add(stair.start, c), center: stair.center ? add(stair.center, c) : stair.center }
: stair;
if (s.shape === "spiral") {
return spiralGeometry(s, totalRise, steps, riserHeight, halfW);
} }
if (stair.shape === "L") { if (s.shape === "L") {
return lGeometry(stair, totalRise, steps, riserHeight, halfW, u, n); return lGeometry(s, totalRise, steps, riserHeight, halfW, u, n);
} }
return straightGeometry(stair, totalRise, steps, riserHeight, halfW, u, n); return straightGeometry(s, totalRise, steps, riserHeight, halfW, u, n);
} }
/** Gerade Treppe: gleich breite Tritte über die Lauflänge. */ /** Gerade Treppe: gleich breite Tritte über die Lauflänge. */
@@ -530,11 +565,19 @@ export function stairOutline(
): { polygon: Vec2[] | null; spiral: SpiralOutlineSegments | null } { ): { polygon: Vec2[] | null; spiral: SpiralOutlineSegments | null } {
void totalRise; // Signatur-Konsistenz mit stairGeometry; derzeit ungenutzt void totalRise; // Signatur-Konsistenz mit stairGeometry; derzeit ungenutzt
const halfW = Math.max(0.05, stair.width / 2); const halfW = Math.max(0.05, stair.width / 2);
if (stair.shape === "spiral") { // Referenz (links/mitte/rechts): die gespeicherte Achse zuerst auf die
return { polygon: null, spiral: spiralOutline(stair, halfW) }; // visuelle Treppen-Mitte versetzen, danach die um die Mitte zentrierte
// Aussenlinien-Logik unverändert anwenden (identisch zu `stairGeometry`).
const u = norm(stair.dir);
const c = referenzMidShift(stair, u);
const s = stair.referenz && stair.referenz !== "mitte"
? { ...stair, start: add(stair.start, c), center: stair.center ? add(stair.center, c) : stair.center }
: stair;
if (s.shape === "spiral") {
return { polygon: null, spiral: spiralOutline(s, halfW) };
} }
if (stair.shape === "L") { if (s.shape === "L") {
return { polygon: lOutlinePoints(stair, halfW), spiral: null }; return { polygon: lOutlinePoints(s, halfW), spiral: null };
} }
return { polygon: straightOutlinePoints(stair, halfW), spiral: null }; return { polygon: straightOutlinePoints(s, halfW), spiral: null };
} }
+64 -1
View File
@@ -11,7 +11,7 @@
*/ */
import { describe, it, expect } from "vitest"; import { describe, it, expect } from "vitest";
import { stairOutline } from "./stair"; import { stairOutline, stairGeometry } from "./stair";
import type { Stair, Vec2 } from "../model/types"; import type { Stair, Vec2 } from "../model/types";
/** Einfache Distanz-Funktion. */ /** Einfache Distanz-Funktion. */
@@ -146,3 +146,66 @@ describe("stairOutline — Wendeltreppe", () => {
expect(d).toBeCloseTo(arcIn.r, 4); expect(d).toBeCloseTo(arcIn.r, 4);
}); });
}); });
describe("stairOutline — Referenz links/mitte/rechts (Item 4)", () => {
const totalRise = 3.0;
// Gerade Treppe entlang +X, Breite 1.2 m. Perp (linke Normale) = (0, +1).
const base: Stair = { ...geradeStair, referenz: "mitte" };
const mitte = stairOutline(base, totalRise).polygon!;
const links = stairOutline({ ...base, referenz: "links" }, totalRise).polygon!;
const rechts = stairOutline({ ...base, referenz: "rechts" }, totalRise).polygon!;
/** Mittlere Y-Höhe eines Rechteck-Polygons. */
const midY = (pts: Vec2[]) => pts.reduce((s, p) => s + p.y, 0) / pts.length;
it("Breite bleibt bei allen Referenzen gleich", () => {
for (const poly of [mitte, links, rechts]) {
expect(dist(poly[0], poly[1])).toBeCloseTo(geradeStair.width, 5);
}
});
it("referenz=links: Treppe liegt RECHTS der Achse (n) → Mitte um width/2 nach Y", () => {
// Achse (start/dir) = linke Kante; die Fläche versetzt sich in n = (0,1).
expect(midY(links) - midY(mitte)).toBeCloseTo(-geradeStair.width / 2, 5);
});
it("referenz=rechts: Treppe liegt LINKS der Achse (+n) → Mitte um width/2 nach +Y", () => {
expect(midY(rechts) - midY(mitte)).toBeCloseTo(+geradeStair.width / 2, 5);
});
it("bei referenz=links liegt die Achse (y=0) auf der linken Kante der Treppe", () => {
// Linke Kante = max-Y-Kante der Fläche (n = +Y). Sie soll bei y=0 liegen.
const maxY = Math.max(...links.map((p) => p.y));
expect(maxY).toBeCloseTo(0, 5);
});
it("mitte entspricht dem Default (kein referenz-Feld gesetzt)", () => {
const noRef = stairOutline({ ...geradeStair }, totalRise).polygon!;
for (let i = 0; i < noRef.length; i++) {
expect(noRef[i].x).toBeCloseTo(mitte[i].x, 6);
expect(noRef[i].y).toBeCloseTo(mitte[i].y, 6);
}
});
});
describe("stairGeometry — Lauflinie bleibt visuell zentriert (Item 4)", () => {
const totalRise = 3.0;
const links = stairGeometry({ ...geradeStair, referenz: "links" }, totalRise);
const mitte = stairGeometry({ ...geradeStair, referenz: "mitte" }, totalRise);
it("Lauflinie liegt auf der Treppen-Mitte, nicht auf der Referenzkante (y=0)", () => {
// Bei referenz=links liegt die Mitte bei y = width/2 = 0.6.
for (const p of links.runLine) {
expect(p.y).toBeCloseTo(-geradeStair.width / 2, 5);
}
// Bei mitte liegt die Lauflinie auf y=0 (Achse = Mitte).
for (const p of mitte.runLine) {
expect(p.y).toBeCloseTo(0, 5);
}
});
it("Tritte sind bei referenz=links um width/2 versetzt", () => {
const dy = links.treads[0].pts[0].y - mitte.treads[0].pts[0].y;
expect(dy).toBeCloseTo(-geradeStair.width / 2, 5);
});
});
+29
View File
@@ -731,6 +731,22 @@ export interface Opening {
height: number; height: number;
/** Brüstungshöhe (Unterkante der Öffnung) über der Wand-UK; 0 bei Türen. */ /** Brüstungshöhe (Unterkante der Öffnung) über der Wand-UK; 0 bei Türen. */
sillHeight: number; sillHeight: number;
/**
* Nur Fenster: Anzahl der Flügel (14). Bei > 1 werden im Plan `wingCount 1`
* Mittelpfosten (Querlinien quer zur Öffnungsrichtung) gleichmäßig innerhalb des
* Rahmens gezeichnet — analog den Mittelpfosten in `_make_oeffnung_pieces` des
* Rhino-Plugins (`oeff_fluegel`). Fehlt es, gilt 1 (= heutiges Verhalten: keine
* Pfosten).
*/
wingCount?: number;
/**
* Nur Tür: Tür-Typ (analog `oeff_tuer_typ` im Rhino-Plugin).
* • "normal" — Türblatt + Schwenkbogen (Default = heutiges Verhalten).
* • "wandoeffnung" — reiner Wanddurchbruch: KEIN Türblatt, KEIN Schwenkbogen;
* nur die Öffnung/Laibung (Wandlücke + ggf. Sturz-/Anschlaglinien) bleibt.
* Fehlt es, gilt "normal".
*/
doorType?: "normal" | "wandoeffnung";
/** Nur Tür: an welchem Pfosten das Scharnier sitzt. */ /** Nur Tür: an welchem Pfosten das Scharnier sitzt. */
hinge?: "start" | "end"; hinge?: "start" | "end";
/** Nur Tür: auf welche Seite der Wandachse das Blatt aufschlägt. */ /** Nur Tür: auf welche Seite der Wandachse das Blatt aufschlägt. */
@@ -814,6 +830,19 @@ export interface Stair {
sweep?: number; sweep?: number;
/** Laufbreite in Metern (quer zur Laufrichtung). */ /** Laufbreite in Metern (quer zur Laufrichtung). */
width: number; width: number;
/**
* Lage der gespeicherten Achse (`start`/`dir` bzw. Wendel-Bogen) über die
* Laufbreite — analog `treppe_referenz` im Rhino-Plugin. Bestimmt, ob die Achse
* die LINKE, MITTLERE oder RECHTE Kante der Treppe repräsentiert (in
* Laufrichtung gesehen). Fehlt sie, gilt "mitte" (= heutiges Verhalten: Achse =
* Treppen-Mitte).
* • "links" — Achse = linke Kante; die Treppe liegt rechts der Achse.
* • "mitte" — Achse = Mitte (Default).
* • "rechts" — Achse = rechte Kante; die Treppe liegt links der Achse.
* Die Lauflinie samt Pfeil bleibt IMMER auf der visuellen Treppen-Mitte
* (nicht auf der Referenzkante) — siehe `stairGeometry`.
*/
referenz?: "links" | "mitte" | "rechts";
/** /**
* Gesamt-Steighöhe in Metern (OKFF → OKFF nächstes Geschoss). Fehlt sie, gilt * Gesamt-Steighöhe in Metern (OKFF → OKFF nächstes Geschoss). Fehlt sie, gilt
* beim Auflösen die Geschosshöhe des zugehörigen Geschosses. * beim Auflösen die Geschosshöhe des zugehörigen Geschosses.
+149
View File
@@ -0,0 +1,149 @@
/**
* Unit-Tests für die Öffnungs-Symbole im Grundriss (generatePlan):
* • Item 5 — Fenster Flügel-Mittelpfosten: bei wingCount=3 werden genau 2
* Pfosten-Linien (Querlinien) erzeugt; wingCount=1/undefined ⇒ keine.
* • Item 6 — Tür-Typ „wandoeffnung": kein Türblatt, kein Schwenkbogen (arc);
* „normal" (Default) zeichnet Blatt + Bogen wie bisher.
*/
import { describe, it, expect } from "vitest";
import { generatePlan } from "./generatePlan";
import type { Opening, Project, Wall } from "../model/types";
/** Minimalprojekt mit einer Wand + einer Öffnung, konfigurierbar. */
function projectWithOpening(opening: Opening): Project {
const wall: Wall = {
id: "W1",
type: "wall",
floorId: "eg",
categoryCode: "20",
start: { x: 0, y: 0 },
end: { x: 4, y: 0 },
wallTypeId: "aw",
height: 2.6,
};
return {
id: "t",
name: "T",
lineStyles: [{ id: "thin", name: "d", weight: 0.13, color: "#111", dash: null }],
hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }],
components: [{ id: "a", name: "A", color: "#d8d2c7", hatchId: "none", joinPriority: 10 }],
wallTypes: [{ id: "aw", name: "AW", layers: [{ componentId: "a", thickness: 0.3 }] }],
drawingLevels: [
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
],
layers: [
{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false },
{ code: "21", name: "Öffnungen", color: "#0a0a0a", lw: 0.3, visible: true, locked: false },
],
walls: [wall],
doors: [],
openings: [opening],
ceilings: [],
stairs: [],
rooms: [],
drawings2d: [],
context: [],
};
}
const visible = new Set(["20", "21"]);
/** Basis-Fenster (Brüstung 0, damit keine Sill-Linie mitzählt). */
const baseWindow: Opening = {
id: "F1",
type: "opening",
hostWallId: "W1",
categoryCode: "21",
kind: "window",
position: 1.0,
width: 1.6,
height: 1.2,
sillHeight: 0,
};
/** Basis-Tür. */
const baseDoor: Opening = {
id: "T1",
type: "opening",
hostWallId: "W1",
categoryCode: "21",
kind: "door",
position: 1.0,
width: 0.9,
height: 2.0,
sillHeight: 0,
swing: "left",
hinge: "start",
};
/** Zählt Mittelpfosten-Linien (cls "window-mullion") im Plan. */
const mullionCount = (p: Project) =>
generatePlan(p, "eg", visible, undefined, "mittel").primitives.filter(
(pr) => pr.kind === "line" && pr.cls === "window-mullion",
).length;
describe("generatePlan — Fenster Flügel-Mittelpfosten (Item 5)", () => {
it("wingCount=3 erzeugt genau 2 Pfosten-Linien", () => {
expect(mullionCount(projectWithOpening({ ...baseWindow, wingCount: 3 }))).toBe(2);
});
it("wingCount=1 erzeugt keine Pfosten", () => {
expect(mullionCount(projectWithOpening({ ...baseWindow, wingCount: 1 }))).toBe(0);
});
it("ohne wingCount (Default) keine Pfosten — rückwärtskompatibel", () => {
expect(mullionCount(projectWithOpening({ ...baseWindow }))).toBe(0);
});
it("wingCount=4 erzeugt 3 Pfosten", () => {
expect(mullionCount(projectWithOpening({ ...baseWindow, wingCount: 4 }))).toBe(3);
});
it("Pfosten sind reine Linien ohne Strichmuster (kein dash)", () => {
const plan = generatePlan(projectWithOpening({ ...baseWindow, wingCount: 3 }), "eg", visible, undefined, "mittel");
const mullions = plan.primitives.filter(
(pr) => pr.kind === "line" && pr.cls === "window-mullion",
);
for (const m of mullions) {
expect((m as { dash?: number[] | null }).dash ?? null).toBeNull();
}
});
it("bei grob keine Pfosten (nur mittel/fein)", () => {
const plan = generatePlan(projectWithOpening({ ...baseWindow, wingCount: 3 }), "eg", visible, undefined, "grob");
const n = plan.primitives.filter((pr) => pr.kind === "line" && pr.cls === "window-mullion").length;
expect(n).toBe(0);
});
});
describe("generatePlan — Tür-Typ wandoeffnung (Item 6)", () => {
/** Zählt Bögen (arc-Primitive) im Plan. */
const arcCount = (p: Project, detail: "grob" | "mittel" | "fein" = "mittel") =>
generatePlan(p, "eg", visible, undefined, detail).primitives.filter((pr) => pr.kind === "arc").length;
/** Zählt Türblatt-Linien (cls "door-leaf"). */
const leafCount = (p: Project) =>
generatePlan(p, "eg", visible, undefined, "mittel").primitives.filter(
(pr) => pr.kind === "line" && pr.cls === "door-leaf",
).length;
it("wandoeffnung erzeugt KEINE Bögen (arc)", () => {
expect(arcCount(projectWithOpening({ ...baseDoor, doorType: "wandoeffnung" }))).toBe(0);
});
it("wandoeffnung erzeugt KEIN Türblatt", () => {
expect(leafCount(projectWithOpening({ ...baseDoor, doorType: "wandoeffnung" }))).toBe(0);
});
it("normal (Default) erzeugt Türblatt + Schwenkbogen", () => {
const normal = projectWithOpening({ ...baseDoor, doorType: "normal" });
expect(leafCount(normal)).toBe(1);
expect(arcCount(normal)).toBe(1);
});
it("ohne doorType (Default) verhält sich wie normal — rückwärtskompatibel", () => {
const def = projectWithOpening({ ...baseDoor });
expect(leafCount(def)).toBe(1);
expect(arcCount(def)).toBe(1);
});
});
+39 -18
View File
@@ -1588,29 +1588,36 @@ function addOpeningSymbol(
if (o.kind === "door") { if (o.kind === "door") {
const sym = doorSymbol(wall, o); const sym = doorSymbol(wall, o);
if (!sym) return; if (!sym) return;
// Türblatt (alle Stufen). // Tür-Typ „wandoeffnung": reiner Wanddurchbruch — KEIN Türblatt, KEIN
out.push({ // Schwenkbogen. Nur die Öffnung/Laibung bleibt (die Wandlücke kommt aus der
kind: "line", // ausgesparten Poché; Sturz-/Anschlaglinien werden weiter unten gezeichnet).
a: sym.hinge, // Entspricht `oeff_tuer_typ == "wandoeffnung"` im Rhino-Plugin.
b: sym.openEnd, const isWandoeffnung = (o.doorType ?? "normal") === "wandoeffnung";
cls: "door-leaf", if (!isWandoeffnung) {
weightMm: SYMBOL_HAIRLINE_MM, // Türblatt (alle Stufen).
greyed,
openingId: o.id,
});
// mittel + fein: Schwenkbogen von geschlossen → offen.
if (detail !== "grob") {
out.push({ out.push({
kind: "arc", kind: "line",
center: sym.hinge, a: sym.hinge,
from: sym.closedEnd, b: sym.openEnd,
to: sym.openEnd, cls: "door-leaf",
r: sym.radius,
cls: "door-swing",
weightMm: SYMBOL_HAIRLINE_MM, weightMm: SYMBOL_HAIRLINE_MM,
greyed, greyed,
openingId: o.id, openingId: o.id,
}); });
// mittel + fein: Schwenkbogen von geschlossen → offen.
if (detail !== "grob") {
out.push({
kind: "arc",
center: sym.hinge,
from: sym.closedEnd,
to: sym.openEnd,
r: sym.radius,
cls: "door-swing",
weightMm: SYMBOL_HAIRLINE_MM,
greyed,
openingId: o.id,
});
}
} }
// fein: Anschlag-Striche an beiden Pfosten (quer zur Wand). // fein: Anschlag-Striche an beiden Pfosten (quer zur Wand).
if (detail === "fein") { if (detail === "fein") {
@@ -1697,6 +1704,20 @@ function addOpeningSymbol(
greyed, greyed,
openingId: o.id, openingId: o.id,
}); });
// Flügel-Mittelpfosten (wingCount 1) — reine Querlinien innerhalb des
// Rahmens (kein Strichmuster). Nur mittel/fein; entfällt bei grob/schema.
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,
});
}
} }
for (const [a, b] of sym.glassLines) { for (const [a, b] of sym.glassLines) {
out.push({ out.push({