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.
This commit is contained in:
2026-07-09 00:57:29 +02:00
parent 889cbb2c12
commit 35299307d6
131 changed files with 26501 additions and 852 deletions
+162
View File
@@ -0,0 +1,162 @@
/**
* 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);
}
});
});