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.
This commit is contained in:
@@ -0,0 +1,187 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
+177
-6
@@ -47,6 +47,15 @@ export interface SectionCutPolygon {
|
|||||||
fill?: string;
|
fill?: string;
|
||||||
/** Echte Schnitt-Schraffur des geschnittenen Bauteils, analog zu `fill`. */
|
/** Echte Schnitt-Schraffur des geschnittenen Bauteils, analog zu `fill`. */
|
||||||
hatch?: HatchRender;
|
hatch?: HatchRender;
|
||||||
|
/**
|
||||||
|
* Verschneidungs-Rang der Schicht dieses Bands (`Component.joinPriority` des
|
||||||
|
* Ursprungs-Bauteils). Steuert die Schnitt-Boolean-Dominanz in
|
||||||
|
* `attachCutStyles`: ein Band mit STRIKT höherer Priorität schneidet ein
|
||||||
|
* überlappendes schwächeres Band per Rechteck-Subtraktion weg (element-
|
||||||
|
* übergreifend, z. B. Decken-Beton schneidet Wand-Putz). Fehlt der Wert
|
||||||
|
* (unauflösbares Bauteil), nimmt das Band an keiner Subtraktion teil.
|
||||||
|
*/
|
||||||
|
joinPriority?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ein projiziertes Liniensegment in (u, v)-Metern. */
|
/** Ein projiziertes Liniensegment in (u, v)-Metern. */
|
||||||
@@ -254,7 +263,11 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
|||||||
const uWorld = sectionUAxisModel(plane);
|
const uWorld = sectionUAxisModel(plane);
|
||||||
const walls = wallSegmentOwners(project);
|
const walls = wallSegmentOwners(project);
|
||||||
const slabs = slabSegmentOwners(project);
|
const slabs = slabSegmentOwners(project);
|
||||||
const next: SectionCutPolygon[] = [];
|
|
||||||
|
// Phase 1 — ALLE Cut-Bänder (aus allen Wänden UND Decken, mehr- wie einschichtig)
|
||||||
|
// als flache Liste sammeln, jedes mit seiner `joinPriority` getaggt (aus dem
|
||||||
|
// Bauteil der Schicht bzw. dem repräsentativen Bauteil bei einschichtig).
|
||||||
|
const bands: SectionCutPolygon[] = [];
|
||||||
for (const cp of output.cutPolygons) {
|
for (const cp of output.cutPolygons) {
|
||||||
const ref = cp.component;
|
const ref = cp.component;
|
||||||
if (ref.kind === "wall") {
|
if (ref.kind === "wall") {
|
||||||
@@ -267,7 +280,7 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
|||||||
if (owner) {
|
if (owner) {
|
||||||
const wt = getWallType(project, owner);
|
const wt = getWallType(project, owner);
|
||||||
if (wt.layers.length > 1) {
|
if (wt.layers.length > 1) {
|
||||||
next.push(...splitWallLayers(cp, project, wt, owner, uWorld));
|
bands.push(...splitWallLayers(cp, project, wt, owner, uWorld));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -276,7 +289,8 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
|||||||
cp.fill = style.fill;
|
cp.fill = style.fill;
|
||||||
cp.hatch = style.hatch;
|
cp.hatch = style.hatch;
|
||||||
}
|
}
|
||||||
next.push(cp);
|
cp.joinPriority = owner ? wallRepresentativePriority(project, owner) : undefined;
|
||||||
|
bands.push(cp);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Decke: bei mehrschichtigem Deckentyp wird das EINE geschnittene Rechteck
|
// Decke: bei mehrschichtigem Deckentyp wird das EINE geschnittene Rechteck
|
||||||
@@ -288,7 +302,7 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
|||||||
if (owner) {
|
if (owner) {
|
||||||
const wt = getCeilingType(project, owner);
|
const wt = getCeilingType(project, owner);
|
||||||
if (wt.layers.length > 1) {
|
if (wt.layers.length > 1) {
|
||||||
next.push(...splitSlabLayers(cp, project, wt));
|
bands.push(...splitSlabLayers(cp, project, wt));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const style = resolveCeilingSectionStyle(project, owner);
|
const style = resolveCeilingSectionStyle(project, owner);
|
||||||
@@ -296,10 +310,165 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
|||||||
cp.fill = style.fill;
|
cp.fill = style.fill;
|
||||||
cp.hatch = style.hatch;
|
cp.hatch = style.hatch;
|
||||||
}
|
}
|
||||||
|
cp.joinPriority = ceilingRepresentativePriority(project, owner);
|
||||||
}
|
}
|
||||||
next.push(cp);
|
bands.push(cp);
|
||||||
}
|
}
|
||||||
output.cutPolygons = next;
|
|
||||||
|
// Phase 2 — Boolean-Dominanz: die stärkere Schicht schneidet die schwächere.
|
||||||
|
output.cutPolygons = subtractDominantBands(bands);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentative `joinPriority` einer Wand für die Schnitt-Dominanz, wenn ihre
|
||||||
|
* Poché einschichtig (ein Band über die volle Dicke) gezeichnet wird: die
|
||||||
|
* HÖCHSTE joinPriority ihrer Schichten — dieselbe Regel, mit der
|
||||||
|
* `resolveWallSectionStyle` das tragende Bauteil (dessen Füllung/Schraffur das
|
||||||
|
* Band trägt) wählt. `undefined`, wenn der Wandtyp/keine Schicht auflösbar ist
|
||||||
|
* (das Band nimmt dann an keiner Subtraktion teil).
|
||||||
|
*/
|
||||||
|
function wallRepresentativePriority(project: Project, wall: Wall): number | undefined {
|
||||||
|
try {
|
||||||
|
const wt = getWallType(project, wall);
|
||||||
|
let best: number | undefined;
|
||||||
|
for (const layer of wt.layers) {
|
||||||
|
const p = getComponent(project, layer.componentId).joinPriority;
|
||||||
|
if (best === undefined || p > best) best = p;
|
||||||
|
}
|
||||||
|
return best;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repräsentative `joinPriority` einer Decke für die Schnitt-Dominanz, wenn ihre
|
||||||
|
* Poché einschichtig (ein Band über die volle Dicke) gezeichnet wird: die der
|
||||||
|
* ERSTEN Schicht — dasselbe Bauteil, dessen Füllung/Schraffur
|
||||||
|
* `resolveCeilingSectionStyle` dem Band gibt. `undefined`, wenn Deckentyp/keine
|
||||||
|
* Schicht auflösbar ist.
|
||||||
|
*/
|
||||||
|
function ceilingRepresentativePriority(project: Project, ceiling: Ceiling): number | undefined {
|
||||||
|
try {
|
||||||
|
const wt = getCeilingType(project, ceiling);
|
||||||
|
const first = wt.layers[0];
|
||||||
|
if (!first) return undefined;
|
||||||
|
return getComponent(project, first.componentId).joinPriority;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Schnitt-Boolean-Dominanz (achsparallele Rechteck-Subtraktion) ───────────
|
||||||
|
// Kleine Toleranz für Überlappungs-/Rest-Rechtecke: Null-/Negativflächen und
|
||||||
|
// Rest-Streifen dünner als EPS werden verworfen (Float-Rauschen der u-/v-Meter).
|
||||||
|
const RECT_EPS = 1e-6;
|
||||||
|
|
||||||
|
/** Achsparalleles Rechteck in (u, v)-Metern. */
|
||||||
|
export interface Rect {
|
||||||
|
uMin: number;
|
||||||
|
uMax: number;
|
||||||
|
vMin: number;
|
||||||
|
vMax: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Bounding-Box eines (achsparallelen) Cut-Bands als `Rect`, oder `null` bei <3 Ecken. */
|
||||||
|
function rectOfBand(band: SectionCutPolygon): Rect | null {
|
||||||
|
if (band.pts.length < 3) return null;
|
||||||
|
const us = band.pts.map((p) => p[0]);
|
||||||
|
const vs = band.pts.map((p) => p[1]);
|
||||||
|
return {
|
||||||
|
uMin: Math.min(...us),
|
||||||
|
uMax: Math.max(...us),
|
||||||
|
vMin: Math.min(...vs),
|
||||||
|
vMax: Math.max(...vs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechteck-minus-Rechteck (beide achsparallel): `base` ohne die Überlappung mit
|
||||||
|
* `cutter`, zerlegt in 0–4 disjunkte Rest-Rechtecke (links/rechts über die volle
|
||||||
|
* Höhe, dann oben/unten im mittleren u-Streifen — überlappungsfrei). Kein echter
|
||||||
|
* Überlapp (Fläche ≤ EPS in u oder v) ⇒ `base` bleibt ganz. Vollständige
|
||||||
|
* Überdeckung ⇒ leere Liste. Rest-Streifen dünner als EPS werden verworfen.
|
||||||
|
*/
|
||||||
|
export function subtractRect(base: Rect, cutter: Rect): Rect[] {
|
||||||
|
const oMinU = Math.max(base.uMin, cutter.uMin);
|
||||||
|
const oMaxU = Math.min(base.uMax, cutter.uMax);
|
||||||
|
const oMinV = Math.max(base.vMin, cutter.vMin);
|
||||||
|
const oMaxV = Math.min(base.vMax, cutter.vMax);
|
||||||
|
// Kein (nennenswerter) Überlapp — `base` unverändert.
|
||||||
|
if (oMaxU - oMinU <= RECT_EPS || oMaxV - oMinV <= RECT_EPS) return [base];
|
||||||
|
|
||||||
|
const out: Rect[] = [];
|
||||||
|
const push = (r: Rect) => {
|
||||||
|
if (r.uMax - r.uMin > RECT_EPS && r.vMax - r.vMin > RECT_EPS) out.push(r);
|
||||||
|
};
|
||||||
|
// Links / rechts der Überlappung, jeweils über die volle Basis-Höhe.
|
||||||
|
push({ uMin: base.uMin, uMax: oMinU, vMin: base.vMin, vMax: base.vMax });
|
||||||
|
push({ uMin: oMaxU, uMax: base.uMax, vMin: base.vMin, vMax: base.vMax });
|
||||||
|
// Unten / oben im mittleren u-Streifen (nur der von der Überlappung befreite Rest).
|
||||||
|
push({ uMin: oMinU, uMax: oMaxU, vMin: base.vMin, vMax: oMinV });
|
||||||
|
push({ uMin: oMinU, uMax: oMaxU, vMin: oMaxV, vMax: base.vMax });
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Globale Schnitt-Dominanz: für jedes Band wird die Rechteck-Überlappung ALLER
|
||||||
|
* Bänder mit STRIKT höherer `joinPriority` subtrahiert (elementübergreifend —
|
||||||
|
* z. B. schneidet ein Decken-Beton-Band 100 ein überlappendes Wand-Putz-Band 10
|
||||||
|
* weg). Gleiche Priorität schneidet NICHT (koexistiert → durchgehende Fläche).
|
||||||
|
* Bänder ohne `joinPriority` (unauflösbares Bauteil) nehmen weder als Basis noch
|
||||||
|
* als Cutter teil und bleiben unverändert erhalten.
|
||||||
|
*
|
||||||
|
* Kanten: jedes emittierte Rest-Rechteck erhält in `generateSectionPlan` seine
|
||||||
|
* eigene Schnitt-Umrisslinie (unverändert). An der Schnittkante fällt die
|
||||||
|
* Rest-Rechteck-Kante mit der Kante des stärkeren Bands zusammen — das ist die
|
||||||
|
* gewünschte Material-/Fugenlinie zwischen den Schichten; im Inneren einer
|
||||||
|
* Schicht entstehen durch die überlappungsfreie Zerlegung keine falschen Kanten.
|
||||||
|
*/
|
||||||
|
export function subtractDominantBands(bands: SectionCutPolygon[]): SectionCutPolygon[] {
|
||||||
|
const rects = bands.map(rectOfBand);
|
||||||
|
const out: SectionCutPolygon[] = [];
|
||||||
|
for (let i = 0; i < bands.length; i++) {
|
||||||
|
const band = bands[i];
|
||||||
|
const base = rects[i];
|
||||||
|
const prio = band.joinPriority;
|
||||||
|
// Nicht-rechteckig oder ohne Priorität: unverändert durchreichen.
|
||||||
|
if (base === null || prio === undefined) {
|
||||||
|
out.push(band);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pieces: Rect[] = [base];
|
||||||
|
for (let j = 0; j < bands.length && pieces.length > 0; j++) {
|
||||||
|
if (j === i) continue;
|
||||||
|
const otherPrio = bands[j].joinPriority;
|
||||||
|
const cutter = rects[j];
|
||||||
|
if (cutter === null || otherPrio === undefined || otherPrio <= prio) continue;
|
||||||
|
const next: Rect[] = [];
|
||||||
|
for (const p of pieces) next.push(...subtractRect(p, cutter));
|
||||||
|
pieces = next;
|
||||||
|
}
|
||||||
|
for (const p of pieces) out.push(bandFromRect(band, p));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Erzeugt aus einem Rest-`Rect` ein Cut-Band, das Stil/Referenz von `src` erbt. */
|
||||||
|
function bandFromRect(src: SectionCutPolygon, r: Rect): SectionCutPolygon {
|
||||||
|
return {
|
||||||
|
component: src.component,
|
||||||
|
color: src.color,
|
||||||
|
pts: [
|
||||||
|
[r.uMin, r.vMax],
|
||||||
|
[r.uMax, r.vMax],
|
||||||
|
[r.uMax, r.vMin],
|
||||||
|
[r.uMin, r.vMin],
|
||||||
|
],
|
||||||
|
fill: src.fill,
|
||||||
|
hatch: src.hatch,
|
||||||
|
joinPriority: src.joinPriority,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -349,6 +518,7 @@ function splitSlabLayers(
|
|||||||
],
|
],
|
||||||
fill: hatch.pattern === "solid" ? "#000000" : "#ffffff",
|
fill: hatch.pattern === "solid" ? "#000000" : "#ffffff",
|
||||||
hatch,
|
hatch,
|
||||||
|
joinPriority: comp.joinPriority,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
@@ -420,6 +590,7 @@ export function splitWallLayers(
|
|||||||
],
|
],
|
||||||
fill: hatch.pattern === "solid" ? "#000000" : "#ffffff",
|
fill: hatch.pattern === "solid" ? "#000000" : "#ffffff",
|
||||||
hatch,
|
hatch,
|
||||||
|
joinPriority: comp.joinPriority,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
|
|||||||
Reference in New Issue
Block a user