Dach-Modell + Geometrie-Kern (Flach/Pult/Sattel/Walm/Mansarde/Zelt)
Roof-Element (rechteckiger Grundriss + Form/Neigung/Überstand/Firstrichtung) und Roof/RoofShape im Modell (Project.roofs). Reine Geometrie geometry/roof.ts: roofGeometry(roof, eavesZ) liefert Grundriss-Linien (Traufe/First/Grat/Knick) UND die 3D-Dachflächen (+ Giebel) für alle sechs Formen — gerechnet auf der Bounding-Box (First entlang X/Y, „y" per Transponierung). roofBaseElevation löst die Traufhöhe auf (Geschoss-Oberkante als Default). +9 Tests (Firsthöhe/-lage, Flächen-/Grat-/Knick-Zahl je Form, Transponierung, Überstand). Reine Modell-/Datenschicht; 2D/3D/Werkzeug folgen. tsc + vitest grün.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { roofGeometry, roofBBox } from "./roof";
|
||||
import type { Roof, Vec2 } from "../model/types";
|
||||
|
||||
// Rechteck 6×4 (x:0..6, y:0..4), Neigung 45° (tan=1), Traufhöhe 10, kein Überstand.
|
||||
const RECT: Vec2[] = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 6, y: 0 },
|
||||
{ x: 6, y: 4 },
|
||||
{ x: 0, y: 4 },
|
||||
];
|
||||
const E = 10;
|
||||
|
||||
function roof(over: Partial<Roof>): Roof {
|
||||
return {
|
||||
id: "R1",
|
||||
type: "roof",
|
||||
floorId: "eg",
|
||||
categoryCode: "35",
|
||||
outline: RECT,
|
||||
shape: "sattel",
|
||||
pitchDeg: 45,
|
||||
overhang: 0,
|
||||
ridgeAxis: "x",
|
||||
thickness: 0.2,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
/** Alle 3D-Punkte einer Geometrie (Flächen + Giebel). */
|
||||
function allZ(g: ReturnType<typeof roofGeometry>): number[] {
|
||||
const zs: number[] = [];
|
||||
for (const pl of g.planes) for (const p of pl.pts) zs.push(p[2]);
|
||||
for (const gp of g.gables) for (const p of gp) zs.push(p[2]);
|
||||
return zs;
|
||||
}
|
||||
|
||||
describe("roofBBox", () => {
|
||||
it("liefert die Bounding-Box", () => {
|
||||
expect(roofBBox(RECT)).toEqual({ x0: 0, y0: 0, x1: 6, y1: 4 });
|
||||
});
|
||||
});
|
||||
|
||||
describe("roofGeometry — Formen (6×4, 45°, First entlang X)", () => {
|
||||
it("flach: eine waagrechte Fläche auf Traufhöhe, kein First", () => {
|
||||
const g = roofGeometry(roof({ shape: "flach" }), E);
|
||||
expect(g.planes).toHaveLength(1);
|
||||
expect(g.ridges).toHaveLength(0);
|
||||
expect(g.ridgeHeight).toBe(0);
|
||||
expect(allZ(g).every((z) => z === E)).toBe(true);
|
||||
});
|
||||
|
||||
it("sattel: Firsthöhe = halbe Tiefe · tan, First mittig, 2 Flächen + 2 Giebel", () => {
|
||||
const g = roofGeometry(roof({ shape: "sattel" }), E);
|
||||
expect(g.ridgeHeight).toBeCloseTo(2, 6); // halfD 2 · tan45 1
|
||||
expect(g.planes).toHaveLength(2);
|
||||
expect(g.gables).toHaveLength(2);
|
||||
expect(g.ridges).toHaveLength(1);
|
||||
// First auf yc=2, z=E+2.
|
||||
expect(g.ridges[0][0]).toEqual({ x: 0, y: 2 });
|
||||
expect(g.ridges[0][1]).toEqual({ x: 6, y: 2 });
|
||||
expect(Math.max(...allZ(g))).toBeCloseTo(E + 2, 6);
|
||||
expect(Math.min(...allZ(g))).toBeCloseTo(E, 6);
|
||||
});
|
||||
|
||||
it("walm: First um halbe Tiefe verkürzt, 4 Flächen, 4 Grate, keine Giebel", () => {
|
||||
const g = roofGeometry(roof({ shape: "walm" }), E);
|
||||
expect(g.planes).toHaveLength(4);
|
||||
expect(g.hips).toHaveLength(4);
|
||||
expect(g.gables).toHaveLength(0);
|
||||
// ra=2, rb=4.
|
||||
expect(g.ridges[0][0]).toEqual({ x: 2, y: 2 });
|
||||
expect(g.ridges[0][1]).toEqual({ x: 4, y: 2 });
|
||||
expect(g.ridgeHeight).toBeCloseTo(2, 6);
|
||||
});
|
||||
|
||||
it("zelt: Spitze über der Mitte, 4 Flächen, 4 Grate zur Mitte", () => {
|
||||
const g = roofGeometry(roof({ shape: "zelt" }), E);
|
||||
expect(g.planes).toHaveLength(4);
|
||||
expect(g.hips).toHaveLength(4);
|
||||
expect(g.ridges).toHaveLength(0);
|
||||
// hz = min(halfW 3, halfD 2)=2.
|
||||
expect(g.ridgeHeight).toBeCloseTo(2, 6);
|
||||
// Alle Grate laufen zum Mittelpunkt (3,2).
|
||||
for (const hip of g.hips) expect(hip[1]).toEqual({ x: 3, y: 2 });
|
||||
});
|
||||
|
||||
it("pult: eine Fläche, hohe Kante = Breite·tan über der Traufe", () => {
|
||||
const g = roofGeometry(roof({ shape: "pult" }), E);
|
||||
expect(g.planes).toHaveLength(1);
|
||||
expect(g.ridgeHeight).toBeCloseTo(4, 6); // Tiefe 4 · tan45
|
||||
expect(Math.max(...allZ(g))).toBeCloseTo(E + 4, 6);
|
||||
});
|
||||
|
||||
it("mansarde: 4 Flächen, 2 Knicklinien, First mittig, Knick tiefer als First", () => {
|
||||
const g = roofGeometry(roof({ shape: "mansarde", pitchDeg: 70, pitchUpperDeg: 30 }), E);
|
||||
expect(g.planes).toHaveLength(4);
|
||||
expect(g.breaks).toHaveLength(2);
|
||||
expect(g.ridges).toHaveLength(1);
|
||||
expect(g.gables).toHaveLength(2); // Pentagon-Giebel
|
||||
// First höher als 0, Knicklinien innerhalb der Tiefe.
|
||||
expect(g.ridgeHeight).toBeGreaterThan(0);
|
||||
expect(g.breaks[0][0].y).toBeCloseTo(0 + 2 * 0.4, 6); // yf = y0 + halfD·0.4
|
||||
});
|
||||
});
|
||||
|
||||
describe("roofGeometry — First entlang Y (Transponierung)", () => {
|
||||
it("dreht die Sattel-First-Linie auf die X-Mitte", () => {
|
||||
const g = roofGeometry(roof({ shape: "sattel", ridgeAxis: "y" }), E);
|
||||
// Firstrichtung Y → First liegt bei x = xc = 3, läuft in Y.
|
||||
expect(g.ridges[0][0]).toEqual({ x: 3, y: 0 });
|
||||
expect(g.ridges[0][1]).toEqual({ x: 3, y: 4 });
|
||||
// Firsthöhe nun aus der halben BREITE (3) · tan = 3.
|
||||
expect(g.ridgeHeight).toBeCloseTo(3, 6);
|
||||
});
|
||||
});
|
||||
|
||||
describe("roofGeometry — Überstand", () => {
|
||||
it("weitet die Traufe um overhang nach aussen", () => {
|
||||
const g = roofGeometry(roof({ shape: "flach", overhang: 0.5 }), E);
|
||||
const bb = roofBBox(g.eaves);
|
||||
expect(bb).toEqual({ x0: -0.5, y0: -0.5, x1: 6.5, y1: 4.5 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,267 @@
|
||||
// Dach-Geometrie: leitet aus einem (rechteckigen) Grundriss-Umriss + Dachform +
|
||||
// Neigung die Grundriss-Linien (Traufe/First/Grat/Knick) UND die 3D-Dachflächen
|
||||
// ab. Bewusst auf der BOUNDING-BOX des Umrisses gerechnet (First entlang einer
|
||||
// Hauptachse) — die gängige, intuitive Vereinfachung, die alle Standardformen
|
||||
// (Flach/Pult/Sattel/Walm/Mansarde/Zelt) exakt und ohne Straight-Skeleton abdeckt.
|
||||
//
|
||||
// Koordinaten: Grundriss (x, y) in Metern; 3D-Punkte als [x, y, z] mit z = Höhe.
|
||||
// Reine Datenschicht (kein React/Store), voll unit-testbar.
|
||||
//
|
||||
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
|
||||
|
||||
import type { Project, Roof, Vec2 } from "../model/types";
|
||||
import { getFloor } from "../model/types";
|
||||
|
||||
/** 3D-Punkt [x, y, z=Höhe] in Modell-Metern. */
|
||||
export type Vec3 = [number, number, number];
|
||||
|
||||
/** Eine ebene Dachfläche als Polygon (3D-Eckpunkte in Umlaufreihenfolge). */
|
||||
export interface RoofPlane {
|
||||
pts: Vec3[];
|
||||
}
|
||||
|
||||
/** Vollständige Dach-Geometrie (Grundriss-Linien + 3D-Flächen). */
|
||||
export interface RoofGeometry {
|
||||
/** Traufe-Umriss (Grundriss, inkl. Überstand) als geschlossener Ring. */
|
||||
eaves: Vec2[];
|
||||
/** Firstlinie(n) im Grundriss (leer bei Flach/Zelt). */
|
||||
ridges: [Vec2, Vec2][];
|
||||
/** Gratlinien (Walm/Zelt: Ecke → First/Spitze). */
|
||||
hips: [Vec2, Vec2][];
|
||||
/** Knicklinien (Mansarde). */
|
||||
breaks: [Vec2, Vec2][];
|
||||
/** 3D-Dachflächen. */
|
||||
planes: RoofPlane[];
|
||||
/** Giebelflächen (vertikale Polygone an Sattel-/Mansarde-Enden). */
|
||||
gables: Vec3[][];
|
||||
/** Firsthöhe über der Traufe (Meter). */
|
||||
ridgeHeight: number;
|
||||
}
|
||||
|
||||
const DEG = Math.PI / 180;
|
||||
|
||||
/** Bounding-Box eines Umrisses. */
|
||||
export function roofBBox(outline: Vec2[]): { x0: number; y0: number; x1: number; y1: number } {
|
||||
let x0 = Infinity;
|
||||
let y0 = Infinity;
|
||||
let x1 = -Infinity;
|
||||
let y1 = -Infinity;
|
||||
for (const p of outline) {
|
||||
if (p.x < x0) x0 = p.x;
|
||||
if (p.y < y0) y0 = p.y;
|
||||
if (p.x > x1) x1 = p.x;
|
||||
if (p.y > y1) y1 = p.y;
|
||||
}
|
||||
return { x0, y0, x1, y1 };
|
||||
}
|
||||
|
||||
/**
|
||||
* Traufhöhe (absolutes Z) eines Dachs: `baseElevation`, sonst die OBERKANTE des
|
||||
* zugehörigen Geschosses (baseElevation + floorHeight) — das Dach sitzt also
|
||||
* standardmäßig auf der Geschossdecke auf.
|
||||
*/
|
||||
export function roofBaseElevation(project: Project, roof: Roof): number {
|
||||
if (roof.baseElevation !== undefined) return roof.baseElevation;
|
||||
try {
|
||||
const floor = getFloor(project, roof.floorId);
|
||||
return (floor.baseElevation ?? 0) + (floor.floorHeight ?? 0);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const P = (x: number, y: number): Vec2 => ({ x, y });
|
||||
|
||||
/**
|
||||
* Kanonische Berechnung mit First entlang der X-Achse auf dem Rechteck
|
||||
* [x0,x1]×[y0,y1] (Traufe, inkl. Überstand) auf Traufhöhe `e`. Die „y"-First-
|
||||
* Richtung wird im Wrapper durch Transponieren (x↔y) erreicht.
|
||||
*/
|
||||
function computeCanonical(
|
||||
x0: number,
|
||||
y0: number,
|
||||
x1: number,
|
||||
y1: number,
|
||||
e: number,
|
||||
roof: Roof,
|
||||
): RoofGeometry {
|
||||
const xc = (x0 + x1) / 2;
|
||||
const yc = (y0 + y1) / 2;
|
||||
const halfW = (x1 - x0) / 2;
|
||||
const halfD = (y1 - y0) / 2;
|
||||
const a = Math.max(0, roof.pitchDeg) * DEG;
|
||||
const tan = Math.tan(a);
|
||||
const h = halfD * tan; // Firsthöhe (Sattel/Walm/Mansarde)
|
||||
|
||||
const eaves: Vec2[] = [P(x0, y0), P(x1, y0), P(x1, y1), P(x0, y1)];
|
||||
const flat = (): RoofGeometry => ({
|
||||
eaves,
|
||||
ridges: [],
|
||||
hips: [],
|
||||
breaks: [],
|
||||
planes: [{ pts: [[x0, y0, e], [x1, y0, e], [x1, y1, e], [x0, y1, e]] }],
|
||||
gables: [],
|
||||
ridgeHeight: 0,
|
||||
});
|
||||
|
||||
switch (roof.shape) {
|
||||
case "flach":
|
||||
return flat();
|
||||
|
||||
case "pult": {
|
||||
// Eine Fläche: tiefe Seite y0 auf Traufe, hohe Seite y1 auf e+H.
|
||||
const H = (y1 - y0) * tan;
|
||||
return {
|
||||
eaves,
|
||||
ridges: [[P(x0, y1), P(x1, y1)]], // hohe Kante (First-Ersatz)
|
||||
hips: [],
|
||||
breaks: [],
|
||||
planes: [{ pts: [[x0, y0, e], [x1, y0, e], [x1, y1, e + H], [x0, y1, e + H]] }],
|
||||
gables: [
|
||||
[[x0, y0, e], [x0, y1, e + H], [x0, y1, e]],
|
||||
[[x1, y0, e], [x1, y1, e + H], [x1, y1, e]],
|
||||
],
|
||||
ridgeHeight: H,
|
||||
};
|
||||
}
|
||||
|
||||
case "sattel": {
|
||||
const rz = e + h;
|
||||
return {
|
||||
eaves,
|
||||
ridges: [[P(x0, yc), P(x1, yc)]],
|
||||
hips: [],
|
||||
breaks: [],
|
||||
planes: [
|
||||
{ pts: [[x0, y0, e], [x1, y0, e], [x1, yc, rz], [x0, yc, rz]] },
|
||||
{ pts: [[x0, y1, e], [x1, y1, e], [x1, yc, rz], [x0, yc, rz]] },
|
||||
],
|
||||
gables: [
|
||||
[[x0, y0, e], [x0, y1, e], [x0, yc, rz]],
|
||||
[[x1, y0, e], [x1, y1, e], [x1, yc, rz]],
|
||||
],
|
||||
ridgeHeight: h,
|
||||
};
|
||||
}
|
||||
|
||||
case "walm": {
|
||||
const rz = e + h;
|
||||
// First um die halbe Tiefe an beiden Enden verkürzt (45°-Grate bei
|
||||
// gleicher Neigung). Bei schmalem Baukörper (halfW<halfD) degeneriert er
|
||||
// zum Punkt (xc) → Walm wird zeltartig.
|
||||
let ra = x0 + halfD;
|
||||
let rb = x1 - halfD;
|
||||
if (ra > rb) ra = rb = xc;
|
||||
return {
|
||||
eaves,
|
||||
ridges: [[P(ra, yc), P(rb, yc)]],
|
||||
hips: [
|
||||
[P(x0, y0), P(ra, yc)],
|
||||
[P(x0, y1), P(ra, yc)],
|
||||
[P(x1, y0), P(rb, yc)],
|
||||
[P(x1, y1), P(rb, yc)],
|
||||
],
|
||||
breaks: [],
|
||||
planes: [
|
||||
{ pts: [[x0, y0, e], [x1, y0, e], [rb, yc, rz], [ra, yc, rz]] },
|
||||
{ pts: [[x0, y1, e], [x1, y1, e], [rb, yc, rz], [ra, yc, rz]] },
|
||||
{ pts: [[x0, y0, e], [x0, y1, e], [ra, yc, rz]] },
|
||||
{ pts: [[x1, y0, e], [x1, y1, e], [rb, yc, rz]] },
|
||||
],
|
||||
gables: [],
|
||||
ridgeHeight: h,
|
||||
};
|
||||
}
|
||||
|
||||
case "mansarde": {
|
||||
// Sattel mit Knick: steile untere Neigung (pitchDeg) über den äusseren
|
||||
// Tiefenanteil, dann flache obere (pitchUpperDeg ?? halbe Hauptneigung).
|
||||
const aLow = a;
|
||||
const aUp = Math.max(0, roof.pitchUpperDeg ?? roof.pitchDeg / 2) * DEG;
|
||||
const d1 = halfD * 0.4; // Knick-Einzug von der Traufe (äussere 40% steil)
|
||||
const z1 = e + d1 * Math.tan(aLow);
|
||||
const z2 = z1 + (halfD - d1) * Math.tan(aUp);
|
||||
const yf = y0 + d1; // vordere Knicklinie
|
||||
const yb = y1 - d1; // hintere Knicklinie
|
||||
return {
|
||||
eaves,
|
||||
ridges: [[P(x0, yc), P(x1, yc)]],
|
||||
hips: [],
|
||||
breaks: [
|
||||
[P(x0, yf), P(x1, yf)],
|
||||
[P(x0, yb), P(x1, yb)],
|
||||
],
|
||||
planes: [
|
||||
{ pts: [[x0, y0, e], [x1, y0, e], [x1, yf, z1], [x0, yf, z1]] }, // vorn unten
|
||||
{ pts: [[x0, yf, z1], [x1, yf, z1], [x1, yc, z2], [x0, yc, z2]] }, // vorn oben
|
||||
{ pts: [[x0, y1, e], [x1, y1, e], [x1, yb, z1], [x0, yb, z1]] }, // hinten unten
|
||||
{ pts: [[x0, yb, z1], [x1, yb, z1], [x1, yc, z2], [x0, yc, z2]] }, // hinten oben
|
||||
],
|
||||
gables: [
|
||||
[[x0, y0, e], [x0, yf, z1], [x0, yc, z2], [x0, yb, z1], [x0, y1, e]],
|
||||
[[x1, y0, e], [x1, yf, z1], [x1, yc, z2], [x1, yb, z1], [x1, y1, e]],
|
||||
],
|
||||
ridgeHeight: z2 - e,
|
||||
};
|
||||
}
|
||||
|
||||
case "zelt": {
|
||||
// Allseitig zur Spitze über der Mitte.
|
||||
const hz = Math.min(halfW, halfD) * tan;
|
||||
const az = e + hz;
|
||||
return {
|
||||
eaves,
|
||||
ridges: [],
|
||||
hips: [
|
||||
[P(x0, y0), P(xc, yc)],
|
||||
[P(x1, y0), P(xc, yc)],
|
||||
[P(x1, y1), P(xc, yc)],
|
||||
[P(x0, y1), P(xc, yc)],
|
||||
],
|
||||
breaks: [],
|
||||
planes: [
|
||||
{ pts: [[x0, y0, e], [x1, y0, e], [xc, yc, az]] },
|
||||
{ pts: [[x1, y0, e], [x1, y1, e], [xc, yc, az]] },
|
||||
{ pts: [[x1, y1, e], [x0, y1, e], [xc, yc, az]] },
|
||||
{ pts: [[x0, y1, e], [x0, y0, e], [xc, yc, az]] },
|
||||
],
|
||||
gables: [],
|
||||
ridgeHeight: hz,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const swap2 = (p: Vec2): Vec2 => ({ x: p.y, y: p.x });
|
||||
const swap3 = (p: Vec3): Vec3 => [p[1], p[0], p[2]];
|
||||
const swapLine = (l: [Vec2, Vec2]): [Vec2, Vec2] => [swap2(l[0]), swap2(l[1])];
|
||||
|
||||
/**
|
||||
* Dach-Geometrie aus Form/Neigung/Überstand + Traufhöhe `eavesZ`. Rechnet auf
|
||||
* der Bounding-Box des Umrisses, um `overhang` nach aussen geweitet. Für
|
||||
* `ridgeAxis:"y"` wird kanonisch (First entlang X) gerechnet und danach x↔y
|
||||
* transponiert.
|
||||
*/
|
||||
export function roofGeometry(roof: Roof, eavesZ: number): RoofGeometry {
|
||||
const bb = roofBBox(roof.outline);
|
||||
const o = Math.max(0, roof.overhang);
|
||||
let x0 = bb.x0 - o;
|
||||
let y0 = bb.y0 - o;
|
||||
let x1 = bb.x1 + o;
|
||||
let y1 = bb.y1 + o;
|
||||
const flip = roof.ridgeAxis === "y";
|
||||
if (flip) {
|
||||
[x0, y0, x1, y1] = [y0, x0, y1, x1];
|
||||
}
|
||||
const g = computeCanonical(x0, y0, x1, y1, eavesZ, roof);
|
||||
if (!flip) return g;
|
||||
return {
|
||||
eaves: g.eaves.map(swap2),
|
||||
ridges: g.ridges.map(swapLine),
|
||||
hips: g.hips.map(swapLine),
|
||||
breaks: g.breaks.map(swapLine),
|
||||
planes: g.planes.map((pl) => ({ pts: pl.pts.map(swap3) })),
|
||||
gables: g.gables.map((poly) => poly.map(swap3)),
|
||||
ridgeHeight: g.ridgeHeight,
|
||||
};
|
||||
}
|
||||
@@ -954,6 +954,53 @@ export interface Ceiling {
|
||||
bottom?: VerticalAnchor;
|
||||
}
|
||||
|
||||
// ── Dach ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Dachform (analog DOSSIER-Rhino):
|
||||
* • "flach" — Flachdach (waagrechte Fläche auf Traufhöhe).
|
||||
* • "pult" — Pultdach (eine geneigte Fläche, First an EINER Traufseite).
|
||||
* • "sattel" — Satteldach (First mittig, zwei Flächen, Giebel an den Enden).
|
||||
* • "walm" — Walmdach (allseitig geneigt, First verkürzt, vier Flächen).
|
||||
* • "mansarde" — Mansarddach (Sattel mit Knick: steile untere + flache obere Neigung).
|
||||
* • "zelt" — Zeltdach (allseitig zur Spitze, First-Länge 0, Pyramide).
|
||||
*/
|
||||
export type RoofShape = "flach" | "pult" | "sattel" | "walm" | "mansarde" | "zelt";
|
||||
|
||||
/**
|
||||
* Ein Dach über einem (rechteckigen) Grundriss-Umriss. Die Geometrie rechnet auf
|
||||
* der Bounding-Box des Umrisses (die gängige, intuitive Vereinfachung — First
|
||||
* entlang einer Hauptachse); der Umriss selbst wird beim Zeichnen als Rechteck
|
||||
* angelegt. Die Traufhöhe (`baseElevation`) ist die Unterkante der Dachfläche;
|
||||
* darüber steigt das Dach je nach Form/Neigung auf.
|
||||
*/
|
||||
export interface Roof {
|
||||
id: string;
|
||||
type: "roof";
|
||||
/** Zugehörige Zeichnungsebene (Geschoss). */
|
||||
floorId: string;
|
||||
/** Grafik-Kategorie (Ebene), z. B. "35" für Dächer. */
|
||||
categoryCode: string;
|
||||
/** Geschlossener Grundriss-Umriss (Meter); Geometrie nutzt die Bounding-Box. */
|
||||
outline: Vec2[];
|
||||
/** Dachform. */
|
||||
shape: RoofShape;
|
||||
/** Hauptneigung in Grad (0 = flach). */
|
||||
pitchDeg: number;
|
||||
/** Nur Mansarde: obere (flachere) Neigung in Grad; fehlt ⇒ halbe Hauptneigung. */
|
||||
pitchUpperDeg?: number;
|
||||
/** Dachüberstand an der Traufe (Meter, ringsum); 0 = bündig mit dem Umriss. */
|
||||
overhang: number;
|
||||
/** Firstrichtung (Sattel/Walm/Mansarde/Pult): entlang der X- oder Y-Achse. */
|
||||
ridgeAxis: "x" | "y";
|
||||
/** Traufhöhe (absolutes Z, Meter); fehlt ⇒ Oberkante des Geschosses. */
|
||||
baseElevation?: number;
|
||||
/** Dachdicke (Meter, senkrecht zur Fläche gemessen). */
|
||||
thickness: number;
|
||||
/** Optionale Übersteuerung der Strich-/Umrissfarbe; sonst Kategorie-Farbe. */
|
||||
color?: string;
|
||||
}
|
||||
|
||||
/** Schwenkrichtung der Tür relativ zur Wandachse. */
|
||||
export type SwingSide = "left" | "right";
|
||||
|
||||
@@ -1870,6 +1917,8 @@ export interface Project {
|
||||
* (Default: leer behandeln).
|
||||
*/
|
||||
ceilings?: Ceiling[];
|
||||
/** Dächer über (rechteckigen) Grundriss-Umrissen. Optional (Alt-Projekte). */
|
||||
roofs?: Roof[];
|
||||
doors: Door[];
|
||||
/**
|
||||
* Öffnungen (Fenster/Türen), gehostet in Wänden. Optional, damit bestehende
|
||||
|
||||
Reference in New Issue
Block a user