Files
DOSSIER-STANDALONE/src/plan/generatePlan.viewwall.test.ts
T
karim 0240a23a02 Wand-Poché bei "grob" immer vollschwarz (SIA 400 Fig. 36)
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.
2026-07-12 16:20:36 +02:00

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);
});
});