From d0f26cd874288de25fefbb1017354e663fbcc459 Mon Sep 17 00:00:00 2001 From: Karim Date: Sat, 4 Jul 2026 02:19:38 +0200 Subject: [PATCH] Attribute: By-Layer/By-Object-Quelle fuer Farbe, Strichstaerke, Schraffur Vordergrund, Hintergrund, Strichstaerke und Schraffur je Element (Wand/Decke/ Drawing2D) haben jetzt einen 3-Wege-Quellen-Dropdown: Nach Ebene / Nach Bauteil / eigener Wert. Aufloesungsreihenfolge: expliziter Wert > (Quelle 'layer' => LayerCategory-Wert color/lw/hatch) > Bauteil/LineStyle-Default. Neue *Source-Felder + strokeWeight/hatchId-Overrides am Modell (additiv, optional), getLayerCategory-Accessor, resolveHatchId/resolveStrokeWeight; resolveForeground/Background um category+source erweitert. Sample rendert identisch (kein Override/Source gesetzt => Default 'object' = altes Verhalten). Offen: LayerCategory-Schraffur noch nicht im Kategorie-Dialog editierbar. --- src/App.tsx | 63 ++++++++- src/i18n/de.ts | 5 +- src/i18n/en.ts | 5 +- src/model/types.ts | 92 +++++++++++++ src/panels/AttributesPanel.tsx | 235 ++++++++++++++++++++++----------- src/panels/host.ts | 36 ++++- src/plan/generatePlan.ts | 194 ++++++++++++++++++++++----- src/state/projectSlice.ts | 14 ++ src/state/selectionInfo.ts | 55 +++++++- 9 files changed, 571 insertions(+), 128 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index fd41ab1..afe77d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1769,6 +1769,7 @@ export default function App() { const setElementFillColor = useStore((s) => s.setElementFillColor); const setElementForeground = useStore((s) => s.setElementForeground); const setElementBackground = useStore((s) => s.setElementBackground); + const updateDrawing2D = useStore((s) => s.updateDrawing2D); const resizeElement = useStore((s) => s.resizeElement); const updateWall = useStore((s) => s.updateWall); const setWallThickness = useStore((s) => s.setWallThickness); @@ -2065,13 +2066,22 @@ export default function App() { updateRoom(selection.id, { color }); }, onSetSelectionWeight: (weightMm) => { - // Strichstärke gibt es als direktes Feld nur bei Drawing2D. - if (selection?.kind === "drawing2d") + // Strichstärke-Override: Wand/Decke (`strokeWeight`) oder Drawing2D + // (`weightMm`, Alt-Feldname bleibt). + if (selection?.kind === "wall") updateWall(selection.id, { strokeWeight: weightMm }); + else if (selection?.kind === "ceiling") + updateCeiling(selection.id, { strokeWeight: weightMm }); + else if (selection?.kind === "drawing2d") setElementWeight(selection.id, weightMm); }, onSetSelectionFill: (hatchId) => { - // Füllschraffur nur bei Drawing2D (geschlossene Formen). - if (selection?.kind === "drawing2d") + // Schraffur-Override: Wand, Decke oder Drawing2D (geschlossene Formen). + // `null` löscht das Feld (→ „Nach System"/Quelle). + if (selection?.kind === "wall") + updateWall(selection.id, { hatchId: hatchId ?? undefined }); + else if (selection?.kind === "ceiling") + updateCeiling(selection.id, { hatchId: hatchId ?? undefined }); + else if (selection?.kind === "drawing2d") setElementFill(selection.id, hatchId); }, onSetSelectionFillColor: (color) => { @@ -2097,6 +2107,51 @@ export default function App() { ) setElementBackground(selection.kind, selection.id, color); }, + // ── By-Layer/By-Object-Quellen (Vordergrund/Hintergrund/Strichstärke/ + // Schraffur): "layer" erzwingt die Ebene, "object" erbt vom Bauteil — + // beide löschen dabei den expliziten Wert (siehe Modell-Kommentar + // `AttributeSource`); „eigener Wert" läuft weiterhin über die + // onSetSelection{Foreground,Background,Weight,Fill}-Setter oben. + onSetSelectionForegroundSource: (source) => { + if (!selection) return; + const patch = { + foregroundSource: source === "layer" ? ("layer" as const) : undefined, + foreground: undefined, + }; + if (selection.kind === "wall") updateWall(selection.id, patch); + else if (selection.kind === "ceiling") updateCeiling(selection.id, patch); + else if (selection.kind === "drawing2d") updateDrawing2D(selection.id, patch); + }, + onSetSelectionBackgroundSource: (source) => { + if (!selection) return; + const patch = { + backgroundSource: source === "layer" ? ("layer" as const) : undefined, + background: undefined, + }; + if (selection.kind === "wall") updateWall(selection.id, patch); + else if (selection.kind === "ceiling") updateCeiling(selection.id, patch); + else if (selection.kind === "drawing2d") updateDrawing2D(selection.id, patch); + }, + onSetSelectionStrokeWeightSource: (source) => { + if (!selection) return; + const strokeWeightSource = source === "layer" ? ("layer" as const) : undefined; + if (selection.kind === "wall") + updateWall(selection.id, { strokeWeightSource, strokeWeight: undefined }); + else if (selection.kind === "ceiling") + updateCeiling(selection.id, { strokeWeightSource, strokeWeight: undefined }); + else if (selection.kind === "drawing2d") + updateDrawing2D(selection.id, { strokeWeightSource, weightMm: undefined }); + }, + onSetSelectionHatchSource: (source) => { + if (!selection) return; + const hatchSource = source === "layer" ? ("layer" as const) : undefined; + if (selection.kind === "wall") + updateWall(selection.id, { hatchSource, hatchId: undefined }); + else if (selection.kind === "ceiling") + updateCeiling(selection.id, { hatchSource, hatchId: undefined }); + else if (selection.kind === "drawing2d") + updateDrawing2D(selection.id, { hatchSource, hatchId: undefined }); + }, onResizeSelection: (w, h, anchor) => { if (selection?.kind === "wall" || selection?.kind === "drawing2d") resizeElement(selection.kind, selection.id, w, h, anchor); diff --git a/src/i18n/de.ts b/src/i18n/de.ts index 9739dff..98634e6 100644 --- a/src/i18n/de.ts +++ b/src/i18n/de.ts @@ -220,8 +220,9 @@ export const de = { "attr.bySystem": "Nach System", "attr.hatch": "Schraffur", "attr.none": "keine", - "attr.inheritedFromLayer": "Wände erben die Strichstärke aus der Ebene", - "attr.fillNeedsClosed": "Füllung nur für geschlossene Formen", + "attr.source.layer": "Nach Ebene", + "attr.source.object": "Nach Bauteil", + "attr.source.custom": "eigener Wert", "attr.size": "Maße", "attr.width": "Breite", "attr.height": "Höhe", diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 4884fcd..2b94d8a 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -219,8 +219,9 @@ export const en: Record = { "attr.bySystem": "By system", "attr.hatch": "Hatch", "attr.none": "none", - "attr.inheritedFromLayer": "Walls inherit line weight from the layer", - "attr.fillNeedsClosed": "Fill only for closed shapes", + "attr.source.layer": "By layer", + "attr.source.object": "By component", + "attr.source.custom": "Custom", "attr.size": "Size", "attr.width": "Width", "attr.height": "Height", diff --git a/src/model/types.ts b/src/model/types.ts index e66147e..0908461 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -500,6 +500,19 @@ export interface LayerCategory { 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 @@ -553,6 +566,37 @@ export interface Wall { * `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). @@ -625,6 +669,25 @@ export interface Ceiling { * `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). @@ -914,6 +977,25 @@ export interface Drawing2D { * 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; } /** @@ -1190,6 +1272,16 @@ export const flattenCategories = (cats: LayerCategory[]): LayerCategory[] => { 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 => { const codes = new Set(); diff --git a/src/panels/AttributesPanel.tsx b/src/panels/AttributesPanel.tsx index 546e107..9f15a25 100644 --- a/src/panels/AttributesPanel.tsx +++ b/src/panels/AttributesPanel.tsx @@ -3,6 +3,14 @@ // ersten selektierten Elements und erlaubt das direkte Bearbeiten (Farbe, // Strichstärke, Füllschraffur). // +// Vordergrund/Hintergrund/Strichstärke/Schraffur tragen je einen 3-Optionen- +// Quellen-Dropdown: „Nach Ebene" (die LayerCategory erzwingt den Wert), „Nach +// Bauteil" (erbt vom Component-Manager, heutiges Default-Verhalten) oder +// „eigener Wert" (expliziter Override, gewinnt immer). Das Eingabe-Element +// (Farb-Swatch/mm-Zahlenfeld/Schraffur-Dropdown) erscheint NUR bei „eigener +// Wert"; ein Wechsel auf „Nach Ebene"/„Nach Bauteil" löscht den Wert wieder +// (siehe host.ts: onSetSelection{Foreground,Background,StrokeWeight,Hatch}Source). +// // Daten/Handler kommen ausschließlich über usePanelHost (kein Prop-Drilling). // Es werden NUR Eigenschaften angeboten, die der Host-Kontrakt tatsächlich // setzen kann — keine Stubs/Fake-Setter (Konvention wire-dont-stub). Felder ohne @@ -14,11 +22,24 @@ // Label→Wert-Grid. Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md). // Alle sichtbaren Texte über t(...). +import type { ReactNode } from "react"; import { t } from "../i18n"; import { formatM } from "../model/types"; import { PEN_WEIGHTS } from "../model/types"; +import type { AttributeSource } from "../model/types"; import { usePanelHost } from "./host"; +/** UI-Zustand des 3-Optionen-Quellen-Dropdowns (nicht 1:1 das Modell-Feld: + * „custom" ist abgeleitet aus „ist ein expliziter Wert gesetzt?", nicht + * selbst gespeichert). */ +type UiSource = "layer" | "object" | "custom"; + +/** Leitet den UI-Quellen-Zustand ab: ein gesetzter Wert gewinnt immer („eigener + * Wert"), sonst das rohe Source-Feld (`undefined` ⇒ „Nach Bauteil", Default). */ +function uiSourceOf(hasValue: boolean, raw: AttributeSource | undefined): UiSource { + return hasValue ? "custom" : raw ?? "object"; +} + export function AttributesPanel() { const host = usePanelHost(); const sel = host.selection; @@ -34,11 +55,13 @@ export function AttributesPanel() { } const isDrawing = sel.kind === "drawing2d"; - // Strichstärke wirkt laut Kontrakt nur auf Drawing2D; Wände erben sie aus der - // Ebene und sind daher hier nicht editierbar. - const weightEditable = isDrawing; - // Füllschraffur nur bei geschlossener 2D-Form sinnvoll. - const fillEditable = isDrawing && sel.closed === true; + // Strichstärke: Wand/Decke/Drawing2D tragen einen eigenen Override + // (`strokeWeight`/`weightMm`) + Quellen-Dropdown; andere Elementarten erben + // stets aus der Kategorie (kein Setter im Kontrakt → nicht editierbar). + const weightEditable = sel.kind === "wall" || sel.kind === "ceiling" || isDrawing; + // Schraffur: Wand/Decke (Schnitt-/Ansichts-Schraffur) oder geschlossene 2D-Form. + const fillEditable = + sel.kind === "wall" || sel.kind === "ceiling" || (isDrawing && sel.closed === true); // Vordergrund/Hintergrund (Muster-/Füllfarbe) tragen Wand, Decke und // geschlossene 2D-Formen als Override; `undefined` = „Nach System". const pocheEditable = @@ -46,43 +69,84 @@ export function AttributesPanel() { sel.kind === "ceiling" || (isDrawing && sel.closed === true); - // Ein Override-Farbfeld mit „Nach System"-Zustand: ist der Wert `undefined`, - // zeigt es „Nach System" (leerer Swatch) statt einer Farbe und erbt; ein - // Klick auf × stellt zurück auf „Nach System" (schreibt `null` → Feld weg). - const overrideColor = ( - value: string | undefined, - onSet: (color: string | null) => void, + // Der 3-Optionen-Quellen-Dropdown, gemeinsam für alle vier Felder. Deaktiviert, + // wenn das Feld für diese Elementart gar nicht gilt (z. B. Strichstärke bei + // einer Treppe). + const sourceSelect = ( + editable: boolean, + ui: UiSource, + onChange: (next: UiSource) => void, ) => ( - - - + ); + // Farb-Swatch-Eingabe für „eigener Wert" (Vordergrund/Hintergrund). Erscheint + // nur, wenn die Quelle „custom" ist — sonst zeigt allein der Dropdown den + // Zustand. + const colorValue = (value: string | undefined, onSet: (color: string) => void) => ( + + ); + + // Eine Attribut-Zeile: Label + Quellen-Dropdown + (nur bei „eigener Wert") + // das passende Eingabe-Element. + const sourceRow = ( + labelKey: string, + editable: boolean, + ui: UiSource, + onSource: (next: UiSource) => void, + valueEditor: ReactNode, + ) => ( + <> + {t(labelKey)} + + {ui === "custom" && valueEditor} + {sourceSelect(editable, ui, onSource)} + + + ); + + // Vordergrund/Hintergrund: „eigener Wert" ⇒ auf Quelle "object" (Nach + // Bauteil, Default) zurückfallen; „Nach Ebene"/„Nach Bauteil" schreiben die + // Quelle direkt (der Host löscht dabei den expliziten Wert). + const fgUi = uiSourceOf(sel.foreground !== undefined, sel.foregroundSource); + const bgUi = uiSourceOf(sel.background !== undefined, sel.backgroundSource); + const weightUi = uiSourceOf(sel.strokeWeightOverride !== undefined, sel.strokeWeightSource); + const hatchUi = uiSourceOf( + sel.fillHatchId !== undefined && sel.fillHatchId !== null, + sel.hatchSource, + ); + + const onForegroundSourceChange = (next: UiSource) => { + if (next === "custom") host.onSetSelectionForeground(sel.foreground ?? sel.color); + else host.onSetSelectionForegroundSource(next); + }; + const onBackgroundSourceChange = (next: UiSource) => { + if (next === "custom") host.onSetSelectionBackground(sel.background ?? sel.color); + else host.onSetSelectionBackgroundSource(next); + }; + const onWeightSourceChange = (next: UiSource) => { + if (next === "custom") host.onSetSelectionWeight(sel.weightMm); + else host.onSetSelectionStrokeWeightSource(next); + }; + const onHatchSourceChange = (next: UiSource) => { + if (next === "custom") host.onSetSelectionFill(project.hatches[0]?.id ?? null); + else host.onSetSelectionHatchSource(next); + }; + // Effektiver Linienstil rein informativ (kein Setter im Kontrakt → kein Fake). // Wir leiten ihn aus dem Modell-Element ab, wenn explizit gesetzt; sonst „—". const drawing = isDrawing @@ -119,29 +183,34 @@ export function AttributesPanel() { - {/* Strichstärke (mm) — nur Drawing2D editierbar; Wände erben aus Ebene. */} - {t("attr.weight")} - - { - const v = Number(e.target.value); - if (Number.isFinite(v)) host.onSetSelectionWeight(v); - }} - /> - - {PEN_WEIGHTS.map((w) => ( - - + {/* Strichstärke (mm) — Wand/Decke/Drawing2D, mit Nach-Ebene/Nach-Bauteil/ + eigener-Wert-Quelle; andere Elementarten erben stets aus der Kategorie. */} + {sourceRow( + "attr.weight", + weightEditable, + weightUi, + onWeightSourceChange, + <> + { + const v = Number(e.target.value); + if (Number.isFinite(v)) host.onSetSelectionWeight(v); + }} + /> + + {PEN_WEIGHTS.map((w) => ( + + , + )} {/* Linienstil — read-only/informativ (kein Setter im Kontrakt). */} {t("attr.lineStyle")} @@ -152,26 +221,36 @@ export function AttributesPanel() { {/* ── Füllung (Vordergrund/Hintergrund + Schraffur) ─────────────── */} {/* Vordergrund = Muster-/Schraffurfarbe, Hintergrund = Füllfarbe/Poché. - Beide sind Overrides mit Default „Nach System" (erben vom Bauteil); - `background` ist der Nachfolger des alten reinen `fillColor`-Feldes. */} + Jedes Feld: Nach Ebene (LayerCategory erzwingt den Wert) / Nach Bauteil + (erbt vom Component-Manager, Default) / eigener Wert (Override). */}
{t("attr.fill")}
- {t("attr.foreground")} - {overrideColor(sel.foreground, host.onSetSelectionForeground)} + {sourceRow( + "attr.foreground", + pocheEditable, + fgUi, + onForegroundSourceChange, + colorValue(sel.foreground, host.onSetSelectionForeground), + )} - {t("attr.background")} - {overrideColor(sel.background, host.onSetSelectionBackground)} + {sourceRow( + "attr.background", + pocheEditable, + bgUi, + onBackgroundSourceChange, + colorValue(sel.background, host.onSetSelectionBackground), + )} - {t("attr.hatch")} - + {sourceRow( + "attr.hatch", + fillEditable, + hatchUi, + onHatchSourceChange, - + , + )}
{/* ── Maße (informativ, aus der bbox) ───────────────────────────── */} diff --git a/src/panels/host.ts b/src/panels/host.ts index 4e07780..e8b565d 100644 --- a/src/panels/host.ts +++ b/src/panels/host.ts @@ -15,6 +15,7 @@ import { useContext } from "react"; import { PanelHostContext } from "./types"; import type { DisplayMode } from "./types"; import type { + AttributeSource, CeilingType, Component, ContextObject, @@ -139,9 +140,17 @@ export interface PanelHostValue { selection: Selection | null; /** Setzt die (Strich-)Farbe der aktuellen Selektion (Wand oder Drawing2D). */ onSetSelectionColor: (color: string) => void; - /** Setzt die Strichstärke (mm) — wirkt NUR auf Drawing2D (sonst No-op). */ + /** + * Setzt den Strichstärke-Override (mm, „eigener Wert") — Wand/Decke + * (`strokeWeight`) oder Drawing2D (`weightMm`). Die Quelle (Ebene/Bauteil) + * setzt {@link onSetSelectionStrokeWeightSource}. + */ onSetSelectionWeight: (weightMm: number) => void; - /** Setzt/entfernt die Füllschraffur — wirkt NUR auf Drawing2D (sonst No-op). */ + /** + * Setzt/entfernt den Schraffur-Override („eigener Wert") — Wand, Decke oder + * Drawing2D (geschlossene Formen). `null` löscht den Wert (fällt zurück auf + * die Quelle, siehe {@link onSetSelectionHatchSource}). + */ onSetSelectionFill: (hatchId: string | null) => void; /** * Setzt/entfernt die Vollton-Füllfarbe — wirkt NUR auf Drawing2D (sonst No-op). @@ -149,15 +158,30 @@ export interface PanelHostValue { */ onSetSelectionFillColor: (color: string | null) => void; /** - * Setzt/entfernt den Vordergrund-Override (Muster-/Schraffurfarbe) der - * Selektion — Wand, Decke oder geschlossene Drawing2D. `null` = „Nach System". + * Setzt/entfernt den Vordergrund-Override (Muster-/Schraffurfarbe, „eigener + * Wert") der Selektion — Wand, Decke oder geschlossene Drawing2D. `null` = + * „Nach System" (fällt zurück auf die Quelle, siehe + * {@link onSetSelectionForegroundSource}). */ onSetSelectionForeground: (color: string | null) => void; /** - * Setzt/entfernt den Hintergrund-Override (Füllfarbe/Poché) der Selektion — - * Wand, Decke oder geschlossene Drawing2D. `null` = „Nach System". + * Setzt/entfernt den Hintergrund-Override (Füllfarbe/Poché, „eigener Wert") + * der Selektion — Wand, Decke oder geschlossene Drawing2D. `null` = „Nach + * System" (siehe {@link onSetSelectionBackgroundSource}). */ onSetSelectionBackground: (color: string | null) => void; + /** + * Setzt die Quelle des Vordergrunds, wenn KEIN „eigener Wert" gilt: "layer" = + * Nach Ebene, "object" = Nach Bauteil. Löscht dabei IMMER den expliziten + * Vordergrund-Override (die Quelle gewinnt nur, wenn kein Wert gesetzt ist). + */ + onSetSelectionForegroundSource: (source: AttributeSource) => void; + /** Setzt die Quelle des Hintergrunds, analog zu {@link onSetSelectionForegroundSource}. */ + onSetSelectionBackgroundSource: (source: AttributeSource) => void; + /** Setzt die Quelle der Strichstärke, analog zu {@link onSetSelectionForegroundSource}. */ + onSetSelectionStrokeWeightSource: (source: AttributeSource) => void; + /** Setzt die Quelle der Schraffur, analog zu {@link onSetSelectionForegroundSource}. */ + onSetSelectionHatchSource: (source: AttributeSource) => void; /** * Skaliert die Selektion auf Zielbreite×-höhe (Meter) um einen Anker * (fx/fy ∈ [0,1] relativ zur bbox; 0,0 = oben-links … 1,1 = unten-rechts). diff --git a/src/plan/generatePlan.ts b/src/plan/generatePlan.ts index bc5b517..b95fa2d 100644 --- a/src/plan/generatePlan.ts +++ b/src/plan/generatePlan.ts @@ -6,6 +6,7 @@ // Öffnungen) — exakt, schnell, sauber. import type { + AttributeSource, Ceiling, Component, Drawing2D, @@ -23,6 +24,7 @@ import { getCeilingType, getComponent, getHatch, + getLayerCategory, getLineStyle, getWallType, openingsOfWall, @@ -334,22 +336,82 @@ export function resolveHatch( /** * Kollabiert die Vordergrund-Farbkette (Muster-/Schraffurlinienfarbe) eines - * Bauteils zu einem einzelnen Wert für {@link resolveHatch}: - * (Attribut-Override `override`) ?? (`Component.foreground`) ?? `undefined`. - * `undefined` heißt „Nach System" — die Kette läuft dann in resolveHatch auf den - * HatchStyle.color-Backward-Compat-Fallback weiter (Sample-Verhalten). + * Bauteils zu einem einzelnen Wert für {@link resolveHatch}. AUFLÖSUNGSREIHENFOLGE + * (By-Layer/By-Object, siehe `docs`/Attribut-Panel): + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.color`) ?? + * (`Component.foreground`) ?? `undefined`. + * `source` fehlt/`"object"` ⇒ „Nach Bauteil" (heutiges Verhalten, unverändert). + * `undefined` als Endergebnis heißt „Nach System" — die Kette läuft dann in + * resolveHatch auf den HatchStyle.color-Backward-Compat-Fallback weiter + * (Sample-Verhalten: kein Element setzt `override`/`source`, also identisch). */ -export function resolveForeground(comp: Component, override?: string): string | undefined { - return override ?? comp.foreground; +export function resolveForeground( + comp: Component, + override?: string, + category?: LayerCategory, + source?: AttributeSource, +): string | undefined { + if (override != null) return override; + if (source === "layer") return category?.color ?? comp.foreground; + return comp.foreground; } /** - * Kollabiert die Hintergrund-/Füllfarbkette (Poché) eines Bauteils: - * (Attribut-Override `override`) ?? (`Component.background`) ?? `Component.color`. + * Kollabiert die Hintergrund-/Füllfarbkette (Poché) eines Bauteils, analog zu + * {@link resolveForeground}: + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.color`) ?? + * (`Component.background`) ?? `Component.color`. * Immer definiert (endet spätestens bei `Component.color` = heutiges Verhalten). */ -export function resolveBackground(comp: Component, override?: string): string { - return override ?? comp.background ?? comp.color; +export function resolveBackground( + comp: Component, + override?: string, + category?: LayerCategory, + source?: AttributeSource, +): string { + if (override != null) return override; + if (source === "layer") return category?.color ?? comp.background ?? comp.color; + return comp.background ?? comp.color; +} + +/** + * Kollabiert die Schraffur-Referenz (Hatch Manager) eines Elements zu einer + * einzelnen `hatchId` für {@link resolveHatch} — By-Layer/By-Object, analog zu + * {@link resolveForeground}: + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.hatch`) ?? + * `fallback` (Bauteil-Schraffur, z. B. `Component.hatchId`/`viewHatchId`, oder + * `undefined` bei Drawing2D ohne Bauteil-Bezug). + * `source` fehlt/`"object"` ⇒ „Nach Bauteil" (heutiges Verhalten, unverändert). + * Ergebnis kann `undefined` sein — der Aufrufer fällt dann auf `NO_HATCH` zurück. + */ +export function resolveHatchId( + override: string | undefined, + source: AttributeSource | undefined, + category: LayerCategory | undefined, + fallback?: string, +): string | undefined { + if (override != null) return override; + if (source === "layer") return category?.hatch ?? fallback; + return fallback; +} + +/** + * Kollabiert die Strichstärke-Kette (mm Papier) eines Elements, By-Layer/By- + * Object, analog zu {@link resolveHatchId}: + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.lw`) ?? + * `fallback` (bisherige Bauteil-/LineStyle-/Kategorie-Default-Kette des + * Aufrufers — heutiges Verhalten). + * `source` fehlt/`"object"` ⇒ „Nach Bauteil" (heutiges Verhalten, unverändert). + */ +export function resolveStrokeWeight( + override: number | undefined, + source: AttributeSource | undefined, + category: LayerCategory | undefined, + fallback: number, +): number { + if (override != null) return override; + if (source === "layer") return category?.lw ?? fallback; + return fallback; } // ── SIA-Poché-Füllung ──────────────────────────────────────────────────── @@ -386,13 +448,15 @@ export function resolveWallSectionStyle(project: Project, wall: Wall): SectionCu if (!best || comp.joinPriority > best.joinPriority) best = comp; } if (!best) return null; + const category = getLayerCategory(project, wall.categoryCode); const wallAngleDeg = (Math.atan2(wall.end.y - wall.start.y, wall.end.x - wall.start.x) * 180) / Math.PI; + const hatchId = resolveHatchId(wall.hatchId, wall.hatchSource, category, best.hatchId) ?? best.hatchId; const hatch = resolveHatch( project, - best.hatchId, + hatchId, wallAngleDeg, - resolveForeground(best, wall.foreground), + resolveForeground(best, wall.foreground, category, wall.foregroundSource), ); // Poché-Hintergrund neutral (weiß) statt Bauteil-Albedo; Vollmuster → schwarz. const solid = hatch.pattern === "solid"; @@ -419,9 +483,17 @@ export function resolveCeilingSectionStyle( const wt = getCeilingType(project, ceiling); if (wt.layers.length === 0) return null; const comp = getComponent(project, wt.layers[0].componentId); + const category = getLayerCategory(project, ceiling.categoryCode); + const hatchId = + resolveHatchId(ceiling.hatchId, ceiling.hatchSource, category, comp.hatchId) ?? comp.hatchId; return { - fill: resolveBackground(comp, ceiling.background), - hatch: resolveHatch(project, comp.hatchId, undefined, resolveForeground(comp, ceiling.foreground)), + fill: resolveBackground(comp, ceiling.background, category, ceiling.backgroundSource), + hatch: resolveHatch( + project, + hatchId, + undefined, + resolveForeground(comp, ceiling.foreground, category, ceiling.foregroundSource), + ), }; } catch { return null; @@ -466,6 +538,9 @@ export function generatePlan( // Modell (Ebene) stammt statt aus einer Magie-Konstante. const lwByCode = categoryLwMap(project.layers); const colorByCode = categoryColorMap(project.layers); + // Code → volle LayerCategory, für die By-Layer-Attribut-Auflösung (Quelle + // "layer" von Vordergrund/Hintergrund/Strichstärke/Schraffur). + const catByCode = categoryMap(project.layers); // Freie 2D-Zeichengeometrie dieses Geschosses (wie Wände nach sichtbaren // Kategorien + Darstellungsmodus gefiltert). Wird über der Wand-Poché @@ -489,8 +564,14 @@ export function generatePlan( ); for (const ceiling of ceilings) { const greyed = categoryDisplay(ceiling.categoryCode).greyed; - const lwMm = lwByCode.get(ceiling.categoryCode) ?? WALL_FALLBACK_MM; - addCeilingPoche(primitives, project, ceiling, greyed, detail, lwMm); + const category = catByCode.get(ceiling.categoryCode); + const lwMm = resolveStrokeWeight( + ceiling.strokeWeight, + ceiling.strokeWeightSource, + category, + lwByCode.get(ceiling.categoryCode) ?? WALL_FALLBACK_MM, + ); + addCeilingPoche(primitives, project, ceiling, greyed, detail, lwMm, category); } // Räume (SIA-416-Flächen) dieses Geschosses: transluzente Farbfüllung + Stempel @@ -510,14 +591,20 @@ export function generatePlan( for (const wall of walls) { const wallGreyed = categoryDisplay(wall.categoryCode).greyed; - const wallLwMm = lwByCode.get(wall.categoryCode) ?? WALL_FALLBACK_MM; + const wallCategory = catByCode.get(wall.categoryCode); + const wallLwMm = resolveStrokeWeight( + wall.strokeWeight, + wall.strokeWeightSource, + wallCategory, + lwByCode.get(wall.categoryCode) ?? WALL_FALLBACK_MM, + ); // Türen (Legacy) + Öffnungen (Fenster/Türen) am Wandhost: BEIDE sparen die // Wand-Poché aus. Aus beiden wird eine gemeinsame Lücken-Intervall-Liste. const doors = project.doors.filter((d) => d.hostWallId === wall.id); const openings = openingsOfWall(project, wall.id); const gaps = wallGaps(wall, doors, openings); const cuts = joins.get(wall.id) ?? { startCut: null, endCut: null }; - addWallPoche(primitives, project, wall, gaps, cuts, wallGreyed, detail, wallLwMm); + addWallPoche(primitives, project, wall, gaps, cuts, wallGreyed, detail, wallLwMm, wallCategory); // Referenzlinie: dünne gestrichelte Wand-Achslinie (von der Oberleiste // umschaltbar). Wird über der Poché gezeichnet, damit sie sichtbar bleibt. if (showReferenceLines) addReferenceLine(primitives, wall, wallGreyed); @@ -557,7 +644,7 @@ export function generatePlan( // 2D-Zeichengeometrie zuletzt (über der Poché). for (const d of drawings) { const greyed = categoryDisplay(d.categoryCode).greyed; - addDrawing2D(primitives, project, d, greyed, lwByCode, colorByCode); + addDrawing2D(primitives, project, d, greyed, lwByCode, colorByCode, catByCode); } // Kontext-Konturen (Höhenlinien) als dezente Linien zeichnen — UNTER der @@ -780,6 +867,17 @@ function categoryColorMap(layers: LayerCategory[]): Map { return map; } +/** + * Baut eine Map Kategorie-Code → volle LayerCategory über den ganzen Baum, für + * die By-Layer-Attribut-Auflösung (Quelle "layer" von Vordergrund/Hintergrund/ + * Strichstärke/Schraffur, siehe {@link resolveForeground} und Geschwister). + */ +function categoryMap(layers: LayerCategory[]): Map { + const map = new Map(); + for (const c of flattenCategories(layers)) map.set(c.code, c); + return map; +} + /** * Leitet ein 2D-Zeichenelement in Plan-Primitive ab (docs/design/drawing-tools.md * §7.2). Farbe/Strichstärke kommen aus dem optionalen LineStyle, sonst aus der @@ -793,15 +891,30 @@ function addDrawing2D( greyed: boolean, lwByCode: Map, colorByCode: Map, + catByCode: Map, ): void { + const category = catByCode.get(d.categoryCode); const ls = d.lineStyleId ? project.lineStyles.find((l) => l.id === d.lineStyleId) : undefined; // Plan-Tinte fix dunkel: Fallback auf MONO_INK (statt hell „#d0d0d0"), damit // ein kategorieloses 2D-Element auf dem hellen Papier sichtbar bleibt. const color = d.color ?? ls?.color ?? colorByCode.get(d.categoryCode) ?? MONO_INK; - const weightMm = d.weightMm ?? ls?.weight ?? lwByCode.get(d.categoryCode) ?? WALL_FALLBACK_MM; + // Strichstärke: "layer" erzwingt die Ebenen-Strichstärke (umgeht das + // LineStyle-Gewicht), sonst (Default "object") die heutige Kette. + const weightMm = resolveStrokeWeight( + d.weightMm, + d.strokeWeightSource, + category, + ls?.weight ?? lwByCode.get(d.categoryCode) ?? WALL_FALLBACK_MM, + ); const dash = ls?.dash ?? null; + // Drawing2D trägt kein Bauteil → die By-Layer-Quelle ist hier die einzige + // Alternative zum expliziten Wert (kein Component-Fallback); Default + // "object"/undefined bleibt exakt das heutige Verhalten. + const foreground = d.foreground ?? (d.foregroundSource === "layer" ? category?.color : undefined); + const background = d.background ?? (d.backgroundSource === "layer" ? category?.color : undefined); + const hatchId = resolveHatchId(d.hatchId, d.hatchSource, category, undefined); // Zickzack-Parameter aus dem LineStyle durchreichen (nur kind==="zigzag"). // Additiv: fehlt es, bleibt die Linie gerade/gestrichelt wie bisher. const zigzag = ls?.kind === "zigzag" ? ls.zigzag : undefined; @@ -821,16 +934,18 @@ function addDrawing2D( out.push({ kind: "polygon", pts, - // Hintergrund-Override hat Vorrang vor dem Alt-Feld fillColor (beide leer = - // transparente Fläche, nur Schraffur) — Sample bleibt identisch. - fill: d.background ?? d.fillColor ?? "none", + // Hintergrund-Override (Wert ?? By-Layer) hat Vorrang vor dem Alt-Feld + // fillColor (alle leer = transparente Fläche, nur Schraffur) — Sample + // bleibt identisch (kein Element setzt background/backgroundSource). + fill: background ?? d.fillColor ?? "none", // Umriss zeichnen die separaten Linien-Primitive (mit Strichmuster); die // Füllfläche selbst bleibt randlos, um doppelte Striche zu vermeiden. stroke: "none", strokeWidthMm: 0, - // Drawing2D trägt kein Bauteil → Vordergrund-Override direkt aus d.foreground - // (Attribut „Nach System" ⇒ undefined ⇒ HatchStyle.color-Fallback). - hatch: d.hatchId ? resolveHatch(project, d.hatchId, undefined, d.foreground) : NO_HATCH, + // Drawing2D trägt kein Bauteil → Vordergrund/Schraffur-Override direkt aus + // d.foreground/d.hatchId bzw. der Ebene (Attribut „Nach System" ⇒ + // undefined ⇒ HatchStyle.color-Fallback bzw. NO_HATCH). + hatch: hatchId ? resolveHatch(project, hatchId, undefined, foreground) : NO_HATCH, greyed, drawingId: d.id, }); @@ -929,6 +1044,7 @@ function addWallPoche( greyed: boolean, detail: DetailLevel, wallLwMm: number, + category: LayerCategory | undefined, ): void { const wt = getWallType(project, wall); const total = wallTypeThickness(wt); @@ -1015,11 +1131,13 @@ function addWallPoche( for (let li = 0; li < wt.layers.length; li++) { const layer = wt.layers[li]; const comp = getComponent(project, layer.componentId); + const layerHatchId = + resolveHatchId(wall.hatchId, wall.hatchSource, category, comp.hatchId) ?? comp.hatchId; const layerHatch = resolveHatch( project, - comp.hatchId, + layerHatchId, wallAngleDeg, - resolveForeground(comp, wall.foreground), + resolveForeground(comp, wall.foreground, category, wall.foregroundSource), ); // SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die // Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché. @@ -1319,6 +1437,7 @@ function addCeilingPoche( greyed: boolean, detail: DetailLevel, lwMm: number, + category: LayerCategory | undefined, ): void { const pts = ceiling.outline; if (pts.length < 3) return; @@ -1333,10 +1452,23 @@ function addCeilingPoche( // NICHT die Schnitt-Schraffur (die gehört auf echte Schnittflächen, siehe // resolveCeilingSectionStyle/splitSlabLayers), sondern `viewHatchId`. Fehlt // sie (Default), bleibt die Aufsicht weiss. Im Detailgrad „grob" entfällt - // die Schraffur ganz (nur Fläche + Umriss). + // die Schraffur ganz (nur Fläche + Umriss). Das Attribut-Override + // `ceiling.hatchId` (+ `hatchSource`) überschreibt diese Ansichts-Schraffur + // genauso wie die Schnitt-Schraffur in {@link resolveCeilingSectionStyle}. + const effectiveViewHatchId = resolveHatchId( + ceiling.hatchId, + ceiling.hatchSource, + category, + comp?.viewHatchId, + ); const hatch = - detail !== "grob" && comp?.viewHatchId - ? resolveHatch(project, comp.viewHatchId, undefined, resolveForeground(comp, ceiling.foreground)) + detail !== "grob" && comp && effectiveViewHatchId + ? resolveHatch( + project, + effectiveViewHatchId, + undefined, + resolveForeground(comp, ceiling.foreground, category, ceiling.foregroundSource), + ) : NO_HATCH; const solid = hatch.pattern === "solid"; out.push({ diff --git a/src/state/projectSlice.ts b/src/state/projectSlice.ts index c677d84..d6a2f39 100644 --- a/src/state/projectSlice.ts +++ b/src/state/projectSlice.ts @@ -15,6 +15,7 @@ import { } from "../model/types"; import type { Component, + Drawing2D, DrawingLevel, HatchStyle, LayerCategory, @@ -152,6 +153,13 @@ export interface ProjectSlice { id: string, value: string | null, ) => void; + /** + * Generischer immutabler Patch auf ein Drawing2D (per ID) — analog zu + * `updateWall`/`updateCeiling`. Trägt u. a. die By-Layer/By-Object- + * Quellenfelder (`foregroundSource`/`backgroundSource`/`strokeWeightSource`/ + * `hatchSource`), die kein eigenes Setter-Paar haben. + */ + updateDrawing2D: (id: string, patch: Partial) => void; /** * Skaliert die Geometrie eines Elements auf Zielbreite×-höhe (Meter) um einen * Ankerpunkt (fx/fy ∈ [0,1] relativ zur bbox; 0,0 = oben-links). @@ -748,6 +756,12 @@ export function createProjectSlice( setElementBackground: (kind, id, value) => setProject((p) => setOverrideField(p, kind, id, "background", value)), + updateDrawing2D: (id, patch) => + setProject((p) => ({ + ...p, + drawings2d: p.drawings2d.map((d) => (d.id === id ? { ...d, ...patch } : d)), + })), + // ── Größe (Resize um Anker) ──────────────────────────────────────────── resizeElement: (kind, id, w, h, anchor) => setProject((p) => resizeElement(p, kind, id, w, h, anchor)), diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index 498f4ca..c7b090e 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -18,6 +18,7 @@ import { wallTypeThickness, } from "../model/types"; import type { + AttributeSource, Ceiling, Drawing2D, Drawing2DGeom, @@ -229,6 +230,24 @@ export interface Selection { * 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. */ @@ -310,7 +329,10 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox { function wallSelection(project: Project, wall: Wall): Selection { const cat = findCategory(project.layers, wall.categoryCode); const color = wall.color ?? cat?.color ?? WALL_FALLBACK_COLOR; - const weightMm = cat?.lw ?? WALL_FALLBACK_MM; + // 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); @@ -351,10 +373,15 @@ function wallSelection(project: Project, wall: Wall): Selection { categoryCode: wall.categoryCode, color, weightMm, - fillHatchId: undefined, + 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, @@ -365,7 +392,7 @@ function wallSelection(project: Project, wall: Wall): Selection { 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 = cat?.lw ?? WALL_FALLBACK_MM; + const weightMm = ceiling.strokeWeight ?? cat?.lw ?? WALL_FALLBACK_MM; const box = outlineBBox(ceiling.outline); const wt = getCeilingType(project, ceiling); const thickness = ceilingThickness(project, ceiling); @@ -397,10 +424,15 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection { categoryCode: ceiling.categoryCode, color, weightMm, - fillHatchId: undefined, + 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, @@ -547,7 +579,15 @@ function drawingSelection(project: Project, d: Drawing2D): Selection { : undefined; const cat = findCategory(project.layers, d.categoryCode); const color = d.color ?? ls?.color ?? cat?.color ?? DRAW_FALLBACK_COLOR; - const weightMm = d.weightMm ?? ls?.weight ?? cat?.lw ?? WALL_FALLBACK_MM; + // 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", @@ -559,6 +599,11 @@ function drawingSelection(project: Project, d: Drawing2D): Selection { 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), };