3D-Wände: Top folgt der Dach-Unterkante statt sie zu durchdringen (Z-Fighting-Fix)
Wände wurden bisher unabhängig von Dächern mit fixer Höhe emittiert, während Dächer separat gerendert wurden — wo eine geneigte Dachfläche den flachen Wand-Top kreuzte, überlappten sich beide Volumen (Z-Fighting im 3D-Viewer, Nutzer-Report mit Screenshot). roofUndersideAt (geometry/roof.ts) liefert die Dach-Unterkante an einem Grundriss-Punkt (Ebenengleichung je Dachfläche, dieselbe Herleitung wie im Vertikalschnitt). clipPieceToRoofs (toWalls3d.ts) zerlegt betroffene Wand- Achsenstücke in feine Schritte (~15 cm) und klemmt jeden auf die dort lokal gemessene Dach-Unterkante — eine Treppenstufen-Annäherung an eine echte geneigte Giebelwand-Stirnfläche (render3d-Wandkörper haben nur einen flachen Top; eine echte Schrägfläche bräuchte einen neuen Mesh-Pfad). Ohne Dach im selben Geschoss bleibt das Verhalten unverändert (kein Overhead).
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import { roofGeometry, roofBBox } from "./roof";
|
import { roofGeometry, roofBBox, roofUndersideAt } from "./roof";
|
||||||
import type { Roof, Vec2 } from "../model/types";
|
import type { Roof, Vec2 } from "../model/types";
|
||||||
|
|
||||||
// Rechteck 6×4 (x:0..6, y:0..4), Neigung 45° (tan=1), Traufhöhe 10, kein Überstand.
|
// Rechteck 6×4 (x:0..6, y:0..4), Neigung 45° (tan=1), Traufhöhe 10, kein Überstand.
|
||||||
@@ -181,3 +181,31 @@ describe("roofGeometry — Überstand", () => {
|
|||||||
expect(bb).toEqual({ x0: -0.4, y0: -0.4, x1: 6.4, y1: 4.4 });
|
expect(bb).toEqual({ x0: -0.4, y0: -0.4, x1: 6.4, y1: 4.4 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("roofUndersideAt — Dach-Unterkante an einem Grundriss-Punkt", () => {
|
||||||
|
// Sattel 6×4, First entlang X bei yc=2, Neigung 45° (tan=1) -> Firsthöhe 2.
|
||||||
|
// Fläche 1 (y0=0..yc=2): z(x,y) = 10 + y (linear, unabhängig von x).
|
||||||
|
const g = roofGeometry(roof({ shape: "sattel" }), E);
|
||||||
|
|
||||||
|
it("an der Traufe (y=0): Oberkante 10, Unterkante minus Schichtdicke", () => {
|
||||||
|
expect(roofUndersideAt(g, 0, 3, 0)).toBeCloseTo(10, 6);
|
||||||
|
expect(roofUndersideAt(g, 0.2, 3, 0)).toBeCloseTo(9.8, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("am First (y=2): Oberkante 12 (Firsthöhe 10+2)", () => {
|
||||||
|
expect(roofUndersideAt(g, 0, 3, 2)).toBeCloseTo(12, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("interpoliert linear zwischen Traufe und First (y=1 -> 11)", () => {
|
||||||
|
expect(roofUndersideAt(g, 0, 3, 1)).toBeCloseTo(11, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ausserhalb des Dach-Umrisses -> null", () => {
|
||||||
|
expect(roofUndersideAt(g, 0, 3, -1)).toBeNull();
|
||||||
|
expect(roofUndersideAt(g, 0, 8, 1)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("negative/fehlende Schichtdicke wird auf 0 geklemmt (keine Anhebung)", () => {
|
||||||
|
expect(roofUndersideAt(g, -0.5, 3, 0)).toBeCloseTo(10, 6);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -362,3 +362,55 @@ export function roofGeometry(roof: Roof, eavesZ: number): RoofGeometry {
|
|||||||
ridgeHeight: g.ridgeHeight,
|
ridgeHeight: g.ridgeHeight,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Punkt-in-Polygon (Ray-Casting), 2D-Aufsicht. */
|
||||||
|
function pointInPolygon2D(p: Vec2, poly: Vec2[]): boolean {
|
||||||
|
let inside = false;
|
||||||
|
const n = poly.length;
|
||||||
|
for (let i = 0, j = n - 1; i < n; j = i++) {
|
||||||
|
const a = poly[i];
|
||||||
|
const b = poly[j];
|
||||||
|
const intersect =
|
||||||
|
a.y > p.y !== b.y > p.y &&
|
||||||
|
p.x < ((b.x - a.x) * (p.y - a.y)) / (b.y - a.y || 1e-12) + a.x;
|
||||||
|
if (intersect) inside = !inside;
|
||||||
|
}
|
||||||
|
return inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dach-UNTERKANTE (Innenseite der untersten Schicht) an einem Grundriss-Punkt
|
||||||
|
* (x,y) — `null`, wenn der Punkt ausserhalb aller Dachflächen liegt (Aufsicht).
|
||||||
|
* Ebenengleichung je Fläche über die Newell-Normale (identische Herleitung wie
|
||||||
|
* `toSection.ts::appendRoofSections`, dort inline für den Vertikalschnitt) minus
|
||||||
|
* `totalLayerThickness` (Summe aller Dachschicht-Dicken, s. `roofLayers`).
|
||||||
|
* Dient dem Wand-Zuschnitt unter geneigten Dächern ({@link trimWallTopForRoofs}
|
||||||
|
* in `toWalls3d.ts`) — vermeidet, dass ein flacher Wand-Top die Dachfläche
|
||||||
|
* durchdringt (Z-Fighting).
|
||||||
|
*/
|
||||||
|
export function roofUndersideAt(
|
||||||
|
geo: RoofGeometry,
|
||||||
|
totalLayerThickness: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
): number | null {
|
||||||
|
for (const pl of geo.planes) {
|
||||||
|
const pts = pl.pts;
|
||||||
|
if (pts.length < 3) continue;
|
||||||
|
if (!pointInPolygon2D({ x, y }, pts.map((p) => ({ x: p[0], y: p[1] })))) continue;
|
||||||
|
let A = 0;
|
||||||
|
let B = 0;
|
||||||
|
let C = 0;
|
||||||
|
for (let i = 0; i < pts.length; i++) {
|
||||||
|
const c = pts[i];
|
||||||
|
const d = pts[(i + 1) % pts.length];
|
||||||
|
A += (c[1] - d[1]) * (c[2] + d[2]);
|
||||||
|
B += (c[2] - d[2]) * (c[0] + d[0]);
|
||||||
|
C += (c[0] - d[0]) * (c[1] + d[1]);
|
||||||
|
}
|
||||||
|
if (Math.abs(C) < 1e-9) continue; // (nahezu) vertikale Fläche
|
||||||
|
const topZ = pts[0][2] - (A * (x - pts[0][0]) + B * (y - pts[0][1])) / C;
|
||||||
|
return topZ - Math.max(0, totalLayerThickness);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
+111
-4
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { describe, it, expect } from "vitest";
|
import { describe, it, expect } from "vitest";
|
||||||
import type { Project, Roof } from "../model/types";
|
import type { Project, Roof, Wall } from "../model/types";
|
||||||
import { sampleProject } from "../model/sampleProject";
|
import { sampleProject } from "../model/sampleProject";
|
||||||
import { selectionHighlightLines, projectToWalls3d, projectToModel3d, pickGeometry } from "./toWalls3d";
|
import { selectionHighlightLines, projectToWalls3d, projectToModel3d, pickGeometry } from "./toWalls3d";
|
||||||
import { resolveHatch } from "./generatePlan";
|
import { resolveHatch } from "./generatePlan";
|
||||||
@@ -1282,17 +1282,124 @@ describe("Per-Schicht-Decken-Dominanz Wand/Decke (3D-Entsprechung der Schnitt-Bo
|
|||||||
expect(insul[0].baseElevation + insul[0].height).toBeCloseTo(2.6, 6);
|
expect(insul[0].baseElevation + insul[0].height).toBeCloseTo(2.6, 6);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keine Subtraktion, wenn keine Decke im selben Geschoss überlappt (z.B. OG-Wände ohne Dach-Decke)", () => {
|
it("keine Deckensubtraktion, wenn keine Decke im selben Geschoss überlappt (z.B. OG-Wände ohne Dach-Decke)", () => {
|
||||||
// W6 (OG) hat keine überlappende Decke im Sample-Projekt (C1 ist EG) ->
|
// W6 (OG) hat keine überlappende Decke im Sample-Projekt (C1 ist EG) -> keine
|
||||||
// unverändertes Verhalten (volle wall.height).
|
// Deckenbedingte z-Aufteilung. Volle wall.height, AUSSER das Sample-Projekt
|
||||||
|
// hat auch ein Dach (RF1) über W6 (s. "Dach-Wand-Verschneidung" unten) — eine
|
||||||
|
// an der Gehrungsecke leicht über die Achse hinaus verlängerte, hauchdünne
|
||||||
|
// Putzschicht (0.02 m) reicht dort minimal unter den Dachüberstand und wird
|
||||||
|
// korrekt (nicht durch die Decken-, sondern die Dach-Logik) leicht gekappt.
|
||||||
const w6 = projectToWalls3d(sampleProject).filter((w) => w.wallId === "W6");
|
const w6 = projectToWalls3d(sampleProject).filter((w) => w.wallId === "W6");
|
||||||
expect(w6.length).toBeGreaterThan(0);
|
expect(w6.length).toBeGreaterThan(0);
|
||||||
|
const offHeight = w6.filter((seg) => Math.abs(seg.height - 2.6) > 1e-6);
|
||||||
|
expect(offHeight.length).toBe(1);
|
||||||
|
expect(offHeight[0].thickness).toBeCloseTo(0.02, 6); // die dünne Putz-Gehrungsspitze
|
||||||
|
expect(offHeight[0].height).toBeLessThan(2.6);
|
||||||
|
expect(offHeight[0].height).toBeGreaterThan(2.5); // nur ein paar cm gekappt
|
||||||
for (const seg of w6) {
|
for (const seg of w6) {
|
||||||
|
if (seg === offHeight[0]) continue;
|
||||||
expect(seg.height).toBeCloseTo(2.6, 6);
|
expect(seg.height).toBeCloseTo(2.6, 6);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Dach-Wand-Verschneidung (clipPieceToRoofs) — Wand-Top folgt der Dach-Unterkante", () => {
|
||||||
|
// Giebelwand entlang Y (0..4), unter einem Satteldach (First entlang X bei
|
||||||
|
// y=2, Traufhöhe 3, Neigung 30°, Dicke 0.1). Wandhöhe 3.0 ⇒ an der Traufe
|
||||||
|
// (y=0/4) reicht die Wand exakt bis zur Dachfläche (baseElevation=3), muss
|
||||||
|
// dort also um die Dachdicke (0.1) gekappt werden; am First (y=2) liegt die
|
||||||
|
// Dachunterkante weit darüber ⇒ unverändert volle Höhe.
|
||||||
|
const gableRoof: Roof = {
|
||||||
|
id: "RG",
|
||||||
|
type: "roof",
|
||||||
|
floorId: "eg",
|
||||||
|
categoryCode: "35",
|
||||||
|
outline: [
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
{ x: 6, y: 0 },
|
||||||
|
{ x: 6, y: 4 },
|
||||||
|
{ x: 0, y: 4 },
|
||||||
|
],
|
||||||
|
shape: "sattel",
|
||||||
|
pitchDeg: 30,
|
||||||
|
overhang: 0,
|
||||||
|
ridgeAxis: "x",
|
||||||
|
baseElevation: 3,
|
||||||
|
thickness: 0.1,
|
||||||
|
};
|
||||||
|
const gableWall: Wall = {
|
||||||
|
id: "WG",
|
||||||
|
type: "wall",
|
||||||
|
floorId: "eg",
|
||||||
|
categoryCode: "20",
|
||||||
|
start: { x: 3, y: 0 },
|
||||||
|
end: { x: 3, y: 4 },
|
||||||
|
wallTypeId: sampleProject.walls[0].wallTypeId,
|
||||||
|
height: 3.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
it("kappt die Wand nahe der Traufe, lässt sie am First unverändert", () => {
|
||||||
|
const project: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
walls: [gableWall],
|
||||||
|
roofs: [gableRoof],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
};
|
||||||
|
const walls = projectToWalls3d(project).filter((w) => w.wallId === "WG");
|
||||||
|
expect(walls.length).toBeGreaterThan(0);
|
||||||
|
// EXAKT an der Traufe (start.y = 0 bzw. end nahe 4): das erste ~0.15-m-
|
||||||
|
// Achsenstück ist geklemmt (Dachunterkante dort knapp unter 3.0, s. o.); die
|
||||||
|
// Neigung steigt schnell genug, dass bereits das ZWEITE Achsenstück wieder
|
||||||
|
// unbeeinflusst ist — bewusst NUR das direkt an y=0 anliegende Stück prüfen.
|
||||||
|
const atEave = walls.filter((s) => s.start[1] < 1e-6 || s.end[1] > 4 - 1e-6);
|
||||||
|
expect(atEave.length).toBeGreaterThan(0);
|
||||||
|
for (const seg of atEave) {
|
||||||
|
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);
|
||||||
|
expect(nearRidge.length).toBeGreaterThan(0);
|
||||||
|
for (const seg of nearRidge) {
|
||||||
|
expect(seg.height).toBeCloseTo(3.0, 6);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ohne Dach im selben Geschoss: unverändertes Verhalten (volle Höhe, EIN Stück)", () => {
|
||||||
|
const project: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
walls: [gableWall],
|
||||||
|
roofs: [],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
};
|
||||||
|
const walls = projectToWalls3d(project).filter((w) => w.wallId === "WG");
|
||||||
|
expect(walls.length).toBeGreaterThan(0);
|
||||||
|
for (const seg of walls) {
|
||||||
|
expect(seg.height).toBeCloseTo(3.0, 6);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Dach auf einem ANDEREN Geschoss wirkt nicht (floorId-Filter)", () => {
|
||||||
|
const project: Project = {
|
||||||
|
...sampleProject,
|
||||||
|
walls: [gableWall],
|
||||||
|
roofs: [{ ...gableRoof, floorId: "og" }],
|
||||||
|
doors: [],
|
||||||
|
openings: [],
|
||||||
|
ceilings: [],
|
||||||
|
};
|
||||||
|
const walls = projectToWalls3d(project).filter((w) => w.wallId === "WG");
|
||||||
|
expect(walls.length).toBeGreaterThan(0);
|
||||||
|
for (const seg of walls) {
|
||||||
|
expect(seg.height).toBeCloseTo(3.0, 6);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Geschichteter Bodenaufbau Decke", () => {
|
describe("Geschichteter Bodenaufbau Decke", () => {
|
||||||
it("Decke -> je Schicht ein Slab, oben->unten gestapelt mit Bauteilfarbe/-schraffur", () => {
|
it("Decke -> je Schicht ein Slab, oben->unten gestapelt mit Bauteilfarbe/-schraffur", () => {
|
||||||
// Deckentyp "dg-massiv": screed 0.06 (#c9c2b3), insulation 0.04 (#ffffff),
|
// Deckentyp "dg-massiv": screed 0.06 (#c9c2b3), insulation 0.04 (#ffffff),
|
||||||
|
|||||||
+80
-5
@@ -54,8 +54,8 @@ import { mergeHoles } from "../geometry/polygonHoles";
|
|||||||
import { openingInterval, openingVerticalExtent, openingJambs } from "../geometry/opening";
|
import { openingInterval, openingVerticalExtent, openingJambs } from "../geometry/opening";
|
||||||
import type { DetailLevel } from "../ui/TopBar";
|
import type { DetailLevel } from "../ui/TopBar";
|
||||||
import { stairGeometry, stairBBox } from "../geometry/stair";
|
import { stairGeometry, stairBBox } from "../geometry/stair";
|
||||||
import { roofGeometry, roofBaseElevation } from "../geometry/roof";
|
import { roofGeometry, roofBaseElevation, roofUndersideAt } from "../geometry/roof";
|
||||||
import type { Vec3 as RoofVec3 } from "../geometry/roof";
|
import type { Vec3 as RoofVec3, RoofGeometry } from "../geometry/roof";
|
||||||
import { extrudePolygon, extrudeCircle } from "../engine/truckSolid";
|
import { extrudePolygon, extrudeCircle } from "../engine/truckSolid";
|
||||||
import { computeJoins } from "../model/joins";
|
import { computeJoins } from "../model/joins";
|
||||||
import type { WallCuts } from "../model/joins";
|
import type { WallCuts } from "../model/joins";
|
||||||
@@ -1025,6 +1025,80 @@ function bandCutDistance(wall: Wall, u: Vec2, n: Vec2, bandOffset: number, cut:
|
|||||||
return dot(sub(hit, wall.start), u);
|
return dot(sub(hit, wall.start), u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Schrittweite (Meter) der Dach-Unterkanten-Annäherung, s. {@link clipPieceToRoofs}. */
|
||||||
|
const ROOF_CLIP_STEP = 0.15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kappt ein Wand-Achsenstück [a,bb] (z-Bereich [zb,zt]) an der Dach-UNTERKANTE
|
||||||
|
* (falls ein Dach darüber liegt) und emittiert es via `emit` — in FEINE Achsen-
|
||||||
|
* Schritte (~{@link ROOF_CLIP_STEP}) zerlegt, je Schritt auf die dort gemessene
|
||||||
|
* Dach-Unterkante geklemmt. Das ist eine TREPPENSTUFEN-Annäherung an eine echte
|
||||||
|
* geneigte Giebelwand-Stirnfläche (render3d-Wandkörper (`RWall`) haben nur einen
|
||||||
|
* FLACHEN Top — echte Schrägflächen bräuchten einen neuen Mesh-Pfad); bei der
|
||||||
|
* gewählten Schrittweite optisch glatt genug UND behebt das eigentliche Problem
|
||||||
|
* (Wand durchdringt die Dachfläche → Z-Fighting, s. PENDENZEN.md „Dach-Wand-
|
||||||
|
* Verschneidung"), ohne render3d/Rust anzufassen.
|
||||||
|
*
|
||||||
|
* Ohne Dächer (oder keinem, das dieses Wandstück überdeckt) läuft das Stück
|
||||||
|
* UNVERÄNDERT als EIN Body durch (kein Overhead im Normalfall).
|
||||||
|
*/
|
||||||
|
function clipPieceToRoofs(
|
||||||
|
project: Project,
|
||||||
|
wall: Wall,
|
||||||
|
u: Vec2,
|
||||||
|
n: Vec2,
|
||||||
|
bandOffset: number,
|
||||||
|
a: number,
|
||||||
|
bb: number,
|
||||||
|
zb: number,
|
||||||
|
zt: number,
|
||||||
|
emit: (a: number, bb: number, zb: number, zt: number) => void,
|
||||||
|
): void {
|
||||||
|
if (zt - zb <= EPS || bb - a <= EPS) return;
|
||||||
|
const wallBounds = wallFootprintBounds(wall, Math.abs(bandOffset) + 0.01);
|
||||||
|
const roofs = (project.roofs ?? []).filter((r) => {
|
||||||
|
if (r.floorId !== wall.floorId) return false;
|
||||||
|
const eavesZ = roofBaseElevation(project, r);
|
||||||
|
// Grober Höhen-Ausschluss: Dach-Traufe weit unter dem Wandstück ⇒ kann nicht
|
||||||
|
// schneiden (übliche Dächer haben keine nach unten ausufernden Übermasse).
|
||||||
|
return eavesZ + 50 >= zb; // grosszügige Schranke statt exakter First-Höhe
|
||||||
|
});
|
||||||
|
if (roofs.length === 0) {
|
||||||
|
emit(a, bb, zb, zt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const roofData = roofs
|
||||||
|
.map((r) => {
|
||||||
|
const eavesZ = roofBaseElevation(project, r);
|
||||||
|
const geo = roofGeometry(r, eavesZ);
|
||||||
|
if (!boundsOverlap(wallBounds, outlineBounds(geo.eaves))) return null;
|
||||||
|
const total = roofLayers(project, r).reduce((s, l) => s + Math.max(0, l.thickness), 0);
|
||||||
|
return { geo, total };
|
||||||
|
})
|
||||||
|
.filter((d): d is { geo: RoofGeometry; total: number } => d !== null);
|
||||||
|
if (roofData.length === 0) {
|
||||||
|
emit(a, bb, zb, zt);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = bb - a;
|
||||||
|
const steps = Math.max(1, Math.ceil(span / ROOF_CLIP_STEP));
|
||||||
|
const stepLen = span / steps;
|
||||||
|
for (let i = 0; i < steps; i++) {
|
||||||
|
const sa = a + i * stepLen;
|
||||||
|
const sb = a + (i + 1) * stepLen;
|
||||||
|
const mid = (sa + sb) / 2;
|
||||||
|
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);
|
||||||
|
if (zte - zb > EPS) emit(sa, sb, zb, zte);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Emittiert die Wandkörper EINER Wand (mit oder ohne Öffnungen).
|
* Emittiert die Wandkörper EINER Wand (mit oder ohne Öffnungen).
|
||||||
*
|
*
|
||||||
@@ -1180,8 +1254,9 @@ function emitWall(out: RWall[], project: Project, wall: Wall, layered = true, cu
|
|||||||
.map((c) => ({ from: Math.max(a, c.from), to: Math.min(bb, c.to), zBottom: c.zBottom, zTop: c.zTop }))
|
.map((c) => ({ from: Math.max(a, c.from), to: Math.min(bb, c.to), zBottom: c.zBottom, zTop: c.zTop }))
|
||||||
.filter((c) => c.to > c.from + EPS);
|
.filter((c) => c.to > c.from + EPS);
|
||||||
if (local.length === 0) {
|
if (local.length === 0) {
|
||||||
// Kein Decken-Überlapp auf diesem Achsenstück ⇒ volle Höhe (Band läuft durch).
|
// Kein Decken-Überlapp auf diesem Achsenstück ⇒ volle Höhe (Band läuft
|
||||||
emitPiece(a, bb, zBottom, zTop);
|
// durch) — ausser ein Dach kappt den Top (s. {@link clipPieceToRoofs}).
|
||||||
|
clipPieceToRoofs(project, wall, u, n, b.offset, a, bb, zBottom, zTop, emitPiece);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Achse [a,bb] an allen Überdeckungs-Grenzen in elementare Teilstücke
|
// Achse [a,bb] an allen Überdeckungs-Grenzen in elementare Teilstücke
|
||||||
@@ -1205,7 +1280,7 @@ function emitWall(out: RWall[], project: Project, wall: Wall, layered = true, cu
|
|||||||
// endet die Wand an der Decke, statt oben wieder aufzutauchen (s.
|
// endet die Wand an der Decke, statt oben wieder aufzutauchen (s.
|
||||||
// {@link terminateOrSubtractSpans}). "both"/undefined = heutiges Verhalten.
|
// {@link terminateOrSubtractSpans}). "both"/undefined = heutiges Verhalten.
|
||||||
for (const [zb, zt] of terminateOrSubtractSpans(zBottom, zTop, zCutters, wall.sliceTermination)) {
|
for (const [zb, zt] of terminateOrSubtractSpans(zBottom, zTop, zCutters, wall.sliceTermination)) {
|
||||||
emitPiece(xa, xb, zb, zt);
|
clipPieceToRoofs(project, wall, u, n, b.offset, xa, xb, zb, zt, emitPiece);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user