Files
DOSSIER-STANDALONE/src/plan/toSection.boolean.test.ts
T
karim c34b7e5771 Schnitt: Boolean-Dominanz der Schichten nach joinPriority
Wo sich Schicht-Baender verschiedener Elemente im Schnitt ueberlappen, gewinnt
die staerkere Schicht (hoehere joinPriority) und schneidet die schwaechere per
Rechteck-Subtraktion weg (elementuebergreifend: die Betondecke schneidet den
Wand-Putz). Gleiche Prioritaet koexistiert (durchgehende Betonflaeche Wand+
Decke). attachCutStyles sammelt jetzt alle Baender mit joinPriority und laesst
subtractDominantBands global gegen alle staerkeren Cutter subtrahieren; die
ueberlappungsfreie Zerlegung liefert exakt die Material-/Fugenlinien zwischen
den Schichten. Neuer subtractRect-Helfer, 11 neue Tests, 152 gruen.
2026-07-04 01:46:01 +02:00

188 lines
7.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Unit-Tests für die Schnitt-Boolean-Dominanz (toSection): an Wand-Decken-
* Übergängen schneidet die STÄRKERE Schicht (höhere `Component.joinPriority`)
* die schwächere per achsparalleler Rechteck-Subtraktion weg. Gleiche Priorität
* koexistiert (kein Schnitt).
*
* Getestet ohne WASM:
* 1. `subtractRect` direkt (Ecken-/Kanten-/Enthalten-/Kein-Overlap-Fälle).
* 2. `subtractDominantBands` gegen den Nutzer-Testfall — ELEMENTÜBERGREIFEND:
* ein Decken-Beton-Band (100) schneidet ein überlappendes Wand-Putz-Band
* (10) weg; Beton bleibt voll; gleiche Priorität koexistiert.
*/
import { describe, it, expect } from "vitest";
import {
subtractRect,
subtractDominantBands,
type Rect,
type SectionCutPolygon,
} from "./toSection";
/** Rechteck-Kurzform. */
function r(uMin: number, uMax: number, vMin: number, vMax: number): Rect {
return { uMin, uMax, vMin, vMax };
}
/** Rechteck einer Rest-Menge normalisieren (auf 1e-9 gerundet, sortiert) für stabile Vergleiche. */
function norm(rects: Rect[]): Rect[] {
const round = (x: number) => Math.round(x * 1e9) / 1e9;
return rects
.map((x) => ({ uMin: round(x.uMin), uMax: round(x.uMax), vMin: round(x.vMin), vMax: round(x.vMax) }))
.sort((a, b) => a.uMin - b.uMin || a.vMin - b.vMin);
}
/** Ein achsparalleles Cut-Band mit joinPriority. */
function band(
uMin: number,
uMax: number,
vMin: number,
vMax: number,
joinPriority: number,
tag = "",
): SectionCutPolygon {
return {
component: { kind: "wall", index: 0 },
color: [0.5, 0.5, 0.5],
pts: [
[uMin, vMax],
[uMax, vMax],
[uMax, vMin],
[uMin, vMin],
],
fill: tag || "#fff",
joinPriority,
};
}
/** Bounding-Box eines Bands als Rect. */
function boxOf(b: SectionCutPolygon): Rect {
const us = b.pts.map((p) => p[0]);
const vs = b.pts.map((p) => p[1]);
return { uMin: Math.min(...us), uMax: Math.max(...us), vMin: Math.min(...vs), vMax: Math.max(...vs) };
}
/** Gesamtfläche einer Band-/Rect-Menge. */
function area(items: Array<Rect | SectionCutPolygon>): number {
return items.reduce((s, it) => {
const b = "pts" in it ? boxOf(it) : it;
return s + (b.uMax - b.uMin) * (b.vMax - b.vMin);
}, 0);
}
describe("subtractRect: achsparallele Rechteck-Subtraktion", () => {
it("kein Overlap → base unverändert", () => {
expect(norm(subtractRect(r(0, 1, 0, 1), r(2, 3, 2, 3)))).toEqual(norm([r(0, 1, 0, 1)]));
});
it("nur Kantenberührung (Fläche 0) → base unverändert", () => {
// cutter grenzt bei u=1 an, überlappt aber nicht.
expect(norm(subtractRect(r(0, 1, 0, 1), r(1, 2, 0, 1)))).toEqual(norm([r(0, 1, 0, 1)]));
});
it("cutter enthält base vollständig → leere Restmenge", () => {
expect(subtractRect(r(1, 2, 1, 2), r(0, 3, 0, 3))).toEqual([]);
});
it("cutter mittig in base → 4 Rest-Rechtecke, Flächenbilanz stimmt", () => {
const rest = subtractRect(r(0, 3, 0, 3), r(1, 2, 1, 2));
expect(rest).toHaveLength(4);
// 9 (base) 1 (overlap) = 8.
expect(area(rest)).toBeCloseTo(8, 9);
// Rechtecke sind disjunkt: Summe der Einzelflächen = Gesamtfläche.
const sum = rest.reduce((s, x) => s + (x.uMax - x.uMin) * (x.vMax - x.vMin), 0);
expect(sum).toBeCloseTo(8, 9);
});
it("Ecküberlappung → 2 Rest-Rechtecke (L-Form)", () => {
// cutter überdeckt die obere-rechte Ecke von base.
const rest = subtractRect(r(0, 2, 0, 2), r(1, 3, 1, 3));
// overlap = [1,2]×[1,2] (Fläche 1). Rest = 4 1 = 3.
expect(area(rest)).toBeCloseTo(3, 9);
expect(norm(rest)).toEqual(
norm([
r(0, 1, 0, 2), // links, volle Höhe
r(1, 2, 0, 1), // mittlerer Streifen unten
]),
);
});
it("Kantenüberlappung (cutter deckt einen Randstreifen) → 1 Rest-Rechteck", () => {
// cutter deckt den oberen Rand v∈[1,2] über die volle Breite.
const rest = subtractRect(r(0, 2, 0, 2), r(-1, 3, 1, 3));
expect(norm(rest)).toEqual(norm([r(0, 2, 0, 1)]));
});
});
describe("subtractDominantBands: stärkere Schicht schneidet schwächere", () => {
it("Nutzer-Testfall ELEMENTÜBERGREIFEND: Decken-Beton(100) schneidet Wand-Putz(10) weg", () => {
// Wand-Putz-Band (Element B): schwach, joinPriority 10, u∈[0,2], v∈[0,3].
const putz = band(0, 2, 0, 3, 10, "putz");
// Decken-Beton-Band (Element A): stark, joinPriority 100, überlappt den
// oberen Teil des Putzes: u∈[0,2], v∈[2,4].
const beton = band(0, 2, 2, 4, 100, "beton");
const out = subtractDominantBands([putz, beton]);
// Beton bleibt voll erhalten (kein stärkeres Band).
const betonOut = out.filter((b) => b.fill === "beton");
expect(betonOut).toHaveLength(1);
expect(boxOf(betonOut[0])).toEqual(boxOf(beton));
// Putz ist im Überlappungsbereich v∈[2,3] weggeschnitten → Rest nur v∈[0,2].
const putzOut = out.filter((b) => b.fill === "putz");
expect(putzOut).toHaveLength(1);
expect(boxOf(putzOut[0])).toEqual(r(0, 2, 0, 2));
// In der Überlappungsregion existiert KEIN Putz mehr.
expect(putzOut[0].pts.every((p) => p[1] <= 2 + 1e-9)).toBe(true);
});
it("gestaffelte Kette Beton100 > Backstein50 > Dämmung20 > Putz10", () => {
// Alle vier Bänder überlappen in derselben Region u∈[0,1], gestapelt in v.
// Jedes schwächere wird von JEDEM stärkeren geschnitten, wo sie überlappen.
const beton = band(0, 1, 3, 4, 100, "beton");
const backstein = band(0, 1, 2.5, 3.5, 50, "backstein"); // überlappt Beton in v∈[3,3.5]
const daemmung = band(0, 1, 2, 3, 20, "daemmung"); // überlappt Backstein in v∈[2.5,3]
const putz = band(0, 1, 0, 3.2, 10, "putz"); // überlappt alle darüber
const out = subtractDominantBands([beton, backstein, daemmung, putz]);
// Beton unangetastet.
expect(area(out.filter((b) => b.fill === "beton"))).toBeCloseTo(1, 9);
// Backstein: [2.5,3.5] minus Beton-Overlap [3,3.5] = [2.5,3] → Fläche 0.5.
expect(area(out.filter((b) => b.fill === "backstein"))).toBeCloseTo(0.5, 9);
// Dämmung: [2,3] minus Backstein-Overlap [2.5,3] = [2,2.5] → Fläche 0.5.
// (Beton [3,4] überlappt Dämmung nicht.)
expect(area(out.filter((b) => b.fill === "daemmung"))).toBeCloseTo(0.5, 9);
// Putz: [0,3.2] minus Overlap mit ALLEN stärkeren.
// Beton[3,4]→[3,3.2], Backstein[2.5,3.5]→[2.5,3.2], Dämmung[2,3]→[2,3] überdecken
// zusammen v∈[2,3.2]; Rest = v∈[0,2] → Fläche 2.
expect(area(out.filter((b) => b.fill === "putz"))).toBeCloseTo(2, 9);
});
it("gleiche Priorität koexistiert: Betonwand + Betondecke schneiden sich NICHT", () => {
const betonWand = band(0, 1, 0, 3, 100, "wand");
const betonDecke = band(0, 1, 2, 4, 100, "decke"); // überlappt in v∈[2,3]
const out = subtractDominantBands([betonWand, betonDecke]);
// Beide voll erhalten (durchgehende Betonfläche).
expect(area(out.filter((b) => b.fill === "wand"))).toBeCloseTo(3, 9);
expect(area(out.filter((b) => b.fill === "decke"))).toBeCloseTo(2, 9);
});
it("Band ohne joinPriority nimmt an keiner Subtraktion teil", () => {
const stark = band(0, 2, 0, 2, 100, "stark");
const ohne: SectionCutPolygon = { ...band(0, 2, 0, 2, 0, "ohne"), joinPriority: undefined };
const out = subtractDominantBands([stark, ohne]);
// Beide unverändert durchgereicht.
expect(area(out.filter((b) => b.fill === "stark"))).toBeCloseTo(4, 9);
expect(area(out.filter((b) => b.fill === "ohne"))).toBeCloseTo(4, 9);
});
it("vollständig überdecktes schwächeres Band verschwindet", () => {
const stark = band(0, 3, 0, 3, 100, "stark");
const schwach = band(1, 2, 1, 2, 10, "schwach"); // ganz im starken enthalten
const out = subtractDominantBands([stark, schwach]);
expect(out.filter((b) => b.fill === "schwach")).toHaveLength(0);
expect(area(out.filter((b) => b.fill === "stark"))).toBeCloseTo(9, 9);
});
});