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
+49
View File
@@ -4,6 +4,7 @@ import {
roomStampToDoc,
roomStampExtraLines,
roomDisplayName,
formatStampArea,
} from "./roomStamp";
import type { Room } from "./types";
@@ -82,6 +83,54 @@ describe("roomStampExtraLines", () => {
);
expect(lines).toHaveLength(0);
});
it("rundet die Fläche auf roundingStep, falls gesetzt", () => {
const lines = roomStampExtraLines(
{ name: "Bad", showFloorArea: true, showUsage: false, roundingStep: 0.5 },
{ netArea: 12.34, siaCategory: "HNF", siaLabel: "Hauptnutzfläche" },
);
expect(lines[0].text).toBe("12.5 m²");
});
it("nutzt 2 Nachkommastellen ohne roundingStep (Alt-Verhalten)", () => {
const lines = roomStampExtraLines(
{ name: "Bad", showFloorArea: true, showUsage: false },
{ netArea: 12.345, siaCategory: "HNF", siaLabel: "Hauptnutzfläche" },
);
expect(lines[0].text).toBe("12.35 m²");
});
it("zeigt die Personenzahl als eigene Zeile, wenn gesetzt", () => {
const lines = roomStampExtraLines(
{ name: "Bad", showFloorArea: false, showUsage: false, occupancy: 4 },
{ netArea: 12.345, siaCategory: "HNF", siaLabel: "Hauptnutzfläche" },
);
expect(lines).toHaveLength(1);
expect(lines[0].text).toBe("4 Pers.");
expect(lines[0].align).toBe("center");
});
it("blendet die Personenzahl-Zeile aus, wenn occupancy fehlt", () => {
const lines = roomStampExtraLines(
{ name: "Bad", showFloorArea: false, showUsage: false },
{ netArea: 12.345, siaCategory: "HNF", siaLabel: "Hauptnutzfläche" },
);
expect(lines).toHaveLength(0);
});
});
describe("formatStampArea", () => {
it("2 Nachkommastellen ohne roundingStep", () => {
expect(formatStampArea(12.345)).toBe("12.35");
expect(formatStampArea(12)).toBe("12.00");
});
it("rundet auf das nächste Vielfache von roundingStep", () => {
expect(formatStampArea(12.24, 0.5)).toBe("12");
expect(formatStampArea(12.26, 0.5)).toBe("12.5");
expect(formatStampArea(12.3, 0.1)).toBe("12.3");
expect(formatStampArea(12.34, 1)).toBe("12");
});
});
describe("roomDisplayName", () => {
+24 -3
View File
@@ -67,17 +67,32 @@ export interface RoomStampLine {
align: Align;
}
/**
* Formatiert die Bodenfläche für die Stempel-Anzeige: ohne `roundingStep` wie
* bisher auf 2 Nachkommastellen; mit `roundingStep` auf das nächste Vielfache
* davon gerundet (z. B. 0.5 → halbe m²), ohne Nachkommastellen-Rauschen.
*/
export function formatStampArea(area: number, roundingStep?: number): string {
if (roundingStep && roundingStep > 0) {
const rounded = Math.round(area / roundingStep) * roundingStep;
// Rundungsfehler aus der Fliesskomma-Division kappen (max. 2 Nachkommastellen).
return `${Math.round(rounded * 100) / 100}`;
}
return area.toFixed(2);
}
/**
* Die Live-Zeilen unter dem Stempel: Bodenfläche (mit Präfix) wenn showFloorArea,
* Nutzung (SIA-Kürzel · Bezeichnung) wenn showUsage. Die Ausrichtung folgt den
* Stempel-Feldern; fehlt sie, gilt „zentriert" (Alt-Verhalten).
* Nutzung (SIA-Kürzel · Bezeichnung) wenn showUsage, Personenzahl wenn gesetzt.
* Die Ausrichtung folgt den Stempel-Feldern; fehlt sie, gilt „zentriert"
* (Alt-Verhalten).
*/
export function roomStampExtraLines(stamp: RoomStamp, ctx: RoomStampContext): RoomStampLine[] {
const lines: RoomStampLine[] = [];
if (stamp.showFloorArea) {
const prefix = stamp.floorAreaPrefix ?? "";
lines.push({
text: `${prefix}${ctx.netArea.toFixed(2)}`,
text: `${prefix}${formatStampArea(ctx.netArea, stamp.roundingStep)}`,
align: stamp.floorAreaAlign ?? "center",
});
}
@@ -87,6 +102,12 @@ export function roomStampExtraLines(stamp: RoomStamp, ctx: RoomStampContext): Ro
align: stamp.usageAlign ?? "center",
});
}
if (stamp.occupancy != null) {
lines.push({
text: `${stamp.occupancy} Pers.`,
align: "center",
});
}
return lines;
}
+121 -4
View File
@@ -1,5 +1,15 @@
import type { Project } from "./types";
import { recomputeFloorElevations } from "./types";
import { MATERIAL_LIBRARY } from "../materials/library";
import { materialFromAsset } from "../materials/runtime";
// Bibliotheks-Materialien für die Basis-Components ab Werk (PBR-Texturen,
// s. `src/materials/library.ts`). Zuordnung nur, wo ein Bibliotheks-Asset
// sinnvoll passt — Wärmedämmung und Zementestrich bleiben bewusst ohne
// Zuweisung (kein passendes Asset in der Bibliothek), statt zu raten.
const PLASTER_ASSET = MATERIAL_LIBRARY.find((a) => a.id === "Plaster001")!;
const BRICK_ASSET = MATERIAL_LIBRARY.find((a) => a.id === "Bricks104")!;
const CONCRETE_ASSET = MATERIAL_LIBRARY.find((a) => a.id === "Concrete048")!;
// Demo-Haus mit zwei Geschossen:
// EG — rechteckiger Raum (5 × 4 m) mit einer Tür in der Südwand.
@@ -111,14 +121,44 @@ export const sampleProject: Project = {
// Bauteil-Materialien: color = Poché-Füllung im Grundriss UND 3D-Diffusfarbe;
// joinPriority höher = läuft am Stoß durch (für spätere T-Stöße).
components: [
{ id: "render-ext", name: "Aussenputz", color: "#d8d2c7", hatchId: "none", joinPriority: 10 },
{
id: "render-ext",
name: "Aussenputz",
color: "#d8d2c7",
hatchId: "none",
joinPriority: 10,
material: materialFromAsset(PLASTER_ASSET),
},
// Weißer Grund unter den durchgezogenen Dämmungs-Haarlinien (quer zur Wand).
// Kein passendes Bibliotheks-Asset (Dämmung) → bewusst ohne `material`.
{ id: "insulation", name: "Wärmedämmung", color: "#ffffff", hatchId: "sia-insulation", joinPriority: 20 },
{ id: "brick", name: "Backstein", color: "#8c5544", hatchId: "sia-brick", joinPriority: 50 },
{ id: "render-int", name: "Innenputz", color: "#efece6", hatchId: "none", joinPriority: 10 },
{
id: "brick",
name: "Backstein",
color: "#8c5544",
hatchId: "sia-brick",
joinPriority: 50,
material: materialFromAsset(BRICK_ASSET),
},
{
id: "render-int",
name: "Innenputz",
color: "#efece6",
hatchId: "none",
joinPriority: 10,
material: materialFromAsset(PLASTER_ASSET),
},
// Für spätere T-Stöße: Beton als durchlaufender Backbone (höchste Priorität).
{ id: "concrete", name: "Beton", color: "#9aa0a6", hatchId: "sia-concrete", joinPriority: 100 },
{
id: "concrete",
name: "Beton",
color: "#9aa0a6",
hatchId: "sia-concrete",
joinPriority: 100,
material: materialFromAsset(CONCRETE_ASSET),
},
// Zementestrich — oberste Schicht des mehrschichtigen Deckenaufbaus (Decken-Stile).
// Kein passendes Bibliotheks-Asset (Estrich) → bewusst ohne `material`.
{ id: "screed", name: "Zementestrich", color: "#c9c2b3", hatchId: "diagonal", joinPriority: 30 },
],
wallTypes: [
@@ -162,6 +202,80 @@ export const sampleProject: Project = {
],
},
],
// Bauteil-Typen (Bibliothek) — Tür/Fenster/Treppe, analog Wand-/Deckenstile.
// Elemente referenzieren sie per `typeId`; ohne Referenz gilt Inline-Verhalten.
doorTypes: [
{
id: "door-standard",
name: "Zimmertür 0.9 m",
kind: "dreh",
leafCount: 1,
leafStyle: "glatt",
frameThickness: 0.05,
defaultWidth: 0.9,
defaultHeight: 2.0,
},
],
windowTypes: [
{
id: "window-standard",
name: "Fenster 1.2 × 1.4 m",
kind: "drehkipp",
wingCount: 1,
glazing: "zweifach",
frameThickness: 0.06,
defaultSillHeight: 0.9,
defaultWidth: 1.2,
defaultHeight: 1.4,
},
],
stairTypes: [
{
// Standard: normale Ortbeton-Laufplatte (schräge, offene Untersicht).
id: "stair-standard",
name: "Betontreppe 1.0 m",
structure: "beton",
closedRisers: true,
treadThickness: 0.05,
nosing: 0.03,
railing: "keine",
defaultWidth: 1.0,
},
{
id: "stair-massiv",
name: "Massivtreppe (auf Sockel)",
structure: "massiv",
closedRisers: true,
treadThickness: 0.04,
nosing: 0.03,
railing: "keine",
defaultWidth: 1.0,
},
{
// Offene Wangentreppe: schwebende Tritte (unten offen), zum direkten
// Vergleich mit der Massivtreppe — im ObjectInfoPanel der Treppe zuweisen.
id: "stair-wange-holz",
name: "Wangentreppe Holz (offen)",
structure: "wange",
closedRisers: false,
treadThickness: 0.05,
nosing: 0.03,
railing: "keine",
defaultWidth: 1.0,
},
{
// Aufgesattelte Treppe mit geschlossenen Setzstufen (Tritt + Setzstufe,
// aber unten offen — der Raum darunter bleibt frei).
id: "stair-aufgesattelt",
name: "Aufgesattelt (Setzstufen)",
structure: "aufgesattelt",
closedRisers: true,
treadThickness: 0.05,
nosing: 0.025,
railing: "keine",
defaultWidth: 1.0,
},
],
// Oberste Schnitte: Geschosse + Schnitt/Ansicht + freie Zeichnung.
// baseElevation wird über recomputeFloorElevations gestapelt: EG=0, OG=2.6.
drawingLevels: recomputeFloorElevations([
@@ -257,6 +371,7 @@ export const sampleProject: Project = {
hostWallId: "W1",
categoryCode: "21",
kind: "window",
typeId: "window-standard",
position: 3.0, // Startkante 3.0 m → Fenster x 3.04.0, rechts von W9 mit Massiv-Abstand
width: 1.0,
height: 1.2,
@@ -268,6 +383,7 @@ export const sampleProject: Project = {
hostWallId: "W3",
categoryCode: "21",
kind: "window",
typeId: "window-standard",
// Startkante 0.6 m ab W3-Start (x=5) → Fenster x 3.04.4, rechts von W9 (x=2.4)
// mit sichtbarer Massiv-Wand dazwischen (die Querwand stösst auf Mauerwerk).
position: 0.6,
@@ -306,6 +422,7 @@ export const sampleProject: Project = {
floorId: "eg",
categoryCode: "40",
shape: "straight",
typeId: "stair-standard",
start: { x: 3.7, y: 0.4 },
dir: { x: 0, y: 1 },
runLength: 3.2,
+693 -2
View File
@@ -18,6 +18,10 @@ export type { SiaCategory } from "../geometry/roomArea";
// Rich-Text-Dokument für den Raum-Stempel (frei editierbarer Teil).
import type { RichTextDoc, Align } from "../text/richText";
export type { RichTextDoc } from "../text/richText";
// Ansichts-Enums für Ausschnitte/View-Snapshots (Ansichtstyp, Kamera-Preset,
// Detailgrad). Rein type-only importiert — zur Laufzeit erased, kein Zyklus
// (TopBar importiert seinerseits nur `Project` als Typ). Wie SiaCategory oben.
import type { DetailLevel, View3d, ViewType } from "../ui/TopBar";
// ── Ressourcen-Bibliotheken (Vectorworks-/DOSSIER-Stil) ────────────────────
// Verwaltete, per id verwiesene Stil-Ressourcen. Verweis-Kette:
@@ -272,6 +276,166 @@ export interface CeilingType {
layers: Layer[];
}
// ── Bauteil-Typen: Tür / Fenster / Treppe ──────────────────────────────────
// Wiederverwendbare Stile (Presets) für Türen, Fenster und Treppen — analog
// `WallType`/`CeilingType` (Bibliothek im Projekt, referenziert per `typeId`).
// Getrennte Typen (kein gemeinsames „OpeningType"), damit türspezifische und
// fensterspezifische Parameter sauber je Gattung wohnen. Ein Element ohne
// `typeId` behält sein heutiges Inline-Verhalten (rückwärtskompatibel).
// Detailstufe der Bauteil-Darstellung (Grundriss + 3D) nutzt das bestehende
// {@link DetailLevel}-Vokabular ("grob" | "mittel" | "fein", aus ui/TopBar) —
// bewusst KEIN zweites Detail-Enum, damit Ansicht und Bauteil dieselbe Skala
// teilen. Ausgewertet an der Symbol-/Mesh-Erzeugung (Phase 2).
/**
* Ein Türtyp (Türstil) — wiederverwendbares Preset für {@link Opening} mit
* `kind: "door"`. Bündelt Bauart, Blattausführung und Standardmaße. Werte am
* einzelnen Element (Breite/Höhe/Anschlag) übersteuern die Typ-Defaults.
*/
export interface DoorType {
id: string;
name: string;
/**
* Bauart: normaler Drehflügel, reine Wandöffnung (kein Blatt/Schwenk),
* Schiebetür. Ersetzt/ergänzt das Inline-Feld `Opening.doorType`.
*/
kind: "dreh" | "schiebe" | "wandoeffnung";
/** Anzahl Türblätter (1 = einflügelig, 2 = zweiflügelig). */
leafCount: 1 | 2;
/** Blatt-Ausführung (3D/Detail): glatt, Kassette, Glasfüllung. */
leafStyle: "glatt" | "kassette" | "glas";
/** Glasanteil des Blatts (0..1); nur bei `leafStyle: "glas"` relevant. */
glazingRatio?: number;
/** Zargen-/Rahmenstärke quer zur Wand (Meter). */
frameThickness: number;
/** Rahmen-/Zargen-Tiefe in Wandrichtung (Meter); fehlt ⇒ aus Wanddicke. */
frameDepth?: number;
/** Default-Lichtbreite (Meter) für neu platzierte Türen dieses Typs. */
defaultWidth: number;
/** Default-Lichthöhe (Meter). */
defaultHeight: number;
/** Bodenschwelle/Anschlag zeichnen (3D/Detail). */
threshold?: boolean;
}
/**
* Ein Fenstertyp (Fensterstil) — wiederverwendbares Preset für {@link Opening}
* mit `kind: "window"`. Bündelt Öffnungsart, Flügelzahl, Verglasung und
* Standardmaße inkl. Brüstungshöhe.
*/
export interface WindowType {
id: string;
name: string;
/** Öffnungsart: Dreh, Kipp, Dreh-Kipp, fest verglast, Schiebe. */
kind: "dreh" | "kipp" | "drehkipp" | "fest" | "schiebe";
/** Anzahl Flügel (14) → Mittelpfosten = wingCount 1 (vgl. `Opening.wingCount`). */
wingCount: number;
/** Verglasung: Einfach/Zweifach/Dreifach (3D-Scheibenzahl). */
glazing: "einfach" | "zweifach" | "dreifach";
/** Rahmenstärke quer zur Wand (Meter). */
frameThickness: number;
/** Rahmen-Tiefe in Wandrichtung (Meter); fehlt ⇒ aus Wanddicke. */
frameDepth?: number;
/** Default-Brüstungshöhe über Wand-UK (Meter). */
defaultSillHeight: number;
/** Default-Lichtbreite (Meter). */
defaultWidth: number;
/** Default-Lichthöhe (Meter). */
defaultHeight: number;
/** Fensterbank zeichnen (Detail): keine / innen / aussen / beide. */
sillBoard?: "keine" | "innen" | "aussen" | "beide";
}
/**
* Ein Treppentyp (Treppenstil) — wiederverwendbares Preset für {@link Stair}.
* Bündelt Tragart, Stufenausbildung und Geländer. Geometrische Grundform
* (gerade/L/Wendel) + Lauflänge/Stufenzahl bleiben am einzelnen Element.
*/
export interface StairType {
id: string;
name: string;
/**
* Tragart:
* • "massiv" — Vollblock bis zur Treppen-UK (Treppe auf Erdreich/Sockel).
* • "beton" — Ortbeton-Laufplatte mit SCHRÄGER, offener Untersicht
* (die klassische „normale Betontreppe" zwischen zwei Geschossen).
* • "wange" — Wangentreppe (schwebende Tritte, offen).
* • "aufgesattelt" — aufgesattelte Tritte (mit Setzstufen, offen).
* • "spindel" — Spindel-/Wendeltreppe (offen).
*/
structure: "massiv" | "beton" | "wange" | "aufgesattelt" | "spindel";
/** Setzstufen geschlossen (true) oder offen (false, durchsichtige Tritte). */
closedRisers: boolean;
/** Trittstufendicke (Meter). */
treadThickness: number;
/** Trittkanten-Überstand / Nase (Meter); fehlt ⇒ 0. */
nosing?: number;
/** Handlauf/Geländer: keine / links / rechts / beide (in Laufrichtung). */
railing?: "keine" | "links" | "rechts" | "beide";
/** Default-Laufbreite (Meter) für neu platzierte Treppen dieses Typs. */
defaultWidth: number;
}
// ── Grafische Overrides (Regel-Engine) ─────────────────────────────────────
// ArchiCAD-/Vectorworks-Stil: benannte Regeln `condition → actions`, die beim
// RENDERN als oberste Schicht über die By-Layer/By-Object-Attribut-Auflösung
// gelegt werden (reines Rendering-Overlay — die Elementdaten bleiben
// unverändert, jederzeit reversibel). Regeln werden additiv angewendet: pro
// Aktions-Feld gewinnt die OBERSTE (erste) aktive Regel, die es setzt.
// Auswertung: `src/overrides/engine.ts`; Einhängepunkt: `plan/generatePlan.ts`.
/**
* Bedingungs-Typ einer Override-Regel — wogegen der Wert verglichen wird:
* • "layer_name" — Name ODER Code der Ebene (LayerCategory) des Elements.
* • "object_name" — Element-/Typ-Name (z. B. Wandtyp-Name, Raum-Name,
* Öffnungs-Label, Drawing2D-Formname).
* Ein `user_string`-Tag (DOSSIER-Rhino) existiert am Element noch nicht und
* ist deshalb bewusst NICHT enthalten (kein Schein-Feature).
*/
export type OverrideConditionType = "layer_name" | "object_name";
/** Vergleichs-Operator einer Override-Bedingung (case-insensitiv). */
export type OverrideOperator =
| "equals"
| "contains"
| "starts_with"
| "not_equals";
/** Bedingung einer Override-Regel. */
export interface OverrideCondition {
type: OverrideConditionType;
operator: OverrideOperator;
/** Vergleichswert (Freitext, case-insensitiv verglichen). */
value: string;
}
/**
* Aktionen einer Override-Regel — jede optional (Teilaktionen sind erlaubt;
* fehlende Felder lassen die reguläre Auflösung unberührt).
*/
export interface OverrideActions {
/** Strich-/Umrandungsfarbe (hex). */
color?: string;
/** Strichstärke in mm Papier. */
lineweight?: number;
/** Linienstil (Line Manager) — wirkt, wo Elemente einen LineStyle tragen (Drawing2D). */
linetypeId?: string;
}
/**
* Eine grafische Override-Regel. Lebt in `Project.overrideRules` (Reihenfolge
* = Priorität, oben gewinnt) und ist einzeln aktivier-/deaktivierbar.
*/
export interface OverrideRule {
id: string;
name: string;
/** Deaktivierte Regeln werden bei der Auswertung übersprungen. */
enabled: boolean;
condition: OverrideCondition;
actions: OverrideActions;
}
// ── Parametrische Wände (Parametric Walls) ────────────────────────────────
// Parametrische Wand-Regeln generieren automatisch Wall[]-Arrays — analog zu
// FreeCAD BIM. Sie leben in Project.resources.parametricWalls[] und werden
@@ -619,8 +783,26 @@ export interface Wall {
* Z-Wert; „floor" bindet die OK an ein (z. B. nächsthöheres) Geschoss.
*/
top?: VerticalAnchor;
/**
* TERMINIERUNGS-Regel am horizontalen Deckenanschluss (Zuschnitt, NICHT
* Priorität — bewusst getrennt von `Component.joinPriority`/der Schnitt-
* Boolean-Dominanz gehalten). Steuert, ob ein Wandband, das eine dominante
* Deckenschicht durchstösst, oberhalb der Decke „wieder auftaucht":
* • `undefined`/"both" — heutiges Verhalten: die dominante Deckenschicht
* stanzt nur ihr z-Band aus, das Wandband bleibt oben UND unten erhalten.
* • "below" — die Wand ENDET an der Decke: nur der Teil UNTER dem höchst-
* gelegenen dominanten Cut bleibt (Regelfall, Wand steigt von unten in die
* Decke). Der oberhalb der Decke stehende Rest wird verworfen.
* • "above" — spiegelbildlich: nur der Teil ÜBER dem tiefsten dominanten Cut
* bleibt (Brüstung/Attika, die von oben an die Decke stösst).
* Additiv; Alt-Projekte laden unverändert (Default = "both").
*/
sliceTermination?: SliceTermination;
}
/** Terminierungs-Regel einer Wand am Deckenanschluss (siehe {@link Wall.sliceTermination}). */
export type SliceTermination = "both" | "below" | "above";
/**
* Eine Decke (Slab) — ein geschossgebundenes, mehrschichtiges Flächenbauteil,
* definiert über einen GESCHLOSSENEN Umriss (Polygon) im Grundriss und eine
@@ -631,7 +813,9 @@ export interface Wall {
* Vertikale Lage: die OBERKANTE (OK) der Decke. Fehlt `top`, liegt die OK an der
* Oberkante des Geschosses (baseElevation + floorHeight — also bündig mit dem
* Wandkopf); die Decke wächst um `thickness` nach UNTEN. `top: custom` setzt eine
* absolute Z-Höhe, `top: floor` bindet die OK an ein Geschoss.
* absolute Z-Höhe, `top: floor` bindet die OK an ein Geschoss. Optional übersteuert
* `bottom` die UNTERKANTE (UK) direkt (analog zur Wand); fehlt `bottom`, bleibt es
* beim heutigen Verhalten UK = OK `thickness`.
*/
export interface Ceiling {
id: string;
@@ -699,6 +883,12 @@ export interface Ceiling {
* Oberkante des Geschosses (baseElevation + floorHeight).
*/
top?: VerticalAnchor;
/**
* Vertikale Bindung der UNTERKANTE (UK). Fehlt sie, ergibt sich die UK aus
* OK `thickness` (= heutiges Verhalten). „custom" setzt einen absoluten
* Z-Wert; „floor" bindet die UK an ein Geschoss.
*/
bottom?: VerticalAnchor;
}
/** Schwenkrichtung der Tür relativ zur Wandachse. */
@@ -772,6 +962,18 @@ export interface Opening {
* "beide" → beide Linien (Default wenn nicht gesetzt)
*/
lintelLines?: "keine" | "innen" | "aussen" | "beide";
/**
* Referenz auf einen Bauteil-Typ (Bibliothek): {@link DoorType} bei
* `kind: "door"`, {@link WindowType} bei `kind: "window"`. Fehlt sie, gilt
* das heutige Inline-Verhalten (rückwärtskompatibel). Typ-Defaults liefern
* Bauart/Rahmen/Verglasung; Element-Felder (Breite/Höhe/…) übersteuern.
*/
typeId?: string;
/**
* Optionale per-Element-Übersteuerung der Detailstufe (grob/normal/
* detailliert); fehlt sie, gilt die Ansichts-/Projekt-Detailstufe.
*/
detailLevel?: DetailLevel;
/**
* Optionale Übersteuerung der Strich-/Symbolfarbe; sonst gilt die
* Kategorie-Farbe.
@@ -862,6 +1064,17 @@ export interface Stair {
* `dir` (Default). false kehrt Auf-/Abpfeil um (Treppe steigt zum Start hin).
*/
up?: boolean;
/**
* Referenz auf einen {@link StairType} (Bibliothek). Fehlt sie, gilt das
* heutige Inline-Verhalten (rückwärtskompatibel). Typ-Defaults liefern
* Tragart/Stufenausbildung/Geländer; Element-Felder übersteuern.
*/
typeId?: string;
/**
* Optionale per-Element-Übersteuerung der Detailstufe (grob/normal/
* detailliert); fehlt sie, gilt die Ansichts-/Projekt-Detailstufe.
*/
detailLevel?: DetailLevel;
/**
* Optionale Übersteuerung der Strich-/Umrandungsfarbe; sonst gilt die
* Kategorie-Farbe.
@@ -948,6 +1161,17 @@ export interface RoomStamp {
floorAreaAlign?: Align;
/** Ausrichtung der Nutzungs-Zeile. Fehlt sie, gilt „zentriert" (Alt-Verhalten). */
usageAlign?: Align;
/**
* Personenzahl (Live-Zeile), z. B. für Nutzungsauflagen. Fehlt/undefined:
* keine Personenzahl-Zeile (Alt-Verhalten).
*/
occupancy?: number;
/**
* Rundungsschritt der angezeigten Bodenfläche in m² (z. B. 0.5 → auf halbe
* m² gerundet). Fehlt er, gilt das Alt-Verhalten: 2 Nachkommastellen ohne
* Schrittrundung.
*/
roundingStep?: number;
}
/** Eine Tür, gehostet in einer Wand. Ihr Geschoss ergibt sich aus der Wand. */
@@ -980,7 +1204,19 @@ export type Drawing2DGeom =
| { shape: "rect"; min: Vec2; max: Vec2 }
| { shape: "circle"; center: Vec2; r: number }
| { shape: "arc"; center: Vec2; r: number; a0: number; a1: number }
| { shape: "text"; at: Vec2; text: string; height: number; angle: number };
| {
shape: "text";
at: Vec2;
text: string;
height: number;
angle: number;
/**
* Optionale Spaltenbreite in Metern (Textspalte/Absatztext). Ist sie gesetzt,
* wird der Text beim Rendern wortweise auf diese Breite umgebrochen; fehlt
* sie, bleibt es einzeiliger Text (heutiges Verhalten).
*/
width?: number;
};
/** Ein freies 2D-Zeichenelement auf einer Zeichnungsebene. */
export interface Drawing2D {
@@ -1067,6 +1303,96 @@ export interface EdgeGrip {
export type Element = Wall | Ceiling | Opening | Door | Stair | Room | Drawing2D;
// ── Extrudierter Körper (truck-Integration, docs/design/truck-plan.md) ─────
// Ein per `extrude`-Befehl aus einem geschlossenen 2D-Profil (Polylinie-Ring/
// Rechteck) erzeugter 3D-Körper. Die eigentliche B-Rep-Extrusion (truck-WASM,
// `engine/truckSolid.ts`) läuft NICHT hier, sondern asynchron in
// `toWalls3d.ts` (emitExtrudedSolids) — dieser Typ hält nur die Rohdaten.
/** Ein extrudierter Körper: geschlossenes Profil (Modell-Meter) + Höhe. */
export interface ExtrudedSolid {
id: string;
type: "extrudedSolid";
/** Geschoss, dessen `baseElevation` die UK des Körpers bestimmt. */
levelId: string;
/**
* Geschlossenes Profil in der XY-Ebene (Grundriss), ≥3 Punkte. Bei einem
* Kreis-Profil (`circle` gesetzt) eine Tessellierung des Kreises (48-Eck) —
* für 2D-Footprint/Auswahl/bbox/Verschieben; die 3D-Extrusion nutzt in dem
* Fall stattdessen `circle` (echte runde truck-Extrusion, `extrudeCircle`).
*/
points: Vec2[];
/** Extrusionshöhe in Metern (> 0), nach +Z ab der Geschoss-UK. */
height: number;
/**
* Gesetzt, wenn das Profil ein Kreis ist (aus einem `Drawing2D` mit
* `shape:"circle"`) — `points` bleibt die Tessellierung, `toWalls3d.ts`
* (emitExtrudedSolids) nutzt `circle` für die runde 3D-Extrusion.
*/
circle?: { center: Vec2; r: number };
/**
* Verjüngung 0 (Prisma, Default/fehlend) … 1 (Spitze — Kegel bei Kreis-,
* Pyramide bei Polygon-Profil). Linear zum Profil-Schwerpunkt skaliert.
*/
taper?: number;
}
// ── Stütze (Column, Tragwerk) ──────────────────────────────────────────────
// Eine Stütze ist im Kern eine PLATZIERTE PROFIL-EXTRUSION: ein an `position`
// gesetztes 2D-Profil (Rechteck oder Kreis), um `rotation` gedreht, über `height`
// nach oben extrudiert. Anders als der generische `ExtrudedSolid` trägt sie
// vollständige BIM-Attribute (Geschoss, Kategorie, optionales Bauteil, UK/OK-
// Anker) — sie lebt daher in `Project.columns` mit eigenem Selektionskanal.
/**
* Querschnitt einer Stütze im Grundriss (Modell-Meter), lokal um `position`.
* • "rect" — Rechteck `width` (X, quer) × `depth` (Y, längs), vor Rotation.
* • "round" — Kreis mit `radius`.
*/
export type ColumnProfile =
| { kind: "rect"; width: number; depth: number }
| { kind: "round"; radius: number };
/**
* Eine Stütze (Column) — ein geschossgebundenes Tragwerk-Bauteil (Kategorie
* „50"), definiert über einen Einfügepunkt `position`, ein Profil, eine Drehung
* und eine Höhe. Vertikale Lage analog Wand (UK/OK-Anker, sonst Geschoss-UK +
* `height`).
*/
export interface Column {
id: string;
type: "column";
/** Zugehörige Zeichnungsebene (Geschoss). */
floorId: string;
/** Grafik-Kategorie (Ebene), z. B. "50" für Tragwerk. */
categoryCode: string;
/** Einfügepunkt (Profil-Mitte) im Grundriss (Meter). */
position: Vec2;
/** Querschnitt (Rechteck oder Kreis). */
profile: ColumnProfile;
/** Drehung des Profils um `position` (Radiant, CCW; Default 0). */
rotation: number;
/** Höhe in Metern (> 0), nach +Z ab der UK. */
height: number;
/** Optionales tragendes Bauteil (Component) für Poché/3D-Farbe/Schraffur. */
componentId?: string;
/**
* Vertikale Bindung der Unterkante (UK). Fehlt sie, sitzt die UK auf der
* baseElevation des zugehörigen Geschosses (= Wand-Default).
*/
bottom?: VerticalAnchor;
/**
* Vertikale Bindung der Oberkante (OK). Fehlt sie, ergibt sich die OK aus
* UK + `height`.
*/
top?: VerticalAnchor;
/**
* Optionale Übersteuerung der Strich-/Umrandungsfarbe; sonst gilt die
* Kategorie-Farbe.
*/
color?: string;
}
// ── Kontext-Geometrie (importiert / abgeleitet, NICHT semantisch) ───────────
// Importierte Geometrie ist „dumme" KONTEXT-Geometrie (Anzeige + späteres
// Snap-Ziel), KEIN Teil des semantischen BIM-Modells. Sie lebt in einer eigenen
@@ -1160,6 +1486,278 @@ export interface TerrainMesh {
export type ContextObject = ImportedMesh | ContourSet | TerrainMesh;
/** Das gesamte Projekt. */
/**
* Ein Ausschnitt (View-Snapshot) — eine benannte, gespeicherte Ansicht, die den
* kompletten Darstellungszustand einfängt und per Klick wiederherstellt
* (DOSSIER A2, ROADMAP §2c/§11). KEIN neuer Zustand, sondern ein Container, der
* bereits vorhandene Bausteine KOMPONIERT + benennt: Ansicht (Typ/Kamera/
* Massstab/Detail), Sichtbarkeit (Ebenen-/Zeichnungskombination, gleiches
* Payload-Format wie {@link https LayerCombo}/`DrawingCombo`), aktive Overrides
* und das aktive Geschoss. Lebt im DOKUMENT (`Project.viewSnapshots`), nicht im
* localStorage — Ausschnitte gehören zum Projekt (Export/Teilen).
*/
export interface ViewSnapshot {
id: string;
name: string;
/**
* LEGACY: name-basierte Flach-Gruppierung (Phase 1). Wird von der neuen
* Ordner-/Baum-UI nicht mehr genutzt, bleibt aber für Alt-Projekte gültig.
* Neue Ausschnitte tragen stattdessen `folderId` (echte Ordner-Referenz).
*/
folder?: string;
/**
* Ordner (Baumstruktur) des Ausschnitte-Panels, in dem der Ausschnitt liegt
* ({@link ViewSnapshotFolder}). Fehlt er (oder verweist er ins Leere), liegt
* der Ausschnitt auf der Wurzelebene. Additiv — Alt-Projekte laden unverändert.
*/
folderId?: string;
// ── Ansicht ──────────────────────────────────────────────────────────────
/** Ansichtstyp (Grundriss / Perspektive). */
viewType: ViewType;
/** Kanonischer 3D-Blickwinkel (Front/Top/Iso/… bzw. freie Perspektive). */
view3d: View3d;
/** Sichtwinkel (FOV) der 3D-Perspektivkamera in Grad. Optional (nur 3D relevant). */
fov?: number;
/** Papier-Massstab „1:N" (der Nenner). */
scaleDenominator: number;
/** Detailgrad der Darstellung (grob/mittel/fein). */
detail: DetailLevel;
// ── Zustand ──────────────────────────────────────────────────────────────
/** Aktives Geschoss/Zeichnungsebene beim Erfassen. */
activeLevelId: string;
/** Ebenen-Sichtbarkeit: Kategorie-`code` → sichtbar (wie `LayerCombo.codes`). */
layerVisibility: Record<string, boolean>;
/** Zeichnungsebenen-Sichtbarkeit: DrawingLevel-`id` → sichtbar (wie `DrawingCombo.ids`). */
drawingVisibility: Record<string, boolean>;
/**
* Ids der beim Erfassen AKTIVEN (`enabled`) Override-Regeln. Beim
* Wiederherstellen wird jede Regel des Projekts auf `enabled = ids.includes(id)`
* gesetzt. Optional, damit hand-/altangelegte Snapshots ohne Feld gültig sind.
*/
enabledOverrideRuleIds?: string[];
}
/**
* Ein Ordner im Ausschnitte-Baum (DOSSIER A2). Ausschnitte verweisen per
* `folderId` auf ihren Ordner; `parentId` erlaubt Verschachtelung (Ordner in
* Ordner). Fehlt `parentId`, liegt der Ordner auf der Wurzelebene. Additiv —
* Alt-Projekte laden unverändert (spiegelbildlich zu {@link LayoutFolder}).
*/
export interface ViewSnapshotFolder {
id: string;
name: string;
/** Übergeordneter Ordner; ohne → Wurzelebene. */
parentId?: string;
}
// ── Layout-Blätter mit Masterlayout (DOSSIER A3, ROADMAP §11) ───────────────
//
// Ein „Layout" ist ein Druck-/Plan-Blatt (A4/A3), auf dem mehrere Viewports
// platziert werden; jeder Viewport ist an einen Ausschnitt ({@link ViewSnapshot})
// gebunden und rendert dessen Plan im gewählten Massstab. Ein Masterlayout
// liefert die gemeinsamen Blatt-Elemente (Titelblock/Rahmen), die die Layouts
// erben. Alles lebt im DOKUMENT (`Project.layouts`/`Project.masterLayouts`),
// nicht im localStorage — Layouts gehören zum Projekt. Additiv: bestehende
// Projekte ohne diese Felder bleiben unverändert gültig.
/**
* Papierformat eines Layout-Blatts (Blattmasse in mm identisch zum PDF-Export).
* ISO-216-Reihen A0A6 und B0B6; Alt-Projekte (nur A4/A3) laden unverändert.
*/
export type LayoutPaperFormat =
| "a0"
| "a1"
| "a2"
| "a3"
| "a4"
| "a5"
| "a6"
| "b0"
| "b1"
| "b2"
| "b3"
| "b4"
| "b5"
| "b6";
/** Ausrichtung eines Layout-Blatts. */
export type LayoutOrientation = "portrait" | "landscape";
/**
* Einfache Textfelder des Titelblocks (Master). Alle optional — leere Felder
* werden beim Auflösen aus dem Projekt/Layout mit sinnvollen Defaults gefüllt
* (Projektname ← `Project.name`, Blattname ← `Layout.name`, Datum ← heute).
*/
export interface LayoutTitleBlock {
projectName?: string;
sheetName?: string;
scale?: string;
date?: string;
author?: string;
}
/**
* Masterlayout — gemeinsame Blatt-Elemente (Titelblock + optionaler Rahmen), die
* einzelne {@link Layout}s per `masterId` erben. Eine Änderung am Master schlägt
* auf alle Layouts durch, die ihn referenzieren (InDesign-/ArchiCAD-Muster).
*/
export interface MasterLayout {
id: string;
name: string;
paper: LayoutPaperFormat;
orientation: LayoutOrientation;
/** Titelblock-Textfelder (Vorlage; leere Felder werden aufgelöst/aufgefüllt). */
titleBlock: LayoutTitleBlock;
/**
* Ordner (Master-Baum), in dem das Masterlayout liegt ({@link LayoutFolder} mit
* `kind:"master"`). Fehlt er (oder verweist auf einen gelöschten/Layout-Ordner),
* liegt der Master auf der Wurzelebene. Additiv — Alt-Projekte laden unverändert.
*/
folderId?: string;
/** Blattrahmen zeichnen (Randlinie). Optional, Default `true`. */
border?: boolean;
/**
* Freie Blattbreite in mm. Ist sie (zusammen mit `customHeightMm`) gesetzt,
* überschreibt sie `paper`/A4·A3 (die effektive Blattgrösse ist dann
* `customWidthMm`×`customHeightMm`, `orientation` wird ignoriert). Fehlt sie,
* gilt `paper`+`orientation` wie bisher. Additiv — Alt-Projekte laden unverändert.
*/
customWidthMm?: number;
/** Freie Blatthöhe in mm (siehe {@link MasterLayout.customWidthMm}). */
customHeightMm?: number;
}
/**
* Ein platzierter Viewport auf einem Layout-Blatt — Position/Grösse in Papier-mm,
* gebunden an einen Ausschnitt (`snapshotId`). `scaleDenominator` übersteuert
* optional den Massstab des Ausschnitts (Massstab pro Viewport).
*/
export interface LayoutViewport {
id: string;
/** Id des gebundenen Ausschnitts ({@link ViewSnapshot}). */
snapshotId: string;
/** Linke Kante auf dem Blatt (mm, von links). */
xMm: number;
/** Obere Kante auf dem Blatt (mm, von oben). */
yMm: number;
/** Breite des Viewport-Rahmens auf dem Blatt (mm). */
widthMm: number;
/** Höhe des Viewport-Rahmens auf dem Blatt (mm). */
heightMm: number;
/** Optionaler Massstab-Nenner (1:N); übersteuert `snapshot.scaleDenominator`. */
scaleDenominator?: number;
}
/**
* Eine 2D-Annotation (Linie/Rechteck/Text), direkt auf dem Layout-Blatt in
* Papier-mm-Koordinaten gezeichnet — eine vom Viewport-Baum unabhängige
* Zusatzschicht (Markup). `color`/`weightMm` sind optional (Editor-Defaults:
* `#111111` / 0.25 mm); Text trägt statt `weightMm` seine Zeilenhöhe.
*/
export type LayoutAnnotation =
| {
id: string;
kind: "line";
x1Mm: number;
y1Mm: number;
x2Mm: number;
y2Mm: number;
color?: string;
weightMm?: number;
}
| {
id: string;
kind: "rect";
xMm: number;
yMm: number;
widthMm: number;
heightMm: number;
color?: string;
weightMm?: number;
}
| {
id: string;
kind: "text";
xMm: number;
yMm: number;
text: string;
heightMm: number;
color?: string;
};
/**
* Flaches Patch-Objekt für {@link LayoutAnnotation} — alle Felder ALLER Arten
* optional (statt eines pro Art unterscheidenden Unions), damit CRUD/Handler
* unabhängig von der konkreten `kind` bleiben (der Aufrufer setzt ohnehin nur
* die zur gewählten Annotation passenden Felder).
*/
export type LayoutAnnotationPatch = Partial<{
x1Mm: number;
y1Mm: number;
x2Mm: number;
y2Mm: number;
xMm: number;
yMm: number;
widthMm: number;
heightMm: number;
text: string;
color: string;
weightMm: number;
}>;
/**
* Ein Layout-Blatt — Papierformat/Ausrichtung, optionaler Master (`masterId`)
* und die platzierten Viewports.
*/
export interface Layout {
id: string;
name: string;
paper: LayoutPaperFormat;
orientation: LayoutOrientation;
/** Referenz auf ein {@link MasterLayout}; ohne → kein Master (blanko Blatt). */
masterId?: string;
/**
* Ordner (Baumstruktur), in dem das Layout liegt ({@link LayoutFolder}). Fehlt
* er (oder verweist auf einen gelöschten Ordner), liegt das Layout auf der
* Wurzelebene. Additiv — Alt-Projekte laden unverändert.
*/
folderId?: string;
/**
* Freie Blattbreite in mm — überschreibt `paper`/A4·A3, siehe
* {@link MasterLayout.customWidthMm}. Bei gesetztem `masterId` erbt das Blatt
* ohnehin die Master-Grösse; eigene Custom-Werte gelten nur ohne Master.
*/
customWidthMm?: number;
/** Freie Blatthöhe in mm (siehe {@link Layout.customWidthMm}). */
customHeightMm?: number;
viewports: LayoutViewport[];
/**
* 2D-Markups direkt auf dem Blatt (Linie/Rechteck/Text), siehe
* {@link LayoutAnnotation}. Additiv — Alt-Layouts ohne dieses Feld laden
* unverändert (Editor/CRUD behandeln ein fehlendes Array wie ein leeres).
*/
annotations?: LayoutAnnotation[];
}
/**
* Ein Ordner im Layouts-Baum (DOSSIER A3). Layouts verweisen per `folderId` auf
* ihren Ordner; `parentId` erlaubt Verschachtelung (Ordner in Ordner). Fehlt
* `parentId`, liegt der Ordner auf der Wurzelebene. Masterlayouts liegen NICHT
* in Ordnern (sie sind Vorlagen). Additiv — Alt-Projekte laden unverändert.
*/
export interface LayoutFolder {
id: string;
name: string;
/** Übergeordneter Ordner; ohne → Wurzelebene. */
parentId?: string;
/**
* Baum-Zugehörigkeit: `"layout"`-Ordner tragen Layouts, `"master"`-Ordner
* tragen Masterlayouts. Die beiden Bäume bleiben getrennt (ein Master-Ordner
* erscheint nie im Layout-Baum und umgekehrt). Fehlt das Feld, gilt `"layout"`
* (rückwärtskompatibel — Alt-Projekte ohne `kind` bleiben Layout-Ordner).
*/
kind?: "layout" | "master";
}
export interface Project {
id: string;
name: string;
@@ -1177,6 +1775,20 @@ export interface Project {
* `Ceiling.wallTypeId` gegen `wallTypes` auf (siehe `getCeilingType`).
*/
ceilingTypes?: CeilingType[];
/**
* Türtypen-Bibliothek (Türstile), analog `wallTypes`. Optional, damit
* bestehende Projekte/Tests ohne `doorTypes` gültig bleiben; Türen ohne
* `typeId` verhalten sich wie bisher (Inline-Felder).
*/
doorTypes?: DoorType[];
/**
* Fenstertypen-Bibliothek (Fensterstile). Optional (siehe `doorTypes`).
*/
windowTypes?: WindowType[];
/**
* Treppentypen-Bibliothek (Treppenstile). Optional (siehe `doorTypes`).
*/
stairTypes?: StairType[];
/** Oberste Schnitte: Geschosse + Schnitte/Ansichten. */
drawingLevels: DrawingLevel[];
/** Grafik-Kategorie-Baum (geschossübergreifend). */
@@ -1213,6 +1825,18 @@ export interface Project {
* Projekte/Tests ohne `context` gültig bleiben (Default: leer behandeln).
*/
context?: ContextObject[];
/**
* Extrudierte Körper (truck-Integration) — geschlossene 2D-Profile mit Höhe,
* siehe {@link ExtrudedSolid}. Optional, damit bestehende Projekte/Tests ohne
* `extrudedSolids` gültig bleiben (Default: leer behandeln).
*/
extrudedSolids?: ExtrudedSolid[];
/**
* Stützen (Columns) — geschossgebundene Tragwerk-Bauteile (platzierte
* Profil-Extrusionen), siehe {@link Column}. Optional, damit bestehende
* Projekte/Tests ohne `columns` gültig bleiben (Default: leer behandeln).
*/
columns?: Column[];
/**
* Bibliothek parametrischer Wand-Regelwerke. Optional, damit bestehende
* Projekte ohne `parametricWalls` gültig bleiben (Default: leer behandeln).
@@ -1220,6 +1844,45 @@ export interface Project {
* aufgelöst — generiert Wall[]-Objekte bei Bedarf.
*/
parametricWalls?: ParametricWall[];
/**
* Grafische Override-Regeln (Regel-Engine, Reihenfolge = Priorität, oben
* gewinnt) — reines Rendering-Overlay, siehe {@link OverrideRule}. Optional,
* damit bestehende Projekte/Tests ohne `overrideRules` gültig bleiben
* (Default: leer behandeln).
*/
overrideRules?: OverrideRule[];
/**
* Ausschnitte / View-Snapshots (DOSSIER A2) — benannte, wiederherstellbare
* Ansichten (siehe {@link ViewSnapshot}). Optional, damit bestehende
* Projekte/Tests ohne `viewSnapshots` unverändert gültig bleiben (Default:
* leer behandeln). Gehört ins Dokument (nicht localStorage).
*/
viewSnapshots?: ViewSnapshot[];
/**
* Ordner des Ausschnitte-Baums (DOSSIER A2) — Gruppierung der
* {@link ViewSnapshot}s per `folderId`. Optional, damit bestehende
* Projekte/Tests ohne `viewSnapshotFolders` gültig bleiben (Default: leer →
* alle Ausschnitte auf Wurzelebene).
*/
viewSnapshotFolders?: ViewSnapshotFolder[];
/**
* Layout-Blätter (DOSSIER A3) — Druck-/Plan-Blätter mit platzierten Viewports
* (siehe {@link Layout}). Optional, damit bestehende Projekte/Tests ohne
* `layouts` gültig bleiben (Default: leer behandeln). Gehört ins Dokument.
*/
layouts?: Layout[];
/**
* Masterlayouts (DOSSIER A3) — gemeinsame Blatt-Elemente (Titelblock/Rahmen),
* die einzelne {@link Layout}s erben (siehe {@link MasterLayout}). Optional,
* damit bestehende Projekte/Tests ohne `masterLayouts` gültig bleiben.
*/
masterLayouts?: MasterLayout[];
/**
* Ordner des Layouts-Baums (DOSSIER A3) — Gruppierung der {@link Layout}s per
* `folderId`. Optional, damit bestehende Projekte/Tests ohne `layoutFolders`
* gültig bleiben (Default: leer → alle Layouts auf Wurzelebene).
*/
layoutFolders?: LayoutFolder[];
/**
* Referenzhöhe des Erdgeschosses in Metern über Meer (m ü. M.), editierbar
* im Einstellungs-Fenster. Optional, damit bestehende Projekte ohne diesen
@@ -1270,6 +1933,22 @@ export const ceilingThickness = (project: Project, ceiling: Ceiling): number =>
}
};
/**
* Löst den {@link DoorType} einer Tür-Öffnung auf, oder `undefined` (keine
* `typeId` bzw. unbekannt ⇒ Inline-Verhalten). Bewusst NICHT werfend — der
* Typ ist eine optionale Übersteuerung, kein Pflichtbezug wie beim Wandtyp.
*/
export const getDoorType = (project: Project, opening: Opening): DoorType | undefined =>
opening.typeId ? (project.doorTypes ?? []).find((t) => t.id === opening.typeId) : undefined;
/** Löst den {@link WindowType} einer Fenster-Öffnung auf, oder `undefined`. */
export const getWindowType = (project: Project, opening: Opening): WindowType | undefined =>
opening.typeId ? (project.windowTypes ?? []).find((t) => t.id === opening.typeId) : undefined;
/** Löst den {@link StairType} einer Treppe auf, oder `undefined`. */
export const getStairType = (project: Project, stair: Stair): StairType | undefined =>
stair.typeId ? (project.stairTypes ?? []).find((t) => t.id === stair.typeId) : undefined;
/** Alle Öffnungen einer Wand (leere Liste, wenn keine oder `openings` fehlt). */
export const openingsOfWall = (project: Project, wallId: string): Opening[] =>
(project.openings ?? []).filter((o) => o.hostWallId === wallId);
@@ -1291,6 +1970,18 @@ export const stairLabel = (s: Stair): string => {
return `${shape} ${s.stepCount} STG`;
};
/** Alle Stützen eines Geschosses (leere Liste, wenn keine oder `columns` fehlt). */
export const columnsOfFloor = (project: Project, floorId: string): Column[] =>
(project.columns ?? []).filter((c) => c.floorId === floorId);
/** Menschenlesbarer Standardname einer Stütze (Profil + Masse in cm). */
export const columnLabel = (c: Column): string => {
if (c.profile.kind === "round") {
return `Stütze Ø${(c.profile.radius * 200).toFixed(0)}`;
}
return `Stütze ${(c.profile.width * 100).toFixed(0)}×${(c.profile.depth * 100).toFixed(0)}`;
};
/** Alle Räume eines Geschosses (leere Liste, wenn keine oder `rooms` fehlt). */
export const roomsOfFloor = (project: Project, floorId: string): Room[] =>
(project.rooms ?? []).filter((r) => r.floorId === floorId);
+115
View File
@@ -0,0 +1,115 @@
// Unit-Tests für die Wand-/Decken-Resolver (wallVerticalExtent /
// ceilingVerticalExtent): reine Datenschicht, kein React/Store.
import { describe, it, expect } from "vitest";
import { ceilingVerticalExtent } from "./wall";
import type { Ceiling, Project } from "./types";
/** Minimalprojekt mit einem Geschoss (baseElevation 0, floorHeight 2.6) und
* einem einschichtigen Wandtyp (0.2 m), den die Decke als Aufbau nutzt. */
function makeProject(overrides?: Partial<Project>): Project {
return {
id: "t",
name: "T",
lineStyles: [],
hatches: [],
components: [{ id: "c", name: "C", color: "#ccc", hatchId: "none", joinPriority: 10 }],
wallTypes: [
{ id: "dt", name: "Decke", layers: [{ componentId: "c", thickness: 0.2 }] },
],
drawingLevels: [
{
id: "eg",
name: "EG",
kind: "floor",
visible: true,
locked: false,
floorHeight: 2.6,
cutHeight: 1.0,
baseElevation: 0,
},
{
id: "og",
name: "OG",
kind: "floor",
visible: true,
locked: false,
floorHeight: 2.6,
cutHeight: 1.0,
baseElevation: 2.8,
},
],
layers: [{ code: "30", name: "Decken", color: "#0a0a0a", lw: 0.5, visible: true, locked: false }],
walls: [],
doors: [],
openings: [],
ceilings: [],
stairs: [],
rooms: [],
drawings2d: [],
context: [],
...overrides,
};
}
function makeCeiling(overrides?: Partial<Ceiling>): Ceiling {
return {
id: "cl1",
type: "ceiling",
floorId: "eg",
categoryCode: "30",
outline: [
{ x: 0, y: 0 },
{ x: 4, y: 0 },
{ x: 4, y: 4 },
{ x: 0, y: 4 },
],
wallTypeId: "dt",
...overrides,
};
}
describe("ceilingVerticalExtent", () => {
it("ohne top/bottom: OK = Geschoss-Oberkante, UK = OK Dicke (heutiges Verhalten)", () => {
const project = makeProject();
const ceiling = makeCeiling();
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
expect(zTop).toBeCloseTo(2.6); // baseElevation(0) + floorHeight(2.6)
expect(zBottom).toBeCloseTo(2.4); // zTop thickness(0.2)
});
it("mit top: custom übersteuert die Oberkante; UK bleibt OK Dicke", () => {
const project = makeProject();
const ceiling = makeCeiling({ top: { mode: "custom", z: 3.0 } });
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
expect(zTop).toBeCloseTo(3.0);
expect(zBottom).toBeCloseTo(2.8);
});
it("mit bottom: custom übersteuert die Unterkante — gewinnt über OK Dicke", () => {
const project = makeProject();
const ceiling = makeCeiling({ bottom: { mode: "custom", z: 2.2 } });
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
expect(zTop).toBeCloseTo(2.6); // OK unverändert (Geschoss-Default)
expect(zBottom).toBeCloseTo(2.2); // UK-Override statt OK Dicke (2.4)
});
it("mit top UND bottom: beide Overrides wirken unabhängig (Dicke wird ignoriert)", () => {
const project = makeProject();
const ceiling = makeCeiling({
top: { mode: "custom", z: 5.0 },
bottom: { mode: "custom", z: 4.5 },
});
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
expect(zTop).toBeCloseTo(5.0);
expect(zBottom).toBeCloseTo(4.5);
});
it("bottom: floor bindet die Unterkante an ein Geschoss (+ optionalem offset)", () => {
const project = makeProject();
const ceiling = makeCeiling({ bottom: { mode: "floor", floorId: "og", offset: 0.1 } });
const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling);
expect(zTop).toBeCloseTo(2.6);
expect(zBottom).toBeCloseTo(2.9); // baseElevation(og)=2.8 + offset(0.1)
});
});
+29 -3
View File
@@ -5,7 +5,7 @@
//
// Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md).
import type { Ceiling, Project, Stair, VerticalAnchor, Wall } from "./types";
import type { Ceiling, Column, Project, Stair, VerticalAnchor, Wall } from "./types";
import { ceilingThickness, getFloor } from "./types";
/**
@@ -74,7 +74,9 @@ export function wallVerticalExtent(
* Vertikale Ausdehnung einer Decke (absolute Z-Werte in Metern).
* • zTop: aus `ceiling.top`, sonst Oberkante des Geschosses
* (baseElevation + floorHeight) — also bündig mit dem Wandkopf.
* • zBottom: zTop Deckendicke (die Decke wächst nach UNTEN).
* • zBottom: aus `ceiling.bottom`, sonst zTop Deckendicke (die Decke
* wächst nach UNTEN — heutiges Verhalten). Die `thickness` bleibt bei
* gesetztem `bottom` rein informativ (dient nicht mehr der UK-Ableitung).
* Fehlt das Geschoss, dient 0 als sicherer Rückfall für die Oberkante.
*/
export function ceilingVerticalExtent(
@@ -89,7 +91,9 @@ export function ceilingVerticalExtent(
floorTop = 0;
}
const zTop = resolveAnchor(project, ceiling.top) ?? floorTop;
const zBottom = zTop - ceilingThickness(project, ceiling);
const zBottom =
resolveAnchor(project, ceiling.bottom) ??
zTop - ceilingThickness(project, ceiling);
return { zBottom, zTop };
}
@@ -124,6 +128,28 @@ export function stairVerticalExtent(
return { zBottom: base, zTop: base + rise };
}
/**
* Vertikale Ausdehnung einer Stütze (absolute Z-Werte in Metern) — dieselbe
* Regel wie {@link wallVerticalExtent}:
* • zBottom: aus `column.bottom`, sonst baseElevation des Geschosses.
* • zTop: aus `column.top`, sonst `zBottom + column.height`.
* Fehlt das Geschoss, dient 0 als sicherer Rückfall für die Unterkante.
*/
export function columnVerticalExtent(
project: Project,
column: Column,
): { zBottom: number; zTop: number } {
let floorBase = 0;
try {
floorBase = getFloor(project, column.floorId).baseElevation ?? 0;
} catch {
floorBase = 0;
}
const zBottom = resolveAnchor(project, column.bottom) ?? floorBase;
const zTop = resolveAnchor(project, column.top) ?? zBottom + column.height;
return { zBottom, zTop };
}
/**
* Das Geschoss DIREKT ÜBER dem Wand-Geschoss (in Dokumentreihenfolge der
* "floor"-Ebenen) oder `undefined`, wenn die Wand im obersten Geschoss liegt.