0240a23a02
Bisher war die Füllung bei Detailgrad "grob" nur schwarz, wenn das dominante Wandbauteil selbst ein "solid"-Hatch-Pattern hatte — bei mehrschichtigen Wandtypen (Backstein/Dämmung/Verputz) blieb sie weiss. SIA 400 kennt bei 1:100 aber keine Materialunterscheidung, die Poché ist immer vollschwarz. Fix + Regressionstest von Hermes/Qwen3 übernommen und um den fehlenden Test sowie das Aufräumen der jetzt toten backbonePocheFill-Hilfsfunktion ergänzt.
197 lines
7.6 KiB
TypeScript
197 lines
7.6 KiB
TypeScript
/**
|
|
* Unit-Tests für „Schnitt vs. Ansicht nach Schnitthöhe" (generatePlan):
|
|
* Eine Wand, die die Grundriss-Schnittebene NICHT erreicht (Höhe < `cutHeight`,
|
|
* z. B. eine Brüstung), wird nur als Ansichts-Umriss gezeichnet — Poché-Polygone
|
|
* OHNE Füllung (`fill:"none"`), keine Schraffur. Eine normale Wand (Höhe ≥
|
|
* Schnitthöhe) behält die geschnittene Poché (gefüllte Bänder).
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { generatePlan } from "./generatePlan";
|
|
import type { Project, Wall } from "../model/types";
|
|
|
|
/** Minimalprojekt: eine normale Wand (2.6 m) + eine niedrige Wand (0.3 m). */
|
|
function project(): Project {
|
|
const wall = (id: string, start: Wall["start"], end: Wall["end"], height: number): Wall => ({
|
|
id,
|
|
type: "wall",
|
|
floorId: "eg",
|
|
categoryCode: "20",
|
|
start,
|
|
end,
|
|
wallTypeId: "aw",
|
|
height,
|
|
});
|
|
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.2 }] }],
|
|
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 }],
|
|
walls: [
|
|
wall("HI", { x: 0, y: 0 }, { x: 4, y: 0 }, 2.6), // erreicht Schnitt → Poché
|
|
wall("LOW", { x: 0, y: 2 }, { x: 4, y: 2 }, 0.3), // Brüstung → nur Ansicht
|
|
],
|
|
doors: [],
|
|
openings: [],
|
|
ceilings: [],
|
|
stairs: [],
|
|
rooms: [],
|
|
drawings2d: [],
|
|
context: [],
|
|
};
|
|
}
|
|
|
|
const visible = new Set(["20"]);
|
|
|
|
describe("generatePlan — Schnitt vs. Ansicht nach Schnitthöhe", () => {
|
|
it("zeichnet die niedrige Wand nur als Ansichts-Umriss (fill none), die normale mit Poché", () => {
|
|
const plan = generatePlan(project(), "eg", visible, undefined, "mittel");
|
|
const wallPolys = (id: string) =>
|
|
plan.primitives.filter(
|
|
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
|
p.kind === "polygon" && p.wallId === id,
|
|
);
|
|
|
|
const hi = wallPolys("HI");
|
|
const low = wallPolys("LOW");
|
|
expect(hi.length).toBeGreaterThan(0);
|
|
expect(low.length).toBeGreaterThan(0);
|
|
|
|
// Normale Wand: mindestens ein gefülltes Poché-Band (Bauteil-Farbe).
|
|
expect(hi.some((p) => p.fill !== "none")).toBe(true);
|
|
// Niedrige Wand: ALLE Polygone ohne Füllung (reiner Ansichts-Umriss).
|
|
expect(low.every((p) => p.fill === "none")).toBe(true);
|
|
});
|
|
|
|
it("behandelt die niedrige Wand als geschnitten, wenn die Schnitthöhe darunter liegt", () => {
|
|
const p = project();
|
|
p.drawingLevels[0].cutHeight = 0.2; // Schnittebene unter 0.3 m → auch LOW geschnitten
|
|
const plan = generatePlan(p, "eg", visible, undefined, "mittel");
|
|
const low = plan.primitives.filter(
|
|
(pr): pr is Extract<typeof pr, { kind: "polygon" }> =>
|
|
pr.kind === "polygon" && pr.wallId === "LOW",
|
|
);
|
|
expect(low.some((pr) => pr.fill !== "none")).toBe(true);
|
|
});
|
|
});
|
|
|
|
// ── Decke über der Schnittebene → gestrichelte Überkopf-Umrisslinie ──────────
|
|
|
|
// ── grob (1:100) → Wand-Poché IMMER vollschwarz (SIA 400 Fig. 36) ──────────
|
|
|
|
/** Minimalprojekt mit einer Wand, deren Bauteil ein NICHT-"solid" Hatch-Pattern
|
|
* hat (Backstein/diagonal — der Normalfall bei mehrschichtigen Wandtypen). */
|
|
function nonSolidWallProject(): 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: "diag", name: "Diagonal", pattern: "diagonal", scale: 1, angle: 45, color: "#111" }],
|
|
components: [{ id: "brick", name: "Backstein", color: "#c9a876", hatchId: "diag", joinPriority: 10 }],
|
|
wallTypes: [{ id: "aw", name: "AW", layers: [{ componentId: "brick", 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 }],
|
|
walls: [wall],
|
|
doors: [],
|
|
openings: [],
|
|
ceilings: [],
|
|
stairs: [],
|
|
rooms: [],
|
|
drawings2d: [],
|
|
context: [],
|
|
};
|
|
}
|
|
|
|
describe("generatePlan — grob: Wand-Poché immer vollschwarz (SIA 400 Fig. 36)", () => {
|
|
const wallPolys = (plan: ReturnType<typeof generatePlan>, id: string) =>
|
|
plan.primitives.filter(
|
|
(p): p is Extract<typeof p, { kind: "polygon" }> => p.kind === "polygon" && p.wallId === id,
|
|
);
|
|
|
|
it("grob: Vollschwarz-Füllung auch wenn das Bauteil-Pattern NICHT 'solid' ist (Regression)", () => {
|
|
const plan = generatePlan(nonSolidWallProject(), "eg", new Set(["20"]), undefined, "grob");
|
|
const polys = wallPolys(plan, "W1");
|
|
expect(polys.length).toBeGreaterThan(0);
|
|
expect(polys.every((p) => p.fill === "#0f0f0f")).toBe(true);
|
|
expect(polys.every((p) => p.hatch.pattern === "none")).toBe(true);
|
|
});
|
|
|
|
it("mittel/fein: Füllung bleibt bauteilfarben (KEIN Vollschwarz-Zwang) — unverändertes Verhalten", () => {
|
|
for (const detail of ["mittel", "fein"] as const) {
|
|
const plan = generatePlan(nonSolidWallProject(), "eg", new Set(["20"]), undefined, detail);
|
|
const polys = wallPolys(plan, "W1");
|
|
expect(polys.length).toBeGreaterThan(0);
|
|
expect(polys.some((p) => p.fill !== "#0f0f0f")).toBe(true);
|
|
}
|
|
});
|
|
});
|
|
|
|
/** Minimalprojekt mit EINER Decke (Slab) ohne Wände → voller Umriss sichtbar. */
|
|
function ceilingProject(): Project {
|
|
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.2 }] }],
|
|
drawingLevels: [
|
|
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
|
|
],
|
|
layers: [{ code: "30", name: "Decken", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
|
|
walls: [],
|
|
doors: [],
|
|
openings: [],
|
|
ceilings: [
|
|
{
|
|
id: "C1",
|
|
type: "ceiling",
|
|
floorId: "eg",
|
|
categoryCode: "30",
|
|
outline: [
|
|
{ x: 0, y: 0 },
|
|
{ x: 2, y: 0 },
|
|
{ x: 2, y: 2 },
|
|
{ x: 0, y: 2 },
|
|
],
|
|
wallTypeId: "aw",
|
|
},
|
|
],
|
|
stairs: [],
|
|
rooms: [],
|
|
drawings2d: [],
|
|
context: [],
|
|
};
|
|
}
|
|
|
|
describe("generatePlan — Decke = gestrichelte Überkopf-Ansicht", () => {
|
|
it("zeichnet den freien Decken-Umriss gestrichelt (Überkopf-Konvention)", () => {
|
|
const plan = generatePlan(ceilingProject(), "eg", new Set(["30"]), undefined, "mittel");
|
|
const outlines = plan.primitives.filter(
|
|
(p): p is Extract<typeof p, { kind: "line" }> =>
|
|
p.kind === "line" && p.cls === "ceiling-outline",
|
|
);
|
|
expect(outlines.length).toBeGreaterThan(0);
|
|
// JEDE Decken-Umrisslinie ist gestrichelt (dash gesetzt, nicht null).
|
|
expect(outlines.every((p) => Array.isArray(p.dash) && p.dash.length >= 2)).toBe(true);
|
|
});
|
|
});
|