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[];
|
frame: Vec2[];
|
||||||
/** Glaslinien-Segmente quer über die Lücke (1–2 parallele Linien). */
|
/** Glaslinien-Segmente quer über die Lücke (1–2 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 1–2 Glaslinien (dünne parallele Linien in der Wandmitte,
|
* 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(
|
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
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 (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. */
|
/** 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.
|
||||||
|
|||||||
@@ -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") {
|
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({
|
||||||
|
|||||||
Reference in New Issue
Block a user