// Selektions-Attribut-Kontrakt: leitet aus der aktuellen Auswahl // (selectedWallIds/selectedDrawingId) + dem Projekt ein normalisiertes, // darstellungsfertiges Attribut-Objekt für das ERSTE selektierte Element ab. // Die kommenden Paletten (Attributes, Object-Info) lesen ausschließlich diesen // Kontrakt — sie kennen weder das Modell noch die Auflösungs-Logik. // // „Effektiv aufgelöst" = dieselbe Reihenfolge wie generatePlan: explizite // Übersteuerung am Element → LineStyle → Kategorie-Default. So zeigt die Palette // den TATSÄCHLICH gezeichneten Wert, nicht nur das (oft leere) Override-Feld. // // Bezeichner englisch, Kommentare deutsch (CONVENTIONS.md). import { ceilingThickness, flattenCategories, getCeilingType, getComponent, getWallType, wallTypeLabel, wallTypeThickness, } from "../model/types"; import type { AttributeSource, Ceiling, Column, ColumnProfile, Drawing2D, Drawing2DGeom, ExtrudedSolid, LayerCategory, Opening, Project, Roof, RoofShape, Room, SiaCategory, SliceTermination, Stair, StairShape, VerticalAnchor, Wall, WallReferenceLine, } from "../model/types"; import { columnVerticalExtent } from "../model/wall"; import { columnFootprint } from "../geometry/column"; import { evaluateRoom, polygonArea } from "../geometry/roomArea"; import { ceilingVerticalExtent, nextFloorAbove, stairVerticalExtent, wallVerticalExtent, } from "../model/wall"; import { ceilingArea, outlineBBox } from "../geometry/ceiling"; import { roofBBox, roofBaseElevation, roofGeometry } from "../geometry/roof"; import { openingGapQuad, openingVerticalExtent } from "../geometry/opening"; import { stairGeometry, stairBBox } from "../geometry/stair"; /** Eintrag für die WallType-Auswahl (Preset-Dropdown) im Object-Info-Panel. */ export interface WallTypeChoice { id: string; name: string; /** Kurz-Label für das Dropdown (aus wallTypeLabel: Kürzel+Dicke oder Name). */ label: string; /** Gesamtdicke (Meter) — informativ im Dropdown-Label. */ thickness: number; /** Anzahl Schichten (1 = einschichtig). */ layerCount: number; } /** Eintrag für die Geschoss-Auswahl (Referenzgeschoss / OK-Bindung). */ export interface FloorChoice { id: string; name: string; } /** Dach-Attribute fürs Objekt-Info-Panel (nur bei Auswahl genau eines Dachs). */ export interface RoofInfo { shape: RoofShape; /** Hauptneigung (Grad). */ pitchDeg: number; /** Obere Neigung (Grad) — nur Mansarde relevant. */ pitchUpperDeg?: number; /** Dachüberstand an der Traufe (Meter). */ overhang: number; /** Firstrichtung entlang X oder Y. */ ridgeAxis: "x" | "y"; /** Dachdicke (Meter). */ thickness: number; /** Firsthöhe über der Traufe (Meter, abgeleitet). */ ridgeHeight: number; /** Grundfläche des Umriss-Rechtecks (m², ohne Überstand). */ footprintArea: number; } /** * Eine wählbare Schichttrennlinie (Fuge zwischen zwei Schichten) als * Referenzlinie einer mehrschichtigen Wand. `offset` ist der fertige * Achsversatz entlang +n (Meter), `label` benennt die angrenzenden Schichten. */ export interface LayerBoundaryChoice { offset: number; label: string; } /** * Wand-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl einer * einzelnen Wand gesetzt). Effektiv aufgelöste, anzeigefertige Werte plus die * Listen, die die Dropdowns brauchen. */ export interface WallInfo { referenceLine: WallReferenceLine; /** Freier Achsversatz (Schichttrennlinie), falls gesetzt — sonst undefined. */ referenceOffset?: number; /** * Interne Schichttrennlinien der Wand als wählbare Referenz (leer bei * einschichtigen Wänden). Aussen-/Innenfläche stecken bereits in * left/right — hier stehen nur die FUGEN dazwischen. */ layerBoundaries: LayerBoundaryChoice[]; /** Terminierungs-Regel am Deckenanschluss (Default "both" = heutiges Verhalten). */ sliceTermination: SliceTermination; wallTypeId: string; /** Aktuelle Gesamtdicke (Meter). */ thickness: number; /** Ob der aktuelle Wandtyp einschichtig ist (1 Schicht). */ singleLayer: boolean; /** Verfügbare Wandtyp-Presets (für den Mehrschicht-Aufbau). */ wallTypes: WallTypeChoice[]; /** Geschoss, dem die Wand zugeordnet ist (Referenzgeschoss). */ floorId: string; floorName: string; /** Alle Geschosse (für UK/OK-„an Geschoss gebunden"-Auswahl). */ floors: FloorChoice[]; /** Geschoss direkt darüber (für „Verknüpfung zum oberen Geschoss"). */ floorAbove?: FloorChoice; /** UK-Bindung (undefined = Geschoss-Default). */ bottom?: VerticalAnchor; /** OK-Bindung (undefined = UK + height). */ top?: VerticalAnchor; /** Aufgelöste absolute UK/OK (Meter) + abgeleitete Höhe. */ zBottom: number; zTop: number; /** Achslänge der Wand (Meter). */ length: number; /** Bruttovolumen = Länge × Dicke × Höhe (m³, ohne Öffnungsabzug). */ grossVolume: number; /** Summe der Öffnungsvolumina (Fenster/Türen) dieser Wand (m³). */ openingVolume: number; /** Nettovolumen = Brutto − Öffnungen (m³, ≥ 0). */ netVolume: number; } /** * Decken-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau * einer Decke gesetzt). Anzeigefertige Werte + Listen für die Dropdowns. */ export interface CeilingInfo { /** Effektiv aktiver Aufbau-Typ: `ceilingTypeId`, sonst LEGACY-`wallTypeId`. */ typeId: string; /** Aktuelle Gesamtdicke (Meter). */ thickness: number; /** Ob der aktuelle Aufbau-Typ einschichtig ist (1 Schicht). */ singleLayer: boolean; /** Grundfläche der Decke (m²). */ area: number; /** Volumen = Fläche × Dicke (m³). */ volume: number; /** Verfügbare Deckentyp-Presets (dediziert, `project.ceilingTypes`). */ ceilingTypes: WallTypeChoice[]; /** Geschoss, dem die Decke zugeordnet ist. */ floorId: string; floorName: string; /** Alle Geschosse (für die OK-„an Geschoss gebunden"-Auswahl). */ floors: FloorChoice[]; /** OK-Bindung (undefined = Geschoss-Oberkante). */ top?: VerticalAnchor; /** UK-Bindung (undefined = OK − Dicke). */ bottom?: VerticalAnchor; /** Aufgelöste absolute UK/OK (Meter). */ zBottom: number; zTop: number; } /** * Öffnungs-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl * genau einer Öffnung gesetzt). Anzeigefertige Werte + Wirts-Wand-Bezug. */ export interface OpeningInfo { kind: "window" | "door"; /** Wirts-Wand (ID + Name/Geschoss zur Anzeige). */ hostWallId: string; hostWallName: string; /** Abstand vom Wand-Startpunkt (Meter). */ position: number; width: number; height: number; /** Brüstungshöhe (Meter); 0 bei Türen. */ sillHeight: number; /** Nur Tür: Anschlag/Aufschlag/Winkel/Richtung. */ hinge?: "start" | "end"; swing?: "left" | "right"; swingAngle?: number; openingDir?: "in" | "out"; /** Nur Fenster: Anzahl der Flügel (1–4). Fehlt → 1 (Default). */ wingCount?: number; /** Nur Tür: Tür-Typ (normal / Wandöffnung). Fehlt → "normal". */ doorType?: "normal" | "wandoeffnung"; /** Nur Tür: Sturzlinien (keine/innen/aussen/beide). Fehlt → "beide". */ lintelLines?: "keine" | "innen" | "aussen" | "beide"; /** Zugewiesener Tür-/Fenstertyp (Bibliothek, `Opening.typeId`) oder undefined. */ typeId?: string; /** Aufgelöste absolute UK/OK (Meter). */ zBottom: number; zTop: number; } /** * Treppen-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau * einer Treppe gesetzt). Anzeigefertige Werte + Listen für die Dropdowns. */ export interface StairInfo { shape: StairShape; /** Laufbreite (Meter). */ width: number; /** Stufenanzahl (Setzstufen). */ stepCount: number; /** Gesamt-Steighöhe (Meter, OKFF → OKFF). */ totalRise: number; /** Abgeleitete Steigungshöhe (Meter) = totalRise / stepCount. */ riserHeight: number; /** Abgeleitete Auftrittstiefe (Meter). */ treadDepth: number; /** Laufrichtung aufwärts (Auf-/Abpfeil-Richtung). */ up: boolean; /** Referenzpunkt der Laufbreite (links/mitte/rechts). Fehlt → "mitte". */ referenz?: "links" | "mitte" | "rechts"; /** Zugewiesener Treppentyp (Bibliothek, `Stair.typeId`) oder undefined. */ typeId?: string; /** Geschoss, dem die Treppe zugeordnet ist. */ floorId: string; floorName: string; /** Aufgelöste absolute UK/OK (Meter). */ zBottom: number; zTop: number; } /** * Raum-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau * eines Raums gesetzt). Anzeigefertige Werte; Fläche/Umfang werden aus dem * Umriss abgeleitet (nie gespeichert). */ export interface RoomInfo { /** Raum-Name (editierbar). */ name: string; /** SIA-416-Blatt-Kategorie (HNF/NNF/VF/FF/KGF). */ siaCategory: SiaCategory; /** Netto-anrechenbare Fläche (m²). */ area: number; /** Umfang des Umrisses (m). */ perimeter: number; /** Geschoss, dem der Raum zugeordnet ist. */ floorId: string; floorName: string; } /** * Extrusions-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl * genau eines extrudierten Körpers, truck-Integration, gesetzt). */ export interface ExtrudedSolidInfo { /** Extrusionshöhe in Metern (editierbar). */ height: number; /** Verjüngung 0 (Prisma) … 1 (Spitze), editierbar. */ taper: number; /** Grundfläche des Profils (m²). */ area: number; /** Geschoss, dessen `baseElevation` die UK des Körpers bestimmt. */ floorId: string; floorName: string; } /** * Stützen-spezifische Attribute für das Object-Info-Panel (nur bei Auswahl genau * einer Stütze gesetzt). Anzeigefertige Werte für die Panel-Felder. */ export interface ColumnInfo { /** Profil (Rechteck/Kreis) — bestimmt, welche Masse editierbar sind. */ profile: ColumnProfile; /** Höhe (Meter, editierbar). */ height: number; /** Drehung (Grad, editierbar) — intern in Radiant gespeichert. */ rotationDeg: number; /** Geschoss, dem die Stütze zugeordnet ist. */ floorId: string; floorName: string; /** Aufgelöste absolute UK/OK (Meter). */ zBottom: number; zTop: number; } /** Achsparallele Bounding-Box eines Elements in Modell-Metern. */ export interface SelectionBBox { minX: number; minY: number; maxX: number; maxY: number; } /** * Normalisierte Sicht auf das erste selektierte Element. Farbe/Strichstärke sind * EFFEKTIV aufgelöst (Override sonst Default), damit die Palette den real * gezeichneten Wert zeigt. `fillHatchId`/`closed` sind nur für Drawing2D sinnvoll. */ export interface Selection { kind: | "wall" | "ceiling" | "opening" | "stair" | "room" | "drawing2d" | "door" | "extrudedSolid" | "column" | "roof"; id: string; /** Grafik-Kategorie (Ebene) des Elements. */ categoryCode: string; /** Effektiv aufgelöste Strich-/Umrandungsfarbe (hex). */ color: string; /** Effektiv aufgelöste Strichstärke in Millimetern. */ weightMm: number; /** * Schraffur-ID der Füllung (nur Drawing2D, geschlossene Form); `null` = keine. * Bei Wänden/Türen `undefined` (Konzept nicht anwendbar). */ fillHatchId?: string | null; /** * Vollton-Füllfarbe der Füllung (nur Drawing2D, geschlossene Form); `null` = * keine (transparent). Bei Wänden/Türen `undefined` (nicht anwendbar). */ fillColor?: string | null; /** * Vordergrund-Override (Muster-/Schraffurfarbe) des Elements; `undefined` = * „Nach System" (erbt vom Bauteil/System). Gesetzt bei Wand, Decke und * geschlossener Drawing2D. */ foreground?: string; /** * Hintergrund-Override (Füllfarbe/Poché) des Elements; `undefined` = „Nach * System". Nachfolger von `fillColor`; hat im Plan Vorrang. */ background?: string; /** * Quelle des Vordergrunds, wenn `foreground` NICHT gesetzt ist ("layer" = * Nach Ebene, "object"/`undefined` = Nach Bauteil, Default). Gesetzt bei * Wand, Decke und geschlossener Drawing2D (wie `foreground`/`pocheEditable`). */ foregroundSource?: AttributeSource; /** Quelle des Hintergrunds, analog zu {@link Selection.foregroundSource}. */ backgroundSource?: AttributeSource; /** * RAW Strichstärke-Override (mm) DIESES Elements; `undefined` = kein Override * (Herkunft dann `strokeWeightSource`). Gesetzt bei Wand, Decke, Drawing2D. * `weightMm` bleibt der EFFEKTIVE (aufgelöste) Wert für die Anzeige. */ strokeWeightOverride?: number; /** Quelle der Strichstärke, wenn kein `strokeWeightOverride` gesetzt ist. */ strokeWeightSource?: AttributeSource; /** Quelle der Schraffur, wenn kein `fillHatchId` gesetzt ist. */ hatchSource?: AttributeSource; /** Ob die Form geschlossen ist (rect/circle/closed polyline) — nur Drawing2D. */ closed?: boolean; /** Achsparallele Bounding-Box in Modell-Metern. */ bbox: SelectionBBox; /** * Wand-Attribute (nur bei `kind === "wall"`). Trägt Referenzlinie, Aufbau-Typ * (Wandtyp/Dicke), Referenzgeschoss + UK/OK für den Wand-Abschnitt im Panel. */ wall?: WallInfo; /** Decken-Attribute (nur bei `kind === "ceiling"`). */ ceiling?: CeilingInfo; /** Dach-Attribute (nur bei `kind === "roof"`). */ roof?: RoofInfo; /** Öffnungs-Attribute (nur bei `kind === "opening"`). */ opening?: OpeningInfo; /** Treppen-Attribute (nur bei `kind === "stair"`). */ stair?: StairInfo; /** Raum-Attribute (nur bei `kind === "room"`). */ room?: RoomInfo; /** Extrusions-Attribute (nur bei `kind === "extrudedSolid"`). */ extrudedSolid?: ExtrudedSolidInfo; /** Stützen-Attribute (nur bei `kind === "column"`). */ column?: ColumnInfo; } const WALL_FALLBACK_MM = 0.18; const DRAW_FALLBACK_COLOR = "#d0d0d0"; const WALL_FALLBACK_COLOR = "#2b3039"; /** Kategorie (per Code) im Baum suchen — flach, da Codes baumweit eindeutig sind. */ function findCategory( layers: LayerCategory[], code: string, ): LayerCategory | undefined { return flattenCategories(layers).find((c) => c.code === code); } /** Ob eine 2D-Form geschlossen ist (Füllung/Schraffur sinnvoll). */ function isClosedGeom(g: Drawing2DGeom): boolean { return ( g.shape === "rect" || g.shape === "circle" || (g.shape === "polyline" && g.closed) ); } /** Bounding-Box einer 2D-Form. */ function geomBBox(g: Drawing2DGeom): SelectionBBox { let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; const acc = (x: number, y: number) => { minX = Math.min(minX, x); minY = Math.min(minY, y); maxX = Math.max(maxX, x); maxY = Math.max(maxY, y); }; switch (g.shape) { case "line": acc(g.a.x, g.a.y); acc(g.b.x, g.b.y); break; case "polyline": for (const p of g.pts) acc(p.x, p.y); break; case "rect": acc(g.min.x, g.min.y); acc(g.max.x, g.max.y); break; case "circle": case "arc": acc(g.center.x - g.r, g.center.y - g.r); acc(g.center.x + g.r, g.center.y + g.r); break; case "text": acc(g.at.x, g.at.y); break; } if (!isFinite(minX)) return { minX: 0, minY: 0, maxX: 0, maxY: 0 }; return { minX, minY, maxX, maxY }; } /** Selektion für eine Wand ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */ function wallSelection(project: Project, wall: Wall): Selection { const cat = findCategory(project.layers, wall.categoryCode); const color = wall.color ?? cat?.color ?? WALL_FALLBACK_COLOR; // Effektive Strichstärke: expliziter Override zuerst, sonst die Kategorie // (die Quelle "layer"/"object" macht heute keinen Unterschied, da Component // keine eigene Strichstärke trägt — siehe resolveStrokeWeight). const weightMm = wall.strokeWeight ?? cat?.lw ?? WALL_FALLBACK_MM; const minX = Math.min(wall.start.x, wall.end.x); const minY = Math.min(wall.start.y, wall.end.y); const maxX = Math.max(wall.start.x, wall.end.x); const maxY = Math.max(wall.start.y, wall.end.y); // Wand-Attribute auflösen (Referenzlinie, Aufbau, Referenzgeschoss, UK/OK). const wt = getWallType(project, wall); const thickness = wallTypeThickness(wt); const floor = project.drawingLevels.find((z) => z.id === wall.floorId); const { zBottom, zTop } = wallVerticalExtent(project, wall); const above = nextFloorAbove(project, wall); // Volumen aus Achslänge × Gesamtdicke × Höhe, abzüglich der Öffnungen (jede // Öffnung durchbricht die volle Wanddicke: Breite × Höhe × Dicke). const length = Math.hypot(wall.end.x - wall.start.x, wall.end.y - wall.start.y); const wallHeight = Math.max(0, zTop - zBottom); const grossVolume = length * thickness * wallHeight; const openingVolume = (project.openings ?? []) .filter((o) => o.hostWallId === wall.id) .reduce((sum, o) => sum + o.width * o.height * thickness, 0); const netVolume = Math.max(0, grossVolume - openingVolume); // Interne Schichttrennlinien als wählbare Referenz: von aussen (Schicht 0) // nach innen aufsummiert; der Achsversatz +n = halbe Dicke − Fugenlage. const half = thickness / 2; const layerBoundaries: WallInfo["layerBoundaries"] = []; let cum = 0; const abbrevOf = (componentId: string): string => { const c = getComponent(project, componentId); return c ? c.abbrev ?? c.name : "?"; }; for (let i = 0; i < wt.layers.length - 1; i++) { cum += wt.layers[i].thickness; layerBoundaries.push({ offset: half - cum, label: `${abbrevOf(wt.layers[i].componentId)}|${abbrevOf(wt.layers[i + 1].componentId)}`, }); } const wallInfo: WallInfo = { referenceLine: wall.referenceLine ?? "center", referenceOffset: wall.referenceOffset, layerBoundaries, sliceTermination: wall.sliceTermination ?? "both", wallTypeId: wall.wallTypeId, thickness, singleLayer: wt.layers.length <= 1, wallTypes: project.wallTypes.map((t) => ({ id: t.id, name: t.name, label: wallTypeLabel(t, project.components), thickness: wallTypeThickness(t), layerCount: t.layers.length, })), floorId: wall.floorId, floorName: floor?.name ?? wall.floorId, floors: project.drawingLevels .filter((z) => z.kind === "floor") .map((z) => ({ id: z.id, name: z.name })), floorAbove: above, bottom: wall.bottom, top: wall.top, zBottom, zTop, length, grossVolume, openingVolume, netVolume, }; // bbox aus der Wandachse (start/end); die Schichtdicke bleibt unberücksichtigt, // damit die Box der Achs-Geometrie folgt (genügt für die Paletten). return { kind: "wall", id: wall.id, categoryCode: wall.categoryCode, color, weightMm, fillHatchId: wall.hatchId, fillColor: undefined, foreground: wall.foreground, background: wall.background, foregroundSource: wall.foregroundSource, backgroundSource: wall.backgroundSource, strokeWeightOverride: wall.strokeWeight, strokeWeightSource: wall.strokeWeightSource, hatchSource: wall.hatchSource, closed: undefined, bbox: { minX, minY, maxX, maxY }, wall: wallInfo, }; } /** Selektion für eine Decke ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */ function ceilingSelection(project: Project, ceiling: Ceiling): Selection { const cat = findCategory(project.layers, ceiling.categoryCode); const color = ceiling.color ?? cat?.color ?? WALL_FALLBACK_COLOR; const weightMm = ceiling.strokeWeight ?? cat?.lw ?? WALL_FALLBACK_MM; const box = outlineBBox(ceiling.outline); const wt = getCeilingType(project, ceiling); const thickness = ceilingThickness(project, ceiling); const floor = project.drawingLevels.find((z) => z.id === ceiling.floorId); const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling); const ceilingInfo: CeilingInfo = { typeId: ceiling.ceilingTypeId ?? ceiling.wallTypeId, thickness, singleLayer: wt.layers.length <= 1, area: ceilingArea(ceiling.outline), volume: ceilingArea(ceiling.outline) * thickness, ceilingTypes: (project.ceilingTypes ?? []).map((t) => ({ id: t.id, name: t.name, label: wallTypeLabel(t, project.components), thickness: wallTypeThickness(t), layerCount: t.layers.length, })), floorId: ceiling.floorId, floorName: floor?.name ?? ceiling.floorId, floors: project.drawingLevels .filter((z) => z.kind === "floor") .map((z) => ({ id: z.id, name: z.name })), top: ceiling.top, bottom: ceiling.bottom, zBottom, zTop, }; return { kind: "ceiling", id: ceiling.id, categoryCode: ceiling.categoryCode, color, weightMm, fillHatchId: ceiling.hatchId, fillColor: undefined, foreground: ceiling.foreground, background: ceiling.background, foregroundSource: ceiling.foregroundSource, backgroundSource: ceiling.backgroundSource, strokeWeightOverride: ceiling.strokeWeight, strokeWeightSource: ceiling.strokeWeightSource, hatchSource: ceiling.hatchSource, closed: true, bbox: { minX: box.minX, minY: box.minY, maxX: box.maxX, maxY: box.maxY }, ceiling: ceilingInfo, }; } /** Selektion für ein Dach ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */ function roofSelection(project: Project, roof: Roof): Selection { const cat = findCategory(project.layers, roof.categoryCode); const color = roof.color ?? cat?.color ?? WALL_FALLBACK_COLOR; const weightMm = cat?.lw ?? WALL_FALLBACK_MM; const bb = roofBBox(roof.outline); const eavesZ = roofBaseElevation(project, roof); const g = roofGeometry(roof, eavesZ); const footprintArea = Math.abs(bb.x1 - bb.x0) * Math.abs(bb.y1 - bb.y0); const roofInfo: RoofInfo = { shape: roof.shape, pitchDeg: roof.pitchDeg, pitchUpperDeg: roof.pitchUpperDeg, overhang: roof.overhang, ridgeAxis: roof.ridgeAxis, thickness: roof.thickness, ridgeHeight: g.ridgeHeight, footprintArea, }; return { kind: "roof", id: roof.id, categoryCode: roof.categoryCode, color, weightMm, closed: true, bbox: { minX: bb.x0, minY: bb.y0, maxX: bb.x1, maxY: bb.y1 }, roof: roofInfo, }; } /** Selektion für eine Öffnung ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */ function openingSelection(project: Project, o: Opening): Selection { const cat = findCategory(project.layers, o.categoryCode); const color = o.color ?? cat?.color ?? WALL_FALLBACK_COLOR; const weightMm = cat?.lw ?? WALL_FALLBACK_MM; const wall = project.walls.find((w) => w.id === o.hostWallId); const floor = wall ? project.drawingLevels.find((z) => z.id === wall.floorId) : undefined; // Bounding-Box aus dem Lücken-Quad (volle Wanddicke); Fallback: leere Box. const quad = wall ? openingGapQuad(project, wall, o) : null; let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const p of quad ?? []) { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); } if (!isFinite(minX)) { minX = minY = maxX = maxY = 0; } const ext = wall ? openingVerticalExtent(project, wall, o) : { zBottom: 0, zTop: o.height }; const openingInfo: OpeningInfo = { kind: o.kind, hostWallId: o.hostWallId, hostWallName: floor ? `${floor.name}` : o.hostWallId, position: o.position, width: o.width, height: o.height, sillHeight: o.sillHeight, hinge: o.hinge, swing: o.swing, swingAngle: o.swingAngle, openingDir: o.openingDir, wingCount: o.wingCount, doorType: o.doorType, lintelLines: o.lintelLines, typeId: o.typeId, zBottom: ext.zBottom, zTop: ext.zTop, }; return { kind: "opening", id: o.id, categoryCode: o.categoryCode, color, weightMm, fillHatchId: undefined, fillColor: undefined, closed: undefined, bbox: { minX, minY, maxX, maxY }, opening: openingInfo, }; } /** Selektion für eine Treppe ableiten (Farbe übersteuerbar, Strichstärke = Kategorie). */ function stairSelection(project: Project, s: Stair): Selection { const cat = findCategory(project.layers, s.categoryCode); const color = s.color ?? cat?.color ?? WALL_FALLBACK_COLOR; const weightMm = cat?.lw ?? WALL_FALLBACK_MM; const { zBottom, zTop } = stairVerticalExtent(project, s); const totalRise = zTop - zBottom; const geo = stairGeometry(s, totalRise); const box = stairBBox(geo); const floor = project.drawingLevels.find((z) => z.id === s.floorId); const stairInfo: StairInfo = { shape: s.shape, width: s.width, stepCount: s.stepCount, totalRise, riserHeight: geo.riserHeight, treadDepth: geo.treadDepth, up: s.up !== false, referenz: s.referenz, typeId: s.typeId, floorId: s.floorId, floorName: floor?.name ?? s.floorId, zBottom, zTop, }; return { kind: "stair", id: s.id, categoryCode: s.categoryCode, color, weightMm, fillHatchId: undefined, fillColor: undefined, closed: undefined, bbox: { minX: box.minX, minY: box.minY, maxX: box.maxX, maxY: box.maxY }, stair: stairInfo, }; } /** Selektion für einen Raum ableiten (Fläche/Umfang aus dem Umriss abgeleitet). */ function roomSelection(project: Project, r: Room): Selection { const cat = findCategory(project.layers, r.categoryCode); const color = r.color ?? cat?.color ?? WALL_FALLBACK_COLOR; const weightMm = cat?.lw ?? WALL_FALLBACK_MM; const res = evaluateRoom(r.boundary, r.siaCategory); const floor = project.drawingLevels.find((z) => z.id === r.floorId); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const p of r.boundary) { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); } if (!isFinite(minX)) minX = minY = maxX = maxY = 0; const roomInfo: RoomInfo = { name: r.name, siaCategory: r.siaCategory, area: res.netArea, perimeter: res.perimeter, floorId: r.floorId, floorName: floor?.name ?? r.floorId, }; return { kind: "room", id: r.id, categoryCode: r.categoryCode, color, weightMm, fillHatchId: undefined, fillColor: undefined, closed: true, bbox: { minX, minY, maxX, maxY }, room: roomInfo, }; } /** Auswahl-Farbe der Extrusions-Footprints (identisch zum Grundriss-Umriss/3D-Orange). */ const EXTRUSION_COLOR = "#d98c40"; /** Selektion für einen extrudierten Körper ableiten (truck-Integration). */ function extrudedSolidSelection(project: Project, s: ExtrudedSolid): Selection { const floor = project.drawingLevels.find((z) => z.id === s.levelId); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const p of s.points) { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); } if (!isFinite(minX)) minX = minY = maxX = maxY = 0; const info: ExtrudedSolidInfo = { height: s.height, taper: s.taper ?? 0, area: polygonArea(s.points), floorId: s.levelId, floorName: floor?.name ?? s.levelId, }; return { kind: "extrudedSolid", id: s.id, categoryCode: "", color: EXTRUSION_COLOR, weightMm: WALL_FALLBACK_MM, fillHatchId: null, fillColor: null, closed: true, bbox: { minX, minY, maxX, maxY }, extrudedSolid: info, }; } /** Umriss-/Auswahlfarbe der Stützen-Poché (neutrales Tragwerks-Grau). */ const COLUMN_COLOR = "#3a3f47"; /** Selektion für eine Stütze (Column) ableiten. */ function columnSelection(project: Project, col: Column): Selection { const floor = project.drawingLevels.find((z) => z.id === col.floorId); const category = findCategory(project.layers, col.categoryCode); const pts = columnFootprint(col); let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (const p of pts) { minX = Math.min(minX, p.x); minY = Math.min(minY, p.y); maxX = Math.max(maxX, p.x); maxY = Math.max(maxY, p.y); } if (!isFinite(minX)) minX = minY = maxX = maxY = 0; const { zBottom, zTop } = columnVerticalExtent(project, col); const info: ColumnInfo = { profile: col.profile, height: col.height, rotationDeg: ((col.rotation ?? 0) * 180) / Math.PI, floorId: col.floorId, floorName: floor?.name ?? col.floorId, zBottom, zTop, }; return { kind: "column", id: col.id, categoryCode: col.categoryCode, color: col.color ?? category?.color ?? COLUMN_COLOR, weightMm: WALL_FALLBACK_MM, bbox: { minX, minY, maxX, maxY }, column: info, }; } /** Selektion für ein 2D-Zeichenelement ableiten (gleiche Auflösung wie generatePlan). */ function drawingSelection(project: Project, d: Drawing2D): Selection { const ls = d.lineStyleId ? project.lineStyles.find((l) => l.id === d.lineStyleId) : undefined; const cat = findCategory(project.layers, d.categoryCode); const color = d.color ?? ls?.color ?? cat?.color ?? DRAW_FALLBACK_COLOR; // Effektive Strichstärke, dieselbe Kette wie generatePlan/addDrawing2D // (resolveStrokeWeight): Override zuerst, "layer" erzwingt die Kategorie // (umgeht das LineStyle-Gewicht), sonst die heutige LineStyle-/Kategorie-Kette. const weightMm = d.weightMm ?? (d.strokeWeightSource === "layer" ? cat?.lw : undefined) ?? ls?.weight ?? cat?.lw ?? WALL_FALLBACK_MM; const closed = isClosedGeom(d.geom); return { kind: "drawing2d", id: d.id, categoryCode: d.categoryCode, color, weightMm, fillHatchId: closed ? d.hatchId ?? null : null, fillColor: closed ? d.fillColor ?? null : null, foreground: closed ? d.foreground : undefined, background: closed ? d.background : undefined, foregroundSource: closed ? d.foregroundSource : undefined, backgroundSource: closed ? d.backgroundSource : undefined, strokeWeightOverride: d.weightMm, strokeWeightSource: d.strokeWeightSource, hatchSource: closed ? d.hatchSource : undefined, closed, bbox: geomBBox(d.geom), }; } /** * Leitet den Selektions-Kontrakt für das ERSTE selektierte Element ab. * Drawing-Auswahl und Wand-Auswahl schließen sich aus; bei Drawing-Auswahl * hat diese Vorrang. Liefert `null`, wenn nichts (oder kein auffindbares * Element) selektiert ist. */ export function deriveSelection( project: Project, selectedWallIds: string[], selectedDrawingId: string | null, selectedCeilingId: string | null = null, selectedOpeningId: string | null = null, selectedStairId: string | null = null, selectedRoomId: string | null = null, selectedExtrudedSolidId: string | null = null, selectedColumnId: string | null = null, selectedRoofId: string | null = null, ): Selection | null { if (selectedDrawingId) { const d = project.drawings2d.find((x) => x.id === selectedDrawingId); return d ? drawingSelection(project, d) : null; } if (selectedExtrudedSolidId) { const s = (project.extrudedSolids ?? []).find((x) => x.id === selectedExtrudedSolidId); return s ? extrudedSolidSelection(project, s) : null; } if (selectedColumnId) { const c = (project.columns ?? []).find((x) => x.id === selectedColumnId); return c ? columnSelection(project, c) : null; } if (selectedOpeningId) { const o = (project.openings ?? []).find((x) => x.id === selectedOpeningId); if (o) return openingSelection(project, o); } const wallId = selectedWallIds[0]; if (wallId) { const w = project.walls.find((x) => x.id === wallId); return w ? wallSelection(project, w) : null; } if (selectedCeilingId) { const c = (project.ceilings ?? []).find((x) => x.id === selectedCeilingId); return c ? ceilingSelection(project, c) : null; } if (selectedRoofId) { const r = (project.roofs ?? []).find((x) => x.id === selectedRoofId); return r ? roofSelection(project, r) : null; } if (selectedStairId) { const s = (project.stairs ?? []).find((x) => x.id === selectedStairId); return s ? stairSelection(project, s) : null; } if (selectedRoomId) { const r = (project.rooms ?? []).find((x) => x.id === selectedRoomId); return r ? roomSelection(project, r) : null; } return null; }