Files
DOSSIER-STANDALONE/src/plan/column.test.ts
T
karim 35299307d6 Akkumulierten grünen Arbeitsstand landen (Basis für Weiterarbeit)
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.
2026-07-09 00:57:29 +02:00

163 lines
6.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
});
});