Decken-Aussparungen: Treppenauge/Schacht in 2D und 3D (Brücken-Trick)
Ceiling.openings (e469a93) wird jetzt gerendert. Kern: mergeHoles() fügt
Löcher über schmale Brücken (1e-4) in den Aussenring ein → EIN einfacher
Polygonring, paritätskorrekt für SVG-Fill, Scanline-Hatch, Ear-Clipping
(render3d) und Punkt-in-Polygon-Pick — kein Loch-Support in den Primitiven
nötig. Loch-Orientierung gegenläufig (signedArea), Sichtbarkeitstest für die
Brücke, mehrere Löcher iterativ.
- 2D (addCeilingPoche): Poché-Ring mit Löchern; Brückenkanten via
noStrokeEdges unsichtbar, Lochränder werden als Ringkanten gestrichen
(wand-footprint-geclippt wie der Aussenumriss)
- 3D (emitSlabs): derselbe Ring für alle Schichten; Klick ins Loch trifft
die Decke nicht (Pick paritätskorrekt)
- Ohne openings byte-identischer Codepfad (Regressionstests)
+11 Tests (Ring-Einfachheit, Shoelace-Flächen, 2-Loch-Fall, Regressionen).
684/684 grün. 3D visuell in Tauri zu prüfen.
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
/**
|
||||
* Unit-Tests für Decken-Aussparungen (Treppenauge/Schacht) im Grundriss
|
||||
* (generatePlan → addCeilingPoche): der Brücken-Trick (mergeHoles) fügt das
|
||||
* Loch als EINEN erweiterten Poché-Ring ein.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { generatePlan } from "./generatePlan";
|
||||
import type { Ceiling, Project } from "../model/types";
|
||||
import type { Primitive } from "./generatePlan";
|
||||
|
||||
/** Minimalprojekt mit EINER Decke (6x4m), konfigurierbarem `openings`. */
|
||||
function projectWithCeiling(openings?: Ceiling["openings"]): Project {
|
||||
const ceiling: Ceiling = {
|
||||
id: "C1",
|
||||
type: "ceiling",
|
||||
floorId: "eg",
|
||||
categoryCode: "30",
|
||||
outline: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 6, y: 0 },
|
||||
{ x: 6, y: 4 },
|
||||
{ x: 0, y: 4 },
|
||||
],
|
||||
wallTypeId: "aw",
|
||||
...(openings ? { openings } : {}),
|
||||
};
|
||||
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.3 }] }],
|
||||
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.3, visible: true, locked: false }],
|
||||
walls: [],
|
||||
doors: [],
|
||||
openings: [],
|
||||
ceilings: [ceiling],
|
||||
stairs: [],
|
||||
rooms: [],
|
||||
drawings2d: [],
|
||||
context: [],
|
||||
};
|
||||
}
|
||||
|
||||
const visible = new Set(["30"]);
|
||||
|
||||
const plan = (p: Project) => generatePlan(p, "eg", visible, undefined, "mittel");
|
||||
|
||||
/** Die Poché-Fläche der Decke (Fill-Polygon mit `ceilingId`). */
|
||||
const pochePrim = (primitives: Primitive[]) =>
|
||||
primitives.find((pr): pr is Extract<Primitive, { kind: "polygon" }> => pr.kind === "polygon" && pr.ceilingId === "C1");
|
||||
|
||||
const outlineLineCount = (primitives: Primitive[]) =>
|
||||
primitives.filter((pr) => pr.kind === "line" && pr.cls === "ceiling-outline").length;
|
||||
|
||||
describe("generatePlan — Decken-Aussparungen (Brücken-Trick)", () => {
|
||||
it("ohne openings: Poché-Polygon = unveränderte Outline, kein noStrokeEdges (Regression)", () => {
|
||||
const p = projectWithCeiling(undefined);
|
||||
const prims = plan(p).primitives;
|
||||
const poche = pochePrim(prims);
|
||||
expect(poche).toBeDefined();
|
||||
expect(poche!.pts).toEqual(p.ceilings![0].outline);
|
||||
expect(poche!.noStrokeEdges).toBeUndefined();
|
||||
expect("noStrokeEdges" in poche!).toBe(false);
|
||||
// 4 Aussenkanten, keine Wände im Testprojekt -> 4 volle Umrisslinien.
|
||||
expect(outlineLineCount(prims)).toBe(4);
|
||||
});
|
||||
|
||||
it("mit einem 1x1-Loch mittig: Poché-Polygon hat mehr Ecken, noStrokeEdges = Brückenkanten", () => {
|
||||
const hole = [
|
||||
{ x: 2.5, y: 1.5 },
|
||||
{ x: 3.5, y: 1.5 },
|
||||
{ x: 3.5, y: 2.5 },
|
||||
{ x: 2.5, y: 2.5 },
|
||||
];
|
||||
const p = projectWithCeiling([hole]);
|
||||
const prims = plan(p).primitives;
|
||||
const poche = pochePrim(prims);
|
||||
expect(poche).toBeDefined();
|
||||
// 4 (Aussen) + 4 (Loch) + 2 (Brücken-Duplikate) = 10 Ecken.
|
||||
expect(poche!.pts.length).toBe(10);
|
||||
expect(poche!.pts.length).toBeGreaterThan(p.ceilings![0].outline.length);
|
||||
expect(poche!.noStrokeEdges).toBeDefined();
|
||||
expect(poche!.noStrokeEdges!.length).toBe(2);
|
||||
|
||||
// Die Brückenkanten selbst tragen keine Umrisslinie; der Rest (8 Kanten:
|
||||
// 4 Aussen + 4 Lochrand) wird gestrichen — der Lochrand erscheint als
|
||||
// eigenständige Umrisslinien, obwohl `ceiling.outline` unverändert blieb.
|
||||
expect(outlineLineCount(prims)).toBe(poche!.pts.length - poche!.noStrokeEdges!.length);
|
||||
expect(outlineLineCount(prims)).toBe(8);
|
||||
});
|
||||
|
||||
it("Loch ausserhalb der Outline wird ignoriert (Poché bleibt unverändert)", () => {
|
||||
const outsideHole = [
|
||||
{ x: 20, y: 20 },
|
||||
{ x: 21, y: 20 },
|
||||
{ x: 21, y: 21 },
|
||||
{ x: 20, y: 21 },
|
||||
];
|
||||
const p = projectWithCeiling([outsideHole]);
|
||||
const prims = plan(p).primitives;
|
||||
const poche = pochePrim(prims);
|
||||
expect(poche!.pts).toEqual(p.ceilings![0].outline);
|
||||
expect("noStrokeEdges" in poche!).toBe(false);
|
||||
expect(outlineLineCount(prims)).toBe(4);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user