diff --git a/src/plan/toSection.depth.test.ts b/src/plan/toSection.depth.test.ts new file mode 100644 index 0000000..2aed8e0 --- /dev/null +++ b/src/plan/toSection.depth.test.ts @@ -0,0 +1,72 @@ +// Schnitt-Tiefe (DrawingLevel.depth): filterByDepth blendet Ansichtskanten aus, +// deren Quell-Wand weiter als `depth` hinter der Schnittebene (Blickrichtung) +// liegt. Owner-Distanz-Näherung (Wand-Mitte) — reiner TS-Datenpfad, ohne WASM. + +import { describe, it, expect } from "vitest"; +import { filterByDepth } from "./toSection"; +import type { SectionOutput, SectionPlaneSpec } from "./toSection"; +import type { Project, Wall } from "../model/types"; + +// Zwei Wände auf verschiedenen Tiefen: W_near bei y=1, W_far bei y=8. +function makeWall(id: string, y: number): Wall { + return { + id, + type: "wall", + floorId: "eg", + categoryCode: "20", + wallTypeId: "wt", + start: { x: 0, y }, + end: { x: 4, y }, + height: 2.6, + } as unknown as Wall; +} + +const project = { + id: "p", + name: "T", + walls: [makeWall("W_near", 1), makeWall("W_far", 8)], + ceilings: [], + roofs: [], + openings: [], + doors: [], + components: [], + wallTypes: [], + drawingLevels: [{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false }], +} as unknown as Project; + +// Schnittebene bei y=0, Blickrichtung +y (in die Szene). wallSegmentOwners +// nummeriert die Wände in project.walls-Reihenfolge: index 0 = W_near, 1 = W_far. +const plane: SectionPlaneSpec = { point: [0, 0, 0], normal: [0, 0, 1] }; + +function edge(index: number) { + return { component: { kind: "wall" as const, index }, a: [0, 0] as [number, number], b: [1, 1] as [number, number] }; +} + +describe("filterByDepth", () => { + it("behält nahe Kanten, verwirft Kanten hinter der Tiefengrenze", () => { + const out: SectionOutput = { + cutPolygons: [], + visibleEdges: [edge(0), edge(1)], + hiddenEdges: [edge(1)], + }; + filterByDepth(out, project, plane, 4); // Tiefe 4 m: W_near (1) bleibt, W_far (8) fällt weg. + expect(out.visibleEdges.map((e) => e.component.index)).toEqual([0]); + expect(out.hiddenEdges).toHaveLength(0); + }); + + it("behält alles, wenn die Tiefe alle Bauteile umfasst", () => { + const out: SectionOutput = { + cutPolygons: [], + visibleEdges: [edge(0), edge(1)], + hiddenEdges: [], + }; + filterByDepth(out, project, plane, 20); + expect(out.visibleEdges).toHaveLength(2); + }); + + it("depth <= 0 ist ein No-op", () => { + const out: SectionOutput = { cutPolygons: [], visibleEdges: [edge(0), edge(1)], hiddenEdges: [] }; + filterByDepth(out, project, plane, 0); + expect(out.visibleEdges).toHaveLength(2); + }); +}); diff --git a/src/plan/toSection.ts b/src/plan/toSection.ts index 6e8c56d..7699247 100644 --- a/src/plan/toSection.ts +++ b/src/plan/toSection.ts @@ -1048,6 +1048,72 @@ export function appendRoofSections( }); } +/** Mittelpunkt (Modell-2D) einer Punktliste (Outline-Schwerpunkt, naiv). */ +function centroid2d(pts: Vec2[]): Vec2 | null { + if (pts.length === 0) return null; + let x = 0; + let y = 0; + for (const p of pts) { + x += p.x; + y += p.y; + } + return { x: x / pts.length, y: y / pts.length }; +} + +/** + * Blendet Ansichtskanten (sichtbar/verdeckt) aus, deren QUELL-Bauteil weiter als + * `depth` Meter HINTER der Schnittebene (in Blickrichtung) liegt — die + * Schnitt-Tiefe (`DrawingLevel.depth`). Die Distanz wird über den REPRÄSENTATIVEN + * Bauteilpunkt (Wand-Mitte bzw. Outline-Schwerpunkt) gegen die Ebenennormale + * gemessen (in-place). + * + * NÄHERUNG (bewusst, kein Rust-Hack): der Rust-Extraktor liefert die Kanten nur in + * (u, v) OHNE echte Tiefe je Kante — die exakte Lösung bräuchte eine Tiefen-Achse + * aus `section.rs`. Bis dahin filtert dieser Owner-Distanz-Test grob nach dem + * Bauteil-Mittelpunkt: ein Bauteil wird GANZ ein- oder ausgeblendet (kein + * Teil-Clipping an der Tiefengrenze). Cut-Polygone liegen auf der Ebene (Distanz + * ~0) und bleiben immer erhalten. TODO: exakte per-Kante-Tiefe aus render3d + * (section.rs) zurückgeben, dann hier hart nach Kanten-Tiefe clippen. + */ +export function filterByDepth( + output: SectionOutput, + project: Project, + plane: SectionPlaneSpec, + depth: number, +): void { + if (!(depth > 0)) return; + // Blickrichtung + Ebenenaufpunkt in Modell-2D (x = world.x, y = world.z). + const blick = normalize({ x: plane.normal[0], y: plane.normal[2] }); + const origin = { x: plane.point[0], y: plane.point[2] }; + const walls = wallSegmentOwners(project); + const slabs = slabSegmentOwners(project); + const roofs = project.roofs ?? []; + + const ownerPoint = (ref: SectionComponentRef): Vec2 | null => { + if (ref.kind === "wall") { + const w = walls[ref.index]; + return w ? { x: (w.start.x + w.end.x) / 2, y: (w.start.y + w.end.y) / 2 } : null; + } + if (ref.kind === "slab") { + const c = slabs[ref.index]; + return c ? centroid2d(c.outline) : null; + } + const r = roofs[ref.index]; + return r ? centroid2d(r.outline) : null; + }; + + const keep = (e: SectionEdge): boolean => { + const p = ownerPoint(e.component); + if (!p) return true; // Unauflösbar: konservativ behalten. + // Distanz HINTER der Ebene entlang der Blickrichtung; ≤ depth ⇒ behalten. + const d = dot(sub(p, origin), blick); + return d <= depth + 1e-6; + }; + + output.visibleEdges = output.visibleEdges.filter(keep); + output.hiddenEdges = output.hiddenEdges.filter(keep); +} + export async function computeSection( project: Project, level: DrawingLevel, @@ -1078,5 +1144,7 @@ export async function computeSection( // Dächer TS-seitig ergänzen (der Rust-Extraktor kennt nur Wände + Decken). appendRoofSections(output, project, plane); attachCutStyles(output, project, plane); + // Schnitt-Tiefe: Kanten weiter als `level.depth` hinter der Ebene ausblenden. + if (level.depth !== undefined) filterByDepth(output, project, plane, level.depth); return output; }