diff --git a/src-tauri/geometry/src/lib.rs b/src-tauri/geometry/src/lib.rs index 063a414..2ab63b3 100644 --- a/src-tauri/geometry/src/lib.rs +++ b/src-tauri/geometry/src/lib.rs @@ -117,8 +117,12 @@ fn resolve_join_priority( /// von `applyLayerCuts` aus `model/joins.ts`. Vergleicht jede Schicht der /// Abzweig-Wand mit dem Rueckgrat (hoechste `joinPriority`) der Durchgangswand; /// verschmilzt sie (gleiches Rueckgrat-Material, oder die Abzweig-Schicht ist -/// sogar prioritaerer), bekommt sie keinen Cut. Getrimmte Schichten neben einer -/// verschmelzenden Nachbarschicht bekommen zusaetzlich die seitliche L-Linie. +/// sogar prioritaerer), bekommt sie einen Cut an der NAHFLAECHE DES RUECKGRAT- +/// KERNS der Durchgangswand -- der verschmelzende Kern sticht nur durch den +/// Nah-Putz und stoppt am Durchgangs-Kern, statt bis zur Achse durchzulaufen. +/// Getrimmte Schichten neben einer verschmelzenden Nachbarschicht bekommen +/// zusaetzlich die seitliche L-Linie an eben dieser Rueckgrat-Nahflaeche. +/// `sign`/`off_t` = Nahseite bzw. Referenzversatz der Durchgangswand. #[allow(clippy::too_many_arguments)] fn apply_layer_cuts( result: &mut [WallCuts], @@ -129,16 +133,21 @@ fn apply_layer_cuts( face_cut: Line, axis_point: Vec2, through_dir: Vec2, + sign: f64, + off_t: f64, ) { if branch_layers.is_empty() || through_layers.is_empty() { return; } - // Rueckgrat der Durchgangswand: die Schicht mit der hoechsten joinPriority. + // Rueckgrat der Durchgangswand: die Schicht mit der hoechsten joinPriority + // (Index mitgefuehrt fuer die Nah-Putz-Dicke davor). let mut backbone = &through_layers[0]; - for l in through_layers { + let mut backbone_idx = 0usize; + for (i, l) in through_layers.iter().enumerate() { if l.join_priority > backbone.join_priority { backbone = l; + backbone_idx = i; } } @@ -171,13 +180,33 @@ fn apply_layer_cuts( return; // Schicht-Anzahl passt nicht zusammen (sollte nicht vorkommen) } - let side_line = Line { point: axis_point, dir: through_dir }; + // Nahflaechen-Cut des verschmelzenden Kerns: von der Durchgangswand-Nah- + // flaeche (off_t + sign*t_t/2) um die Nah-Putz-Dicke nach innen versetzt = + // die dem Abzweig zugewandte Flaeche des Rueckgrat-Kerns. Nah-Putz = Summe + // der Durchgangswand-Schichten zwischen Nahseite (sign) und Rueckgrat: auf + // der +nT-Nahseite (sign>0) die Schichten hinter dem Rueckgrat + // (Index > backbone_idx), auf der -nT-Nahseite die davor. + let t_t: f64 = through_layers.iter().map(|l| l.thickness).sum(); + let mut near_plaster = 0.0; + for (i, l) in through_layers.iter().enumerate() { + let near = if sign > 0.0 { i > backbone_idx } else { i < backbone_idx }; + if near { + near_plaster += l.thickness; + } + } + let n_t = left_normal(through_dir); + let merge_point = add(axis_point, scale(n_t, off_t + sign * (t_t / 2.0 - near_plaster))); + let merge_cut = Line { point: merge_point, dir: through_dir }; + + // Getrimmte Nachbarschichten schliessen mit ihrer L-Seitenlinie an dieser + // Rueckgrat-Nahflaeche ab (nicht an der Wandachse). + let side_line = merge_cut; let (face_arr, side_arr) = match branch_end { WallEndKind::Start => (&mut lc.start, &mut lc.start_side), WallEndKind::End => (&mut lc.end, &mut lc.end_side), }; for i in 0..n { - face_arr[i] = if merged[i] { None } else { Some(face_cut) }; + face_arr[i] = if merged[i] { Some(merge_cut) } else { Some(face_cut) }; let adj_merged = !merged[i] && ((i > 0 && merged[i - 1]) || (i + 1 < n && merged[i + 1])); side_arr[i] = if adj_merged { Some(side_line) } else { None }; @@ -454,6 +483,8 @@ fn apply_t_junction( face_cut, j, u_t, + sign, + off_t, ); } @@ -542,6 +573,8 @@ fn apply_mid_span_tee( face_cut, axis_point, u_w, + sign, + off_w, ); } @@ -825,9 +858,10 @@ mod tests { #[test] fn t_junction_layer_cuts_merge_and_trim() { // Wie t_junction_branch_gets_face_cut, aber alle drei Waende vom - // W9-artigen Wandtyp: der Backstein-Kern (Index 1) verschmilzt (kein - // Cut), die beiden Innenputz-Schichten (Index 0/2) werden getrimmt und - // bekommen zusaetzlich die seitliche L-Linie. + // W9-artigen Wandtyp: der Backstein-Kern (Index 1) sticht durch den + // Nah-Putz und stoppt an der Kern-Nahflaeche (y=0.06), die beiden + // Innenputz-Schichten (Index 0/2) werden getrimmt und bekommen + // zusaetzlich die seitliche L-Linie an derselben Flaeche. let input = JoinInput { walls: vec![ w_iw("A", 0.0, 0.0, 5.0, 0.0), @@ -846,8 +880,15 @@ mod tests { let lc = cb.layer_cuts.as_ref().expect("layerCuts gesetzt (Backstein verschmilzt)"); assert_eq!(lc.start.len(), 3); - // Schicht 1 (Backstein-Kern): kein Cut, keine Seitenlinie. - assert!(lc.start[1].is_none()); + let dist = |cut: Line, q: Vec2| cross(cut.dir, sub(q, cut.point)).abs() / len(cut.dir); + + // Schicht 1 (Backstein-Kern): Cut an der Rueckgrat-Nahflaeche y=0.06 + // (Halbdicke 0.075 minus Nah-Putz 0.015), NICHT null/Achse; keine + // Seitenlinie (verschmilzt). + let l1 = lc.start[1].expect("Schicht 1 (Kern) hat Merge-Cut"); + assert!(dist(l1, Vec2 { x: 5.0, y: 0.06 }) < 1e-9); + assert!(dist(l1, Vec2 { x: 0.0, y: 0.06 }) < 1e-9); + assert!(dist(l1, Vec2 { x: 5.0, y: 0.0 }) > 1e-6); // nicht bis zur Achse assert!(lc.start_side[1].is_none()); // Schichten 0/2 (Innenputz): derselbe Nahflaechen-Cut wie das Aggregat... @@ -855,14 +896,13 @@ mod tests { let l2 = lc.start[2].expect("Schicht 2 getrimmt"); assert!((l0.point.y - 0.075).abs() < 1e-9); assert!((l2.point.y - 0.075).abs() < 1e-9); - // ... UND die seitliche L-Linie entlang der Durchgangsachse (durch den - // Knoten (5,0), parallel zur x-Achse). + // ... UND die seitliche L-Linie an der Rueckgrat-Nahflaeche (y=0.06, + // parallel zur x-Achse). let s0 = lc.start_side[0].expect("Schicht 0 hat L-Seitenlinie"); let s2 = lc.start_side[2].expect("Schicht 2 hat L-Seitenlinie"); - let dist = |cut: Line, q: Vec2| cross(cut.dir, sub(q, cut.point)).abs() / len(cut.dir); - assert!(dist(s0, Vec2 { x: 5.0, y: 0.0 }) < 1e-9); - assert!(dist(s0, Vec2 { x: 0.0, y: 0.0 }) < 1e-9); - assert!(dist(s2, Vec2 { x: 5.0, y: 0.0 }) < 1e-9); + assert!(dist(s0, Vec2 { x: 5.0, y: 0.06 }) < 1e-9); + assert!(dist(s0, Vec2 { x: 0.0, y: 0.06 }) < 1e-9); + assert!(dist(s2, Vec2 { x: 5.0, y: 0.06 }) < 1e-9); // Die kollineare Durchgangswand bleibt unangetastet (kein layerCuts). let ca = find(&out, "A"); diff --git a/src/model/joins.test.ts b/src/model/joins.test.ts index 90242a4..6fa60f3 100644 --- a/src/model/joins.test.ts +++ b/src/model/joins.test.ts @@ -267,7 +267,7 @@ function iwWall(id: string, start: Vec2, end: Vec2): Wall { } describe("computeJoins — materialbewusster T-Stoss (layerCuts)", () => { - it("W9-artiger T-Knoten: Backstein-Kern des Abzweigs verschmilzt (kein Cut), Putz wird getrimmt + L-Seitenlinie", () => { + it("W9-artiger T-Knoten: Backstein-Kern des Abzweigs sticht bis zur Rückgrat-Nahfläche durch, Putz wird getrimmt + L-Seitenlinie an derselben Fläche", () => { // Durchgangswand WA1 (0,0)→(5,0) / WA2 (5,0)→(10,0), Abzweig WB (5,0)→(5,3), // alle vom selben Wandtyp "iw" (Innenputz/Backstein/Innenputz, T=0.15). const proj = layeredWallProject( @@ -287,8 +287,14 @@ describe("computeJoins — materialbewusster T-Stoss (layerCuts)", () => { const lc = cuts.layerCuts!; expect(lc.start.length).toBe(3); - // Schicht 1 (Backstein-Kern, Index 1) verschmilzt: KEIN Cut. - expect(lc.start[1]).toBeNull(); + // Schicht 1 (Backstein-Kern, Index 1) verschmilzt: Cut an der Nahfläche des + // Durchgangs-Rückgrats — NICHT null (Achse) und NICHT die Aggregat-Nahfläche + // (0.075). Durchgangswand-Halbdicke 0.075 minus Nah-Putz 0.015 → y = 0.06. + expect(lc.start[1]).not.toBeNull(); + expect(distToLine(lc.start[1]!, { x: 5, y: 0.06 })).toBeCloseTo(0, 6); + expect(distToLine(lc.start[1]!, { x: 0, y: 0.06 })).toBeCloseTo(0, 6); + // Der Kern läuft NICHT bis zur Achse (y=0) durch. + expect(distToLine(lc.start[1]!, { x: 5, y: 0 })).not.toBeCloseTo(0, 6); expect(lc.startSide[1]).toBeNull(); // Schichten 0 und 2 (Innenputz) werden getrimmt: derselbe Nahflächen-Cut @@ -297,12 +303,13 @@ describe("computeJoins — materialbewusster T-Stoss (layerCuts)", () => { expect(lc.start[2]).not.toBeNull(); expect(distToLine(lc.start[0]!, { x: 5, y: 0.075 })).toBeCloseTo(0, 6); expect(distToLine(lc.start[2]!, { x: 5, y: 0.075 })).toBeCloseTo(0, 6); - // ... UND bekommen die zusätzliche L-Seitenlinie entlang der - // Durchgangsachse (durch den Knoten (5,0), Richtung parallel zur x-Achse). + // ... UND bekommen die zusätzliche L-Seitenlinie an der Rückgrat-Nahfläche + // (y = 0.06, parallel zur Durchgangsachse) — dort endet der durchstechende + // Kern. expect(lc.startSide[0]).not.toBeNull(); expect(lc.startSide[2]).not.toBeNull(); - expect(distToLine(lc.startSide[0]!, { x: 5, y: 0 })).toBeCloseTo(0, 6); - expect(distToLine(lc.startSide[0]!, { x: 0, y: 0 })).toBeCloseTo(0, 6); + 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). diff --git a/src/model/joins.ts b/src/model/joins.ts index db39551..d69475a 100644 --- a/src/model/joins.ts +++ b/src/model/joins.ts @@ -285,7 +285,7 @@ function applyTJunction( const faceCut: Line = { point: facePoint, dir: uT }; setCut(result, branchEnd, faceCut); - applyLayerCuts(project, result, branchEnd, branchWall, throughWall, faceCut, j, uT); + applyLayerCuts(project, result, branchEnd, branchWall, throughWall, faceCut, j, uT, sign); } /** @@ -293,16 +293,22 @@ function applyTJunction( * {@link resolveJoinPriority}): vergleicht jede Schicht der ABZWEIG-Wand mit * dem Rückgrat (höchste `joinPriority`) der DURCHGANGSWAND. Verschmilzt eine * Schicht (gleiche Komponente + Priorität wie das Rückgrat, oder die - * Abzweig-Schicht ist sogar prioritärer), bekommt sie KEINEN Cut — das Band - * läuft bis zum Wandachsenpunkt durch (die Durchgangswand selbst ist an einem - * T-Stoss unzerschnitten und deckt den Rest ab, s. `applyTJunction`-Doc). + * Abzweig-Schicht ist sogar prioritärer), bekommt sie einen Cut an der + * NAHFLÄCHE DES RÜCKGRAT-KERNS der Durchgangswand: der verschmelzende Abzweig- + * Kern (z. B. Backstein) sticht nur durch den Nah-Putz der Durchgangswand und + * stoppt am Durchgangs-Kern — er läuft NICHT bis zur Achse durch und überlappt + * die Durchgangswand nicht (dort verschmelzen die beiden Kerne nahtlos). * Getrimmte Schichten, die direkt an eine verschmelzende Nachbarschicht - * grenzen, bekommen zusätzlich eine seitliche Cut-Linie entlang der - * Durchgangsachse (L-Anschluss, z. B. Putz neben durchlaufendem Kern). + * grenzen, bekommen zusätzlich eine seitliche Cut-Linie an eben dieser + * Rückgrat-Nahfläche (L-Anschluss, z. B. Putz neben durchstechendem Kern). * * Setzt `WallCuts.layerCuts` NUR, wenn mindestens eine Schicht verschmilzt — * sonst bleibt das Feld `undefined` (additiv, bit-identisches Default- * Verhalten für den ganz überwiegenden Fall ohne Materialübereinstimmung). + * + * `sign` ist die Nahseite der Durchgangswand (der Abzweig-zugewandte `nT`- + * Vorzeichen, aus {@link applyTJunction}/{@link applyMidSpanTee}); er bestimmt, + * welche Durchgangswand-Schichten als Nah-Putz vor dem Rückgrat liegen. */ function applyLayerCuts( project: Project, @@ -313,16 +319,22 @@ function applyLayerCuts( faceCut: Line, axisPoint: Vec2, throughDir: Vec2, + sign: number, ): void { const branchWt = getWallType(project, branchWall); const throughWt = getWallType(project, throughWall); if (branchWt.layers.length === 0 || throughWt.layers.length === 0) return; - // Rückgrat der Durchgangswand: die Schicht mit der höchsten joinPriority. + // Rückgrat der Durchgangswand: die Schicht mit der höchsten joinPriority + // (Index mitgeführt, um die Nah-Putz-Dicke davor zu bestimmen). let backbone = getComponent(project, throughWt.layers[0].componentId); - for (const l of throughWt.layers) { - const c = getComponent(project, l.componentId); - if (c.joinPriority > backbone.joinPriority) backbone = c; + let backboneIdx = 0; + for (let i = 0; i < throughWt.layers.length; i++) { + const c = getComponent(project, throughWt.layers[i].componentId); + if (c.joinPriority > backbone.joinPriority) { + backbone = c; + backboneIdx = i; + } } const n = branchWt.layers.length; @@ -352,13 +364,35 @@ function applyLayerCuts( const faceArr = branchEnd.end === "start" ? lc.start : lc.end; const sideArr = branchEnd.end === "start" ? lc.startSide : lc.endSide; - // Seitliche Cut-Linie entlang der Durchgangsachse, auf Höhe des Wand- - // Achsenpunkts (= die Tiefe, bis zu der eine verschmelzende Nachbarschicht - // ungekappt durchläuft). - const sideLine: Line = { point: axisPoint, dir: throughDir }; + + // Nahflächen-Cut des verschmelzenden Kerns: von der Durchgangswand-Nahfläche + // (`offT + sign·tT/2`) um die Nah-Putz-Dicke nach innen versetzt — also genau + // die dem Abzweig zugewandte Fläche des Rückgrat-Kerns. Nah-Putz = Summe der + // Durchgangswand-Schichten, die zwischen der Nahseite (sign) und dem Rückgrat + // liegen. Die Schichten stapeln entlang +nT (Index 0 = −tT/2); auf der + // +nT-Nahseite (sign>0) sind das die Schichten HINTER dem Rückgrat + // (Index > backboneIdx), auf der −nT-Nahseite die davor (Index < backboneIdx). + const tT = wallTypeThickness(throughWt); + const offT = wallReferenceOffset(throughWall, tT); + let nearPlaster = 0; + for (let i = 0; i < throughWt.layers.length; i++) { + if (sign > 0 ? i > backboneIdx : i < backboneIdx) + nearPlaster += throughWt.layers[i].thickness; + } + const nT = leftNormal(throughDir); + const mergePoint = add( + axisPoint, + scale(nT, offT + sign * (tT / 2 - nearPlaster)), + ); + const mergeCut: Line = { point: mergePoint, dir: throughDir }; + + // Getrimmte Nachbarschichten schließen mit ihrer L-Seitenlinie an eben dieser + // Rückgrat-Nahfläche (nicht an der Wandachse) — dort endet der Kern und die + // Nachbarschicht kappt ihre Breite. + const sideLine: Line = mergeCut; for (let i = 0; i < n; i++) { - faceArr[i] = merged[i] ? null : faceCut; + faceArr[i] = merged[i] ? mergeCut : faceCut; const adjMerged = !merged[i] && ((i > 0 && merged[i - 1]) || (i < n - 1 && merged[i + 1])); sideArr[i] = adjMerged ? sideLine : null; @@ -420,5 +454,5 @@ function applyMidSpanTee( // Endes (nicht zwingend w.start) — Anker für die materialbewusste // Pro-Schicht-Erweiterung (analog zum T-Knoten-Fall). const axisPoint = add(w.start, scale(uW, best.distAlong)); - applyLayerCuts(project, result, freeEnd, freeWall, w, faceCut, axisPoint, uW); + applyLayerCuts(project, result, freeEnd, freeWall, w, faceCut, axisPoint, uW, sign); } diff --git a/src/plan/generatePlan.tjunction.test.ts b/src/plan/generatePlan.tjunction.test.ts index 5f2a13b..abb614f 100644 --- a/src/plan/generatePlan.tjunction.test.ts +++ b/src/plan/generatePlan.tjunction.test.ts @@ -3,8 +3,9 @@ * 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 läuft UNGEKAPPT bis zur Durchgangsachse - * durch (kein Cut an der Nahfläche) — "durchgehender Kern". + * • 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 weiterhin an der Nahfläche * UND bekommen eine zusätzliche seitliche L-Linie (cls "layer-joint-l"). */ @@ -88,10 +89,11 @@ describe("generatePlan — materialbewusster T-Stoss (Poché-Bänder)", () => { expect(render2.pts[0].y).toBeCloseTo(0.075, 6); expect(render2.pts[3].y).toBeCloseTo(0.075, 6); - // Der Backstein-Kern verschmilzt: KEIN Cut an der Nahfläche → die - // Randpunkte liegen auf der Durchgangsachse (y=0), nicht auf y=0.075. - expect(brick.pts[0].y).toBeCloseTo(0, 6); - expect(brick.pts[3].y).toBeCloseTo(0, 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 die L-Seitenlinie nur für die getrimmten Putzschichten, nicht für den durchlaufenden Kern", () => { @@ -103,11 +105,11 @@ describe("generatePlan — materialbewusster T-Stoss (Poché-Bänder)", () => { // bekommt keine (er verschmilzt, kein Sprung im Material). expect(lLines.length).toBe(2); - // Jede L-Linie verläuft entlang der Durchgangsachse (y=0, parallel zur + // Jede L-Linie verläuft an der Rückgrat-Nahfläche (y=0.06, parallel zur // x-Achse) und spannt genau die Breite EINER Putzschicht (0.015 m) auf. for (const l of lLines) { - expect(l.a.y).toBeCloseTo(0, 6); - expect(l.b.y).toBeCloseTo(0, 6); + expect(l.a.y).toBeCloseTo(0.06, 6); + expect(l.b.y).toBeCloseTo(0.06, 6); expect(Math.abs(l.a.x - l.b.x)).toBeCloseTo(0.015, 6); } });