diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index 8d6836b..7470bdb 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -677,7 +677,14 @@ export function generatePlan( const openings = openingsOfWall(project, wall.id); const gaps = wallGaps(wall, doors, openings); const cuts = joins.get(wall.id) ?? { startCut: null, endCut: null }; - addWallPoche(primitives, project, wall, gaps, cuts, wallGreyed, detail, wallLwMm, wallCategory); + // Schnitt vs. Ansicht nach Schnitthöhe (BIM-Standard): erreicht die Wand die + // Grundriss-Schnittebene nicht (Höhe < Schnitthöhe des Geschosses, z. B. eine + // Brüstung/ein Podestrand), wird sie nur als Ansichts-Umriss gezeichnet — + // keine Schnitt-Poché. Normale Wände (Höhe ≥ Schnitthöhe) sind unberührt. + const wallViewOnly = wall.height < (floor?.cutHeight ?? 1.0); + addWallPoche( + primitives, project, wall, gaps, cuts, wallGreyed, detail, wallLwMm, wallCategory, wallViewOnly, + ); // Referenzlinie: dünne gestrichelte Wand-Achslinie (von der Oberleiste // umschaltbar). Wird über der Poché gezeichnet, damit sie sichtbar bleibt. if (showReferenceLines) addReferenceLine(primitives, wall, wallGreyed); @@ -1258,6 +1265,10 @@ function addWallPoche( detail: DetailLevel, wallLwMm: number, category: LayerCategory | undefined, + // Wand liegt GANZ unter der Grundriss-Schnittebene (z. B. 30-cm-Brüstung bei + // 1 m Schnitthöhe) → „von oben gesehen": nur Ansichts-Umriss (Haarlinie), KEINE + // Schnitt-Poché/Schraffur/Schichten. Der reguläre Schnittfall bleibt `false`. + viewOnly: boolean, ): void { const wt = getWallType(project, wall); const total = wallTypeThickness(wt); @@ -1326,6 +1337,24 @@ function addWallPoche( const startCut = s <= 1e-6 ? cuts.startCut : null; const endCut = e >= axisLen - 1e-6 ? cuts.endCut : null; + if (viewOnly) { + // Ansichts-Wand (unter der Schnittebene): ein Umriss über die volle Dicke + // als Haarlinie, ohne Füllung/Schraffur. Segment-/Gehrungs-/Öffnungs-Logik + // bleibt dieselbe wie im Schnittfall (nur eben ohne Poché). + out.push({ + kind: "polygon", + pts: clippedBand(p1, p2, refOff - total / 2, refOff + total / 2, startCut, endCut), + fill: "none", + stroke, + strokeWidthMm: layerLineMm, + hatch: NO_HATCH, + greyed, + noStrokeEdges: joinEdges(startCut, endCut), + wallId, + }); + continue; + } + if (detail === "grob") { // Eine Sammelfläche über die volle Dicke, ohne Schraffur, dicker Umriss. out.push({ diff --git a/src/plan/generatePlan.viewwall.test.ts b/src/plan/generatePlan.viewwall.test.ts new file mode 100644 index 0000000..2e00174 --- /dev/null +++ b/src/plan/generatePlan.viewwall.test.ts @@ -0,0 +1,82 @@ +/** + * 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 => + 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 => + pr.kind === "polygon" && pr.wallId === "LOW", + ); + expect(low.some((pr) => pr.fill !== "none")).toBe(true); + }); +});