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:
+27
-2
@@ -170,12 +170,21 @@ export interface WindowSymbol {
|
||||
frame: Vec2[];
|
||||
/** Glaslinien-Segmente quer über die Lücke (1–2 parallele Linien). */
|
||||
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
|
||||
* Wanddicke) plus 1–2 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(
|
||||
project: Project,
|
||||
@@ -205,5 +214,21 @@ export function windowSymbol(
|
||||
const off = refOff + (count === 1 ? 0 : (i - (count - 1) / 2) * spread * 2);
|
||||
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
@@ -27,6 +27,32 @@ const norm = (a: Vec2): Vec2 => {
|
||||
/** Linke Normale (n = (−u.y, u.x)) — quer zur Laufrichtung. */
|
||||
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. */
|
||||
export const IDEAL_RISER = 0.17;
|
||||
export const IDEAL_TREAD = 0.29;
|
||||
@@ -105,13 +131,22 @@ export function stairGeometry(stair: Stair, totalRise: number): StairGeometry {
|
||||
const u = norm(stair.dir);
|
||||
const n = leftN(u);
|
||||
|
||||
if (stair.shape === "spiral") {
|
||||
return spiralGeometry(stair, totalRise, steps, riserHeight, halfW);
|
||||
// Referenz (links/mitte/rechts): die gespeicherte Achse wird auf die visuelle
|
||||
// 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") {
|
||||
return lGeometry(stair, totalRise, steps, riserHeight, halfW, u, n);
|
||||
if (s.shape === "L") {
|
||||
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. */
|
||||
@@ -530,11 +565,19 @@ export function stairOutline(
|
||||
): { polygon: Vec2[] | null; spiral: SpiralOutlineSegments | null } {
|
||||
void totalRise; // Signatur-Konsistenz mit stairGeometry; derzeit ungenutzt
|
||||
const halfW = Math.max(0.05, stair.width / 2);
|
||||
if (stair.shape === "spiral") {
|
||||
return { polygon: null, spiral: spiralOutline(stair, halfW) };
|
||||
// Referenz (links/mitte/rechts): die gespeicherte Achse zuerst auf die
|
||||
// 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") {
|
||||
return { polygon: lOutlinePoints(stair, halfW), spiral: null };
|
||||
if (s.shape === "L") {
|
||||
return { polygon: lOutlinePoints(s, halfW), spiral: null };
|
||||
}
|
||||
return { polygon: straightOutlinePoints(stair, halfW), spiral: null };
|
||||
return { polygon: straightOutlinePoints(s, halfW), spiral: null };
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { stairOutline } from "./stair";
|
||||
import { stairOutline, stairGeometry } from "./stair";
|
||||
import type { Stair, Vec2 } from "../model/types";
|
||||
|
||||
/** Einfache Distanz-Funktion. */
|
||||
@@ -146,3 +146,66 @@ describe("stairOutline — Wendeltreppe", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -731,6 +731,22 @@ export interface Opening {
|
||||
height: number;
|
||||
/** Brüstungshöhe (Unterkante der Öffnung) über der Wand-UK; 0 bei Türen. */
|
||||
sillHeight: number;
|
||||
/**
|
||||
* Nur Fenster: Anzahl der Flügel (1–4). 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. */
|
||||
hinge?: "start" | "end";
|
||||
/** Nur Tür: auf welche Seite der Wandachse das Blatt aufschlägt. */
|
||||
@@ -814,6 +830,19 @@ export interface Stair {
|
||||
sweep?: number;
|
||||
/** Laufbreite in Metern (quer zur Laufrichtung). */
|
||||
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
|
||||
* beim Auflösen die Geschosshöhe des zugehörigen Geschosses.
|
||||
|
||||
@@ -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
@@ -1588,29 +1588,36 @@ function addOpeningSymbol(
|
||||
if (o.kind === "door") {
|
||||
const sym = doorSymbol(wall, o);
|
||||
if (!sym) return;
|
||||
// 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ür-Typ „wandoeffnung": reiner Wanddurchbruch — KEIN Türblatt, KEIN
|
||||
// Schwenkbogen. Nur die Öffnung/Laibung bleibt (die Wandlücke kommt aus der
|
||||
// ausgesparten Poché; Sturz-/Anschlaglinien werden weiter unten gezeichnet).
|
||||
// Entspricht `oeff_tuer_typ == "wandoeffnung"` im Rhino-Plugin.
|
||||
const isWandoeffnung = (o.doorType ?? "normal") === "wandoeffnung";
|
||||
if (!isWandoeffnung) {
|
||||
// Türblatt (alle Stufen).
|
||||
out.push({
|
||||
kind: "arc",
|
||||
center: sym.hinge,
|
||||
from: sym.closedEnd,
|
||||
to: sym.openEnd,
|
||||
r: sym.radius,
|
||||
cls: "door-swing",
|
||||
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") {
|
||||
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).
|
||||
if (detail === "fein") {
|
||||
@@ -1697,6 +1704,20 @@ function addOpeningSymbol(
|
||||
greyed,
|
||||
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) {
|
||||
out.push({
|
||||
|
||||
Reference in New Issue
Block a user