Schnitt-Tiefe: Kanten hinter DrawingLevel.depth ausblenden
computeSection filtert Ansichtskanten (sichtbar/verdeckt), deren Quell-Bauteil weiter als level.depth hinter der Ebene liegt (Owner-Distanz-Naeherung ueber den Bauteil-Mittelpunkt; Cut-Polygone bleiben). Naeherung dokumentiert (TODO: exakte per-Kante-Tiefe aus render3d/section.rs). Feld ist in der Schnittlinien-Sektion editierbar (Teil 3). Test fuer filterByDepth.
This commit is contained in:
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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(
|
export async function computeSection(
|
||||||
project: Project,
|
project: Project,
|
||||||
level: DrawingLevel,
|
level: DrawingLevel,
|
||||||
@@ -1078,5 +1144,7 @@ export async function computeSection(
|
|||||||
// Dächer TS-seitig ergänzen (der Rust-Extraktor kennt nur Wände + Decken).
|
// Dächer TS-seitig ergänzen (der Rust-Extraktor kennt nur Wände + Decken).
|
||||||
appendRoofSections(output, project, plane);
|
appendRoofSections(output, project, plane);
|
||||||
attachCutStyles(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;
|
return output;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user