Joins Phase 1c: Durchgangswand am T-Stoss echt aufbrechen (spanCutouts)
Am materialbewussten T-Stoss wurde die Durchgangswand bisher nur uebermalt (Zeichenreihenfolge), nicht geometrisch ausgeschnitten: ihr Nah-Putz-Band, die Schichtfugen- und die Nahflaechenlinie liefen weiter ueber die Durchstoss-Breite des Abzweig-Backsteins. Neu liefert computeJoins erstmals auch Cuts fuer die DURCHGANGSWAND: WallCuts.spanCutouts (Achsen-Intervall = Projektion der Abzweig- Kernbreite, Quer-Offsetzone = Nahflaeche bis Rueckgrat-Nahflaeche = Nah-Putz- Tiefe aus Phase 1b). addWallPoche splittet betroffene Schicht-Baender entlang der Achse in Teil-Baender vor/nach dem Intervall, unterbricht die Schichtfugen im Merge-Bereich und macht die Nahflaechen-Umrisskante ueber dem Durchstoss strokelos (separate Segmente davor/danach) — keine Trennlinie an der Verschmelzungsflaeche, der T-Stoss liest als EIN Join. Rust-Paritaet additiv (span_cutouts), Aggregat-startCut/endCut unveraendert -> parity gruen. vitest 227 (+5), cargo 8 (+1), tsc sauber.
This commit is contained in:
+60
-2
@@ -311,10 +311,68 @@ describe("computeJoins — materialbewusster T-Stoss (layerCuts)", () => {
|
||||
expect(distToLine(lc.startSide[0]!, { x: 5, y: 0.06 })).toBeCloseTo(0, 6);
|
||||
expect(distToLine(lc.startSide[0]!, { x: 0, y: 0.06 })).toBeCloseTo(0, 6);
|
||||
|
||||
// Die kollinearen Hälften der Durchgangswand bleiben unangetastet
|
||||
// (unverändertes Verhalten, keine layerCuts für sie).
|
||||
// Die kollinearen Hälften der Durchgangswand bleiben bei den layerCuts
|
||||
// unangetastet (keine Pro-Schicht-Cuts für sie).
|
||||
expect(joins.get("WA1")!.layerCuts).toBeUndefined();
|
||||
expect(joins.get("WA2")!.layerCuts).toBeUndefined();
|
||||
|
||||
// Die als Durchgang gewählte Wandhälfte bekommt eine Durchgangswand-Aussparung
|
||||
// (Phase 1c): über die Backstein-Kernbreite (0.12) im Nah-Putz-Bereich
|
||||
// (Offset 0.06…0.075). Welche Hälfte den Durchgang bildet, hängt von der
|
||||
// Knoten-Reihenfolge ab — wir prüfen die, die den Cutout trägt.
|
||||
const throughCuts = joins.get("WA1")!.spanCutouts ?? joins.get("WA2")!.spanCutouts;
|
||||
expect(throughCuts).toBeDefined();
|
||||
expect(throughCuts!.length).toBe(1);
|
||||
const sc = throughCuts![0];
|
||||
// Achsen-Intervall: Kernbreite 0.12, zentriert am Knoten (x=5 → [4.94,5.06]).
|
||||
expect(sc.to - sc.from).toBeCloseTo(0.12, 6);
|
||||
// offA = Nahfläche (0.075), offB = Rückgrat-Nahfläche (0.06).
|
||||
expect(sc.offA).toBeCloseTo(0.075, 6);
|
||||
expect(sc.offB).toBeCloseTo(0.06, 6);
|
||||
// Der Abzweig selbst trägt keine Aussparung (die gehört der Durchgangswand).
|
||||
expect(joins.get("WB")!.spanCutouts).toBeUndefined();
|
||||
// L-Ecke bzw. Wände ohne T bleiben cutout-frei (die kollineare Nicht-Durchgangs-
|
||||
// Hälfte hat keinen Cutout).
|
||||
expect(
|
||||
joins.get("WA1")!.spanCutouts === undefined ||
|
||||
joins.get("WA2")!.spanCutouts === undefined,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("Mittelspannen-T-Stoss: die Durchgangswand bekommt einen spanCutout mit korrekter Kernbreite und Nah-Putz-Offsetzone", () => {
|
||||
// Realer W9-Fall: Durchgangswand als EINE Wand W (0,0)→(10,0), Abzweig WB
|
||||
// trifft mit seinem freien Ende (4,0) mittig auf ihre Seite. Beide iw.
|
||||
const proj = layeredWallProject(
|
||||
iwWall("W", { x: 0, y: 0 }, { x: 10, y: 0 }),
|
||||
iwWall("WB", { x: 4, y: 3 }, { x: 4, y: 0 }),
|
||||
iwWall("Filler", { x: 100, y: 100 }, { x: 100, y: 103 }), // unbeteiligt
|
||||
);
|
||||
const joins = computeJoins(proj, proj.walls);
|
||||
|
||||
const wCuts = joins.get("W")!;
|
||||
expect(wCuts.spanCutouts).toBeDefined();
|
||||
expect(wCuts.spanCutouts!.length).toBe(1);
|
||||
const sc = wCuts.spanCutouts![0];
|
||||
// Intervall um x=4, Breite = Kernbreite 0.12 → [3.94, 4.06].
|
||||
expect(sc.from).toBeCloseTo(3.94, 6);
|
||||
expect(sc.to).toBeCloseTo(4.06, 6);
|
||||
expect(sc.offA).toBeCloseTo(0.075, 6);
|
||||
expect(sc.offB).toBeCloseTo(0.06, 6);
|
||||
// Der Abzweig und die unbeteiligte Wand tragen keine Aussparung.
|
||||
expect(joins.get("WB")!.spanCutouts).toBeUndefined();
|
||||
expect(joins.get("Filler")!.spanCutouts).toBeUndefined();
|
||||
});
|
||||
|
||||
it("L-Ecke und einfacher (nicht materialbewusster) Stoss bekommen KEINEN spanCutout", () => {
|
||||
// Reine L-Ecke zweier Wände: keine Durchgangswand, kein Durchstoss.
|
||||
const proj = twoWallProject(
|
||||
wall("L1", { x: 0, y: 0 }, { x: 5, y: 0 }),
|
||||
wall("L2", { x: 5, y: 0 }, { x: 5, y: 4 }),
|
||||
0.4,
|
||||
);
|
||||
const joins = computeJoins(proj, proj.walls);
|
||||
expect(joins.get("L1")!.spanCutouts).toBeUndefined();
|
||||
expect(joins.get("L2")!.spanCutouts).toBeUndefined();
|
||||
});
|
||||
|
||||
it("setzt layerCuts NICHT, wenn kein Bauteil des Abzweigs mit dem Rückgrat der Durchgangswand übereinstimmt", () => {
|
||||
|
||||
@@ -35,6 +35,29 @@ export interface LayerCuts {
|
||||
endSide: (Line | null)[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Aussparung der DURCHGANGSWAND an einem materialbewussten T-Stoss: über das
|
||||
* Achsen-Intervall `[from,to]` (Meter ab `wall.start` entlang der Achse) und den
|
||||
* Quer-Offsetbereich `[offA,offB]` wird die Nah-Putz-Zone der Durchgangswand
|
||||
* echt ausgeschnitten, damit der durchstechende Abzweig-Kern (Phase 1b) die
|
||||
* Durchgangswand nicht nur übermalt, sondern geometrisch aufbricht.
|
||||
*
|
||||
* OFFSET-KONVENTION (identisch zu den Schicht-Bändern in `addWallPoche`):
|
||||
* `offA`/`offB` sind Querabstände von der Wandachse ENTLANG der linken Normalen
|
||||
* `leftNormal(dir)`, inkl. Referenzversatz — also im selben Bereich wie
|
||||
* `refOff − total/2` (außen) … `refOff + total/2` (innen). `offA` ist stets die
|
||||
* NAHFLÄCHE (Wandoberfläche, `refOff + sign·total/2`), `offB` die dem Abzweig
|
||||
* zugewandte Fläche des Rückgrat-KERNS (`refOff + sign·(total/2 − nearPlaster)`)
|
||||
* — die Zone dazwischen ist exakt der Nah-Putz, den der Abzweig-Kern durchstösst.
|
||||
* `from ≤ to` ist die Projektion der Abzweig-Kernbreite auf die Durchgangsachse.
|
||||
*/
|
||||
export interface SpanCutout {
|
||||
from: number;
|
||||
to: number;
|
||||
offA: number;
|
||||
offB: number;
|
||||
}
|
||||
|
||||
/** Schnittlinien einer Wand an ihren beiden Achsenden. */
|
||||
export interface WallCuts {
|
||||
startCut: Line | null;
|
||||
@@ -46,6 +69,13 @@ export interface WallCuts {
|
||||
* verschmilzt (siehe {@link resolveJoinPriority}).
|
||||
*/
|
||||
layerCuts?: LayerCuts;
|
||||
/**
|
||||
* Additive Erweiterung (Durchgangswand-Seite): Aussparungen der Nah-Putz-Zone,
|
||||
* durch die ein Abzweig-Kern sticht. Fehlt/leer ⇒ die Wand wird unverändert
|
||||
* gezeichnet. Wird nur an der DURCHGANGSWAND gesetzt (nie am Abzweig), analog
|
||||
* additiv zu {@link layerCuts}. Siehe {@link SpanCutout}.
|
||||
*/
|
||||
spanCutouts?: SpanCutout[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,6 +427,53 @@ function applyLayerCuts(
|
||||
!merged[i] && ((i > 0 && merged[i - 1]) || (i < n - 1 && merged[i + 1]));
|
||||
sideArr[i] = adjMerged ? sideLine : null;
|
||||
}
|
||||
|
||||
// Durchgangswand-Aussparung (Phase 1c): den Nah-Putz der Durchgangswand über
|
||||
// die Breite des durchstechenden Abzweig-Kerns ECHT ausschneiden. Nur nötig,
|
||||
// wenn davor überhaupt Nah-Putz liegt (nearPlaster > 0) — sonst schliesst der
|
||||
// Kern flächenbündig an und es gibt nichts aufzubrechen.
|
||||
if (nearPlaster > 1e-9) {
|
||||
// Kernbreite des Abzweigs im Quer-Offset (Offset-Konvention der Bänder:
|
||||
// gestapelt entlang leftNormal ab refOff − total/2). Die verschmelzenden
|
||||
// Schichten (`merged`) bilden den Kern; Lo/Hi = ihre Offset-Hülle.
|
||||
const totalBranch = wallTypeThickness(branchWt);
|
||||
const refOffBranch = wallReferenceOffset(branchWall, totalBranch);
|
||||
let bOff = refOffBranch - totalBranch / 2;
|
||||
let coreLo = Infinity;
|
||||
let coreHi = -Infinity;
|
||||
for (let i = 0; i < n; i++) {
|
||||
const th = branchWt.layers[i].thickness;
|
||||
if (merged[i]) {
|
||||
if (bOff < coreLo) coreLo = bOff;
|
||||
if (bOff + th > coreHi) coreHi = bOff + th;
|
||||
}
|
||||
bOff += th;
|
||||
}
|
||||
if (coreHi > coreLo) {
|
||||
// Projektion der Kern-Offsethülle auf die Durchgangsachse: der Achsenpunkt
|
||||
// liegt bei `axisAt` (Meter ab Durchgangs-Start), ein Abzweig-Offset `o`
|
||||
// verschiebt sich entlang der Achse um `o · (nBranch · uT)`.
|
||||
const nBranch = leftNormal(dirOf(branchWall));
|
||||
const proj = dot(nBranch, throughDir);
|
||||
const axisAt = dot(sub(axisPoint, throughWall.start), throughDir);
|
||||
const e1 = axisAt + coreLo * proj;
|
||||
const e2 = axisAt + coreHi * proj;
|
||||
// Quer-Offsetbereich = Nah-Putz-Zone der Durchgangswand: Nahfläche (offA)
|
||||
// bis Rückgrat-Nahfläche (offB, = mergeCut-Offset). Beide in derselben
|
||||
// Offset-Konvention wie die Schicht-Bänder der Durchgangswand.
|
||||
const offA = offT + sign * (tT / 2);
|
||||
const offB = offT + sign * (tT / 2 - nearPlaster);
|
||||
const throughCuts = result.get(throughWall.id);
|
||||
if (throughCuts) {
|
||||
(throughCuts.spanCutouts ??= []).push({
|
||||
from: Math.min(e1, e2),
|
||||
to: Math.max(e1, e2),
|
||||
offA,
|
||||
offB,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user