Bauteile Gruppe A: Treppe-Aussenlinie, Fenster-Bruestung, Tuer-Sturz (Rhino-Vorbild)
- Treppe Aussenlinie/Outline (stair.ts stairOutline): gerade=4-Punkt-Rechteck, L=6-Punkt-Polygon via lineIntersect2d, Wendel=2 Boegen+2 Radiallinien; in addStairSymbol als durchgezogene Umrisslinie. (Rhino _aussen_gerade/_l/_wendel) - Fenster Bruestungslinie: bei sillHeight>0 gepunktete Linie auf der Wandachse zwischen den Pfosten (Klasse window-sill). - Tuer Sturzlinien (SIA): gestrichelte Linien an Wand-Innen/-Aussenkante, neues optionales Opening-Feld lintelLines (keine/innen/aussen/beide, Default beide), rueckwaertskompatibel. - 12 neue Tests (stairOutline.test.ts). vitest 275/275, tsc sauber.
This commit is contained in:
@@ -409,3 +409,132 @@ export function pointHitsStair(p: Vec2, geo: StairGeometry): boolean {
|
||||
if (geo.landing && pointInPolygon(p, geo.landing)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ── Treppen-Aussenlinie (Plan-Outline) ───────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Schliessendes Umriss-Polygon einer geraden Treppe im Grundriss (4 Ecken CCW).
|
||||
* Entspricht `_aussen_gerade` im Rhino-Plugin (Rechteck über Laufbreite + Lauflänge).
|
||||
* Liefert null bei entarteter Geometrie.
|
||||
*/
|
||||
function straightOutlinePoints(stair: Stair, halfW: number): Vec2[] | null {
|
||||
const u = norm({ x: stair.dir.x, y: stair.dir.y });
|
||||
const n = leftN(u);
|
||||
const L = Math.max(1e-4, stair.runLength);
|
||||
const s = stair.start;
|
||||
// Vier Ecken: Start-links → Start-rechts → End-rechts → End-links (CCW).
|
||||
return [
|
||||
add(s, scale(n, halfW)),
|
||||
add(s, scale(n, -halfW)),
|
||||
add(add(s, scale(u, L)), scale(n, -halfW)),
|
||||
add(add(s, scale(u, L)), scale(n, halfW)),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 2D-Linien-Schnittpunkt. Liefert null bei Parallelität (det < ε).
|
||||
* Entspricht `_line_intersect_xy` im Rhino-Plugin.
|
||||
*/
|
||||
function lineIntersect2d(p1: Vec2, d1: Vec2, p2: Vec2, d2: Vec2): Vec2 | null {
|
||||
const det = d1.x * (-d2.y) - d1.y * (-d2.x);
|
||||
if (Math.abs(det) < 1e-9) return null;
|
||||
const dx = p2.x - p1.x;
|
||||
const dy = p2.y - p1.y;
|
||||
const t = (dx * (-d2.y) - dy * (-d2.x)) / det;
|
||||
return { x: p1.x + d1.x * t, y: p1.y + d1.y * t };
|
||||
}
|
||||
|
||||
/**
|
||||
* Umriss-Polygon einer L-Treppe im Grundriss. Entspricht `_aussen_l_polygon`
|
||||
* im Rhino-Plugin: Linien-Schnittpunkte liefern die Ecken des L-Polygons.
|
||||
* Liefert null bei entarteter Geometrie.
|
||||
*/
|
||||
function lOutlinePoints(stair: Stair, halfW: number): Vec2[] | null {
|
||||
const u1 = norm(stair.dir);
|
||||
const n1 = leftN(u1);
|
||||
const L1 = Math.max(1e-4, stair.runLength);
|
||||
const turn = stair.turn ?? 1;
|
||||
const u2: Vec2 = turn > 0 ? n1 : scale(n1, -1);
|
||||
const n2 = leftN(u2);
|
||||
const L2 = Math.max(1e-4, stair.run2Length ?? stair.runLength);
|
||||
|
||||
// Podest-Zentrum am Ende des ersten Laufs (Mitte = halfW hinter dem Lauf-Ende).
|
||||
const corner = add(stair.start, scale(u1, L1 + halfW));
|
||||
const run2Start = add(corner, scale(u2, halfW));
|
||||
|
||||
// Lauf-1-Anfang je Seite
|
||||
const p0l = add(stair.start, scale(n1, halfW));
|
||||
const p0r = add(stair.start, scale(n1, -halfW));
|
||||
// Lauf-2-Ende je Seite
|
||||
const endRun2 = add(run2Start, scale(u2, L2));
|
||||
const p3l = add(endRun2, scale(n2, halfW));
|
||||
const p3r = add(endRun2, scale(n2, -halfW));
|
||||
// Innere Eck-Punkte: Schnittpunkt der parallelen Seitenlinien je Seite.
|
||||
const cornerL = lineIntersect2d(p0l, u1, p3l, scale(u2, -1)) ?? add(corner, scale(n1, halfW));
|
||||
const cornerR = lineIntersect2d(p0r, u1, p3r, scale(u2, -1)) ?? add(corner, scale(n1, -halfW));
|
||||
// CCW-Polygon: p0l → p0r → cornerR → p3r → p3l → cornerL
|
||||
return [p0l, p0r, cornerR, p3r, p3l, cornerL];
|
||||
}
|
||||
|
||||
/**
|
||||
* Spiral-Outline: Innenbogen + Aussenbogen + zwei radiale Schliesslinien.
|
||||
* Entspricht `_aussen_wendel` im Rhino-Plugin (immer vollständig, ohne Cut).
|
||||
* Die Bögen werden als Kreisbogen-Parameter geliefert (cx/cy/r/a0/a1), die
|
||||
* radialen Schliesslinien als Segment-Paare.
|
||||
*/
|
||||
export interface SpiralOutlineSegments {
|
||||
/** Radiale Abschlusslinien (jeweils Innenpunkt → Aussenpunkt). */
|
||||
lines: [Vec2, Vec2][];
|
||||
/** Kreisbogen-Parameter: Innen- und Aussenkreis. */
|
||||
arcs: { cx: number; cy: number; r: number; a0: number; a1: number }[];
|
||||
}
|
||||
|
||||
function spiralOutline(stair: Stair, halfW: number): SpiralOutlineSegments | null {
|
||||
const center = stair.center ?? stair.start;
|
||||
const radius = Math.max(halfW + 0.1, stair.radius ?? stair.width);
|
||||
const sweep = (stair.sweep ?? 270) * (Math.PI / 180);
|
||||
const startVec = sub(stair.start, center);
|
||||
const a0 = Math.atan2(startVec.y, startVec.x);
|
||||
const a1 = a0 + sweep;
|
||||
const rIn = Math.max(0.02, radius - halfW);
|
||||
const rOut = radius + halfW;
|
||||
|
||||
const ptAt = (r: number, a: number): Vec2 => ({
|
||||
x: center.x + r * Math.cos(a),
|
||||
y: center.y + r * Math.sin(a),
|
||||
});
|
||||
|
||||
return {
|
||||
lines: [
|
||||
[ptAt(rIn, a0), ptAt(rOut, a0)],
|
||||
[ptAt(rIn, a1), ptAt(rOut, a1)],
|
||||
],
|
||||
arcs: [
|
||||
{ cx: center.x, cy: center.y, r: rIn, a0, a1 },
|
||||
{ cx: center.x, cy: center.y, r: rOut, a0, a1 },
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Aussenlinie einer Treppe im Grundriss je nach Form:
|
||||
* • gerade → geschlossenes Rechteck als Polygon-Punkte.
|
||||
* • L → L-Polygon via Linien-Schnittpunkten.
|
||||
* • Wendel → `SpiralOutlineSegments` (Bögen + radiale Linien).
|
||||
*
|
||||
* Genau einer von `polygon` / `spiral` ist nicht null.
|
||||
*/
|
||||
export function stairOutline(
|
||||
stair: Stair,
|
||||
totalRise: number,
|
||||
): { 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) };
|
||||
}
|
||||
if (stair.shape === "L") {
|
||||
return { polygon: lOutlinePoints(stair, halfW), spiral: null };
|
||||
}
|
||||
return { polygon: straightOutlinePoints(stair, halfW), spiral: null };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* Unit-Tests für `stairOutline` (Plan-Aussenlinie).
|
||||
*
|
||||
* Prüft:
|
||||
* • Gerade Treppe → 4-Punkte-Rechteck (polygon, keine spiral).
|
||||
* • L-Treppe → 6-Punkte-L-Polygon via Linien-Schnittpunkten.
|
||||
* • Wendeltreppe → SpiralOutlineSegments (2 Bögen, 2 radiale Linien, kein polygon).
|
||||
*
|
||||
* Die genauen Koordinaten-Werte werden nur grob geprüft (Vorzeichen, Länge,
|
||||
* Schliessen); die Geometrie-Logik wird via Abstand-Invarianten verifiziert.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { stairOutline } from "./stair";
|
||||
import type { Stair, Vec2 } from "../model/types";
|
||||
|
||||
/** Einfache Distanz-Funktion. */
|
||||
const dist = (a: Vec2, b: Vec2) => Math.hypot(b.x - a.x, b.y - a.y);
|
||||
|
||||
/** Minimale gerade Treppe nach rechts (+X). */
|
||||
const geradeStair: Stair = {
|
||||
id: "g1",
|
||||
type: "stair",
|
||||
floorId: "eg",
|
||||
categoryCode: "40",
|
||||
shape: "straight",
|
||||
start: { x: 0, y: 0 },
|
||||
dir: { x: 1, y: 0 },
|
||||
runLength: 3.0,
|
||||
width: 1.2,
|
||||
stepCount: 16,
|
||||
};
|
||||
|
||||
/** L-Treppe: erster Lauf +X, zweiter Lauf +Y (turn = +1). */
|
||||
const lStair: Stair = {
|
||||
id: "l1",
|
||||
type: "stair",
|
||||
floorId: "eg",
|
||||
categoryCode: "40",
|
||||
shape: "L",
|
||||
start: { x: 0, y: 0 },
|
||||
dir: { x: 1, y: 0 },
|
||||
runLength: 2.0,
|
||||
run2Length: 2.0,
|
||||
turn: 1,
|
||||
width: 1.2,
|
||||
stepCount: 14,
|
||||
};
|
||||
|
||||
/** Wendeltreppe: 270° im Gegenuhrzeigersinn, Radius 1.5 m. */
|
||||
const wendelStair: Stair = {
|
||||
id: "w1",
|
||||
type: "stair",
|
||||
floorId: "eg",
|
||||
categoryCode: "40",
|
||||
shape: "spiral",
|
||||
start: { x: 1.5, y: 0 },
|
||||
dir: { x: 1, y: 0 },
|
||||
runLength: 3.0, // bei Wendel: Pflichtfeld, inhaltlich nicht genutzt
|
||||
center: { x: 0, y: 0 },
|
||||
radius: 1.5,
|
||||
sweep: 270,
|
||||
width: 1.2,
|
||||
stepCount: 12,
|
||||
};
|
||||
|
||||
describe("stairOutline — gerade Treppe", () => {
|
||||
const totalRise = 3.0;
|
||||
const result = stairOutline(geradeStair, totalRise);
|
||||
|
||||
it("liefert polygon, keine spiral", () => {
|
||||
expect(result.polygon).not.toBeNull();
|
||||
expect(result.spiral).toBeNull();
|
||||
});
|
||||
|
||||
it("polygon hat genau 4 Ecken", () => {
|
||||
expect(result.polygon!.length).toBe(4);
|
||||
});
|
||||
|
||||
it("Lauflaenge entspricht runLength", () => {
|
||||
// Start-Kante (x=0) vs End-Kante (x=3): Abstand der gleichseitigen Kanten = 3.0 m.
|
||||
const pts = result.polygon!;
|
||||
// pts[0] und pts[1] liegen bei x≈0, pts[2] und pts[3] bei x≈runLength.
|
||||
const startX = (pts[0].x + pts[1].x) / 2;
|
||||
const endX = (pts[2].x + pts[3].x) / 2;
|
||||
expect(Math.abs(endX - startX)).toBeCloseTo(geradeStair.runLength, 5);
|
||||
});
|
||||
|
||||
it("Breite entspricht stair.width", () => {
|
||||
const pts = result.polygon!;
|
||||
// Seitenkante 0→3 und 1→2 haben die Breite = stair.width.
|
||||
const w = dist(pts[0], pts[1]);
|
||||
expect(w).toBeCloseTo(geradeStair.width, 5);
|
||||
});
|
||||
});
|
||||
|
||||
describe("stairOutline — L-Treppe", () => {
|
||||
const totalRise = 3.0;
|
||||
const result = stairOutline(lStair, totalRise);
|
||||
|
||||
it("liefert polygon, keine spiral", () => {
|
||||
expect(result.polygon).not.toBeNull();
|
||||
expect(result.spiral).toBeNull();
|
||||
});
|
||||
|
||||
it("polygon hat 6 Ecken (L-Form)", () => {
|
||||
// 4 Aussen-Eck + 2 Innen-Eck via Linien-Schnittpunkten = 6 Punkte.
|
||||
expect(result.polygon!.length).toBe(6);
|
||||
});
|
||||
|
||||
it("keine NaN-Koordinaten", () => {
|
||||
for (const p of result.polygon!) {
|
||||
expect(isFinite(p.x)).toBe(true);
|
||||
expect(isFinite(p.y)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("stairOutline — Wendeltreppe", () => {
|
||||
const totalRise = 3.0;
|
||||
const result = stairOutline(wendelStair, totalRise);
|
||||
|
||||
it("liefert spiral, kein polygon", () => {
|
||||
expect(result.spiral).not.toBeNull();
|
||||
expect(result.polygon).toBeNull();
|
||||
});
|
||||
|
||||
it("zwei radiale Schliesslinien (Start + Ende)", () => {
|
||||
expect(result.spiral!.lines.length).toBe(2);
|
||||
});
|
||||
|
||||
it("zwei Kreisboegen (Innen + Aussen)", () => {
|
||||
expect(result.spiral!.arcs.length).toBe(2);
|
||||
});
|
||||
|
||||
it("Innenradius < Aussenradius", () => {
|
||||
const [arcIn, arcOut] = result.spiral!.arcs;
|
||||
expect(arcIn.r).toBeLessThan(arcOut.r);
|
||||
});
|
||||
|
||||
it("Startlinie Innen-Punkt liegt auf Innenkreis", () => {
|
||||
const arcIn = result.spiral!.arcs[0];
|
||||
const [lineStart] = result.spiral!.lines;
|
||||
const [innerPt] = lineStart;
|
||||
const d = dist({ x: arcIn.cx, y: arcIn.cy }, innerPt);
|
||||
expect(d).toBeCloseTo(arcIn.r, 4);
|
||||
});
|
||||
});
|
||||
@@ -741,6 +741,15 @@ export interface Opening {
|
||||
openingDir?: "in" | "out";
|
||||
/** Optionale Rahmenstärke (quer zur Wand) in Metern für die 3D-Darstellung. */
|
||||
frameThickness?: number;
|
||||
/**
|
||||
* Nur Tür: Sturzlinien (SIA, gestrichelt) quer über die Öffnung an der Wand-
|
||||
* Innen- und/oder Aussenkante. Zeigt die Überkopf-Projektion des Sturzes.
|
||||
* "keine" → keine Sturzlinien
|
||||
* "innen" → eine Linie an der Wand-Innenkante
|
||||
* "aussen"→ eine Linie an der Wand-Aussenkante
|
||||
* "beide" → beide Linien (Default wenn nicht gesetzt)
|
||||
*/
|
||||
lintelLines?: "keine" | "innen" | "aussen" | "beide";
|
||||
/**
|
||||
* Optionale Übersteuerung der Strich-/Symbolfarbe; sonst gilt die
|
||||
* Kategorie-Farbe.
|
||||
|
||||
+137
-2
@@ -38,8 +38,15 @@ import { docFromText } from "../text/richText";
|
||||
import { roomStampToDoc, roomStampExtraLines } from "../model/roomStamp";
|
||||
import type { RoomStampLine } from "../model/roomStamp";
|
||||
import { stairVerticalExtent, wallReferenceOffset } from "../model/wall";
|
||||
import { stairGeometry, stairCut } from "../geometry/stair";
|
||||
import { doorSymbol, openingInterval, windowSymbol } from "../geometry/opening";
|
||||
import { stairGeometry, stairCut, stairOutline } from "../geometry/stair";
|
||||
import type { SpiralOutlineSegments } from "../geometry/stair";
|
||||
import {
|
||||
doorSymbol,
|
||||
openingInterval,
|
||||
openingJambs,
|
||||
wallAxisFrame,
|
||||
windowSymbol,
|
||||
} from "../geometry/opening";
|
||||
import {
|
||||
add,
|
||||
along,
|
||||
@@ -261,6 +268,8 @@ export type Primitive =
|
||||
dash?: number[] | null;
|
||||
/** ID der Öffnung (für Links-Klick-Auswahl von Tür-Schwenkbögen). */
|
||||
openingId?: string;
|
||||
/** ID der Treppe (für Links-Klick-Auswahl von Wendel-Outline-Bögen). */
|
||||
stairId?: string;
|
||||
greyed?: boolean;
|
||||
}
|
||||
| {
|
||||
@@ -1618,6 +1627,57 @@ function addOpeningSymbol(
|
||||
});
|
||||
}
|
||||
}
|
||||
// Sturzlinien (SIA, gestrichelt) — Überkopf-Projektion des Sturzes quer über
|
||||
// die Öffnung an der Wand-Innen-/Aussenkante. Entspricht `_make_tuer_sturz_curves`
|
||||
// im Rhino-Plugin. Default „beide" wenn nicht explizit gesetzt.
|
||||
const lintelMode = o.lintelLines ?? "beide";
|
||||
if (lintelMode !== "keine") {
|
||||
const jambs = openingJambs(wall, o);
|
||||
if (jambs) {
|
||||
const { n } = wallAxisFrame(wall);
|
||||
const total = wallTypeThickness(getWallType(project, wall));
|
||||
const refOff = wallReferenceOffset(wall, total);
|
||||
const halfD = total / 2;
|
||||
const outerOff = refOff + halfD;
|
||||
const innerOff = refOff - halfD;
|
||||
// Strichmuster: gestrichelt (Überkopf-Projektion, SIA-Konvention).
|
||||
const dash: number[] = [0.18, 0.09];
|
||||
const strokeColor = o.color ?? POCHE_STROKE;
|
||||
/** Linie quer über die Öffnung am gegebenen Perp-Offset. */
|
||||
const lintelLine = (perpOff: number) => ({
|
||||
a: add(jambs.jambStart, scale(n, perpOff)),
|
||||
b: add(jambs.jambEnd, scale(n, perpOff)),
|
||||
});
|
||||
if (lintelMode === "aussen" || lintelMode === "beide") {
|
||||
const seg = lintelLine(outerOff);
|
||||
out.push({
|
||||
kind: "line" as const,
|
||||
a: seg.a,
|
||||
b: seg.b,
|
||||
cls: "door-lintel",
|
||||
weightMm: SYMBOL_HAIRLINE_MM,
|
||||
dash,
|
||||
color: strokeColor,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
}
|
||||
if (lintelMode === "innen" || lintelMode === "beide") {
|
||||
const seg = lintelLine(innerOff);
|
||||
out.push({
|
||||
kind: "line" as const,
|
||||
a: seg.a,
|
||||
b: seg.b,
|
||||
cls: "door-lintel",
|
||||
weightMm: SYMBOL_HAIRLINE_MM,
|
||||
dash,
|
||||
color: strokeColor,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1649,6 +1709,26 @@ function addOpeningSymbol(
|
||||
openingId: o.id,
|
||||
});
|
||||
}
|
||||
// Brüstungslinie (gepunktet) — zeigt die Brüstung im Grundriss auf der Wandachse.
|
||||
// Nur bei Fenstern mit Brüstungshöhe > 0. Entspricht der Sill-Linie in
|
||||
// `_make_oeffnung_preview` und dem Brüstungs-Plansymbol im Rhino-Plugin.
|
||||
if (o.sillHeight > 0) {
|
||||
const jambs = openingJambs(wall, o);
|
||||
if (jambs) {
|
||||
out.push({
|
||||
kind: "line",
|
||||
a: jambs.jambStart,
|
||||
b: jambs.jambEnd,
|
||||
cls: "window-sill",
|
||||
weightMm: SYMBOL_HAIRLINE_MM,
|
||||
// Gepunktet (dot-dash) wie in _make_oeffnung_preview DrawDottedLine.
|
||||
dash: [0.04, 0.08],
|
||||
color: o.color ?? POCHE_STROKE,
|
||||
greyed,
|
||||
openingId: o.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Schraffur-Platzhalter „ohne" (für reine Umriss-/Sammelflächen). */
|
||||
@@ -1865,6 +1945,40 @@ function addRoomArea(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Zeichnet die Wendel-Aussenlinie: Innen-/Aussenbogen als Arc-Primitive plus zwei
|
||||
* radiale Schliesslinien. Wird von `addStairSymbol` aufgerufen wenn `shape === "spiral"`.
|
||||
*/
|
||||
function _addSpiralOutline(
|
||||
out: Primitive[],
|
||||
seg: SpiralOutlineSegments,
|
||||
stroke: string,
|
||||
lwMm: number,
|
||||
greyed: boolean,
|
||||
stairId: string,
|
||||
): void {
|
||||
// Radiale Schliesslinien (Startwinkel + Endwinkel).
|
||||
for (const [a, b] of seg.lines) {
|
||||
out.push({ kind: "line", a, b, cls: "stair-outline", weightMm: lwMm, color: stroke, greyed, stairId });
|
||||
}
|
||||
// Innen- und Aussenbogen.
|
||||
for (const arc of seg.arcs) {
|
||||
const from: Vec2 = { x: arc.cx + arc.r * Math.cos(arc.a0), y: arc.cy + arc.r * Math.sin(arc.a0) };
|
||||
const to: Vec2 = { x: arc.cx + arc.r * Math.cos(arc.a1), y: arc.cy + arc.r * Math.sin(arc.a1) };
|
||||
out.push({
|
||||
kind: "arc",
|
||||
center: { x: arc.cx, y: arc.cy },
|
||||
from,
|
||||
to,
|
||||
r: arc.r,
|
||||
cls: "stair-outline",
|
||||
weightMm: lwMm,
|
||||
greyed,
|
||||
stairId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Treppen-Symbol im Grundriss (DOSSIER/SIA):
|
||||
* • Tritte als Rechtecke: die Tritt-Trennlinien werden gezeichnet — unter der
|
||||
@@ -1969,6 +2083,27 @@ function addStairSymbol(
|
||||
const [h1, tip, h2] = geo.arrow.head;
|
||||
out.push({ kind: "line", a: h1, b: tip, cls: "stair-arrow", weightMm: lwMm, color: stroke, greyed, stairId });
|
||||
out.push({ kind: "line", a: h2, b: tip, cls: "stair-arrow", weightMm: lwMm, color: stroke, greyed, stairId });
|
||||
|
||||
// Aussenlinie (Outline) der Treppe — schliessendes Umriss-Polygon (gerade/L)
|
||||
// bzw. Innen-/Aussenbogen + radiale Schliesslinien (Wendel).
|
||||
// Entspricht `_aussen_gerade` / `_aussen_l_polygon` / `_aussen_wendel` im
|
||||
// Rhino-Plugin. Stets durchgezogen, volle Strichstärke.
|
||||
const outline = stairOutline(stair, totalRise);
|
||||
if (outline.polygon && outline.polygon.length >= 3) {
|
||||
out.push({
|
||||
kind: "polygon",
|
||||
pts: outline.polygon,
|
||||
fill: "none",
|
||||
stroke,
|
||||
strokeWidthMm: lwMm,
|
||||
hatch: NO_HATCH,
|
||||
greyed,
|
||||
stairId,
|
||||
});
|
||||
}
|
||||
if (outline.spiral) {
|
||||
_addSpiralOutline(out, outline.spiral, stroke, lwMm, greyed, stairId);
|
||||
}
|
||||
}
|
||||
|
||||
const length = (w: Wall): number => Math.hypot(w.end.x - w.start.x, w.end.y - w.start.y);
|
||||
|
||||
Reference in New Issue
Block a user