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.
This commit is contained in:
2026-07-04 02:19:38 +02:00
parent 777e02c927
commit d0f26cd874
9 changed files with 571 additions and 128 deletions
+59 -4
View File
@@ -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);
+3 -2
View File
@@ -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",
+3 -2
View File
@@ -219,8 +219,9 @@ export const en: Record<TranslationKey, string> = {
"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",
+92
View File
@@ -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 (startend) 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<string> => {
const codes = new Set<string>();
+157 -78
View File
@@ -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,
) => (
<span className="attr-val">
<label className="attr-color">
<span
className="attr-color-swatch"
style={{ background: pocheEditable && value ? value : "transparent" }}
/>
<input
type="color"
value={value ?? "#808080"}
disabled={!pocheEditable}
title={pocheEditable ? undefined : t("attr.fillNeedsClosed")}
onChange={(e) => onSet(e.target.value)}
/>
<span className="attr-color-hex">
{pocheEditable && value ? value : t("attr.bySystem")}
</span>
{pocheEditable && value !== undefined && (
<button
type="button"
className="attr-color-clear"
title={t("attr.bySystem")}
onClick={() => onSet(null)}
>
×
</button>
)}
</label>
</span>
<select
className="attr-select"
style={{ width: 104 }}
value={ui}
disabled={!editable}
onChange={(e) => onChange(e.target.value as UiSource)}
>
<option value="layer">{t("attr.source.layer")}</option>
<option value="object">{t("attr.source.object")}</option>
<option value="custom">{t("attr.source.custom")}</option>
</select>
);
// 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) => (
<label className="attr-color">
<span className="attr-color-swatch" style={{ background: value ?? "#808080" }} />
<input type="color" value={value ?? "#808080"} onChange={(e) => onSet(e.target.value)} />
<span className="attr-color-hex">{value ?? "#808080"}</span>
</label>
);
// 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,
) => (
<>
<span className="attr-key">{t(labelKey)}</span>
<span className="attr-val" style={{ gap: 6 }}>
{ui === "custom" && valueEditor}
{sourceSelect(editable, ui, onSource)}
</span>
</>
);
// 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() {
</label>
</span>
{/* Strichstärke (mm) — nur Drawing2D editierbar; Wände erben aus Ebene. */}
<span className="attr-key">{t("attr.weight")}</span>
<span className="attr-val">
<input
className="attr-num"
type="number"
step={0.01}
min={0}
list="attr-pen-weights"
value={sel.weightMm}
disabled={!weightEditable}
title={weightEditable ? undefined : t("attr.inheritedFromLayer")}
onChange={(e) => {
const v = Number(e.target.value);
if (Number.isFinite(v)) host.onSetSelectionWeight(v);
}}
/>
<datalist id="attr-pen-weights">
{PEN_WEIGHTS.map((w) => (
<option key={w} value={w} />
))}
</datalist>
</span>
{/* 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,
<>
<input
className="attr-num"
style={{ width: 72 }}
type="number"
step={0.01}
min={0}
list="attr-pen-weights"
value={sel.weightMm}
onChange={(e) => {
const v = Number(e.target.value);
if (Number.isFinite(v)) host.onSetSelectionWeight(v);
}}
/>
<datalist id="attr-pen-weights">
{PEN_WEIGHTS.map((w) => (
<option key={w} value={w} />
))}
</datalist>
</>,
)}
{/* Linienstil — read-only/informativ (kein Setter im Kontrakt). */}
<span className="attr-key">{t("attr.lineStyle")}</span>
@@ -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). */}
<div className="attr-section-label">{t("attr.fill")}</div>
<div className="attr-grid">
<span className="attr-key">{t("attr.foreground")}</span>
{overrideColor(sel.foreground, host.onSetSelectionForeground)}
{sourceRow(
"attr.foreground",
pocheEditable,
fgUi,
onForegroundSourceChange,
colorValue(sel.foreground, host.onSetSelectionForeground),
)}
<span className="attr-key">{t("attr.background")}</span>
{overrideColor(sel.background, host.onSetSelectionBackground)}
{sourceRow(
"attr.background",
pocheEditable,
bgUi,
onBackgroundSourceChange,
colorValue(sel.background, host.onSetSelectionBackground),
)}
<span className="attr-key">{t("attr.hatch")}</span>
<span className="attr-val">
{sourceRow(
"attr.hatch",
fillEditable,
hatchUi,
onHatchSourceChange,
<select
className="attr-select"
value={fillEditable ? sel.fillHatchId ?? "" : ""}
disabled={!fillEditable}
title={fillEditable ? undefined : t("attr.fillNeedsClosed")}
onChange={(e) =>
host.onSetSelectionFill(e.target.value || null)
}
style={{ width: 104 }}
value={sel.fillHatchId ?? ""}
onChange={(e) => host.onSetSelectionFill(e.target.value || null)}
>
<option value="">{t("attr.none")}</option>
{project.hatches.map((h) => (
@@ -179,8 +258,8 @@ export function AttributesPanel() {
{h.name}
</option>
))}
</select>
</span>
</select>,
)}
</div>
{/* ── Maße (informativ, aus der bbox) ───────────────────────────── */}
+30 -6
View File
@@ -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).
+163 -31
View File
@@ -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<string, string> {
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<string, LayerCategory> {
const map = new Map<string, LayerCategory>();
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<string, number>,
colorByCode: Map<string, string>,
catByCode: Map<string, LayerCategory>,
): 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({
+14
View File
@@ -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<Drawing2D>) => 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)),
+50 -5
View File
@@ -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),
};