Dächer: Platzier-Werkzeug + 2D-Plan + 3D-Flächen + Demo-Dach
Dach-Feature end-to-end nutzbar: - Befehl „Dach" (Alias dach/rf, BIM-Ribbon, Satteldach-Icon): Grundriss als Rechteck aufziehen; die Dachform (Sattel/Walm/Pult/Mansarde/Zelt/Flach) wählt man als Inline-Option der Befehlszeile vor/während des Aufziehens. First entlang der längeren Seite (Standard). - 2D-Grundriss (generatePlan): Traufe/First/Grat (Walm/Zelt) + gestrichelte Knicklinien (Mansarde), Farbe aus roof.color/Kategorie „31 Dächer". - 3D (toWalls3d emitRoofs): Dachflächen + Giebel als terrakottafarbene Meshes (Fan-Triangulierung, Mansarde-Fünfeck sauber). - Seed: Demo-Satteldach RF1 über dem Baukörper (OG, 5×4, Überstand 0.4). - i18n de/en für Befehl + Formen. +10 Tests (2D 7, 3D 3). tsc + vitest 640 grün. Auswahl/Attribut-Editieren placierter Dächer folgt separat.
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* Unit-Tests für die Dach-Darstellung im Grundriss (generatePlan → addRoof):
|
||||
* `project.roofs` wird nach Geschoss + sichtbarer Kategorie gefiltert (analog
|
||||
* Decken) und pro Dach in Grundriss-Linien übersetzt (Traufe/First/Grat/Knick,
|
||||
* aus `roofGeometry`). Reine Linien-Darstellung ohne Poché — die Form (flach/
|
||||
* sattel/walm/mansarde) entscheidet, welche der vier Linienarten auftauchen.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { generatePlan } from "./generatePlan";
|
||||
import type { Project, Roof } from "../model/types";
|
||||
|
||||
/** Minimalprojekt ohne Wände: nur das Geschoss + die Dach-Kategorie „35". */
|
||||
function baseProject(roofs?: Roof[]): Project {
|
||||
return {
|
||||
id: "t",
|
||||
name: "T",
|
||||
lineStyles: [],
|
||||
hatches: [],
|
||||
components: [],
|
||||
wallTypes: [],
|
||||
drawingLevels: [
|
||||
{
|
||||
id: "eg",
|
||||
name: "EG",
|
||||
kind: "floor",
|
||||
visible: true,
|
||||
locked: false,
|
||||
floorHeight: 2.6,
|
||||
cutHeight: 1.0,
|
||||
baseElevation: 0,
|
||||
},
|
||||
],
|
||||
layers: [{ code: "35", name: "Dächer", color: "#8a4b32", lw: 0.25, visible: true, locked: false }],
|
||||
walls: [],
|
||||
doors: [],
|
||||
openings: [],
|
||||
ceilings: [],
|
||||
stairs: [],
|
||||
rooms: [],
|
||||
drawings2d: [],
|
||||
context: [],
|
||||
...(roofs !== undefined ? { roofs } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
const rectOutline: Roof["outline"] = [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 6, y: 0 },
|
||||
{ x: 6, y: 4 },
|
||||
{ x: 0, y: 4 },
|
||||
];
|
||||
|
||||
const roof = (id: string, shape: Roof["shape"], extra: Partial<Roof> = {}): Roof => ({
|
||||
id,
|
||||
type: "roof",
|
||||
floorId: "eg",
|
||||
categoryCode: "35",
|
||||
outline: rectOutline,
|
||||
shape,
|
||||
pitchDeg: 30,
|
||||
overhang: 0.5,
|
||||
ridgeAxis: "x",
|
||||
thickness: 0.3,
|
||||
...extra,
|
||||
});
|
||||
|
||||
const visible = new Set(["35"]);
|
||||
|
||||
/** Filtert die Grundriss-Primitive nach `kind:"line"` + gegebener Klasse. */
|
||||
function linesOf(primitives: ReturnType<typeof generatePlan>["primitives"], cls: string) {
|
||||
return primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "line" }> => p.kind === "line" && p.cls === cls,
|
||||
);
|
||||
}
|
||||
|
||||
describe("generatePlan — Dächer (Dachaufsicht)", () => {
|
||||
it("Satteldach: Traufe + First, aber kein Grat/Knick", () => {
|
||||
const plan = generatePlan(baseProject([roof("R1", "sattel")]), "eg", visible);
|
||||
expect(linesOf(plan.primitives, "roof-eaves").length).toBeGreaterThan(0);
|
||||
expect(linesOf(plan.primitives, "roof-ridge").length).toBeGreaterThan(0);
|
||||
expect(linesOf(plan.primitives, "roof-hip").length).toBe(0);
|
||||
expect(linesOf(plan.primitives, "roof-break").length).toBe(0);
|
||||
});
|
||||
|
||||
it("Walmdach: Traufe + First + Grat", () => {
|
||||
const plan = generatePlan(baseProject([roof("R2", "walm")]), "eg", visible);
|
||||
expect(linesOf(plan.primitives, "roof-eaves").length).toBeGreaterThan(0);
|
||||
expect(linesOf(plan.primitives, "roof-ridge").length).toBeGreaterThan(0);
|
||||
expect(linesOf(plan.primitives, "roof-hip").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("Mansarddach: Knicklinien gestrichelt", () => {
|
||||
const plan = generatePlan(baseProject([roof("R3", "mansarde")]), "eg", visible);
|
||||
const breaks = linesOf(plan.primitives, "roof-break");
|
||||
expect(breaks.length).toBeGreaterThan(0);
|
||||
for (const b of breaks) {
|
||||
expect(b.dash).toBeTruthy();
|
||||
expect(b.dash?.length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it("Flachdach: nur Traufe, weder First noch Grat noch Knick", () => {
|
||||
const plan = generatePlan(baseProject([roof("R4", "flach")]), "eg", visible);
|
||||
expect(linesOf(plan.primitives, "roof-eaves").length).toBeGreaterThan(0);
|
||||
expect(linesOf(plan.primitives, "roof-ridge").length).toBe(0);
|
||||
expect(linesOf(plan.primitives, "roof-hip").length).toBe(0);
|
||||
expect(linesOf(plan.primitives, "roof-break").length).toBe(0);
|
||||
});
|
||||
|
||||
it("First-Linie ist kräftiger als die Traufe (First > Traufe > Knick)", () => {
|
||||
const plan = generatePlan(baseProject([roof("R5", "mansarde")]), "eg", visible);
|
||||
const eaves = linesOf(plan.primitives, "roof-eaves")[0];
|
||||
const ridge = linesOf(plan.primitives, "roof-ridge")[0];
|
||||
const brk = linesOf(plan.primitives, "roof-break")[0];
|
||||
expect(ridge.weightMm).toBeGreaterThan(eaves.weightMm);
|
||||
expect(eaves.weightMm).toBeGreaterThan(brk.weightMm);
|
||||
});
|
||||
|
||||
it("löst die Strichfarbe über roof.color, sonst die Kategorie-Farbe auf", () => {
|
||||
const withColor = generatePlan(
|
||||
baseProject([roof("R6", "sattel", { color: "#ff0000" })]),
|
||||
"eg",
|
||||
visible,
|
||||
);
|
||||
expect(linesOf(withColor.primitives, "roof-eaves")[0].color).toBe("#ff0000");
|
||||
|
||||
const withoutColor = generatePlan(baseProject([roof("R7", "sattel")]), "eg", visible);
|
||||
expect(linesOf(withoutColor.primitives, "roof-eaves")[0].color).toBe("#8a4b32");
|
||||
});
|
||||
|
||||
it("Rückwärtskompatibilität: ohne Dächer (bzw. Dach auf anderem Geschoss) bleiben die Primitive unverändert", () => {
|
||||
const planNoField = generatePlan(baseProject(undefined), "eg", visible);
|
||||
const planEmpty = generatePlan(baseProject([]), "eg", visible);
|
||||
const planOtherFloor = generatePlan(
|
||||
baseProject([roof("R8", "sattel", { floorId: "og" })]),
|
||||
"eg",
|
||||
visible,
|
||||
);
|
||||
|
||||
expect(planEmpty.primitives).toEqual(planNoField.primitives);
|
||||
expect(planOtherFloor.primitives).toEqual(planNoField.primitives);
|
||||
expect(planNoField.primitives.some((p) => p.kind === "line" && p.cls.startsWith("roof-"))).toBe(
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user