From 1ba6f7bcf4d58edd12ffdda77daf6b9b1ec98523 Mon Sep 17 00:00:00 2001 From: Karim Date: Fri, 31 Jul 2026 17:00:09 +0200 Subject: [PATCH] =?UTF-8?q?Dach/Wand:=20Giebel-Endf=C3=BCllung=20(d=C3=BCn?= =?UTF-8?q?ne=20Einzelfl=C3=A4che)=20nicht=20mehr=20emittiert=20=E2=80=94?= =?UTF-8?q?=20lag=20deckungsgleich=20vor=20der=20ohnehin=20bis=20zur=20Dac?= =?UTF-8?q?h-Unterkante=20hochgeklemmten=20Giebelwand=20und=20wirkte=20als?= =?UTF-8?q?=20sichtbare=20Doppel-Schicht.=20Zus=C3=A4tzlich=20werden=20auf?= =?UTF-8?q?einanderfolgende=20Wandst=C3=BCcke=20gleicher=20geklemmter=20Ob?= =?UTF-8?q?erkante=20(z.=20B.=20am=20First)=20wieder=20zu=20einem=20Body?= =?UTF-8?q?=20verschmolzen=20statt=20in=20viele=200.15-m-Fragmente=20zu=20?= =?UTF-8?q?zerfallen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/plan/toWalls3d.test.ts | 21 +++++++++++-------- src/plan/toWalls3d.ts | 42 +++++++++++++++++++++++--------------- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/plan/toWalls3d.test.ts b/src/plan/toWalls3d.test.ts index 0617d5d..f29c91f 100644 --- a/src/plan/toWalls3d.test.ts +++ b/src/plan/toWalls3d.test.ts @@ -1550,8 +1550,10 @@ describe("Dach-Wand-Verschneidung (clipPieceToRoofs) — Wand-Top folgt der Dach expect(seg.height).toBeLessThan(3.0 - 0.05); expect(seg.height).toBeGreaterThan(2.8); // nicht mehr als die Dachdicke gekappt } - // Am First (y nahe 2): unverändert volle Höhe (Dachunterkante liegt weit darüber). - const nearRidge = walls.filter((s) => s.start[1] > 1.8 && s.start[1] < 2.2); + // Am First (y=2): unverändert volle Höhe. Die ungeклippten Achsenstücke gleicher + // Oberkante werden zu EINEM Body verschmolzen, daher hier das Segment suchen, das + // y=2 ÜBERSPANNT (nicht mehr ein an y≈2 beginnendes 0.15-m-Stück). + const nearRidge = walls.filter((s) => Math.min(s.start[1], s.end[1]) <= 2 && Math.max(s.start[1], s.end[1]) >= 2); expect(nearRidge.length).toBeGreaterThan(0); for (const seg of nearRidge) { expect(seg.height).toBeCloseTo(3.0, 6); @@ -1886,13 +1888,14 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => { thickness: 0.3, }; - it("Satteldach: Flächen + Giebel als EIN Roof-Mesh, First-Höhe passt zur Traufhöhe + ridgeHeight", () => { + it("Satteldach: nur die Dachflächen als Roof-Mesh (keine Giebel-Endfüllung), First-Höhe passt zur Traufhöhe + ridgeHeight", () => { const project: Project = { ...sampleProject, roofs: [baseRoof] }; const { meshes } = projectToModel3d(project); const roofMeshes = meshes.filter(isRoofMesh); - // Einschichtiges Dach (thickness-Fallback): 1 Schicht-Slab-Mesh (Flächen) + - // 1 Giebel-Mesh (Endfüllung) = 2 Roof-Meshes (beide ROOF_RGB). - expect(roofMeshes.length).toBe(2); + // Einschichtiges Dach (thickness-Fallback): NUR das Schicht-Slab-Mesh der + // geneigten Flächen. Die Giebel-Endfüllung wird bewusst nicht mehr emittiert + // (der Giebel entsteht aus der Wand darunter) -> 1 Roof-Mesh. + expect(roofMeshes.length).toBe(1); for (const m of roofMeshes) { expect(m.positions.every((v) => Number.isFinite(v))).toBe(true); expect(m.indices.length % 3).toBe(0); @@ -2024,9 +2027,9 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => { const noField: Project = { ...sampleProject }; delete (noField as { roofs?: unknown }).roofs; expect(projectToModel3d(noField).meshes.filter(isRoofMesh).length).toBe(0); - // sampleProject enthält ein Demo-Satteldach (RF1, einschichtig) -> 2 Roof- - // Meshes: Schicht-Slab (Flächen) + Giebel-Endfüllung. - expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(2); + // sampleProject enthält ein Demo-Satteldach (RF1, einschichtig) -> 1 Roof- + // Mesh: nur der Schicht-Slab der Flächen (keine Giebel-Endfüllung mehr). + expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(1); }); }); diff --git a/src/plan/toWalls3d.ts b/src/plan/toWalls3d.ts index 52a8777..3517436 100644 --- a/src/plan/toWalls3d.ts +++ b/src/plan/toWalls3d.ts @@ -1099,18 +1099,32 @@ function clipPieceToRoofs( const span = bb - a; const steps = Math.max(1, Math.ceil(span / ROOF_CLIP_STEP)); const stepLen = span / steps; + // Je Stufe die geklemmte Oberkante bestimmen … + const tops: number[] = []; for (let i = 0; i < steps; i++) { - const sa = a + i * stepLen; - const sb = a + (i + 1) * stepLen; - const mid = (sa + sb) / 2; + const mid = a + (i + 0.5) * stepLen; const p = add(add(wall.start, scale(u, mid)), scale(n, bandOffset)); let clip = zt; for (const { geo, total } of roofData) { const under = roofUndersideAt(geo, total, p.x, p.y); if (under !== null && under < clip) clip = under; } - const zte = Math.max(zb, clip); + tops.push(Math.max(zb, clip)); + } + // … und AUFEINANDERFOLGENDE Stufen gleicher Oberkante zu EINEM Body verschmelzen. + // Wo das Dach das Stück gar nicht klippt (Oberkante == zt über die ganze Länge, + // z. B. nahe dem First oder wenn kein Dach wirklich überdeckt), entsteht so wieder + // EIN durchgehender Body statt vieler identischer 0.15-m-Fragmente — nur der echt + // geneigte Bereich bleibt gestuft. Das entlastet auch den Live-Schnitt, der genau + // diese Wandkörper cuttet (weniger, saubere Schnitt-Kappen). + let runStart = 0; + for (let i = 1; i <= steps; i++) { + if (i < steps && Math.abs(tops[i] - tops[runStart]) <= EPS) continue; + const sa = a + runStart * stepLen; + const sb = a + i * stepLen; + const zte = tops[runStart]; if (zte - zb > EPS) emit(sa, sb, zb, zte); + runStart = i; } } @@ -1932,19 +1946,13 @@ function emitRoofs(project: Project): RMesh[] { if (indices.length > 0) out.push({ positions, indices, kind: "extrusion", color, materialIndex }); return offsetAbove + t; }, 0); - // Giebel (Endfüllung) als dünne Fläche, äusserste Schichtfarbe/Override. - if (g.gables.length > 0) { - const gp: number[] = []; - const gi: number[] = []; - for (const gable of g.gables) pushFanTriangles(gp, gi, gable); - if (gi.length > 0) { - const first = layers[0]; - const gc = - overrideColor ?? - (first?.componentId ? hexToRgb(getComponent(project, first.componentId).color, ROOF_RGB) : ROOF_RGB); - out.push({ positions: gp, indices: gi, kind: "extrusion", color: gc }); - } - } + // Giebel-ENDFÜLLUNG bewusst NICHT mehr emittieren: die dreieckige Giebelfläche + // wurde früher als papierdünne Einzelfläche gebaut ("komischer Abschluss" an + // den Sattel-/Mansard-Enden) und lag zudem deckungsgleich vor der Giebelwand, + // die ohnehin bis an die Dach-Unterkante hochgeklemmt wird (s. + // {@link clipPieceToRoofs}) — also doppelt und optisch als dünne 2D-Schicht + // sichtbar. Es wird nur noch das Dach SELBST (die geneigten Slab-Flächen) + // konstruiert; der Giebel entsteht aus der Wand darunter. } return out; }