Files
DOSSIER-STANDALONE/src/model/types.ts
T
karim d0b9d22141 Text-Styling-Leiste wirkt auf selektierten Freitext/Textspalte
Die Text-Formatier-Gruppe der Oberleiste formatierte bisher nur den
Raum-Stempel. Jetzt ist auch ein selektierter Freitext/Textspalten-Element
(Drawing2D shape "text") ein Formatier-Ziel:
- Neues optionales Feld Drawing2D-Text.marks (Marks) für einheitliche
  Formatierung (Schriftfamilie, fett/kursiv/unterstrichen, Farbe).
- App.textTarget adaptiert den Text über docFromText/plainText an die
  Rich-Text-Leiste; die GRÖSSE bleibt bewusst über die Modell-Höhe (Meter),
  nicht über pt (massstabslos wäre falsch) — sizePt wird verworfen.
- generatePlan/PlanView rendern die Marks (fontFamily/-weight/-style/
  textDecoration); marks.color hat Vorrang vor der Kategorie-Farbe.
+3 Tests. tsc + vitest grün.
2026-07-09 01:28:37 +02:00

2184 lines
89 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.
// Das semantische Gebäudemodell — der Kern von BIM.
// Bauteile haben Bedeutung, nicht nur Form. Eine Tür "kennt" ihre Wand,
// eine Wand "kennt" ihren mehrschichtigen Aufbau (WallType).
//
// Dokumentmodell (nach DOSSIER): zwei UNABHÄNGIGE Achsen.
// 1. Zeichnungsebenen (DrawingLevel) = oberste Schnitte: Geschosse +
// Schnitte/Ansichten.
// 2. Ebenen (LayerCategory) = Grafik-Kategorie-Schema, das für jedes
// Geschoss gilt. Ein Element lebt auf einer Kategorie (code) UND einem
// Geschoss.
export type Vec2 = { x: number; y: number };
// SIA-416-Blatt-Kategorie eines Raums. Der reine Rechenkern (geometry/roomArea)
// definiert den Typ; hier nur der Typ-Import (kein Zyklus zur Laufzeit).
import type { SiaCategory } from "../geometry/roomArea";
export type { SiaCategory } from "../geometry/roomArea";
// Rich-Text-Dokument für den Raum-Stempel (frei editierbarer Teil).
import type { RichTextDoc, Align, Marks } 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:
// Component → Hatch → LineStyle.
// Darstellung wird beim Rendern aus diesen Ressourcen aufgelöst, nie in die
// Geometrie eingebacken (siehe docs/design/resources-graphics.md).
/** Ein wiederverwendbarer Linienstil (Line Manager). */
export interface LineStyle {
id: string;
name: string;
/**
* @deprecated Die Strichstärke gehört NICHT mehr in den Linienstil — es gibt nur
* „Vollinie/Strich", die tatsächliche Dicke wird per Element-Attribut (bzw.
* Ebene/Default) aufgelöst. Das Feld bleibt für die Backward-Compat-Auflösung
* erhalten und dient den Renderern noch als Fallback für die Muster-/Fugen-Stärke
* (siehe `resolveHatch` → `HatchRender.lineWeight`). Der Linien-Detail-Editor
* zeigt es nicht mehr an; neue Linienstile bekommen einen Haarlinien-Default.
* Die echte per-Element-Strichstärken-Auflösung (Attribut → Ebene → Default)
* folgt separat mit dem By-Layer/By-Object-Refactor — NICHT hier.
*/
weight: number;
/** Farbe (hex). */
color: string;
/**
* Strichmuster in Millimetern, alternierend AN/AUS (`[on, off, on, off, …]`),
* loopend; `null` = durchgezogen (Volllinie). Konvention des modularen
* Segment-Systems (siehe `src/ui/lineSegments.ts`): ein AN-Wert von 0 bedeutet
* einen PUNKT (Dot) — er wird nur mit runder Strichkappe sichtbar (die
* betroffene Linie erhält dann `stroke-linecap: round`). So bilden sich
* Volllinie/Strichlinie/Punktlinie/Strich-Punkt und frei modulare Folgen aus
* Strich/Punkt/Lücke ab. Additiv — kein zusätzliches Feld nötig.
*/
dash: number[] | null;
/**
* Linien-Typ (additiv, Default `undefined` ⇒ "dash"):
* • "dash" — gerade Linie mit `dash`-Strichmuster (bestehendes Verhalten).
* • "zigzag" — der Strich variiert in Y (Zickzack/Welle), Parameter in
* `zigzag`. Fehlt `kind`, gilt „dash"; die heutigen Renderer ignorieren
* `kind` und zeichnen weiterhin gerade Striche (Backward-Compat).
* • "custom" — ein frei gezeichnetes Motiv (offene Polylinie in einer
* Einheitszelle), das sich entlang der Linie wiederholt/loopt. Parameter in
* `motif`. „zigzag" bleibt der eigene, parametrische Sonderfall.
*/
kind?: "dash" | "zigzag" | "custom";
/**
* Parameter der Zickzack-/Wellen-Linie (nur `kind==="zigzag"`), beide in mm
* Papier: `amplitude` = Ausschlag quer zur Linie, `wavelength` = Periodenlänge
* entlang der Linie. Fehlt es, wird die Linie gerade gezeichnet.
*/
zigzag?: { amplitude: number; wavelength: number };
/**
* Frei gezeichnetes Wiederhol-Motiv (nur `kind==="custom"`): eine OFFENE
* Polylinie in einer Einheitszelle. `points` sind die Stützpunkte in mm Papier;
* `x` läuft 0..`length` (mm entlang der Linie = Wiederhollänge), `y` = senkrechter
* Versatz zur Linienachse (mm, +/). Das Motiv wird alle `length` entlang der
* Linie gekachelt. Fehlt es, wird die Linie gerade gezeichnet.
*/
motif?: { points: Vec2[]; length: number };
}
/** Mögliche Schraffur-Muster im Plan/Schnitt. */
export type HatchPattern =
| "none"
| "solid"
| "insulation"
| "diagonal"
| "crosshatch";
/** Eine wiederverwendbare Schraffur (Hatch Manager). */
export interface HatchStyle {
id: string;
name: string;
/**
* Schraffur-Typ (additiv, Default `undefined` ⇒ "vector"):
* • "vector" — Musterlinien (die bestehenden `pattern`/`scale`/`angle`/
* `relativeToWall`/`lineStyleId` sind die Vektor-Parameter). Untermodus
* über `lines` (parallel vs. random).
* • "image" — ein Bild wird als Muster (Pattern-Fill) geladen; Parameter in
* `image` (Skalierung/Verzerrung/Rotation).
* Fehlt `kind`, gilt „vector"; heutige Renderer ignorieren `kind` und zeichnen
* weiterhin nach `pattern` (Backward-Compat).
*/
kind?: "vector" | "image";
/**
* Untermodus einer Vektor-Schraffur (nur `kind` fehlt/"vector", Default
* `undefined` ⇒ "parallel"):
* • "parallel" — regelmäßige Teilung (bestehendes Verhalten).
* • "random" — zufällig verteilte Striche (z. B. Kies/Splitt).
*/
lines?: "parallel" | "random";
/**
* Bild-Muster (nur `kind==="image"`). `src` = Data-URL oder Asset-Referenz;
* `scaleX`/`scaleY` = unabhängige Verzerrung in Breite/Höhe (L×B), `rotation`
* = Drehung in Grad. Bei `kind==="vector"` ungenutzt.
*/
image?: { src: string; scaleX: number; scaleY: number; rotation: number };
/** Muster-Typ. */
pattern: HatchPattern;
/** Grundmaßstab des Musters (1 = Standardteilung). */
scale: number;
/** Drehung des Musters in Grad. */
angle: number;
/**
* Wenn `true`, ist `angle` NICHT bildschirmfest, sondern relativ zur Achse der
* schraffierten Wand: das Muster dreht mit der Wandorientierung mit (z. B. eine
* Diagonale, die stets 45° zur Wand steht, oder Dämmungsstriche quer durch die
* Wanddicke). Nur die Wand-Poché wertet dies aus; ohne Wandkontext (z. B. Decke)
* degradiert es zu einem absoluten `angle`.
*/
relativeToWall?: boolean;
/**
* @deprecated Schraffuren tragen im Zielmodell KEINE eigene Farbe mehr — die
* Muster-/Vordergrundfarbe kommt vom Bauteil (`Component.foreground`) bzw. der
* Attribut-Überschreibung. Feld bleibt für die Backward-Compat-Auflösung als
* LETZTER Fallback erhalten (siehe Farb-Resolve-Reihenfolge in
* `plan/generatePlan.ts` → `resolveHatch`). Bei `pattern==="solid"` ist es die
* Vollfüllfarbe, bei `pattern==="none"` ungenutzt.
*/
color: string;
/** Optionaler Linienstil für die Musterlinien (Line Manager). */
lineStyleId?: string;
/**
* Steuer-Parameter der Random-Vektor-Schraffur (`kind`/"vector" + `lines`===
* "random", z. B. Kies/Splitt). ALLE additiv & optional — fehlen sie, gilt das
* bestehende deterministische Streu-Verhalten (Seed aus der Flächen-Bounding-Box,
* Dichte/Länge aus `scale`). Die Streuung bleibt bei gleichem `seed` + gleicher
* Fläche reproduzierbar über ALLE Renderpfade (single render truth); KEIN
* `Math.random()` zur Renderzeit.
*/
/** Expliziter Streu-Seed. Wird in den Flächen-Seed eingemischt; „Neu würfeln" setzt einen neuen Wert. */
seed?: number;
/** Streudichte als Multiplikator auf den Grundabstand (Default 1; >1 = dichter). */
density?: number;
/** Minimale Strichlänge in mm (Papier). Fehlt es, greift die `scale`-abhängige Default-Länge. */
lengthMin?: number;
/** Maximale Strichlänge in mm (Papier). Fehlt es, greift die `scale`-abhängige Default-Länge. */
lengthMax?: number;
}
/**
* PBR-Material-Karten eines Bauteils (3D-Texturierung). Jede URL verweist auf
* ein Bild — entweder ein eingebautes Bibliotheks-Asset unter
* `/assets/materials/...` ODER eine per Nutzer-Upload erzeugte Object-/Data-URL.
* Fehlende Karten werden weggelassen (z. B. nur `color` bei einem Upload).
* `sizeM` ist die physische Kantenlänge EINER Texturkachel in Metern (für die
* korrekte Skalierung der Wiederholung), Default 1.0 m.
*/
export interface ComponentMaterial {
/** Optionaler Verweis auf ein Bibliotheks-Asset (`MaterialAsset.id`). */
libraryId?: string;
/** Albedo-/Farb-Karte (map). */
color?: string;
/** Normal-Karte (normalMap) — erzeugt die Oberflächentiefe. */
normal?: string;
/** Rauheits-Karte (roughnessMap). */
roughness?: string;
/** Metallizitäts-Karte (metalnessMap). */
metalness?: string;
/** Höhen-/Displacement-Karte (displacementMap, dezent angewandt). */
displacement?: string;
/** Ambient-Occlusion-Karte (aoMap). */
ao?: string;
/** Physische Kachelgröße in Metern (Default 1.0). */
sizeM?: number;
}
/**
* Ein Bauteil-Material (Component Manager) — vereint Plan-Schnittdarstellung
* (Schraffur) und 3D-Erscheinung. Ersetzt das frühere `Material`.
*/
export interface Component {
id: string;
name: string;
/**
* Füllfarbe im Grundriss (Poché) und 3D-Diffusfarbe. Bleibt bestehen; im
* Zielmodell (Vordergrund/Hintergrund, s. u.) dient sie als Fallback für
* `background`. Migrationsabsicht: neue Projekte setzen `foreground`/
* `background`, Alt-Projekte fallen weiterhin auf `color` zurück.
*/
color: string;
/**
* Vordergrundfarbe = Farbe der Muster-/Schraffurlinien (die Schraffur selbst
* trägt keine Farbe mehr). Optional; fehlt sie, greift die Resolve-Kette
* (Attribut-Override ?? Component.foreground ?? HatchStyle.color-Fallback).
*/
foreground?: string;
/**
* Hintergrundfarbe = Füllung (Poché). Optional; fehlt sie, gilt als Fallback
* `color`. Migrationsabsicht: `background` ⇐ `color`.
*/
background?: string;
/** Schnitt-Schraffur → Hatch Manager. Gilt, wo das Bauteil echt geschnitten ist. */
hatchId: string;
/**
* Ansichts-Schraffur → Hatch Manager. Gilt, wo das Bauteil frontal/ungeschnitten
* gesehen wird (z. B. Deckenpoché im Grundriss: die Decke liegt über der
* horizontalen Schnittebene und wird von unten gesehen, nicht aufgeschnitten).
* Leer/`undefined` ⇒ keine Schraffur (weiss).
*/
viewHatchId?: string;
/** Optionale 3D-Textur (vorerst ignoriert). */
texture3d?: string;
/**
* Optionales echtes PBR-Material für die 3D-Texturierung (Bibliothek ODER
* Upload). Fehlt es, bleibt das heutige Verhalten (matte Farbe). Wirkt nur im
* Render-Modus „textured".
*/
material?: ComponentMaterial;
/**
* Optionales Kürzel für Wandtyp-Labels (z. B. „BET", „HLZ", „GKB", „DAE").
* Wird von `wallTypeLabel()` genutzt, um Wandtyp-Dropdowns kompakt darzustellen
* (z. B. „BET 24" statt dem vollen Namen). Fehlt es, greift der Name.
*/
abbrev?: string;
/** Verschneidungs-Rang: höher läuft am Stoß durch (Backbone). */
joinPriority: number;
}
/** Eine Schicht eines mehrschichtigen Bauteils. */
export interface Layer {
/** Verweis auf das Bauteil-Material (Component Manager). */
componentId: string;
/** Schichtdicke in Metern. */
thickness: number;
/**
* Linienstil (LineStyle) der Schichtfuge an der INNEREN Kante DIESER Schicht —
* also der Fuge zwischen dieser und der nächst-inneren Schicht. Bei der
* innersten Schicht ungenutzt (ihre innere Kante ist der Wand-Innenumriss).
* Fehlt das Feld, wird die Standard-Haarlinie (0.02 mm) gezeichnet.
*/
jointLineStyleId?: string;
}
/** Ein Wandtyp = geordneter Schichtaufbau (außen → innen). */
export interface WallType {
id: string;
name: string;
layers: Layer[];
}
/**
* Ein Deckentyp (Deckenstil) = geordneter Schichtaufbau EINER Decke, liegend
* gestapelt (oben → unten) — das horizontale Gegenstück zum `WallType`. Nutzt
* denselben `Layer`-Typ (Bauteil + Dicke + optionaler Schichtfugen-Linienstil),
* damit Fuge/Schraffur-Auflösung identisch zur Wand bleiben. Eine „einschichtige"
* Decke (SOLID) ist einfach ein Deckentyp mit genau einer Schicht.
*/
export interface CeilingType {
id: string;
name: string;
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;
/**
* Rahmenart:
* • "zarge" — Zarge, umschliesst die Laibung (schmales Profil, in der
* Wandlaibung sitzend — der Standard im Innenausbau).
* • "blockrahmen" — Blockrahmen, sitzt als kräftiges Rechteckprofil VOR/auf
* der Laibung (typisch bei Aussentüren/älterem Bestand).
* Fehlt es, gilt "zarge".
*/
frameKind?: "zarge" | "blockrahmen";
/**
* Ansichtsbreite des Rahmenprofils (Meter) — die sichtbare Rahmen-Randbreite
* in der Öffnungsebene (Elevation), NICHT die Tiefe quer zur Wand. Fehlt es,
* gilt ein schmaler Default (~0.06 m). Steuert 2D-Rahmenkontur + 3D-Rahmen.
*/
frameWidth?: number;
/**
* Schichteinzug: Abstand der Rahmen-Vorderkante von einer Wandfläche (Meter).
* 0/fehlt ⇒ bündig. > 0 ⇒ der Rahmen sitzt um diesen Betrag in die Wand
* zurückgesetzt (z. B. hinter die Aussenschale eines mehrschichtigen Aufbaus).
* Gemessen von der über {@link insetFace} gewählten Fläche.
*/
insetFromFace?: number;
/** Von welcher Wandfläche der {@link insetFromFace} gemessen wird; Default "aussen". */
insetFace?: "aussen" | "innen";
/**
* Oberlicht: Höhe eines festen, verglasten Oberlichts über dem Türblatt
* (Meter). 0/fehlt ⇒ kein Oberlicht. Das Oberlicht sitzt oberhalb eines
* Kämpfers innerhalb derselben lichten Öffnungshöhe.
*/
transomHeight?: 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;
/**
* Anzahl horizontaler Felder (Kämpfer-Zeilen): 1 = keine Querteilung, 2 = ein
* Kämpfer, usw. Zusammen mit {@link wingCount} (Spalten) ergibt sich das
* Sprossen-/Flügelraster. Fehlt es, gilt 1.
*/
mullionRows?: 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;
/**
* Ansichtsbreite des Rahmen-/Flügelprofils (Meter) — sichtbare Randbreite in
* der Öffnungsebene. Fehlt es, gilt ein schmaler Default (~0.06 m). Steuert die
* 2D-Rahmen-/Sprossenkontur und den 3D-Rahmen.
*/
frameWidth?: number;
/**
* Schichteinzug: Abstand der Rahmen-Vorderkante von einer Wandfläche (Meter).
* 0/fehlt ⇒ bündig; > 0 ⇒ der Rahmen sitzt zurückgesetzt (klassisch hinter der
* Aussenschale eines mehrschichtigen Wandaufbaus). Von {@link insetFace} gemessen.
*/
insetFromFace?: number;
/** Von welcher Wandfläche der {@link insetFromFace} gemessen wird; Default "aussen". */
insetFace?: "aussen" | "innen";
/**
* Oberlicht: Höhe eines festen, verglasten Oberlichts über dem Hauptflügel
* (Meter), abgeteilt durch einen Kämpfer. 0/fehlt ⇒ kein Oberlicht.
*/
transomHeight?: 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
// durch `resolveParametricWall()` in `src/model/parametricWalls.ts` aufgelöst.
// Ausgabe: normale Wall[]-Objekte (kein neuer Elementtyp).
/**
* Eine parametrische Wand-Regel — generiert automatisch Wall[]-Einträge für
* ein gegebenes Geschoss. Lebt in der Ressourcen-Bibliothek des Projekts.
* Wird über `resolveParametricWall()` aufgelöst, nicht zur Laufzeit gespeichert.
*/
export interface ParametricWall {
id: string;
/** Anzeigename, z. B. „Wohnbau-Raster 3m". */
name: string;
/** Optionale Beschreibung (für den Ressourcen-Manager). */
description?: string;
/**
* Geordnete Liste der anzuwendenden Regeln. Spätere Regeln können die
* Ausgabe früherer Regeln verfeinern (z. B. Dickenzuweisung nach Raster).
*/
rules: ParametricRule[];
/**
* Rückfall-Wandtyp, falls eine Regel keinen eigenen `wallTypeId` nennt.
* Muss auf einen gültigen WallType im Projekt verweisen.
*/
defaultWallTypeId: string;
}
/**
* Eine einzelne parametrische Regel — eine Strategie zur Wandplatzierung oder
* -verfeinerung. Regeln werden als Discriminated Union kodiert; der `type`-Tag
* bestimmt, welche Felder verfügbar sind.
*/
export type ParametricRule =
| GridRule
| ModuleRule
| ConditionalThicknessRule
| ReferenceLineRule
| SequenceRule;
/**
* Raster-Regel: generiert Wände entlang gleichmäßiger X-/Y-Achsen.
* Typischer Anwendungsfall: Tragwerks-Achsraster (z. B. 3 m Abstand).
*
* Ablauf (Engine):
* 1. Rasterachsen aus `spacing` oder (später) verlinkter Grid-Ressource.
* 2. Je Achse eine Wand von Rand zu Rand (begrenzt durch `boundaryId`).
* 3. Wandtyp, Referenzlinie und Höhe gemäß Regelfelder.
*/
export interface GridRule {
type: "grid";
/**
* Optionaler Verweis auf eine Grid-Ressource (Phase 3). Für MVP wird
* stattdessen `spacing` genutzt.
*/
gridId?: string;
/** Rasterabstand in Metern (Default: 3.0). Wird genutzt, wenn kein `gridId`. */
spacing?: number;
/**
* Achsrichtungen: „x" = nur horizontale Wände, „y" = nur vertikale,
* „both" = Vollraster.
*/
directions: "x" | "y" | "both";
/**
* Optionaler Verweis auf eine Drawing2D-Grenze (als Clipping-Polygon).
* Fehlt er, reicht das Raster über den sichtbaren Bereich des Geschosses.
*/
boundaryId?: string;
/** Optionale Wandtyp-Übersteuerung; sonst `defaultWallTypeId`. */
wallTypeId?: string;
/** Lage der Wandachse über die Dicke (Vectorworks-Stil). */
referenceLine?: WallReferenceLine;
/** Optionale Höhenübersteuerung in Metern; sonst Geschosshöhe. */
height?: number;
}
/**
* Modul-Regel: teilt eine Referenzspanne in proportionale Felder auf.
* Typischer Anwendungsfall: Tragwerks-Joche (z. B. 6 m-Module in einem Bauteil).
*
* Ablauf (Engine):
* 1. Gesamtspanne aus `referenceWallId` oder Geschossausdehnung ableiten.
* 2. In Module der Größe `moduleSize` unterteilen.
* 3. Querwände an jedem Modulteilungspunkt setzen.
*/
export interface ModuleRule {
type: "module";
/** Modulmaß in Metern (z. B. 6.0, 3.6). */
moduleSize: number;
/** Ausrichtung der Trennwände: „x" = quer zur X-Achse, „y" = quer zur Y-Achse. */
direction: "x" | "y";
/**
* Optionaler Verweis auf eine Referenzwand, die die Spannweite definiert.
* Fehlt er, wird die Geschoss-Ausdehnung genutzt (Phase 3: Achsen-Referenz).
*/
referenceWallId?: string;
/** Optionale Wandtyp-Übersteuerung; sonst `defaultWallTypeId`. */
wallTypeId?: string;
/** Lage der Wandachse über die Dicke. */
referenceLine?: WallReferenceLine;
/** Optionale Höhenübersteuerung in Metern. */
height?: number;
}
/**
* Bedingte-Dicken-Regel: weist bereits generierten Wänden einen anderen
* Wandtyp zu, wenn eine Bedingung erfüllt ist.
* Typischer Anwendungsfall: Außenwände erhalten einen anderen Aufbau als Innenwände.
*
* Ablauf (Engine):
* Bestehende Wände aus `existingWalls` filtern → `wallTypeId` ändern.
* Gibt modifizierte Kopien zurück (keine Mutation).
*/
export interface ConditionalThicknessRule {
type: "conditional-thickness";
/**
* Bedingung für den Treffer:
* • „exterior" — Wand liegt am Außenrand (ermittelt via Grenzpolygon).
* • „interior" — Wand liegt im Inneren.
* • „bearing" — tragende Wand (über Tag oder Wandtyp-Rang).
* • beliebiger String — benutzerdefiniertes Tag (Phase 3: Wall.tags[]).
*/
condition: "exterior" | "interior" | "bearing" | string;
/** Ziel-Wandtyp, der bei Treffer gesetzt wird. */
wallTypeId: string;
/**
* Verknüpfungslogik für mehrere Bedingungen (Phase 3: mehrere `condition`-Felder).
* Vorerst ungenutzt; Default ist implizites „or".
*/
logic?: "and" | "or";
}
/**
* Referenzlinien-Regel: setzt die `referenceLine`-Eigenschaft bei passenden
* Wänden einheitlich (Vectorworks-Stil).
* Typischer Anwendungsfall: Alle Außenwände auf „left" (linke Fläche = Fassade).
*
* Ablauf (Engine):
* Bestehende Wände aus `existingWalls` filtern → `referenceLine` setzen.
* Gibt modifizierte Kopien zurück.
*/
export interface ReferenceLineRule {
type: "reference-line";
/** Neue Lage der Wandachse, die einheitlich gesetzt wird. */
referenceLine: WallReferenceLine;
/**
* Filterziel:
* • „all" — alle Wände im aktuellen Satz.
* • „exterior" — nur Außenwände (wie bei ConditionalThicknessRule).
* • beliebiger String — benutzerdefiniertes Tag (Phase 3).
*/
target: "all" | "exterior" | string;
}
/**
* Sequenz-Regel: fasst mehrere Unterregeln zusammen und wendet sie geordnet an.
* Jede Unterregel kann die Ausgabe der vorherigen verfeinern.
* Typischer Anwendungsfall: Raster → bedingte Dicke → Referenzlinie als atomare Einheit.
*
* Ablauf (Engine):
* Regeln in `rules` werden sequenziell ausgeführt; das Ergebnis jeder Regel wird
* als `existingWalls` der nächsten übergeben. `stopOnMatch` bricht ab, sobald
* eine Unterregel mindestens eine Wand generiert hat.
*/
export interface SequenceRule {
type: "sequence";
/** Unterregeln, in Ausführungsreihenfolge. */
rules: ParametricRule[];
/**
* Wenn `true`: Abbruch nach der ersten Unterregel, die mindestens eine Wand
* generiert/verändert. Ähnlich wie ein Short-Circuit-Fallback.
*/
stopOnMatch?: boolean;
}
/**
* Art einer Zeichnungsebene.
* • "floor" — Geschoss, trägt Bauteile.
* • "section" — Schnitt (abgeleitete Projektion entlang einer Linie).
* • "elevation" — Ansicht (abgeleitete Projektion entlang einer Linie).
* • "drawing" — freie 2D-Zeichnung, nicht an ein Geschoss gebunden.
*/
export type DrawingLevelKind = "floor" | "section" | "elevation" | "drawing";
/**
* Eine Zeichnungsebene (DrawingLevel) — eine oberste Schnittebene des
* Dokuments. Ein Geschoss trägt die Bauteile (über `floorId`); ein
* Schnitt/Ansicht ist eine abgeleitete Projektion (vorerst Platzhalter); eine
* reine Zeichnung ist eine freie 2D-Ebene ohne Geschossbezug.
*
* Geschoss nutzt floorHeight/cutHeight/baseElevation; Schnitt/Ansicht nutzen
* linePoints/directionSign; "drawing" nutzt keines dieser Felder.
*/
export interface DrawingLevel {
id: string;
name: string;
kind: DrawingLevelKind;
/** Sichtbarkeit der Zeichnungsebene im Navigator. */
visible: boolean;
/** Gesperrt (keine Bearbeitung). */
locked: boolean;
/** Lichte Geschosshöhe in Metern (nur Geschoss). */
floorHeight?: number;
/** Schnitthöhe über OKFF in Metern (nur Geschoss, für den Grundriss). */
cutHeight?: number;
/** Oberkante Fertigfußboden in Metern (nur Geschoss). */
baseElevation?: number;
/** Schnitt-/Ansichtslinie im Grundriss (nur Schnitt/Ansicht). */
linePoints?: [Vec2, Vec2];
/** Blickrichtung relativ zur Schnittlinie (nur Schnitt/Ansicht). */
directionSign?: 1 | -1;
}
/**
* Eine Ebene (LayerCategory) — ein Knoten im Grafik-Kategorie-Baum. Das
* Schema gilt geschossübergreifend; Elemente verweisen über `code` darauf.
*/
export interface LayerCategory {
/** Eindeutiger Kategorie-Code, z. B. "20" für Wände. */
code: string;
name: string;
/** Darstellungsfarbe (hex). */
color: string;
/** Linienstärke in Millimetern. */
lw: number;
visible: boolean;
locked: boolean;
/** Optionale Standard-Schraffur der Kategorie. */
hatch?: string;
/** Unterkategorien (Baum). */
children?: LayerCategory[];
}
/**
* Quelle eines vererbbaren Attributs (Vordergrund/Hintergrund/Strichstärke/
* Schraffur), wenn KEIN expliziter Wert am Element gesetzt ist:
* • "layer" — „Nach Ebene": die LayerCategory des Elements erzwingt den Wert
* (`color`/`lw`/`hatch`).
* • "object" — „Nach Bauteil": erbt vom Bauteil (Component) bzw. dessen
* bisheriger Fallback-Kette. Das ist auch der Default, wenn das Source-Feld
* fehlt (`undefined`) — damit bleibt das heutige Verhalten unverändert.
* Ein gesetzter expliziter Wert (z. B. `foreground`) gewinnt IMMER, unabhängig
* von der Source (s. Resolve-Reihenfolge in `plan/generatePlan.ts`).
*/
export type AttributeSource = "layer" | "object";
/**
* Lage der Wandachse (Referenzlinie) über die Dicke der Wand — analog
* Vectorworks. Gemessen relativ zur Laufrichtung (start→end) mit der
* leftNormal-Konvention `n = (-u.y, u.x)`:
* • "center" — Achse mittig (Default = heutiges Verhalten).
* • "left" — Achse liegt auf der linken Wandfläche (+n-Seite, „außen").
* • "right" — Achse liegt auf der rechten Wandfläche (n-Seite, „innen").
*/
export type WallReferenceLine = "left" | "center" | "right";
/**
* Vertikale Bindung einer Wandkante (UK/OK).
* • "floor" — an ein Geschoss gebunden: Z = baseElevation des Geschosses
* (+ optionalem `offset`). Stapelt automatisch mit dem Geschoss.
* • "custom" — fester absoluter Z-Wert (Meter).
*/
export type VerticalAnchor =
| { mode: "floor"; floorId: string; offset?: number }
| { mode: "custom"; z: number };
/** Eine Wand, definiert über ihre Achse (Centerline) und ihren Typ. */
export interface Wall {
id: string;
type: "wall";
/** Zugehörige Zeichnungsebene (Geschoss). */
floorId: string;
/** Grafik-Kategorie (Ebene), z. B. "20" für Wände. */
categoryCode: string;
/** Achs-Startpunkt im Grundriss (Meter). */
start: Vec2;
/** Achs-Endpunkt im Grundriss (Meter). */
end: Vec2;
/** Verweis auf den (mehrschichtigen) Wandtyp. */
wallTypeId: string;
/** Wandhöhe in Metern. */
height: number;
/**
* Optionale Übersteuerung der Strich-/Umrandungsfarbe der Wand; sonst gilt
* die Kategorie-Farbe. Übersteuert NUR die Linienfarbe (Umriss/Schichtfugen),
* nicht die Schicht-Füllfarben/Schraffuren.
*/
color?: string;
/**
* Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER Wand-
* Instanz. `undefined` = „Nach System" (erben → Component.foreground →
* HatchStyle.color-Fallback). Höchste Priorität in der Farb-Resolve-Kette.
*/
foreground?: string;
/**
* Attribut-Override der Füllfarbe (Hintergrund/Poché) DIESER Wand-Instanz.
* `undefined` = „Nach System" (erben → Component.background → Component.color).
*/
background?: string;
/**
* Attribut-Override der Strichstärke (mm Papier) DIESER Wand-Instanz (Umriss/
* Schichtfugen). `undefined` = „Nach System" (erben → `strokeWeightSource`).
*/
strokeWeight?: number;
/**
* Attribut-Override der Schnitt-Schraffur (Hatch Manager) DIESER Wand-Instanz;
* überschreibt die Schraffur ALLER Schichten einheitlich. `undefined` =
* „Nach System" (erben → `hatchSource`).
*/
hatchId?: string;
/**
* Quelle des Vordergrunds, wenn kein expliziter `foreground`-Wert gesetzt ist:
* "layer" erzwingt die Ebenenfarbe (LayerCategory.color), "object"/`undefined`
* (Default) erbt vom Bauteil (heutiges Verhalten).
*/
foregroundSource?: AttributeSource;
/** Quelle des Hintergrunds, analog zu {@link Wall.foregroundSource}. */
backgroundSource?: AttributeSource;
/**
* Quelle der Strichstärke, wenn kein explizites `strokeWeight` gesetzt ist:
* "layer" erzwingt `LayerCategory.lw`, "object"/`undefined` (Default) fällt auf
* die bisherige Kategorie-/Fallback-Strichstärke zurück (heutiges Verhalten).
*/
strokeWeightSource?: AttributeSource;
/**
* Quelle der Schraffur, wenn kein explizites `hatchId` gesetzt ist: "layer"
* erzwingt `LayerCategory.hatch`, "object"/`undefined` (Default) erbt vom
* Bauteil (heutiges Verhalten).
*/
hatchSource?: AttributeSource;
/**
* Lage der Wandachse über die Dicke. Fehlt sie, gilt "center" (= heutiges
* Verhalten: Schichten symmetrisch T/2 … +T/2 um die Achse).
*/
referenceLine?: WallReferenceLine;
/**
* Freier Referenz-Versatz der Achse entlang der +n-Normalen von der Wandmitte
* (Meter). Ist er gesetzt, ÜBERSTEUERT er {@link referenceLine} — genutzt, um
* bei mehrschichtigen Wänden eine SCHICHTTRENNLINIE (Fuge zwischen zwei
* Schichten) als Referenzlinie zu wählen. +T/2 = Aussenfläche, 0 = Mitte,
* T/2 = Innenfläche; ein interner Fugenwert liegt dazwischen.
*/
referenceOffset?: number;
/**
* Vertikale Bindung der Unterkante (UK). Fehlt sie, sitzt die UK auf der
* baseElevation des zugehörigen Geschosses (= heutiges Verhalten).
*/
bottom?: VerticalAnchor;
/**
* Vertikale Bindung der Oberkante (OK). Fehlt sie, ergibt sich die OK aus
* UK + `height` (= heutiges Verhalten). „custom" setzt einen absoluten
* 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
* Dicke. Analog zur Wand trägt sie ihren Schichtaufbau über einen `wallTypeId`
* (die Bauteil-/Schraffur-/Materialauflösung ist identisch); eine optionale
* `thickness` übersteuert die Gesamtdicke des Typs (Meter).
*
* 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. 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;
type: "ceiling";
/** Zugehörige Zeichnungsebene (Geschoss). */
floorId: string;
/** Grafik-Kategorie (Ebene), z. B. "30" für Decken. */
categoryCode: string;
/**
* Geschlossener Umriss im Grundriss (Meter). Der Schlusspunkt wird NICHT
* dupliziert (die letzte Kante läuft von outline[n-1] zu outline[0]).
*/
outline: Vec2[];
/**
* LEGACY-Verweis auf einen Aufbau-Typ aus `wallTypes` (Rückwärtskompatibilität
* für Projekte von vor den dedizierten Deckenstilen). Nur wirksam, wenn
* `ceilingTypeId` fehlt.
*/
wallTypeId: string;
/**
* Verweis auf einen dedizierten Deckentyp (Deckenstil) aus `ceilingTypes` —
* der reguläre Weg für SOLID (1 Schicht) und MEHRSCHICHTIG (>1 Schicht). Hat
* Vorrang vor `wallTypeId`, falls gesetzt und im Projekt auflösbar.
*/
ceilingTypeId?: string;
/** Optionale Übersteuerung der Gesamtdicke in Metern (sonst Typ-Dicke). */
thickness?: number;
/**
* Optionale Übersteuerung der Strich-/Umrandungsfarbe; sonst gilt die
* Kategorie-Farbe.
*/
color?: string;
/**
* Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER Decken-
* Instanz. `undefined` = „Nach System" (erben → Component.foreground →
* HatchStyle.color-Fallback).
*/
foreground?: string;
/**
* Attribut-Override der Füllfarbe (Hintergrund/Poché) DIESER Decken-Instanz.
* `undefined` = „Nach System" (erben → Component.background → Component.color).
*/
background?: string;
/**
* Attribut-Override der Strichstärke (mm Papier) DIESER Decken-Instanz.
* `undefined` = „Nach System" (erben → `strokeWeightSource`).
*/
strokeWeight?: number;
/**
* Attribut-Override der Schraffur (Hatch Manager) DIESER Decken-Instanz;
* überschreibt sowohl die Schnitt- als auch die Ansichts-Schraffur des
* Bauteils. `undefined` = „Nach System" (erben → `hatchSource`).
*/
hatchId?: string;
/** Quelle des Vordergrunds, analog zu {@link Wall.foregroundSource}. */
foregroundSource?: AttributeSource;
/** Quelle des Hintergrunds, analog zu {@link Wall.backgroundSource}. */
backgroundSource?: AttributeSource;
/** Quelle der Strichstärke, analog zu {@link Wall.strokeWeightSource}. */
strokeWeightSource?: AttributeSource;
/** Quelle der Schraffur, analog zu {@link Wall.hatchSource}. */
hatchSource?: AttributeSource;
/**
* Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der
* 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. */
export type SwingSide = "left" | "right";
/**
* Eine Öffnung (Fenster oder Tür), gehostet in einer Wand. Sie „kennt" ihre Wand
* (`hostWallId`) und liegt über `position` (Abstand vom Wand-Startpunkt entlang
* der Achse) relativ zur Wand — bewegt sich die Wand, folgt die Öffnung, weil die
* Weltkoordinaten beim Rendern IMMER aus der Wandachse abgeleitet werden.
*
* Vertikale Lage (relativ zur Wand-Unterkante, UK):
* • Tür — sitzt am Boden, `sillHeight` = 0, Höhe = lichte Türhöhe.
* • Fenster— Brüstung `sillHeight` > 0, Öffnung reicht von sillHeight bis
* sillHeight + height.
*
* Türspezifisch: `hinge` (Anschlagpfosten) + `swing` (Aufschlagseite) + optional
* `swingAngle` (Öffnungswinkel des Blatts in Grad, Default 90) + `openingDir`
* (Aufschlag nach innen/außen — kippt den Schwenkbogen auf die andere Achsseite).
* Fenster nutzen optional `frameDepth`/`frameThickness` für die 3D-Rahmenstärke.
*/
export interface Opening {
id: string;
type: "opening";
/** Wirts-Wand; das Geschoss ergibt sich aus der Wand. */
hostWallId: string;
/** Grafik-Kategorie (Ebene), z. B. "21" für Türen/Fenster. */
categoryCode: string;
/** Fenster oder Tür. */
kind: "window" | "door";
/** Abstand des ersten Pfostens vom Wand-Startpunkt entlang der Achse (Meter). */
position: number;
/** Lichte Öffnungsbreite in Metern. */
width: number;
/** Lichte Öffnungshöhe in Metern. */
height: number;
/** Brüstungshöhe (Unterkante der Öffnung) über der Wand-UK; 0 bei Türen. */
sillHeight: number;
/**
* Nur Fenster: Anzahl der Flügel (14). Bei > 1 werden im Plan `wingCount 1`
* Mittelpfosten (Querlinien quer zur Öffnungsrichtung) gleichmäßig innerhalb des
* Rahmens gezeichnet — analog den Mittelpfosten in `_make_oeffnung_pieces` des
* Rhino-Plugins (`oeff_fluegel`). Fehlt es, gilt 1 (= heutiges Verhalten: keine
* Pfosten).
*/
wingCount?: number;
/**
* Nur Tür: Tür-Typ (analog `oeff_tuer_typ` im Rhino-Plugin).
* • "normal" — Türblatt + Schwenkbogen (Default = heutiges Verhalten).
* • "wandoeffnung" — reiner Wanddurchbruch: KEIN Türblatt, KEIN Schwenkbogen;
* nur die Öffnung/Laibung (Wandlücke + ggf. Sturz-/Anschlaglinien) bleibt.
* Fehlt es, gilt "normal".
*/
doorType?: "normal" | "wandoeffnung";
/** Nur Tür: an welchem Pfosten das Scharnier sitzt. */
hinge?: "start" | "end";
/** Nur Tür: auf welche Seite der Wandachse das Blatt aufschlägt. */
swing?: SwingSide;
/** Nur Tür: Öffnungswinkel des Blatts in Grad (Default 90). */
swingAngle?: number;
/** Nur Tür: Aufschlagrichtung (nach innen/außen); Default "in". */
openingDir?: "in" | "out";
/** Optionale Rahmenstärke (quer zur Wand) in Metern für die 3D-Darstellung. */
frameThickness?: number;
/**
* Nur Tür: Sturzlinien (SIA, gestrichelt) quer über die Öffnung an der Wand-
* Innen- und/oder Aussenkante. Zeigt die Überkopf-Projektion des Sturzes.
* "keine" → keine Sturzlinien
* "innen" → eine Linie an der Wand-Innenkante
* "aussen"→ eine Linie an der Wand-Aussenkante
* "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.
*/
color?: string;
}
/**
* Grundform einer Treppe (DOSSIER: gerade / L / Wendel).
* • "straight" — ein gerader Lauf (Lauflinie = Start → Richtung).
* • "L" — zwei rechtwinklige Läufe mit Zwischenpodest an der Ecke.
* • "spiral" — Wendeltreppe um ein Zentrum (keilförmige Tritte).
*/
export type StairShape = "straight" | "L" | "spiral";
/**
* Eine Treppe (Treppe/Stair) — ein geschossübergreifendes Bauteil, das über die
* Geschosshöhe (OKFF → OKFF des nächsten Geschosses) steigt. Definiert über eine
* Grundform (gerade / L / Wendel), eine Basis-Geometrie, die Laufbreite und die
* Stufung (Steigungshöhe/Auftrittstiefe, aus der Stufenanzahl abgeleitet).
*
* Basis-Geometrie je Grundform:
* • straight — `start` + `dir` (Einheitsrichtung) + `runLength` (Lauflänge).
* • L — `start` + `dir` (erster Lauf) + `runLength` (erster Lauf) +
* `run2Length` (zweiter Lauf) + `turn` (+1 = links, 1 = rechts abbiegen).
* Das Zwischenpodest sitzt am Ende des ersten Laufs (quadratisch, `width`).
* • spiral — `center` + `radius` (Innenradius zur Lauflinie) + `sweep`
* (Gesamtwinkel in Grad, +/ = Drehrichtung) + `start` (Startpunkt des ersten
* Tritts am äußeren Rand, definiert die Anfangsrichtung).
*
* Vertikale Lage: die UNTERKANTE (`baseZ`, abgeleitet aus dem Geschoss-OKFF) plus
* `totalRise` (Default = Geschosshöhe des zugehörigen Geschosses, sodass die
* Treppe genau ins nächste Geschoss steigt). `stepCount` Tritte/Setzstufen
* verteilen `totalRise` gleichmäßig; die Steigungshöhe = totalRise/stepCount, die
* Auftrittstiefe ergibt sich aus Lauflänge/(stepCount1). Eine SIA-nahe
* Schrittregel (2·Steigung + Auftritt ≈ 0.63 m) liefert die Default-Stufenzahl.
*/
export interface Stair {
id: string;
type: "stair";
/** Zugehörige Zeichnungsebene (Geschoss), von dem die Treppe aufsteigt. */
floorId: string;
/** Grafik-Kategorie (Ebene), z. B. "40" für Treppen. */
categoryCode: string;
/** Grundform (gerade / L / Wendel). */
shape: StairShape;
/** Startpunkt der Lauflinie im Grundriss (Meter). */
start: Vec2;
/** Einheits-Laufrichtung des (ersten) Laufs im Grundriss. */
dir: Vec2;
/** Lauflänge des (ersten) Laufs in Metern (entlang `dir`). */
runLength: number;
/** Nur L: Lauflänge des zweiten Laufs in Metern. */
run2Length?: number;
/** Nur L: Abbiegerichtung des zweiten Laufs (+1 = links, 1 = rechts). */
turn?: 1 | -1;
/** Nur Wendel: Zentrum der Wendeltreppe (Meter). */
center?: Vec2;
/** Nur Wendel: Radius (Meter) von der Mitte zur Lauflinie. */
radius?: number;
/** Nur Wendel: Gesamt-Drehwinkel in Grad (+ = CCW, = CW). */
sweep?: number;
/** Laufbreite in Metern (quer zur Laufrichtung). */
width: number;
/**
* Lage der gespeicherten Achse (`start`/`dir` bzw. Wendel-Bogen) über die
* Laufbreite — analog `treppe_referenz` im Rhino-Plugin. Bestimmt, ob die Achse
* die LINKE, MITTLERE oder RECHTE Kante der Treppe repräsentiert (in
* Laufrichtung gesehen). Fehlt sie, gilt "mitte" (= heutiges Verhalten: Achse =
* Treppen-Mitte).
* • "links" — Achse = linke Kante; die Treppe liegt rechts der Achse.
* • "mitte" — Achse = Mitte (Default).
* • "rechts" — Achse = rechte Kante; die Treppe liegt links der Achse.
* Die Lauflinie samt Pfeil bleibt IMMER auf der visuellen Treppen-Mitte
* (nicht auf der Referenzkante) — siehe `stairGeometry`.
*/
referenz?: "links" | "mitte" | "rechts";
/**
* Gesamt-Steighöhe in Metern (OKFF → OKFF nächstes Geschoss). Fehlt sie, gilt
* beim Auflösen die Geschosshöhe des zugehörigen Geschosses.
*/
totalRise?: number;
/** Anzahl der Steigungen (Setzstufen). Die Trittanzahl = stepCount (letzte =
* Austritt aufs obere Geschoss). Steigungshöhe = totalRise / stepCount. */
stepCount: number;
/**
* Laufrichtung „aufwärts": true = die Lauflinie steigt von `start` in Richtung
* `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.
*/
color?: string;
}
/**
* Ein Raum (Raum/Room) — eine SIA-416-Fläche auf einem Geschoss, definiert über
* einen GESCHLOSSENEN Umriss (Polygon, lichte Innenkontur) im Grundriss. Trägt
* eine SIA-416-Blatt-Kategorie (HNF/NNF/VF/FF/KGF), einen Namen und eine Farbe.
*
* Fläche/Umfang/Schwerpunkt werden NICHT gespeichert, sondern bei jedem Rendern
* aus `boundary` über den reinen Rechenkern (geometry/roomArea) abgeleitet —
* so sind sie nie veraltet. `stampAnchor` (optional) setzt den Ankerpunkt des
* Raum-Stempels; fehlt er, gilt der Flächenschwerpunkt (Centroid).
*/
export interface Room {
id: string;
type: "room";
/** Zugehörige Zeichnungsebene (Geschoss). */
floorId: string;
/** Grafik-Kategorie (Ebene), z. B. "45" für Räume. */
categoryCode: string;
/** SIA-416-Blatt-Kategorie (HNF/NNF/VF/FF/KGF). */
siaCategory: SiaCategory;
/** Raum-Name, z. B. „Wohnen". */
name: string;
/**
* Geschlossener Umriss (lichte Innenkontur) im Grundriss (Meter). Der
* Schlusspunkt wird NICHT dupliziert (die letzte Kante läuft von boundary[n-1]
* zu boundary[0]).
*/
boundary: Vec2[];
/** Strich-/Füllfarbe des Raums (hex). */
color: string;
/**
* Anker des Raum-Stempels (Meter). Wird bei der Erstellung EINMAL auf den
* Zentroid gesetzt und danach nie automatisch neu berechnet — der Stempel ist
* frei verschiebbar und bleibt beim Ändern der Kontur/Fläche an seiner Stelle.
* Fehlt er (Alt-Daten), gilt beim Rendern der Centroid.
*/
stampAnchor?: Vec2;
/**
* Frei editierbarer Rich-Text des Raum-Stempels (Name + Notizen, mit Fett/
* Kursiv/Grösse/Farbe/Ausrichtung). Fehlt er, gilt der Raum-Name als einfacher
* Text. Die LIVE-Flächenzeile wird beim Rendern separat darunter gesetzt.
*/
stampDoc?: RichTextDoc;
/**
* Strukturierter Raum-Stempel (Feldmodell). Ist er gesetzt, hat er Vorrang vor
* `stampDoc`/`name`: der Stempel-Text wird aus den Feldern gebaut, die Live-
* Zeilen (Bodenfläche/Nutzung) aus den Flags. Fehlt er, gilt der Alt-Pfad.
*/
stamp?: RoomStamp;
}
/**
* Feldmodell des Raum-Stempels. Ersetzt den freien Text durch benannte Felder;
* daraus baut roomStamp.ts sowohl das gerenderte Rich-Text-Dokument als auch die
* Live-Zeilen. Minimal gehalten (MVP).
*
* Folge-Arbeit: Fensterfläche (braucht Öffnung-in-Raum-Geometrie) — hier bewusst
* NICHT enthalten.
*/
export interface RoomStamp {
/** Raumnummer (kleiner Präfix in Zeile 1). */
number?: string;
/** Raumname (Zeile 1). */
name: string;
/** Raumname Zeile 2 (in Listen mit `name` zu einem Namen zusammengezogen). */
nameLine2?: string;
/** Bodenfläche anzeigen (Live-Zeile). */
showFloorArea: boolean;
/** Präfix vor der Bodenfläche, z. B. "BF " (Default leer). */
floorAreaPrefix?: string;
/** Nutzung (HNF/… · Bezeichnung) anzeigen (Live-Zeile). */
showUsage: boolean;
/** Ausrichtung der Namenszeile (Zeile 1). Fehlt sie, gilt „links". */
nameAlign?: Align;
/** Ausrichtung der zweiten Namenszeile. Fehlt sie, gilt „links". */
line2Align?: Align;
/** Ausrichtung der Bodenflächen-Zeile. Fehlt sie, gilt „zentriert" (Alt-Verhalten). */
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. */
export interface Door {
id: string;
type: "door";
hostWallId: string;
/** Grafik-Kategorie (Ebene), z. B. "21" für Türen/Fenster. */
categoryCode: string;
/** Abstand des Türanschlags (erster Pfosten) vom Wand-Startpunkt, in Metern. */
position: number;
/** Türbreite (lichte Öffnung) in Metern. */
width: number;
/** Türhöhe in Metern. */
height: number;
/** Auf welche Seite der Wandachse die Tür aufschlägt. */
swing: SwingSide;
/** An welchem Pfosten das Scharnier sitzt. */
hinge: "start" | "end";
}
// ── Freie 2D-Zeichengeometrie (Drawing2D) ──────────────────────────────────
// Ein semantisches 2D-Element (wie Wall/Door), das beim Rendern abgeleitet wird
// (keine vorab erzeugten Plan-Primitive). Siehe docs/design/drawing-tools.md §7.
/** Geometrie-Form eines 2D-Zeichenelements. */
export type Drawing2DGeom =
| { shape: "line"; a: Vec2; b: Vec2 }
| { shape: "polyline"; pts: Vec2[]; closed: boolean }
| { 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;
/**
* 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;
/**
* Optionale einheitliche Formatierung des ganzen Texts (Schriftfamilie, fett/
* kursiv, Farbe …) — gesetzt über die Text-Formatier-Gruppe der Oberleiste,
* wenn dieser Text selektiert ist. Die Grösse bleibt bewusst über `height`
* (Modell-Meter) geführt, nicht über `marks.sizePt`.
*/
marks?: Marks;
};
/** Ein freies 2D-Zeichenelement auf einer Zeichnungsebene. */
export interface Drawing2D {
id: string;
type: "drawing2d";
/** Zeichnungsebene (Geschoss ODER freie 2D-Ebene). */
levelId: string;
/** Grafik-Kategorie (Ebene) — liefert Farbe/Strichstärke als Default. */
categoryCode: string;
geom: Drawing2DGeom;
/** Optionaler Linienstil (Line Manager); sonst Kategorie-Default. */
lineStyleId?: string;
/** Optionale Schraffur für geschlossene Formen (Hatch Manager). */
hatchId?: string;
/**
* Attribut-Override der Muster-/Schraffurfarbe (Vordergrund) DIESER 2D-Form.
* `undefined` = „Nach System" (erben → HatchStyle.color-Fallback). Betrifft die
* Farbe der Schraffur-Musterlinien einer gefüllten Fläche (nicht den Umriss,
* der über `color`/`lineStyleId` läuft).
*/
foreground?: string;
/**
* Attribut-Override der Füllfarbe (Hintergrund) DIESER 2D-Form. `undefined` =
* „Nach System". Synonym/Nachfolger von `fillColor`; ist `background` gesetzt,
* hat es Vorrang vor `fillColor` (der Poché-Hintergrund der geschlossenen Form).
*/
background?: string;
/** Optionale explizite Strichfarbe; sonst Kategorie-Farbe. */
color?: string;
/**
* Optionale Vollton-Füllfarbe für geschlossene Formen (getrennt von der
* Strichfarbe `color`). Fehlt sie, ist die Fläche transparent (nur Schraffur
* bzw. ungefüllt).
*/
fillColor?: string;
/**
* Optionale direkte Strichstärke-Übersteuerung in Millimetern; hat Vorrang
* vor dem LineStyle-Gewicht und der Kategorie-Strichstärke.
*/
weightMm?: number;
/**
* Quelle des Vordergrunds (Schraffur-Musterfarbe), wenn kein explizites
* `foreground` gesetzt ist: "layer" erzwingt die Ebenenfarbe
* (LayerCategory.color), "object"/`undefined` (Default) = heutiges Verhalten
* (kein Bauteil-Bezug bei Drawing2D → HatchStyle.color-Fallback).
*/
foregroundSource?: AttributeSource;
/** Quelle des Hintergrunds (Füllfarbe), analog zu `foregroundSource`. */
backgroundSource?: AttributeSource;
/**
* Quelle der Strichstärke, wenn kein explizites `weightMm` gesetzt ist:
* "layer" erzwingt `LayerCategory.lw` (unter Umgehung des LineStyle-Gewichts),
* "object"/`undefined` (Default) = heutige Kette (LineStyle.weight ?? Kategorie).
*/
strokeWeightSource?: AttributeSource;
/** Quelle der Schraffur, wenn kein explizites `hatchId` gesetzt ist: "layer"
* erzwingt `LayerCategory.hatch`, "object"/`undefined` (Default) = heutiges
* Verhalten (ohne `hatchId` keine Schraffur). */
hatchSource?: AttributeSource;
}
/**
* Ein Kanten-/Seiten-Griff eines selektierten Elements (zusätzlich zu den
* Eckpunkt-Griffen). Liegt am Mittelpunkt einer Seite (Modell-Meter) und zeigt
* mit `normal` als Einheitsvektor nach AUSSEN (vom Element weg). `aIndex`/
* `bIndex` sind die beiden Vertex-Indizes der Kante — passend zur Indizierung
* von `drawingVertices`/`moveGripOf`. Ziehen verschiebt BEIDE Vertices senkrecht
* zur Kante (Form wächst/schrumpft an dieser Seite).
*/
export interface EdgeGrip {
mid: Vec2;
normal: Vec2;
aIndex: number;
bIndex: number;
/**
* Freie Kanten-Verschiebung: gesetzt bei OFFENER Geometrie (Linie/offene
* Polylinie). Das Segment folgt dem vollen Cursor-Delta (nicht nur der Normale),
* beide Endpunkte wandern mit, die Nachbar-Segmente dehnen sich nach. Bei
* geschlossenen Formen/Wänden fehlt das Flag → senkrechte (parallel-)Verschiebung
* entlang der Außennormale wie bisher.
*/
free?: boolean;
}
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
// Schicht `Project.context` und wird beim Rendern wie eine Referenz behandelt.
// Bewusst three-frei: nur rohe Buffer-Daten (positions/indices), three-Objekte
// entstehen erst im Viewport.
/**
* Ein importiertes Dreiecks-Mesh (z. B. aus DXF 3DFACE/POLYFACE/MESH). Rohe
* BufferGeometry-Daten: `positions` = flaches Array (x,y,z, x,y,z, …) in Metern,
* `indices` = Dreiecks-Indizes (je 3 ein Dreieck). Keine three-Objekte.
*/
export interface ImportedMesh {
id: string;
type: "importedMesh";
name: string;
/** Ursprünglicher DXF-Layer-Name (für spätere Kategorisierung). */
layer?: string;
positions: number[];
indices: number[];
}
/**
* Eine einzelne Kontur (Höhenlinie / Polylinie) auf konstanter Höhe `z`. `pts`
* sind 2D-Stützpunkte (x,y) in Metern; `closed` schließt den Linienzug.
*/
export interface Contour {
z: number;
pts: Vec2[];
closed: boolean;
/** Ursprünglicher DXF-Layer-Name (für die Kategorisierung beim 2D-Import). */
layer?: string;
/**
* Gefüllte Fläche (aus einer DXF-HATCH). Beim Drawing-Import wird daraus eine
* geschlossene, gefüllte 2D-Form (Vollton-Füllung); sonst nur ein Umriss.
*/
filled?: boolean;
/**
* Wahre Kurvengeometrie (aus CIRCLE/ARC). `pts` bleibt tesselliert (Kontext/
* 3D), aber der Drawing-Import baut daraus eine GLATTE `{shape:"circle"|"arc"}`-
* Form statt eines Vielecks. Winkel in RADIANT (CCW); Kreis = a0..a1 über 2π.
*/
curve?: {
kind: "circle" | "arc";
cx: number;
cy: number;
r: number;
a0: number;
a1: number;
};
}
/**
* Ein importiertes Text-Element (DXF TEXT/MTEXT). `at` = Einfügepunkt in Metern,
* `height` = Schrifthöhe in Modell-Metern, `angle` = Drehung in RADIANT (CCW).
* Wird beim Drawing-Import zu einem `{shape:"text"}`-Drawing2D.
*/
export interface ImportedText {
at: Vec2;
text: string;
height: number;
angle: number;
/** Ursprünglicher DXF-Layer-Name (für die Kategorisierung beim 2D-Import). */
layer?: string;
}
/** Ein Satz Konturen (z. B. alle Höhenlinien eines DXF-Imports). */
export interface ContourSet {
id: string;
type: "contourSet";
name: string;
/** Ursprünglicher DXF-Layer-Name (für spätere Kategorisierung). */
layer?: string;
contours: Contour[];
}
/**
* Ein TIN-Geländemodell, abgeleitet aus Konturen (Delaunay über (x,y), Z aus der
* jeweiligen Kontur-Höhe). `positions` = flaches (x,y,z…)-Array in Metern,
* `indices` = Dreiecks-Indizes. Gelände ist NICHT semantisch (Kontext-Schicht).
*/
export interface TerrainMesh {
id: string;
type: "terrainMesh";
name: string;
positions: number[];
indices: number[];
}
/** Ein Kontext-Objekt: importiertes Mesh, Konturen-Satz oder abgeleitetes TIN. */
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;
/** Linienstil-Bibliothek (Line Manager). */
lineStyles: LineStyle[];
/** Schraffur-Bibliothek (Hatch Manager). */
hatches: HatchStyle[];
/** Bauteil-Material-Bibliothek (Component Manager). */
components: Component[];
wallTypes: WallType[];
/**
* Deckentypen (Deckenstile) — dediziert für Decken, analog `wallTypes`.
* Optional, damit bestehende Projekte/Tests ohne `ceilingTypes` gültig
* bleiben; Decken ohne `ceilingTypeId` lösen weiterhin über das LEGACY-Feld
* `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). */
layers: LayerCategory[];
walls: Wall[];
/**
* Decken (Slabs) — geschossgebundene Flächenbauteile mit geschlossenem Umriss.
* Optional, damit bestehende Projekte/Tests ohne `ceilings` gültig bleiben
* (Default: leer behandeln).
*/
ceilings?: Ceiling[];
doors: Door[];
/**
* Öffnungen (Fenster/Türen), gehostet in Wänden. Optional, damit bestehende
* Projekte/Tests ohne `openings` gültig bleiben (Default: leer behandeln).
*/
openings?: Opening[];
/**
* Treppen (Treppe) — geschossübergreifende Bauteile (gerade/L/Wendel). Optional,
* damit bestehende Projekte/Tests ohne `stairs` gültig bleiben (Default: leer).
*/
stairs?: Stair[];
/**
* Räume (SIA-416-Flächen) — geschossgebundene Flächen mit geschlossenem Umriss.
* Optional, damit bestehende Projekte/Tests ohne `rooms` gültig bleiben
* (Default: leer behandeln).
*/
rooms?: Room[];
/** Freie 2D-Zeichengeometrie (Line/Polyline/Rect/Circle/Arc/Text). */
drawings2d: Drawing2D[];
/**
* Kontext-Schicht: importierte/abgeleitete „dumme" Geometrie (Meshes,
* Konturen, Gelände-TIN) — NICHT semantisch. Optional, damit bestehende
* 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).
* Wird durch `resolveParametricWall()` in `src/model/parametricWalls.ts`
* 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
* Wert gültig bleiben (Default: unbestimmt/0). NUR Speicherfeld — die
* eigentliche Verwendung (Terrain-Draping/reale Höhen relativ dazu, siehe
* HANDOVER GEO-BLOCK) ist ein separater, späterer Task und liest dieses
* Feld noch nicht.
*/
referenceElevationMasl?: number;
}
// ── Helfer ───────────────────────────────────────────────────────────────
export const getWallType = (project: Project, wall: Wall): WallType => {
const wt = project.wallTypes.find((t) => t.id === wall.wallTypeId);
if (!wt) throw new Error(`Unbekannter Wandtyp: ${wall.wallTypeId}`);
return wt;
};
/**
* Liefert den Aufbau-Typ einer Decke oder wirft. Bevorzugt den dedizierten
* Deckentyp (`ceilingTypeId` → `ceilingTypes`); fehlt er, fällt die Auflösung
* auf das LEGACY-Feld `wallTypeId` → `wallTypes` zurück (Rückwärtskompatibilität
* mit Projekten von vor den Deckenstilen — dort trug die Decke ihren Aufbau
* direkt über einen WallType).
*/
export const getCeilingType = (project: Project, ceiling: Ceiling): CeilingType | WallType => {
if (ceiling.ceilingTypeId) {
const ct = (project.ceilingTypes ?? []).find((t) => t.id === ceiling.ceilingTypeId);
if (ct) return ct;
}
const wt = project.wallTypes.find((t) => t.id === ceiling.wallTypeId);
if (!wt) throw new Error(`Unbekannter Deckentyp: ${ceiling.ceilingTypeId ?? ceiling.wallTypeId}`);
return wt;
};
/**
* Gesamtdicke einer Decke (Meter): eine explizite `thickness`-Übersteuerung hat
* Vorrang, sonst die Summe der Schichtdicken ihres Aufbau-Typs (siehe
* `getCeilingType`).
*/
export const ceilingThickness = (project: Project, ceiling: Ceiling): number => {
if (ceiling.thickness != null && ceiling.thickness > 0) return ceiling.thickness;
try {
return wallTypeThickness(getCeilingType(project, ceiling));
} catch {
return 0.2;
}
};
/**
* 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);
/** Menschenlesbarer Standardname einer Öffnung (Fenster/Tür + Breite×Höhe). */
export const openingLabel = (o: Opening): string =>
`${o.kind === "door" ? "Tür" : "Fenster"} ${(o.width * 100).toFixed(0)}×${(
o.height * 100
).toFixed(0)}`;
/** Alle Treppen eines Geschosses (leere Liste, wenn keine oder `stairs` fehlt). */
export const stairsOfFloor = (project: Project, floorId: string): Stair[] =>
(project.stairs ?? []).filter((s) => s.floorId === floorId);
/** Menschenlesbarer Standardname einer Treppe (Grundform + Stufenanzahl). */
export const stairLabel = (s: Stair): string => {
const shape =
s.shape === "straight" ? "Gerade" : s.shape === "L" ? "L-Treppe" : "Wendel";
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);
/** Liefert ein Bauteil-Material (Component) per ID oder wirft. */
export const getComponent = (project: Project, id: string): Component => {
const c = project.components.find((co) => co.id === id);
if (!c) throw new Error(`Unbekanntes Bauteil-Material: ${id}`);
return c;
};
/** Liefert eine Schraffur (HatchStyle) per ID oder wirft. */
export const getHatch = (project: Project, id: string): HatchStyle => {
const h = project.hatches.find((ht) => ht.id === id);
if (!h) throw new Error(`Unbekannte Schraffur: ${id}`);
return h;
};
/** Liefert einen Linienstil (LineStyle) per ID oder wirft. */
export const getLineStyle = (project: Project, id: string): LineStyle => {
const l = project.lineStyles.find((ls) => ls.id === id);
if (!l) throw new Error(`Unbekannter Linienstil: ${id}`);
return l;
};
/** Liefert eine Zeichnungsebene (Geschoss) per ID oder wirft. */
export const getFloor = (project: Project, id: string): DrawingLevel => {
const g = project.drawingLevels.find((z) => z.id === id);
if (!g) throw new Error(`Unbekanntes Geschoss: ${id}`);
return g;
};
/**
* Stapelt baseElevation der Geschosse in Dokumentreihenfolge: Das erste
* Geschoss beginnt bei 0, jedes weitere bei baseElevation + floorHeight des
* vorigen Geschosses. Nicht-Geschoss-Ebenen behalten baseElevation undefined.
* Liefert eine neue Liste (mit neuen Geschoss-Objekten); die Eingabe bleibt
* unverändert.
*/
export const recomputeFloorElevations = (
levels: DrawingLevel[],
): DrawingLevel[] => {
let nextBase = 0;
return levels.map((level) => {
if (level.kind !== "floor") {
return { ...level, baseElevation: undefined };
}
const baseElevation = nextBase;
nextBase = baseElevation + (level.floorHeight ?? 0);
return { ...level, baseElevation };
});
};
/** Flacht den Kategorie-Baum (Tiefensuche) in eine Liste ab. */
export const flattenCategories = (cats: LayerCategory[]): LayerCategory[] => {
const out: LayerCategory[] = [];
const walk = (list: LayerCategory[]) => {
for (const c of list) {
out.push(c);
if (c.children) walk(c.children);
}
};
walk(cats);
return out;
};
/**
* Liefert die LayerCategory zu einem Kategorie-Code (Baum durchsucht), oder
* `undefined`, falls der Code auf keine Ebene verweist (verwaister
* `categoryCode`). Nicht-werfend, damit die Attribut-Resolve-Kette
* (`plan/generatePlan.ts`) robust auf einen fehlenden Ebenen-Wert zurückfallen
* kann (→ Bauteil-Fallback).
*/
export const getLayerCategory = (project: Project, code: string): LayerCategory | undefined =>
flattenCategories(project.layers).find((c) => c.code === code);
/** Menge aller Codes sichtbarer Kategorien (Baum berücksichtigt). */
export const collectVisibleCodes = (layers: LayerCategory[]): Set<string> => {
const codes = new Set<string>();
for (const c of flattenCategories(layers)) {
if (c.visible) codes.add(c.code);
}
return codes;
};
/** Gesamtdicke eines Wandtyps = Summe der Schichtdicken. */
export const wallTypeThickness = (wt: WallType): number =>
wt.layers.reduce((sum, l) => sum + l.thickness, 0);
/**
* Kompakter Anzeigetext für einen Wandtyp im Dropdown (VW-Stil).
* - Einschichtig mit Kürzel: „BET 24" (Kürzel + Dicke in cm)
* - Mehrschichtig mit Kürzeln: „GKB·BET·DAE" + Gesamtdicke „(25.5)"
* - Ohne Kürzel: Name bleibt — kein Rückfall auf rohen Namen, nur keine Kurzform.
*
* `project.components` wird genutzt, um `Component.abbrev` aufzulösen.
* Ist kein abbrev gesetzt, erscheint der volle Name unverändert.
*/
export function wallTypeLabel(
wt: WallType | CeilingType,
components: Component[],
): string {
const resolve = (id: string) => components.find((c) => c.id === id);
const totalCm = +(wallTypeThickness(wt) * 100).toFixed(1);
// cm-Zahl: "24" statt "24.0", "17.5" bleibt "17.5"
const cmStr = totalCm % 1 === 0 ? String(totalCm | 0) : String(totalCm);
// Alle Schichten haben ein Kürzel → Kurzform bauen
const abbrevs = wt.layers.map((l) => resolve(l.componentId)?.abbrev ?? "");
const allHaveAbbrev = abbrevs.every((a) => a.length > 0);
if (allHaveAbbrev) {
if (wt.layers.length === 1) return `${abbrevs[0]} ${cmStr}`;
return `${abbrevs.join("·")} (${cmStr})`;
}
// Fallback: voller Name
return wt.name;
}
/** Formatiert Meter mit zwei Nachkommastellen, z. B. "0.35 m". */
export const formatM = (meters: number): string => meters.toFixed(2) + " m";
/**
* Standard-Stiftstärken (mm Papier bei 100 %), Vorgabeliste für Linienstil-/
* Strichstärke-Eingaben. Bedeutung: Breite auf dem Papier — die Linien skalieren
* mit dem Massstab (non-scaling-stroke). Werte sind Vorschläge; man darf abweichen.
*/
export const PEN_WEIGHTS: number[] = [
0.02, 0.1, 0.13, 0.18, 0.25, 0.35, 0.5, 0.7, 1.0, 1.4, 2.0,
];