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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -114,17 +114,134 @@ describe("generatePlan — materialbewusster T-Stoss (Poché-Bänder)", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("lässt die kollineare Durchgangswand unverändert (keine L-Linien, normale Bänder)", () => {
|
||||
it("bricht die Nah-Putz-Schicht der Durchgangswand am Durchstoss auf (Phase 1c)", () => {
|
||||
// Am T-Knoten wird eine der kollinearen Hälften als Durchgangswand gewählt
|
||||
// und bekommt eine Aussparung; ihr Innenputz-Band auf der Durchstoss-Seite
|
||||
// (y∈[0.06,0.075]) wird über die Backstein-Breite [4.94,5.06] gekürzt (der
|
||||
// Cutout reicht bis ans Knoten-Ende x=5). Backstein-Kern + Aussenputz bleiben
|
||||
// voll (kein Überlappungsbereich).
|
||||
const plan = generatePlan(tJunctionProject(), "eg", visible, undefined, "mittel");
|
||||
const wa1Bands = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
||||
p.kind === "polygon" && p.wallId === "WA1" && p.stroke === "none",
|
||||
// Die Durchgangs-Hälfte ist die mit spanCutouts — finde sie über ihre
|
||||
// gekürzten Bänder: sammle die Bänder beider Hälften.
|
||||
const bandsOf = (id: string) =>
|
||||
plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
||||
p.kind === "polygon" && p.wallId === id && p.stroke === "none",
|
||||
);
|
||||
const wa1 = bandsOf("WA1");
|
||||
const wa2 = bandsOf("WA2");
|
||||
// Beide Hälften haben weiterhin drei Bänder (der Cutout reicht bis ans
|
||||
// Wandende, splittet also nicht in zwei, sondern kürzt nur).
|
||||
expect(wa1.length).toBe(3);
|
||||
expect(wa2.length).toBe(3);
|
||||
// Genau EINE Hälfte trägt ein gekürztes Nah-Putz-Band: das innere Putzband
|
||||
// (y-Nahfläche 0.075) endet nicht mehr am Knoten x=5, sondern bei 4.94 bzw.
|
||||
// (für die andere Orientierung) bei 5.06.
|
||||
// Nah-Putz-Band = Offsetzone y∈[0.06,0.075] (offA=0.06 → pts[0/1], offB=0.075
|
||||
// → pts[2/3]); dessen X-Kanten verraten die Kürzung.
|
||||
const isNearPlaster = (b: { pts: { y: number }[] }) => {
|
||||
const ys = b.pts.map((p) => p.y);
|
||||
return Math.abs(Math.min(...ys) - 0.06) < 1e-6 && Math.abs(Math.max(...ys) - 0.075) < 1e-6;
|
||||
};
|
||||
const nearPlasterEnds = [...wa1, ...wa2]
|
||||
.filter(isNearPlaster)
|
||||
.flatMap((b) => b.pts.map((p) => p.x));
|
||||
// Mindestens eine Kante liegt bei 4.94 oder 5.06 (Backstein-Breite ab Knoten).
|
||||
const shortened = nearPlasterEnds.some(
|
||||
(x) => Math.abs(x - 4.94) < 1e-6 || Math.abs(x - 5.06) < 1e-6,
|
||||
);
|
||||
expect(wa1Bands.length).toBe(3);
|
||||
// Kein Cut an ihrem gemeinsamen Knoten-Ende (endCut bleibt null, s. joins.ts) →
|
||||
// alle drei Bänder enden exakt am Achsenpunkt (5,0), keine Gehrung nötig.
|
||||
for (const b of wa1Bands) {
|
||||
expect(b.pts[1].x).toBeCloseTo(5, 6);
|
||||
}
|
||||
expect(shortened).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/** Mittelspannen-Variante: EINE Durchgangswand W, Abzweig WB trifft mittig. */
|
||||
function midSpanProject(): Project {
|
||||
const base = tJunctionProject();
|
||||
const wall = (id: string, start: Wall["start"], end: Wall["end"]): Wall => ({
|
||||
id,
|
||||
type: "wall",
|
||||
floorId: "eg",
|
||||
categoryCode: "20",
|
||||
start,
|
||||
end,
|
||||
wallTypeId: "iw",
|
||||
height: 2.6,
|
||||
});
|
||||
base.walls = [
|
||||
wall("W", { x: 0, y: 0 }, { x: 10, y: 0 }),
|
||||
wall("WB", { x: 4, y: 3 }, { x: 4, y: 0 }),
|
||||
];
|
||||
return base;
|
||||
}
|
||||
|
||||
describe("generatePlan — Durchgangswand-Aussparung am Mittelspann-Stoss (Phase 1c)", () => {
|
||||
it("splittet das Innenputz-Band der Durchgangswand über dem Durchstoss in zwei Teil-Bänder", () => {
|
||||
const plan = generatePlan(midSpanProject(), "eg", visible, undefined, "mittel");
|
||||
const bands = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "polygon" }> =>
|
||||
p.kind === "polygon" && p.wallId === "W" && p.stroke === "none",
|
||||
);
|
||||
// Innenputz-Band (Nahfläche y=0.075) wird über [3.94,4.06] entfernt → ZWEI
|
||||
// Teil-Bänder statt einem. Backstein (2 Bänder? nein 1) + Aussenputz bleiben
|
||||
// je ein Band → insgesamt 4 Bänder (1 Aussenputz + 1 Backstein + 2 Innenputz).
|
||||
const innerPlaster = bands.filter((b) => {
|
||||
const ys = b.pts.map((p) => p.y);
|
||||
return Math.abs(Math.min(...ys) - 0.06) < 1e-6 && Math.abs(Math.max(...ys) - 0.075) < 1e-6;
|
||||
});
|
||||
expect(innerPlaster.length).toBe(2);
|
||||
// Das linke Teil-Band endet bei x=3.94, das rechte beginnt bei x=4.06.
|
||||
const xs = innerPlaster.flatMap((b) => b.pts.map((p) => p.x)).sort((a, b) => a - b);
|
||||
// Erwartete Kanten: 0, 3.94 (linkes Band) und 4.06, 10 (rechtes Band).
|
||||
expect(xs.some((x) => Math.abs(x - 3.94) < 1e-6)).toBe(true);
|
||||
expect(xs.some((x) => Math.abs(x - 4.06) < 1e-6)).toBe(true);
|
||||
// Insgesamt bleiben Backstein-Kern (y-Nahfläche 0.06) und Aussenputz (−0.06)
|
||||
// ganze Bänder → 4 Bänder gesamt.
|
||||
expect(bands.length).toBe(4);
|
||||
});
|
||||
|
||||
it("unterbricht die Schichtfuge im Merge-Bereich (keine Trennlinie an der Verschmelzungsfläche)", () => {
|
||||
const plan = generatePlan(midSpanProject(), "eg", visible, undefined, "mittel");
|
||||
// Fugen der Durchgangswand W: layer-joint-Linien parallel zur x-Achse.
|
||||
const joints = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "line" }> =>
|
||||
p.kind === "line" && p.cls === "layer-joint",
|
||||
);
|
||||
// Die Fuge an der Rückgrat-Nahfläche y=0.06 (Merge-Kontakt) darf NICHT über
|
||||
// den Durchstoss [3.94,4.06] laufen: kein Fugen-Segment enthält einen Punkt
|
||||
// mit y≈0.06 und x im offenen Intervall (3.94, 4.06).
|
||||
for (const l of joints) {
|
||||
if (Math.abs(l.a.y - 0.06) < 1e-6 && Math.abs(l.b.y - 0.06) < 1e-6) {
|
||||
const lo = Math.min(l.a.x, l.b.x);
|
||||
const hi = Math.max(l.a.x, l.b.x);
|
||||
// Das Segment darf den Durchstoss nicht überspannen/betreten.
|
||||
expect(lo >= 4.06 - 1e-9 || hi <= 3.94 + 1e-9).toBe(true);
|
||||
}
|
||||
}
|
||||
// Und es MUSS Fugen-Segmente auf beiden Seiten geben (die Fuge existiert weiter,
|
||||
// nur unterbrochen).
|
||||
const at006 = joints.filter(
|
||||
(l) => Math.abs(l.a.y - 0.06) < 1e-6 && Math.abs(l.b.y - 0.06) < 1e-6,
|
||||
);
|
||||
expect(at006.some((l) => Math.max(l.a.x, l.b.x) <= 3.94 + 1e-6)).toBe(true);
|
||||
expect(at006.some((l) => Math.min(l.a.x, l.b.x) >= 4.06 - 1e-6)).toBe(true);
|
||||
});
|
||||
|
||||
it("unterbricht die Nahflächen-Umrisslinie über dem Durchstoss", () => {
|
||||
const plan = generatePlan(midSpanProject(), "eg", visible, undefined, "mittel");
|
||||
// Die separat gezeichneten Nahkanten-Segmente (cls "wall-outline") liegen bei
|
||||
// y=0.075 und lassen [3.94,4.06] frei.
|
||||
const outline = plan.primitives.filter(
|
||||
(p): p is Extract<typeof p, { kind: "line" }> =>
|
||||
p.kind === "line" && p.cls === "wall-outline",
|
||||
);
|
||||
expect(outline.length).toBe(2); // links + rechts vom Durchstoss
|
||||
for (const l of outline) {
|
||||
expect(Math.abs(l.a.y - 0.075) < 1e-6).toBe(true);
|
||||
expect(Math.abs(l.b.y - 0.075) < 1e-6).toBe(true);
|
||||
}
|
||||
// Ein Segment endet bei 3.94, das andere beginnt bei 4.06.
|
||||
const allX = outline.flatMap((l) => [l.a.x, l.b.x]);
|
||||
expect(allX.some((x) => Math.abs(x - 3.94) < 1e-6)).toBe(true);
|
||||
expect(allX.some((x) => Math.abs(x - 4.06) < 1e-6)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
+152
-34
@@ -1045,6 +1045,38 @@ export function wallGaps(
|
||||
return gaps.sort((a, b) => a.from - b.from);
|
||||
}
|
||||
|
||||
/** Toleranz für die Achsen-Intervall-Arithmetik der Durchgangswand-Aussparungen. */
|
||||
const CUTOUT_EPS = 1e-9;
|
||||
|
||||
/**
|
||||
* Zieht die Loch-Intervalle `holes` vom Segment `[s,e]` ab und liefert die
|
||||
* verbleibenden (gezeichneten) Teil-Intervalle in Achsen-Metern. Löcher werden
|
||||
* auf `[s,e]` geklemmt, sortiert und vereinigt; leeres Ergebnis heisst „ganz
|
||||
* ausgespart". Ist `holes` leer, kommt exakt `[[s,e]]` zurück (unverändertes
|
||||
* Verhalten für Wände ohne Aussparung).
|
||||
*/
|
||||
function subtractIntervals(
|
||||
s: number,
|
||||
e: number,
|
||||
holes: Array<[number, number]>,
|
||||
): Array<[number, number]> {
|
||||
const clipped = holes
|
||||
.map(([a, b]): [number, number] => [
|
||||
Math.max(s, Math.min(a, b)),
|
||||
Math.min(e, Math.max(a, b)),
|
||||
])
|
||||
.filter(([a, b]) => b > a + CUTOUT_EPS)
|
||||
.sort((p, q) => p[0] - q[0]);
|
||||
const parts: Array<[number, number]> = [];
|
||||
let cursor = s;
|
||||
for (const [a, b] of clipped) {
|
||||
if (a > cursor + CUTOUT_EPS) parts.push([cursor, a]);
|
||||
if (b > cursor) cursor = b;
|
||||
}
|
||||
if (cursor < e - CUTOUT_EPS) parts.push([cursor, e]);
|
||||
return parts;
|
||||
}
|
||||
|
||||
function addWallPoche(
|
||||
out: Primitive[],
|
||||
project: Project,
|
||||
@@ -1095,6 +1127,13 @@ function addWallPoche(
|
||||
// (höchste Priorität) — weiß, bzw. schwarz falls dessen Muster "solid" ist.
|
||||
const simpleFill = backbonePocheFill(project, wt);
|
||||
|
||||
// Durchgangswand-Aussparungen (Phase 1c): über diese Achsen-Intervalle × Quer-
|
||||
// Offsetbereiche wird die Nah-Putz-Zone der Durchgangswand echt ausgeschnitten
|
||||
// (der Abzweig-Kern sticht durch). `offA` ist stets die Nahfläche, `offB` die
|
||||
// Rückgrat-Nahfläche (siehe SpanCutout in model/joins.ts). Leer ⇒ heutiges
|
||||
// Verhalten (Wand ohne T-Durchstoss).
|
||||
const spanCutouts = cuts.spanCutouts ?? [];
|
||||
|
||||
// Kanten-Indizes der inneren Gehrungs-Stirnflächen (die Diagonalen am Knoten).
|
||||
// clippedBand liefert [A.start, A.end, B.end, B.start] → Kante 1 (A.end→B.end)
|
||||
// ist die endCut-Stirnfläche, Kante 3 (B.start→A.start) die startCut-Stirnfläche.
|
||||
@@ -1137,6 +1176,15 @@ function addWallPoche(
|
||||
// auf. Die Bänder tragen NUR Füllung + Schraffur (kein eigener Umriss); jede
|
||||
// innere Schichtfuge wird als EIGENSTÄNDIGE, stilisierbare Linie gezeichnet.
|
||||
const bandJoinEdges = joinEdges(startCut, endCut);
|
||||
// Nur die Aussparungen, die dieses Segment [s,e] entlang der Achse berühren.
|
||||
const segCutouts = spanCutouts.filter(
|
||||
(c) =>
|
||||
Math.min(c.from, c.to) < e - CUTOUT_EPS &&
|
||||
Math.max(c.from, c.to) > s + CUTOUT_EPS,
|
||||
);
|
||||
// Punkt entlang der Wandachse bei Achsen-Meter `d` (p1/p2 an den Rändern).
|
||||
const axisPt = (d: number): Vec2 =>
|
||||
d === s ? p1 : d === e ? p2 : along(wall.start, wall.end, d);
|
||||
// Materialbewusster T-Stoss (siehe resolveJoinPriority in model/joins.ts):
|
||||
// liegt eine Pro-Schicht-Cut-Überschreibung vor, ersetzt sie PRO SCHICHT den
|
||||
// einheitlichen startCut/endCut — nur an den echten Wandenden (dieselbe
|
||||
@@ -1161,20 +1209,38 @@ function addWallPoche(
|
||||
const layerSolid = layerHatch.pattern === "solid";
|
||||
const layerStartCut = s <= 1e-6 ? (layerCuts ? layerCuts.start[li] : startCut) : null;
|
||||
const layerEndCut = e >= axisLen - 1e-6 ? (layerCuts ? layerCuts.end[li] : endCut) : null;
|
||||
out.push({
|
||||
kind: "polygon",
|
||||
pts: clippedBand(p1, p2, off, off + layer.thickness, layerStartCut, layerEndCut),
|
||||
fill: layerSolid ? HATCH_INK : HATCH_PAPER,
|
||||
// Band ohne Umriss: keine Schichtfuge über die Bandkante, keine 45°-Naht
|
||||
// am Knoten. Die Fugen kommen als separate `line`-Primitive (siehe unten).
|
||||
stroke: "none",
|
||||
strokeWidthMm: 0,
|
||||
hatch: layerHatch,
|
||||
greyed,
|
||||
// Für die Ecktests/Konsistenz beibehalten (bei stroke:"none" wirkungslos).
|
||||
noStrokeEdges: bandJoinEdges,
|
||||
wallId,
|
||||
});
|
||||
// Durchgangswand-Aussparung: liegt der Quer-Offsetbereich dieses Bandes
|
||||
// [off, off+thickness] im Nah-Putz-Bereich einer Aussparung, wird das Band
|
||||
// entlang der Achse in Teil-Bänder VOR/NACH dem Intervall zerlegt (das
|
||||
// Durchstoss-Stück entfällt). Ohne Überlappung bleibt genau ein Band
|
||||
// [s,e] (unverändertes Verhalten).
|
||||
const bandLo = off;
|
||||
const bandHi = off + layer.thickness;
|
||||
const bandHoles: Array<[number, number]> = [];
|
||||
for (const c of segCutouts) {
|
||||
const cLo = Math.min(c.offA, c.offB);
|
||||
const cHi = Math.max(c.offA, c.offB);
|
||||
if (bandLo < cHi - CUTOUT_EPS && bandHi > cLo + CUTOUT_EPS)
|
||||
bandHoles.push([c.from, c.to]);
|
||||
}
|
||||
for (const [a, b] of subtractIntervals(s, e, bandHoles)) {
|
||||
const sc = a <= s + CUTOUT_EPS ? layerStartCut : null;
|
||||
const ec = b >= e - CUTOUT_EPS ? layerEndCut : null;
|
||||
out.push({
|
||||
kind: "polygon",
|
||||
pts: clippedBand(axisPt(a), axisPt(b), off, off + layer.thickness, sc, ec),
|
||||
fill: layerSolid ? HATCH_INK : HATCH_PAPER,
|
||||
// Band ohne Umriss: keine Schichtfuge über die Bandkante, keine 45°-Naht
|
||||
// am Knoten. Die Fugen kommen als separate `line`-Primitive (siehe unten).
|
||||
stroke: "none",
|
||||
strokeWidthMm: 0,
|
||||
hatch: layerHatch,
|
||||
greyed,
|
||||
// Für die Ecktests/Konsistenz beibehalten (bei stroke:"none" wirkungslos).
|
||||
noStrokeEdges: bandJoinEdges,
|
||||
wallId,
|
||||
});
|
||||
}
|
||||
// L-Anschluss: eine getrimmte Schicht (z. B. Putz), die an eine
|
||||
// verschmelzende Nachbarschicht (durchlaufender Kern) grenzt, bekommt
|
||||
// zusätzlich eine seitliche Cut-Linie entlang der Durchgangsachse — sie
|
||||
@@ -1222,43 +1288,95 @@ function addWallPoche(
|
||||
// Pro-Schicht-Cuts): die Fuge markiert den Materialwechsel INNERHALB
|
||||
// dieser Wand und bleibt daher an der bisherigen Nahfläche.
|
||||
if (li < wt.layers.length - 1) {
|
||||
// clippedBand(p1, p2, off, off, …) liefert [edgeStart, edgeEnd, edgeEnd,
|
||||
// edgeStart] → die zwei distinkten Endpunkte sind Index 0 (Start) und 1
|
||||
// (Ende), beide am Cut abgeschnitten wie die Bandlängskante.
|
||||
const jq = clippedBand(p1, p2, off, off, startCut, endCut);
|
||||
const jls = layer.jointLineStyleId
|
||||
? project.lineStyles.find((l) => l.id === layer.jointLineStyleId)
|
||||
: undefined;
|
||||
out.push({
|
||||
kind: "line",
|
||||
a: jq[0],
|
||||
b: jq[1],
|
||||
cls: "layer-joint",
|
||||
weightMm: jls ? jls.weight * LAYER_DETAIL_FACTOR[detail] : layerLineMm,
|
||||
color: jls ? jls.color : stroke,
|
||||
dash: jls ? jls.dash : null,
|
||||
// Zickzack-Fuge (LineStyle kind==="zigzag") ebenfalls durchreichen.
|
||||
zigzag: jls?.kind === "zigzag" ? jls.zigzag : undefined,
|
||||
// Custom-Motiv-Fuge (LineStyle kind==="custom") ebenfalls durchreichen.
|
||||
motif: jls?.kind === "custom" ? jls.motif : undefined,
|
||||
greyed,
|
||||
});
|
||||
// Durchgangswand-Aussparung: liegt die Fuge im (geschlossenen) Nah-Putz-
|
||||
// Bereich einer Aussparung — inkl. der Rückgrat-Nahfläche (Merge-Kontakt)
|
||||
// —, wird sie über das Durchstoss-Intervall unterbrochen. So bleibt an der
|
||||
// Verschmelzungsfläche keine Trennlinie stehen.
|
||||
const fugeHoles: Array<[number, number]> = [];
|
||||
for (const c of segCutouts) {
|
||||
const cLo = Math.min(c.offA, c.offB);
|
||||
const cHi = Math.max(c.offA, c.offB);
|
||||
if (off >= cLo - CUTOUT_EPS && off <= cHi + CUTOUT_EPS)
|
||||
fugeHoles.push([c.from, c.to]);
|
||||
}
|
||||
for (const [a, b] of subtractIntervals(s, e, fugeHoles)) {
|
||||
const sc = a <= s + CUTOUT_EPS ? startCut : null;
|
||||
const ec = b >= e - CUTOUT_EPS ? endCut : null;
|
||||
// clippedBand(pa, pb, off, off, …) liefert [edgeStart, edgeEnd, edgeEnd,
|
||||
// edgeStart] → die zwei distinkten Endpunkte sind Index 0 (Start) und 1
|
||||
// (Ende), beide am Cut abgeschnitten wie die Bandlängskante.
|
||||
const jq = clippedBand(axisPt(a), axisPt(b), off, off, sc, ec);
|
||||
out.push({
|
||||
kind: "line",
|
||||
a: jq[0],
|
||||
b: jq[1],
|
||||
cls: "layer-joint",
|
||||
weightMm: jls ? jls.weight * LAYER_DETAIL_FACTOR[detail] : layerLineMm,
|
||||
color: jls ? jls.color : stroke,
|
||||
dash: jls ? jls.dash : null,
|
||||
// Zickzack-Fuge (LineStyle kind==="zigzag") ebenfalls durchreichen.
|
||||
zigzag: jls?.kind === "zigzag" ? jls.zigzag : undefined,
|
||||
// Custom-Motiv-Fuge (LineStyle kind==="custom") ebenfalls durchreichen.
|
||||
motif: jls?.kind === "custom" ? jls.motif : undefined,
|
||||
greyed,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Kräftige Wand-Umrisslinie über die volle Dicke (nur Kontur, keine Füllung),
|
||||
// damit die Wand sich klar von den dünnen Schichtfugen abhebt. Die Gehrungs-
|
||||
// Stirnkanten am Knoten entfallen → sauberer Eck-Apex ohne überschießende Barbe.
|
||||
// Bei einer Durchgangswand-Aussparung wird zusätzlich die NAHFLÄCHEN-Kante
|
||||
// (die Längskante auf der Durchstoss-Seite) über [from,to] strokelos gemacht
|
||||
// und als separate Segmente vor/nach dem Durchstoss gezeichnet — sonst liefe
|
||||
// die Umrisslinie über die Durchstoss-Breite.
|
||||
const offLo = refOff - total / 2;
|
||||
const offHi = refOff + total / 2;
|
||||
const holesHi: Array<[number, number]> = [];
|
||||
const holesLo: Array<[number, number]> = [];
|
||||
for (const c of segCutouts) {
|
||||
// `offA` ist stets die Nahfläche (Wandoberfläche); ordne die Aussparung der
|
||||
// passenden Längskante (offHi bzw. offLo) zu.
|
||||
if (Math.abs(c.offA - offHi) < 1e-6) holesHi.push([c.from, c.to]);
|
||||
else if (Math.abs(c.offA - offLo) < 1e-6) holesLo.push([c.from, c.to]);
|
||||
}
|
||||
const outlineNoStroke = [...bandJoinEdges];
|
||||
// clippedBand-Kantenreihenfolge [A.start(offLo), A.end(offLo), B.end(offHi),
|
||||
// B.start(offHi)]: Kante 0 ist die offLo-Längskante, Kante 2 die offHi-Längskante.
|
||||
if (holesHi.length) outlineNoStroke.push(2);
|
||||
if (holesLo.length) outlineNoStroke.push(0);
|
||||
out.push({
|
||||
kind: "polygon",
|
||||
pts: clippedBand(p1, p2, refOff - total / 2, refOff + total / 2, startCut, endCut),
|
||||
pts: clippedBand(p1, p2, offLo, offHi, startCut, endCut),
|
||||
fill: "none",
|
||||
stroke,
|
||||
strokeWidthMm: outlineMm,
|
||||
hatch: NO_HATCH,
|
||||
greyed,
|
||||
noStrokeEdges: bandJoinEdges,
|
||||
noStrokeEdges: outlineNoStroke,
|
||||
wallId,
|
||||
});
|
||||
const drawNearEdge = (edgeOff: number, holes: Array<[number, number]>): void => {
|
||||
for (const [a, b] of subtractIntervals(s, e, holes)) {
|
||||
const sc = a <= s + CUTOUT_EPS ? startCut : null;
|
||||
const ec = b >= e - CUTOUT_EPS ? endCut : null;
|
||||
const jq = clippedBand(axisPt(a), axisPt(b), edgeOff, edgeOff, sc, ec);
|
||||
out.push({
|
||||
kind: "line",
|
||||
a: jq[0],
|
||||
b: jq[1],
|
||||
cls: "wall-outline",
|
||||
weightMm: outlineMm,
|
||||
color: stroke,
|
||||
greyed,
|
||||
});
|
||||
}
|
||||
};
|
||||
if (holesHi.length) drawNearEdge(offHi, holesHi);
|
||||
if (holesLo.length) drawNearEdge(offLo, holesLo);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user