Files
DOSSIER-STANDALONE/src/plan/generatePlan.tjunction.test.ts
T
karim 965b29e1fa T-Stoss: materialgleicher Nah-Putz verschmilzt ohne L-Trennnaht (TS+Rust)
Die L-Seitenlinie am T-Stoss wird nur noch gezeichnet, wenn der getrimmte
Abzweig-Putz materialFREMD zum durchgehenden Nah-Putz der Durchgangswand ist.
Bei materialgleichem Putz (z. B. iw-Innenputz auf iw-Innenputz) fuellt der
Nah-Putz die Putzflanken durchgehend (spanCutout schneidet nur die Kernbreite)
-> durchgehendes Putz-L ohne Naht. Tests entsprechend gezogen.
2026-07-04 23:17:18 +02:00

244 lines
11 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 den materialbewussten T-Stoss im 2D-Plan (generatePlan):
* an einem T-Stoss zweier verputzter Mauerwerkswände (W9-artig: Innenputz/
* Backstein-Kern/Innenputz) darf der Abzweig nicht mehr naiv über die volle
* Dicke an der Durchgangswand-Fläche gekappt werden.
* • Der Backstein-Kern des Abzweigs sticht durch den Nah-Putz der Durchgangs-
* wand und stoppt an deren Kern-Nahfläche (kein Durchlaufen bis zur Achse,
* kein Überlappen) — "durchstechender Kern".
* • Die Innenputz-Schichten des Abzweigs enden an der Nahfläche. Ist der
* Nah-Putz der Durchgangswand materialgleich, füllt er die Flanken durchgehend
* → durchgehendes Putz-L OHNE Trennnaht (keine "layer-joint-l"-Linie).
*/
import { describe, it, expect } from "vitest";
import { generatePlan } from "./generatePlan";
import type { Project, Wall } from "../model/types";
/** W9-artiger Wandtyp: Innenputz (0.015) / Backstein-Kern (0.12) / Innenputz (0.015). */
function tJunctionProject(): Project {
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,
});
return {
id: "t",
name: "T",
lineStyles: [],
hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }],
components: [
{ id: "render-int", name: "Innenputz", color: "#efece6", hatchId: "none", joinPriority: 10 },
{ id: "brick", name: "Backstein", color: "#8c5544", hatchId: "none", joinPriority: 50 },
],
wallTypes: [
{
id: "iw",
name: "Innenwand",
layers: [
{ componentId: "render-int", thickness: 0.015 },
{ componentId: "brick", thickness: 0.12 },
{ componentId: "render-int", thickness: 0.015 },
],
},
],
drawingLevels: [
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 2.6, cutHeight: 1.0, baseElevation: 0 },
],
layers: [{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
walls: [
// Durchgangswand WA1/WA2 in zwei Hälften, Abzweig WB nach oben (5,0)→(5,3).
wall("WA1", { x: 0, y: 0 }, { x: 5, y: 0 }),
wall("WA2", { x: 5, y: 0 }, { x: 10, y: 0 }),
wall("WB", { x: 5, y: 0 }, { x: 5, y: 3 }),
],
doors: [],
openings: [],
ceilings: [],
stairs: [],
rooms: [],
drawings2d: [],
context: [],
};
}
const visible = new Set(["20"]);
describe("generatePlan — materialbewusster T-Stoss (Poché-Bänder)", () => {
it("lässt den Backstein-Kern des Abzweigs ungekappt durchlaufen, während der Innenputz an der Nahfläche endet", () => {
const plan = generatePlan(tJunctionProject(), "eg", visible, undefined, "mittel");
// Die Schicht-Bänder der Wand WB: gefüllte Polygone ohne eigenen Umriss
// (stroke:"none"), in Schicht-Reihenfolge gepusht (0=Putz,1=Backstein,2=Putz).
const bands = plan.primitives.filter(
(p): p is Extract<typeof p, { kind: "polygon" }> =>
p.kind === "polygon" && p.wallId === "WB" && p.stroke === "none",
);
expect(bands.length).toBe(3);
const [render0, brick, render2] = bands;
// clippedBand liefert [edgeStart(offA), edgeEnd(offA), edgeEnd(offB), edgeStart(offB)];
// edgeStart liegt am Wandanfang (5,0) = der T-Stoss-Seite. Für die beiden
// Innenputz-Bänder MUSS die Y-Koordinate dort exakt auf der Nahfläche
// (0.075, halbe Wanddicke 0.15/2) liegen — unverändertes Trim-Verhalten.
expect(render0.pts[0].y).toBeCloseTo(0.075, 6);
expect(render0.pts[3].y).toBeCloseTo(0.075, 6);
expect(render2.pts[0].y).toBeCloseTo(0.075, 6);
expect(render2.pts[3].y).toBeCloseTo(0.075, 6);
// Der Backstein-Kern verschmilzt, sticht aber nur durch den Nah-Putz der
// Durchgangswand bis zur Nahfläche des Durchgangs-Kerns (y = 0.075 − 0.015 =
// 0.06) — nicht bis zur Nahfläche (0.075) und NICHT bis zur Achse (0).
expect(brick.pts[0].y).toBeCloseTo(0.06, 6);
expect(brick.pts[3].y).toBeCloseTo(0.06, 6);
});
it("zeichnet KEINE L-Seitenlinie, wenn der getrimmte Putz materialgleich mit dem durchgehenden Nah-Putz der Durchgangswand ist (durchgehendes Putz-L ohne Naht)", () => {
const plan = generatePlan(tJunctionProject(), "eg", visible, undefined, "mittel");
const lLines = plan.primitives.filter(
(p): p is Extract<typeof p, { kind: "line" }> => p.kind === "line" && p.cls === "layer-joint-l",
);
// Alle drei Wände sind vom selben iw-Typ: der Innenputz des Abzweigs trifft
// an der Flanke auf den materialgleichen Innenputz der Durchgangswand (der
// spanCutout schneidet nur die Kernbreite aus, nicht die Putzflanken). Der
// Putz läuft dort durchgehend ums Eck (L) — eine Trennnaht würde eine falsche
// Materialgrenze suggerieren. Daher KEINE L-Linie.
expect(lLines.length).toBe(0);
});
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");
// 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(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);
});
});