Files
DOSSIER-STANDALONE/src/model/types.ts
T
karim 25cebbd98c Linien: modulares Segment-System (Strich/Punkt/Luecke) + Loop-Vorschau
Der Linien-Editor ist modular: eine Linie ist eine geordnete, loopende Folge
aus Segmenten Strich (Laenge), Punkt (Dot) und Luecke (Laenge) — beliebige
Sequenzen (Volllinie/Strichlinie/Punktlinie/Strich-Punkt als Presets, plus
frei), die Schluss-Luecke ist die letzte Luecke. Datenbasis bleibt
LineStyle.dash (mm, alternierend); ein Punkt ist ein 0-Laengen-AN-Segment.
Enthaelt dash eine 0, wird die Linie mit runder Kappe gezeichnet, damit
Punkte als Dots erscheinen (Live + Print; GL/DXF Folgearbeit). Neue reine
Segment-Logik in ui/lineSegments.ts. LineSwatch zeigt den ersten Loop dunkel
und 2 weitere grau (Loop-Kontext). 14 neue Tests, 127 gruen.
2026-07-04 01:21:04 +02:00

1217 lines
48 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 } from "../text/richText";
export type { RichTextDoc } from "../text/richText";
// ── 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;
/** 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[];
}
// ── 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[];
}
/**
* 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;
/**
* Lage der Wandachse über die Dicke. Fehlt sie, gilt "center" (= heutiges
* Verhalten: Schichten symmetrisch T/2 … +T/2 um die Achse).
*/
referenceLine?: WallReferenceLine;
/**
* 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;
}
/**
* 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.
*/
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;
/**
* Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der
* Oberkante des Geschosses (baseElevation + floorHeight).
*/
top?: 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 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;
/**
* 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;
/**
* 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;
/**
* 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;
}
/** 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 };
/** 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;
}
/**
* 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;
// ── 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;
}
/** 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. */
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[];
/** 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[];
/**
* 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[];
}
// ── 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;
}
};
/** 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 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;
};
/** 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);
/** 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,
];