2D-Schnitt = 3D: Wand-Terminierung (sliceTermination) auch im Vertikalschnitt
Nutzer-Report: 'Wand unten geschnitten' wirkte im 3D, aber nicht im 2D-Schnitt — die Terminierung lief nur im layered-3D-Pfad (terminateOrSubtractSpans), der Schnitt stanzte die Decke nur per Prioritäts-Subtraktion heraus (die Wand tauchte darüber wieder auf). Neu: applyWallTermination (Phase 1b in attachCutStyles) spiegelt die 3D-Regel im (u,v)-Schnittraum: 'below' kappt jedes Wand-Band an der UNTERKANTE der dominanten Decken-Bänder (strikt höhere joinPriority + u-Überlapp), 'above' spiegelbildlich an der Oberkante; 'both'/undefined unverändert (heutige Subtraktion). +6 Tests. 696/696 grün.
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
// Decken-Terminierung im 2D-Schnitt (applyWallTermination) — der Spiegel von
|
||||
// terminateOrSubtractSpans (3D): Wände mit sliceTermination "below" enden an
|
||||
// der Decken-Unterkante, "above" beginnen an der Oberkante; "both"/undefined
|
||||
// bleibt bei der reinen Prioritäts-Subtraktion (unverändert hier).
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { applyWallTermination } from "./toSection";
|
||||
import type { SectionCutPolygon } from "./toSection";
|
||||
import type { Wall } from "../model/types";
|
||||
|
||||
const rect = (
|
||||
uMin: number,
|
||||
uMax: number,
|
||||
vMin: number,
|
||||
vMax: number,
|
||||
): Array<[number, number]> => [
|
||||
[uMin, vMax],
|
||||
[uMax, vMax],
|
||||
[uMax, vMin],
|
||||
[uMin, vMin],
|
||||
];
|
||||
|
||||
const wallBand = (joinPriority: number | undefined, pts = rect(0, 0.3, 0, 2.6)): SectionCutPolygon => ({
|
||||
component: { kind: "wall", index: 0 },
|
||||
color: [0.8, 0.8, 0.8],
|
||||
pts,
|
||||
joinPriority,
|
||||
});
|
||||
|
||||
const slabBand = (joinPriority: number, vMin: number, vMax: number, uMin = -2, uMax = 2): SectionCutPolygon => ({
|
||||
component: { kind: "slab", index: 0 },
|
||||
color: [0.9, 0.9, 0.9],
|
||||
pts: rect(uMin, uMax, vMin, vMax),
|
||||
joinPriority,
|
||||
});
|
||||
|
||||
const wall = (sliceTermination?: Wall["sliceTermination"]): Wall =>
|
||||
({
|
||||
id: "W1",
|
||||
start: { x: 0, y: 0 },
|
||||
end: { x: 5, y: 0 },
|
||||
height: 2.6,
|
||||
wallTypeId: "wt",
|
||||
floorId: "eg",
|
||||
categoryCode: "21",
|
||||
sliceTermination,
|
||||
}) as Wall;
|
||||
|
||||
const bbox = (cp: SectionCutPolygon) => {
|
||||
const vs = cp.pts.map((p) => p[1]);
|
||||
return { vMin: Math.min(...vs), vMax: Math.max(...vs) };
|
||||
};
|
||||
|
||||
describe("applyWallTermination — 2D-Schnitt = 3D-Terminierung", () => {
|
||||
it("'below': Wand-Band endet an der Unterkante der dominanten Decke", () => {
|
||||
const bands = [wallBand(50), slabBand(100, 2.2, 2.5)];
|
||||
const out = applyWallTermination(bands, [wall("below")]);
|
||||
const wb = out.find((b) => b.component.kind === "wall")!;
|
||||
expect(bbox(wb).vMax).toBeCloseTo(2.2, 9);
|
||||
expect(bbox(wb).vMin).toBeCloseTo(0, 9);
|
||||
});
|
||||
|
||||
it("'above': Wand-Band beginnt an der Oberkante der dominanten Decke", () => {
|
||||
const bands = [wallBand(50), slabBand(100, 2.2, 2.5)];
|
||||
const out = applyWallTermination(bands, [wall("above")]);
|
||||
const wb = out.find((b) => b.component.kind === "wall")!;
|
||||
expect(bbox(wb).vMin).toBeCloseTo(2.5, 9);
|
||||
expect(bbox(wb).vMax).toBeCloseTo(2.6, 9);
|
||||
});
|
||||
|
||||
it("'both'/undefined: Band bleibt unverändert (Prioritäts-Subtraktion regelt)", () => {
|
||||
const bands = [wallBand(50), slabBand(100, 2.2, 2.5)];
|
||||
for (const term of ["both", undefined] as const) {
|
||||
const out = applyWallTermination(bands, [wall(term)]);
|
||||
const wb = out.find((b) => b.component.kind === "wall")!;
|
||||
expect(wb.pts).toEqual(rect(0, 0.3, 0, 2.6));
|
||||
}
|
||||
});
|
||||
|
||||
it("nur STRIKT dominante Decken terminieren (schwächere/gleiche nicht)", () => {
|
||||
for (const prio of [50, 10]) {
|
||||
const bands = [wallBand(50), slabBand(prio, 2.2, 2.5)];
|
||||
const out = applyWallTermination(bands, [wall("below")]);
|
||||
const wb = out.find((b) => b.component.kind === "wall")!;
|
||||
expect(bbox(wb).vMax).toBeCloseTo(2.6, 9);
|
||||
}
|
||||
});
|
||||
|
||||
it("ohne u-Überlapp keine Terminierung; Band unter der Decken-UK verschwindet nicht", () => {
|
||||
// Decke liegt in u komplett rechts neben der Wand.
|
||||
const far = [wallBand(50), slabBand(100, 2.2, 2.5, 5, 8)];
|
||||
const outFar = applyWallTermination(far, [wall("below")]);
|
||||
expect(bbox(outFar.find((b) => b.component.kind === "wall")!).vMax).toBeCloseTo(2.6, 9);
|
||||
|
||||
// Decke UNTER dem Wandfuss ⇒ 'below'-Clip entfernt das ganze Band.
|
||||
const under = [wallBand(50), slabBand(100, -0.5, -0.2)];
|
||||
const outUnder = applyWallTermination(under, [wall("below")]);
|
||||
expect(outUnder.some((b) => b.component.kind === "wall")).toBe(false);
|
||||
});
|
||||
|
||||
it("Bänder ohne joinPriority (unauflösbares Bauteil) bleiben unangetastet", () => {
|
||||
const bands = [wallBand(undefined), slabBand(100, 2.2, 2.5)];
|
||||
const out = applyWallTermination(bands, [wall("below")]);
|
||||
expect(out.find((b) => b.component.kind === "wall")!.pts).toEqual(rect(0, 0.3, 0, 2.6));
|
||||
});
|
||||
});
|
||||
+105
-1
@@ -344,8 +344,112 @@ function attachCutStyles(output: SectionOutput, project: Project, plane: Section
|
||||
bands.push(cp);
|
||||
}
|
||||
|
||||
// Phase 1b — Decken-Terminierung: Wände mit `sliceTermination` "below"/"above"
|
||||
// ENDEN im Schnitt an der Decke (2D = 3D, s. terminateOrSubtractSpans in
|
||||
// toWalls3d), statt dass die Decke nur per Priorität herausgestanzt wird und
|
||||
// die Wand darüber wieder auftaucht.
|
||||
const terminated = applyWallTermination(bands, walls);
|
||||
|
||||
// Phase 2 — Boolean-Dominanz: die stärkere Schicht schneidet die schwächere.
|
||||
output.cutPolygons = subtractDominantBands(bands);
|
||||
output.cutPolygons = subtractDominantBands(terminated);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decken-Terminierung der Wand-Bänder im (u,v)-Schnittraum — der 2D-Spiegel von
|
||||
* `terminateOrSubtractSpans` (toWalls3d, 3D-Pfad): trägt die Wand eines Bands
|
||||
* `sliceTermination: "below"`, wird das Band an der UNTERKANTE der dominanten
|
||||
* Decken-Bänder gekappt (kein über der Decke wieder auftauchender Rest);
|
||||
* `"above"` spiegelbildlich an der Oberkante. Dominant = Decken-Band mit STRIKT
|
||||
* höherer `joinPriority` UND u-Überlapp (u-Überlapp ↔ Plan-Überdeckung am Ort
|
||||
* der Schnittebene). `"both"`/undefined ⇒ unverändert (die reine Prioritäts-
|
||||
* Subtraktion in Phase 2 bleibt das heutige Verhalten). Bänder sind Rechtecke
|
||||
* (splitWallLayers/Rust-Extraktor) — gekappt wird über die BBox.
|
||||
*/
|
||||
export function applyWallTermination(
|
||||
bands: SectionCutPolygon[],
|
||||
walls: Wall[],
|
||||
): SectionCutPolygon[] {
|
||||
const EPS_T = 1e-9;
|
||||
const bbox = (
|
||||
cp: SectionCutPolygon,
|
||||
): { uMin: number; uMax: number; vMin: number; vMax: number } => {
|
||||
let uMin = Infinity;
|
||||
let uMax = -Infinity;
|
||||
let vMin = Infinity;
|
||||
let vMax = -Infinity;
|
||||
for (const [u, v] of cp.pts) {
|
||||
uMin = Math.min(uMin, u);
|
||||
uMax = Math.max(uMax, u);
|
||||
vMin = Math.min(vMin, v);
|
||||
vMax = Math.max(vMax, v);
|
||||
}
|
||||
return { uMin, uMax, vMin, vMax };
|
||||
};
|
||||
const slabBands = bands
|
||||
.filter((b) => b.component.kind === "slab" && b.joinPriority !== undefined)
|
||||
.map((b) => ({ box: bbox(b), joinPriority: b.joinPriority as number }));
|
||||
if (slabBands.length === 0) return bands;
|
||||
|
||||
const out: SectionCutPolygon[] = [];
|
||||
for (const b of bands) {
|
||||
if (b.component.kind !== "wall") {
|
||||
out.push(b);
|
||||
continue;
|
||||
}
|
||||
const owner = walls[b.component.index];
|
||||
const term = owner?.sliceTermination;
|
||||
if ((term !== "below" && term !== "above") || b.joinPriority === undefined) {
|
||||
out.push(b);
|
||||
continue;
|
||||
}
|
||||
const wb = bbox(b);
|
||||
// Dominante Decken-Cutter mit u-Überlapp — dieselbe Auswahl wie die
|
||||
// bandZCuts im 3D (nur STRIKT höhere Priorität schneidet/terminiert).
|
||||
const cutters = slabBands.filter(
|
||||
(s) =>
|
||||
s.joinPriority > (b.joinPriority as number) &&
|
||||
s.box.uMax > wb.uMin + EPS_T &&
|
||||
s.box.uMin < wb.uMax - EPS_T,
|
||||
);
|
||||
if (cutters.length === 0) {
|
||||
out.push(b);
|
||||
continue;
|
||||
}
|
||||
if (term === "below") {
|
||||
// Wand endet an der Decken-UNTERKANTE (kleinstes vMin der Cutter).
|
||||
let clipTop = Infinity;
|
||||
for (const c of cutters) clipTop = Math.min(clipTop, c.box.vMin);
|
||||
const top = Math.min(clipTop, wb.vMax);
|
||||
if (top > wb.vMin + EPS_T) {
|
||||
out.push({
|
||||
...b,
|
||||
pts: [
|
||||
[wb.uMin, top],
|
||||
[wb.uMax, top],
|
||||
[wb.uMax, wb.vMin],
|
||||
[wb.uMin, wb.vMin],
|
||||
],
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// "above": Wand beginnt an der Decken-OBERKANTE (grösstes vMax der Cutter).
|
||||
let clipBottom = -Infinity;
|
||||
for (const c of cutters) clipBottom = Math.max(clipBottom, c.box.vMax);
|
||||
const bottom = Math.max(clipBottom, wb.vMin);
|
||||
if (bottom < wb.vMax - EPS_T) {
|
||||
out.push({
|
||||
...b,
|
||||
pts: [
|
||||
[wb.uMin, wb.vMax],
|
||||
[wb.uMax, wb.vMax],
|
||||
[wb.uMax, bottom],
|
||||
[wb.uMin, bottom],
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user