35299307d6
Bündelt den über mehrere Sessions gewachsenen, uncommitteten Stand in
einem Basis-Commit, damit Folge-Features isoliert darauf aufsetzen.
Verifikation: tsc --noEmit sauber, vitest 600/600 grün.
Enthalten (Details in PENDENZEN.md ✅-Liste / HANDOVER.md):
- truck-Integration: Profil-Extrusion + Verjüngung + Boolean-CSG (csgrs),
Crate src-tauri/trucksolid, Werkzeug `extrude`, ExtrudedSolid-Modell.
- kernel2d-Port nach Rust/WASM (Phasen 1–5, Diff-Harness).
- render3d 3D-Live-Schnitt = 2D-Schnitt: geschichteter Bodenaufbau,
Prioritäts-Verschneidung (section_boolean.rs), einstellbare
Schichttrennlinien, per-Hatch-Strichstärke, relativeToWall-Orientierung.
- Interop-Export IFC4/STL/OBJ (Loch-Ausschnitt wallMeshCut), Schnellexport.
- Projektdatei .obp + OS-Lock (lock.rs, LockConflictDialog).
- Layout-Blätter (Modell/Editor/Panel/PDF), Ausschnitte, Override-Engine,
Tragwerk-Stützen (Column), BIM-Tree-Panel.
- Bauteil-Typsystem (Tür/Fenster/Treppe-Typen), Betontreppe mit schräger
Laufplatte, Text-/Textbox-Werkzeug, Mess-Werkzeug, 2D/3D-Griffe für
Öffnungen/Treppen, Snap-Symbol-Restyle.
163 lines
6.4 KiB
TypeScript
163 lines
6.4 KiB
TypeScript
/**
|
||
* Unit-Tests für das Stützen-Bauteil (Column) — DOSSIER-Audit A4 Tragwerk:
|
||
* • 2D-Grundriss (generatePlan): eine Stütze erzeugt ein Poché-Polygon mit
|
||
* columnId an der richtigen Position; die Rotation dreht den Rechteck-
|
||
* Querschnitt.
|
||
* • 3D (projectToModel3d/emitColumns): eine Stütze wird zu einem Prisma-Mesh
|
||
* (Rechteck-Box bzw. Kreis-Prisma) mit der erwarteten vertikalen Ausdehnung.
|
||
* • Vertikale Lage (columnVerticalExtent): Geschoss-Default sowie UK/OK-Anker.
|
||
*/
|
||
|
||
import { describe, it, expect } from "vitest";
|
||
import { generatePlan } from "./generatePlan";
|
||
import { projectToModel3d } from "./toWalls3d";
|
||
import { columnVerticalExtent } from "../model/wall";
|
||
import { columnFootprint } from "../geometry/column";
|
||
import type { Column, Project } from "../model/types";
|
||
|
||
/** Minimalprojekt (zwei Geschosse gestapelt) mit einer konfigurierbaren Stütze. */
|
||
function projectWithColumn(column: Column): Project {
|
||
return {
|
||
id: "t",
|
||
name: "T",
|
||
lineStyles: [{ id: "thin", name: "d", weight: 0.13, color: "#111", dash: null }],
|
||
hatches: [{ id: "none", name: "Ohne", pattern: "none", scale: 1, angle: 0, color: "#111" }],
|
||
components: [{ id: "a", name: "A", color: "#c8c8cc", hatchId: "none", joinPriority: 10 }],
|
||
wallTypes: [{ id: "aw", name: "AW", layers: [{ componentId: "a", thickness: 0.3 }] }],
|
||
drawingLevels: [
|
||
{ id: "eg", name: "EG", kind: "floor", visible: true, locked: false, floorHeight: 3, cutHeight: 1.0, baseElevation: 0 },
|
||
{ id: "og", name: "OG", kind: "floor", visible: true, locked: false, floorHeight: 3, cutHeight: 1.0, baseElevation: 3 },
|
||
],
|
||
layers: [
|
||
{ code: "20", name: "Wände", color: "#0a0a0a", lw: 0.5, visible: true, locked: false },
|
||
{ code: "50", name: "Tragwerk", color: "#0a0a0a", lw: 0.5, visible: true, locked: false },
|
||
],
|
||
walls: [],
|
||
doors: [],
|
||
openings: [],
|
||
ceilings: [],
|
||
stairs: [],
|
||
rooms: [],
|
||
columns: [column],
|
||
drawings2d: [],
|
||
context: [],
|
||
};
|
||
}
|
||
|
||
const visible = new Set(["20", "50"]);
|
||
|
||
/** Basis-Stütze: Rechteck 0.4×0.2 an (2,3), Höhe 3, keine Drehung. */
|
||
const rectColumn: Column = {
|
||
id: "C1",
|
||
type: "column",
|
||
floorId: "eg",
|
||
categoryCode: "50",
|
||
position: { x: 2, y: 3 },
|
||
profile: { kind: "rect", width: 0.4, depth: 0.2 },
|
||
rotation: 0,
|
||
height: 3,
|
||
};
|
||
|
||
describe("Stütze — 2D-Grundriss (generatePlan)", () => {
|
||
it("erzeugt ein Poché-Polygon mit columnId am Rechteck-Querschnitt", () => {
|
||
const plan = generatePlan(projectWithColumn(rectColumn), "eg", visible);
|
||
const polys = plan.primitives.filter(
|
||
(p) => p.kind === "polygon" && p.columnId === "C1",
|
||
);
|
||
expect(polys.length).toBe(1);
|
||
const poly = polys[0];
|
||
if (poly.kind !== "polygon") throw new Error("kein Polygon");
|
||
// Rechteck 0.4×0.2 um (2,3): Ecken bei x∈{1.8,2.2}, y∈{2.9,3.1}.
|
||
const xs = poly.pts.map((p) => p.x).sort((a, b) => a - b);
|
||
const ys = poly.pts.map((p) => p.y).sort((a, b) => a - b);
|
||
expect(poly.pts.length).toBe(4);
|
||
expect(xs[0]).toBeCloseTo(1.8, 6);
|
||
expect(xs[3]).toBeCloseTo(2.2, 6);
|
||
expect(ys[0]).toBeCloseTo(2.9, 6);
|
||
expect(ys[3]).toBeCloseTo(3.1, 6);
|
||
});
|
||
|
||
it("dreht den Querschnitt um 90° (Breite/Tiefe vertauscht)", () => {
|
||
const rotated: Column = { ...rectColumn, rotation: Math.PI / 2 };
|
||
const plan = generatePlan(projectWithColumn(rotated), "eg", visible);
|
||
const poly = plan.primitives.find(
|
||
(p) => p.kind === "polygon" && p.columnId === "C1",
|
||
);
|
||
if (!poly || poly.kind !== "polygon") throw new Error("kein Polygon");
|
||
// Nach 90°-Drehung: X-Ausdehnung = ±depth/2 = ±0.1, Y = ±width/2 = ±0.2.
|
||
const xs = poly.pts.map((p) => p.x).sort((a, b) => a - b);
|
||
const ys = poly.pts.map((p) => p.y).sort((a, b) => a - b);
|
||
expect(xs[0]).toBeCloseTo(1.9, 6);
|
||
expect(xs[3]).toBeCloseTo(2.1, 6);
|
||
expect(ys[0]).toBeCloseTo(2.8, 6);
|
||
expect(ys[3]).toBeCloseTo(3.2, 6);
|
||
});
|
||
|
||
it("blendet die Stütze aus, wenn Kategorie 50 unsichtbar ist", () => {
|
||
const plan = generatePlan(projectWithColumn(rectColumn), "eg", new Set(["20"]));
|
||
expect(plan.primitives.some((p) => p.kind === "polygon" && p.columnId)).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe("Stütze — 3D (projectToModel3d / emitColumns)", () => {
|
||
it("erzeugt eine Rechteck-Box (8 Ecken) über die volle Höhe", () => {
|
||
const model = projectToModel3d(projectWithColumn(rectColumn));
|
||
const meshes = model.meshes.filter((m) => m.kind === "extrusion");
|
||
expect(meshes.length).toBe(1);
|
||
const mesh = meshes[0];
|
||
// 4 Footprint-Punkte × (unten + oben) = 8 Vertices.
|
||
expect(mesh.positions.length).toBe(8 * 3);
|
||
// z-Werte (jedes 3. Element) liegen bei 0 (UK) und 3 (OK).
|
||
const zs = mesh.positions.filter((_, i) => i % 3 === 2);
|
||
expect(Math.min(...zs)).toBeCloseTo(0, 6);
|
||
expect(Math.max(...zs)).toBeCloseTo(3, 6);
|
||
});
|
||
|
||
it("erzeugt ein tesselliertes Kreis-Prisma (64 Ecken)", () => {
|
||
const round: Column = {
|
||
...rectColumn,
|
||
profile: { kind: "round", radius: 0.15 },
|
||
};
|
||
const model = projectToModel3d(projectWithColumn(round));
|
||
const mesh = model.meshes.find((m) => m.kind === "extrusion");
|
||
if (!mesh) throw new Error("kein Prisma-Mesh");
|
||
// 32-Eck × (unten + oben) = 64 Vertices.
|
||
expect(mesh.positions.length).toBe(64 * 3);
|
||
});
|
||
});
|
||
|
||
describe("Stütze — vertikale Lage (columnVerticalExtent)", () => {
|
||
it("nutzt Geschoss-UK + Höhe als Default", () => {
|
||
const { zBottom, zTop } = columnVerticalExtent(
|
||
projectWithColumn(rectColumn),
|
||
rectColumn,
|
||
);
|
||
expect(zBottom).toBeCloseTo(0, 6);
|
||
expect(zTop).toBeCloseTo(3, 6);
|
||
});
|
||
|
||
it("bindet UK/OK an Anker (custom + floor)", () => {
|
||
const anchored: Column = {
|
||
...rectColumn,
|
||
bottom: { mode: "custom", z: 0.5 },
|
||
top: { mode: "floor", floorId: "og" }, // baseElevation OG = 3
|
||
};
|
||
const { zBottom, zTop } = columnVerticalExtent(projectWithColumn(anchored), anchored);
|
||
expect(zBottom).toBeCloseTo(0.5, 6);
|
||
expect(zTop).toBeCloseTo(3, 6);
|
||
});
|
||
});
|
||
|
||
describe("Stütze — Footprint-Helfer (columnFootprint)", () => {
|
||
it("tesselliert ein rundes Profil als 32-Eck um die Position", () => {
|
||
const round: Column = { ...rectColumn, profile: { kind: "round", radius: 0.15 } };
|
||
const pts = columnFootprint(round);
|
||
expect(pts.length).toBe(32);
|
||
// Alle Punkte liegen im Radius 0.15 um (2,3).
|
||
for (const p of pts) {
|
||
const d = Math.hypot(p.x - 2, p.y - 3);
|
||
expect(d).toBeCloseTo(0.15, 6);
|
||
}
|
||
});
|
||
});
|