Dach/Wand: Giebel-Endfüllung (dünne Einzelfläche) nicht mehr emittiert — lag

deckungsgleich vor der ohnehin bis zur Dach-Unterkante hochgeklemmten
Giebelwand und wirkte als sichtbare Doppel-Schicht. Zusätzlich werden
aufeinanderfolgende Wandstücke gleicher geklemmter Oberkante (z. B. am First)
wieder zu einem Body verschmolzen statt in viele 0.15-m-Fragmente zu zerfallen.
This commit is contained in:
2026-07-31 17:00:09 +02:00
parent 89c444451c
commit b56095da80
2 changed files with 37 additions and 26 deletions
+12 -9
View File
@@ -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).toBeLessThan(3.0 - 0.05);
expect(seg.height).toBeGreaterThan(2.8); // nicht mehr als die Dachdicke gekappt 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). // Am First (y=2): unverändert volle Höhe. Die ungeклippten Achsenstücke gleicher
const nearRidge = walls.filter((s) => s.start[1] > 1.8 && s.start[1] < 2.2); // 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); expect(nearRidge.length).toBeGreaterThan(0);
for (const seg of nearRidge) { for (const seg of nearRidge) {
expect(seg.height).toBeCloseTo(3.0, 6); expect(seg.height).toBeCloseTo(3.0, 6);
@@ -1886,13 +1888,14 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => {
thickness: 0.3, 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 project: Project = { ...sampleProject, roofs: [baseRoof] };
const { meshes } = projectToModel3d(project); const { meshes } = projectToModel3d(project);
const roofMeshes = meshes.filter(isRoofMesh); const roofMeshes = meshes.filter(isRoofMesh);
// Einschichtiges Dach (thickness-Fallback): 1 Schicht-Slab-Mesh (Flächen) + // Einschichtiges Dach (thickness-Fallback): NUR das Schicht-Slab-Mesh der
// 1 Giebel-Mesh (Endfüllung) = 2 Roof-Meshes (beide ROOF_RGB). // geneigten Flächen. Die Giebel-Endfüllung wird bewusst nicht mehr emittiert
expect(roofMeshes.length).toBe(2); // (der Giebel entsteht aus der Wand darunter) -> 1 Roof-Mesh.
expect(roofMeshes.length).toBe(1);
for (const m of roofMeshes) { for (const m of roofMeshes) {
expect(m.positions.every((v) => Number.isFinite(v))).toBe(true); expect(m.positions.every((v) => Number.isFinite(v))).toBe(true);
expect(m.indices.length % 3).toBe(0); expect(m.indices.length % 3).toBe(0);
@@ -2024,9 +2027,9 @@ describe("emitRoofs (Dachflächen + Giebel als Meshes)", () => {
const noField: Project = { ...sampleProject }; const noField: Project = { ...sampleProject };
delete (noField as { roofs?: unknown }).roofs; delete (noField as { roofs?: unknown }).roofs;
expect(projectToModel3d(noField).meshes.filter(isRoofMesh).length).toBe(0); expect(projectToModel3d(noField).meshes.filter(isRoofMesh).length).toBe(0);
// sampleProject enthält ein Demo-Satteldach (RF1, einschichtig) -> 2 Roof- // sampleProject enthält ein Demo-Satteldach (RF1, einschichtig) -> 1 Roof-
// Meshes: Schicht-Slab (Flächen) + Giebel-Endfüllung. // Mesh: nur der Schicht-Slab der Flächen (keine Giebel-Endfüllung mehr).
expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(2); expect(projectToModel3d(sampleProject).meshes.filter(isRoofMesh).length).toBe(1);
}); });
}); });
+25 -17
View File
@@ -1099,18 +1099,32 @@ function clipPieceToRoofs(
const span = bb - a; const span = bb - a;
const steps = Math.max(1, Math.ceil(span / ROOF_CLIP_STEP)); const steps = Math.max(1, Math.ceil(span / ROOF_CLIP_STEP));
const stepLen = span / steps; const stepLen = span / steps;
// Je Stufe die geklemmte Oberkante bestimmen …
const tops: number[] = [];
for (let i = 0; i < steps; i++) { for (let i = 0; i < steps; i++) {
const sa = a + i * stepLen; const mid = a + (i + 0.5) * stepLen;
const sb = a + (i + 1) * stepLen;
const mid = (sa + sb) / 2;
const p = add(add(wall.start, scale(u, mid)), scale(n, bandOffset)); const p = add(add(wall.start, scale(u, mid)), scale(n, bandOffset));
let clip = zt; let clip = zt;
for (const { geo, total } of roofData) { for (const { geo, total } of roofData) {
const under = roofUndersideAt(geo, total, p.x, p.y); const under = roofUndersideAt(geo, total, p.x, p.y);
if (under !== null && under < clip) clip = under; 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); 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 }); if (indices.length > 0) out.push({ positions, indices, kind: "extrusion", color, materialIndex });
return offsetAbove + t; return offsetAbove + t;
}, 0); }, 0);
// Giebel (Endfüllung) als dünne Fläche, äusserste Schichtfarbe/Override. // Giebel-ENDFÜLLUNG bewusst NICHT mehr emittieren: die dreieckige Giebelfläche
if (g.gables.length > 0) { // wurde früher als papierdünne Einzelfläche gebaut ("komischer Abschluss" an
const gp: number[] = []; // den Sattel-/Mansard-Enden) und lag zudem deckungsgleich vor der Giebelwand,
const gi: number[] = []; // die ohnehin bis an die Dach-Unterkante hochgeklemmt wird (s.
for (const gable of g.gables) pushFanTriangles(gp, gi, gable); // {@link clipPieceToRoofs}) — also doppelt und optisch als dünne 2D-Schicht
if (gi.length > 0) { // sichtbar. Es wird nur noch das Dach SELBST (die geneigten Slab-Flächen)
const first = layers[0]; // konstruiert; der Giebel entsteht aus der Wand darunter.
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 });
}
}
} }
return out; return out;
} }