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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user