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 9695748fbc
commit d02781e8d2
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 setElementFillColor = useStore((s) => s.setElementFillColor);
const setElementForeground = useStore((s) => s.setElementForeground); const setElementForeground = useStore((s) => s.setElementForeground);
const setElementBackground = useStore((s) => s.setElementBackground); const setElementBackground = useStore((s) => s.setElementBackground);
const updateDrawing2D = useStore((s) => s.updateDrawing2D);
const resizeElement = useStore((s) => s.resizeElement); const resizeElement = useStore((s) => s.resizeElement);
const updateWall = useStore((s) => s.updateWall); const updateWall = useStore((s) => s.updateWall);
const setWallThickness = useStore((s) => s.setWallThickness); const setWallThickness = useStore((s) => s.setWallThickness);
@@ -2065,13 +2066,22 @@ export default function App() {
updateRoom(selection.id, { color }); updateRoom(selection.id, { color });
}, },
onSetSelectionWeight: (weightMm) => { onSetSelectionWeight: (weightMm) => {
// Strichstärke gibt es als direktes Feld nur bei Drawing2D. // Strichstärke-Override: Wand/Decke (`strokeWeight`) oder Drawing2D
if (selection?.kind === "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); setElementWeight(selection.id, weightMm);
}, },
onSetSelectionFill: (hatchId) => { onSetSelectionFill: (hatchId) => {
// Füllschraffur nur bei Drawing2D (geschlossene Formen). // Schraffur-Override: Wand, Decke oder Drawing2D (geschlossene Formen).
if (selection?.kind === "drawing2d") // `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); setElementFill(selection.id, hatchId);
}, },
onSetSelectionFillColor: (color) => { onSetSelectionFillColor: (color) => {
@@ -2097,6 +2107,51 @@ export default function App() {
) )
setElementBackground(selection.kind, selection.id, color); 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) => { onResizeSelection: (w, h, anchor) => {
if (selection?.kind === "wall" || selection?.kind === "drawing2d") if (selection?.kind === "wall" || selection?.kind === "drawing2d")
resizeElement(selection.kind, selection.id, w, h, anchor); resizeElement(selection.kind, selection.id, w, h, anchor);
+3 -2
View File
@@ -220,8 +220,9 @@ export const de = {
"attr.bySystem": "Nach System", "attr.bySystem": "Nach System",
"attr.hatch": "Schraffur", "attr.hatch": "Schraffur",
"attr.none": "keine", "attr.none": "keine",
"attr.inheritedFromLayer": "Wände erben die Strichstärke aus der Ebene", "attr.source.layer": "Nach Ebene",
"attr.fillNeedsClosed": "Füllung nur für geschlossene Formen", "attr.source.object": "Nach Bauteil",
"attr.source.custom": "eigener Wert",
"attr.size": "Maße", "attr.size": "Maße",
"attr.width": "Breite", "attr.width": "Breite",
"attr.height": "Höhe", "attr.height": "Höhe",
+3 -2
View File
@@ -219,8 +219,9 @@ export const en: Record<TranslationKey, string> = {
"attr.bySystem": "By system", "attr.bySystem": "By system",
"attr.hatch": "Hatch", "attr.hatch": "Hatch",
"attr.none": "none", "attr.none": "none",
"attr.inheritedFromLayer": "Walls inherit line weight from the layer", "attr.source.layer": "By layer",
"attr.fillNeedsClosed": "Fill only for closed shapes", "attr.source.object": "By component",
"attr.source.custom": "Custom",
"attr.size": "Size", "attr.size": "Size",
"attr.width": "Width", "attr.width": "Width",
"attr.height": "Height", "attr.height": "Height",
+92
View File
@@ -500,6 +500,19 @@ export interface LayerCategory {
children?: 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 * Lage der Wandachse (Referenzlinie) über die Dicke der Wand analog
* Vectorworks. Gemessen relativ zur Laufrichtung (startend) mit der * Vectorworks. Gemessen relativ zur Laufrichtung (startend) mit der
@@ -553,6 +566,37 @@ export interface Wall {
* `undefined` = Nach System" (erben Component.background Component.color). * `undefined` = Nach System" (erben Component.background Component.color).
*/ */
background?: string; 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 * Lage der Wandachse über die Dicke. Fehlt sie, gilt "center" (= heutiges
* Verhalten: Schichten symmetrisch T/2 +T/2 um die Achse). * 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). * `undefined` = Nach System" (erben Component.background Component.color).
*/ */
background?: string; 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 * Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der
* Oberkante des Geschosses (baseElevation + floorHeight). * Oberkante des Geschosses (baseElevation + floorHeight).
@@ -914,6 +977,25 @@ export interface Drawing2D {
* vor dem LineStyle-Gewicht und der Kategorie-Strichstärke. * vor dem LineStyle-Gewicht und der Kategorie-Strichstärke.
*/ */
weightMm?: number; 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; 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). */ /** Menge aller Codes sichtbarer Kategorien (Baum berücksichtigt). */
export const collectVisibleCodes = (layers: LayerCategory[]): Set<string> => { export const collectVisibleCodes = (layers: LayerCategory[]): Set<string> => {
const codes = new Set<string>(); const codes = new Set<string>();
+157 -78
View File
@@ -3,6 +3,14 @@
// ersten selektierten Elements und erlaubt das direkte Bearbeiten (Farbe, // ersten selektierten Elements und erlaubt das direkte Bearbeiten (Farbe,
// Strichstärke, Füllschraffur). // 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). // Daten/Handler kommen ausschließlich über usePanelHost (kein Prop-Drilling).
// Es werden NUR Eigenschaften angeboten, die der Host-Kontrakt tatsächlich // Es werden NUR Eigenschaften angeboten, die der Host-Kontrakt tatsächlich
// setzen kann — keine Stubs/Fake-Setter (Konvention wire-dont-stub). Felder ohne // 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). // Label→Wert-Grid. Bezeichner englisch, UI-Text/Kommentare deutsch (CONVENTIONS.md).
// Alle sichtbaren Texte über t(...). // Alle sichtbaren Texte über t(...).
import type { ReactNode } from "react";
import { t } from "../i18n"; import { t } from "../i18n";
import { formatM } from "../model/types"; import { formatM } from "../model/types";
import { PEN_WEIGHTS } from "../model/types"; import { PEN_WEIGHTS } from "../model/types";
import type { AttributeSource } from "../model/types";
import { usePanelHost } from "./host"; 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() { export function AttributesPanel() {
const host = usePanelHost(); const host = usePanelHost();
const sel = host.selection; const sel = host.selection;
@@ -34,11 +55,13 @@ export function AttributesPanel() {
} }
const isDrawing = sel.kind === "drawing2d"; const isDrawing = sel.kind === "drawing2d";
// Strichstärke wirkt laut Kontrakt nur auf Drawing2D; Wände erben sie aus der // Strichstärke: Wand/Decke/Drawing2D tragen einen eigenen Override
// Ebene und sind daher hier nicht editierbar. // (`strokeWeight`/`weightMm`) + Quellen-Dropdown; andere Elementarten erben
const weightEditable = isDrawing; // stets aus der Kategorie (kein Setter im Kontrakt → nicht editierbar).
// Füllschraffur nur bei geschlossener 2D-Form sinnvoll. const weightEditable = sel.kind === "wall" || sel.kind === "ceiling" || isDrawing;
const fillEditable = isDrawing && sel.closed === true; // 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 // Vordergrund/Hintergrund (Muster-/Füllfarbe) tragen Wand, Decke und
// geschlossene 2D-Formen als Override; `undefined` = „Nach System". // geschlossene 2D-Formen als Override; `undefined` = „Nach System".
const pocheEditable = const pocheEditable =
@@ -46,43 +69,84 @@ export function AttributesPanel() {
sel.kind === "ceiling" || sel.kind === "ceiling" ||
(isDrawing && sel.closed === true); (isDrawing && sel.closed === true);
// Ein Override-Farbfeld mit „Nach System"-Zustand: ist der Wert `undefined`, // Der 3-Optionen-Quellen-Dropdown, gemeinsam für alle vier Felder. Deaktiviert,
// zeigt es „Nach System" (leerer Swatch) statt einer Farbe und erbt; ein // wenn das Feld für diese Elementart gar nicht gilt (z. B. Strichstärke bei
// Klick auf × stellt zurück auf „Nach System" (schreibt `null` → Feld weg). // einer Treppe).
const overrideColor = ( const sourceSelect = (
value: string | undefined, editable: boolean,
onSet: (color: string | null) => void, ui: UiSource,
onChange: (next: UiSource) => void,
) => ( ) => (
<span className="attr-val"> <select
<label className="attr-color"> className="attr-select"
<span style={{ width: 104 }}
className="attr-color-swatch" value={ui}
style={{ background: pocheEditable && value ? value : "transparent" }} disabled={!editable}
/> onChange={(e) => onChange(e.target.value as UiSource)}
<input >
type="color" <option value="layer">{t("attr.source.layer")}</option>
value={value ?? "#808080"} <option value="object">{t("attr.source.object")}</option>
disabled={!pocheEditable} <option value="custom">{t("attr.source.custom")}</option>
title={pocheEditable ? undefined : t("attr.fillNeedsClosed")} </select>
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>
); );
// 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). // Effektiver Linienstil rein informativ (kein Setter im Kontrakt → kein Fake).
// Wir leiten ihn aus dem Modell-Element ab, wenn explizit gesetzt; sonst „—". // Wir leiten ihn aus dem Modell-Element ab, wenn explizit gesetzt; sonst „—".
const drawing = isDrawing const drawing = isDrawing
@@ -119,29 +183,34 @@ export function AttributesPanel() {
</label> </label>
</span> </span>
{/* Strichstärke (mm) — nur Drawing2D editierbar; Wände erben aus Ebene. */} {/* Strichstärke (mm) Wand/Decke/Drawing2D, mit Nach-Ebene/Nach-Bauteil/
<span className="attr-key">{t("attr.weight")}</span> eigener-Wert-Quelle; andere Elementarten erben stets aus der Kategorie. */}
<span className="attr-val"> {sourceRow(
<input "attr.weight",
className="attr-num" weightEditable,
type="number" weightUi,
step={0.01} onWeightSourceChange,
min={0} <>
list="attr-pen-weights" <input
value={sel.weightMm} className="attr-num"
disabled={!weightEditable} style={{ width: 72 }}
title={weightEditable ? undefined : t("attr.inheritedFromLayer")} type="number"
onChange={(e) => { step={0.01}
const v = Number(e.target.value); min={0}
if (Number.isFinite(v)) host.onSetSelectionWeight(v); list="attr-pen-weights"
}} value={sel.weightMm}
/> onChange={(e) => {
<datalist id="attr-pen-weights"> const v = Number(e.target.value);
{PEN_WEIGHTS.map((w) => ( if (Number.isFinite(v)) host.onSetSelectionWeight(v);
<option key={w} value={w} /> }}
))} />
</datalist> <datalist id="attr-pen-weights">
</span> {PEN_WEIGHTS.map((w) => (
<option key={w} value={w} />
))}
</datalist>
</>,
)}
{/* Linienstil — read-only/informativ (kein Setter im Kontrakt). */} {/* Linienstil — read-only/informativ (kein Setter im Kontrakt). */}
<span className="attr-key">{t("attr.lineStyle")}</span> <span className="attr-key">{t("attr.lineStyle")}</span>
@@ -152,26 +221,36 @@ export function AttributesPanel() {
{/* ── Füllung (Vordergrund/Hintergrund + Schraffur) ─────────────── */} {/* ── Füllung (Vordergrund/Hintergrund + Schraffur) ─────────────── */}
{/* Vordergrund = Muster-/Schraffurfarbe, Hintergrund = Füllfarbe/Poché. {/* Vordergrund = Muster-/Schraffurfarbe, Hintergrund = Füllfarbe/Poché.
Beide sind Overrides mit Default Nach System" (erben vom Bauteil); Jedes Feld: Nach Ebene (LayerCategory erzwingt den Wert) / Nach Bauteil
`background` ist der Nachfolger des alten reinen `fillColor`-Feldes. */} (erbt vom Component-Manager, Default) / eigener Wert (Override). */}
<div className="attr-section-label">{t("attr.fill")}</div> <div className="attr-section-label">{t("attr.fill")}</div>
<div className="attr-grid"> <div className="attr-grid">
<span className="attr-key">{t("attr.foreground")}</span> {sourceRow(
{overrideColor(sel.foreground, host.onSetSelectionForeground)} "attr.foreground",
pocheEditable,
fgUi,
onForegroundSourceChange,
colorValue(sel.foreground, host.onSetSelectionForeground),
)}
<span className="attr-key">{t("attr.background")}</span> {sourceRow(
{overrideColor(sel.background, host.onSetSelectionBackground)} "attr.background",
pocheEditable,
bgUi,
onBackgroundSourceChange,
colorValue(sel.background, host.onSetSelectionBackground),
)}
<span className="attr-key">{t("attr.hatch")}</span> {sourceRow(
<span className="attr-val"> "attr.hatch",
fillEditable,
hatchUi,
onHatchSourceChange,
<select <select
className="attr-select" className="attr-select"
value={fillEditable ? sel.fillHatchId ?? "" : ""} style={{ width: 104 }}
disabled={!fillEditable} value={sel.fillHatchId ?? ""}
title={fillEditable ? undefined : t("attr.fillNeedsClosed")} onChange={(e) => host.onSetSelectionFill(e.target.value || null)}
onChange={(e) =>
host.onSetSelectionFill(e.target.value || null)
}
> >
<option value="">{t("attr.none")}</option> <option value="">{t("attr.none")}</option>
{project.hatches.map((h) => ( {project.hatches.map((h) => (
@@ -179,8 +258,8 @@ export function AttributesPanel() {
{h.name} {h.name}
</option> </option>
))} ))}
</select> </select>,
</span> )}
</div> </div>
{/* ── Maße (informativ, aus der bbox) ───────────────────────────── */} {/* ── Maße (informativ, aus der bbox) ───────────────────────────── */}
+30 -6
View File
@@ -15,6 +15,7 @@ import { useContext } from "react";
import { PanelHostContext } from "./types"; import { PanelHostContext } from "./types";
import type { DisplayMode } from "./types"; import type { DisplayMode } from "./types";
import type { import type {
AttributeSource,
CeilingType, CeilingType,
Component, Component,
ContextObject, ContextObject,
@@ -139,9 +140,17 @@ export interface PanelHostValue {
selection: Selection | null; selection: Selection | null;
/** Setzt die (Strich-)Farbe der aktuellen Selektion (Wand oder Drawing2D). */ /** Setzt die (Strich-)Farbe der aktuellen Selektion (Wand oder Drawing2D). */
onSetSelectionColor: (color: string) => void; 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; 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; onSetSelectionFill: (hatchId: string | null) => void;
/** /**
* Setzt/entfernt die Vollton-Füllfarbe wirkt NUR auf Drawing2D (sonst No-op). * 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; onSetSelectionFillColor: (color: string | null) => void;
/** /**
* Setzt/entfernt den Vordergrund-Override (Muster-/Schraffurfarbe) der * Setzt/entfernt den Vordergrund-Override (Muster-/Schraffurfarbe, eigener
* Selektion Wand, Decke oder geschlossene Drawing2D. `null` = Nach System". * 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; onSetSelectionForeground: (color: string | null) => void;
/** /**
* Setzt/entfernt den Hintergrund-Override (Füllfarbe/Poché) der Selektion * Setzt/entfernt den Hintergrund-Override (Füllfarbe/Poché, eigener Wert")
* Wand, Decke oder geschlossene Drawing2D. `null` = Nach System". * der Selektion Wand, Decke oder geschlossene Drawing2D. `null` = Nach
* System" (siehe {@link onSetSelectionBackgroundSource}).
*/ */
onSetSelectionBackground: (color: string | null) => void; 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 * 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). * (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. // Öffnungen) — exakt, schnell, sauber.
import type { import type {
AttributeSource,
Ceiling, Ceiling,
Component, Component,
Drawing2D, Drawing2D,
@@ -23,6 +24,7 @@ import {
getCeilingType, getCeilingType,
getComponent, getComponent,
getHatch, getHatch,
getLayerCategory,
getLineStyle, getLineStyle,
getWallType, getWallType,
openingsOfWall, openingsOfWall,
@@ -334,22 +336,82 @@ export function resolveHatch(
/** /**
* Kollabiert die Vordergrund-Farbkette (Muster-/Schraffurlinienfarbe) eines * Kollabiert die Vordergrund-Farbkette (Muster-/Schraffurlinienfarbe) eines
* Bauteils zu einem einzelnen Wert für {@link resolveHatch}: * Bauteils zu einem einzelnen Wert für {@link resolveHatch}. AUFLÖSUNGSREIHENFOLGE
* (Attribut-Override `override`) ?? (`Component.foreground`) ?? `undefined`. * (By-Layer/By-Object, siehe `docs`/Attribut-Panel):
* `undefined` heißt Nach System" die Kette läuft dann in resolveHatch auf den * (Attribut-Override `override`) ?? (`source==="layer"` `category.color`) ??
* HatchStyle.color-Backward-Compat-Fallback weiter (Sample-Verhalten). * (`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 { export function resolveForeground(
return override ?? comp.foreground; 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: * Kollabiert die Hintergrund-/Füllfarbkette (Poché) eines Bauteils, analog zu
* (Attribut-Override `override`) ?? (`Component.background`) ?? `Component.color`. * {@link resolveForeground}:
* (Attribut-Override `override`) ?? (`source==="layer"` `category.color`) ??
* (`Component.background`) ?? `Component.color`.
* Immer definiert (endet spätestens bei `Component.color` = heutiges Verhalten). * Immer definiert (endet spätestens bei `Component.color` = heutiges Verhalten).
*/ */
export function resolveBackground(comp: Component, override?: string): string { export function resolveBackground(
return override ?? comp.background ?? comp.color; 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 ──────────────────────────────────────────────────── // ── 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 || comp.joinPriority > best.joinPriority) best = comp;
} }
if (!best) return null; if (!best) return null;
const category = getLayerCategory(project, wall.categoryCode);
const wallAngleDeg = const wallAngleDeg =
(Math.atan2(wall.end.y - wall.start.y, wall.end.x - wall.start.x) * 180) / Math.PI; (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( const hatch = resolveHatch(
project, project,
best.hatchId, hatchId,
wallAngleDeg, wallAngleDeg,
resolveForeground(best, wall.foreground), resolveForeground(best, wall.foreground, category, wall.foregroundSource),
); );
// Poché-Hintergrund neutral (weiß) statt Bauteil-Albedo; Vollmuster → schwarz. // Poché-Hintergrund neutral (weiß) statt Bauteil-Albedo; Vollmuster → schwarz.
const solid = hatch.pattern === "solid"; const solid = hatch.pattern === "solid";
@@ -419,9 +483,17 @@ export function resolveCeilingSectionStyle(
const wt = getCeilingType(project, ceiling); const wt = getCeilingType(project, ceiling);
if (wt.layers.length === 0) return null; if (wt.layers.length === 0) return null;
const comp = getComponent(project, wt.layers[0].componentId); 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 { return {
fill: resolveBackground(comp, ceiling.background), fill: resolveBackground(comp, ceiling.background, category, ceiling.backgroundSource),
hatch: resolveHatch(project, comp.hatchId, undefined, resolveForeground(comp, ceiling.foreground)), hatch: resolveHatch(
project,
hatchId,
undefined,
resolveForeground(comp, ceiling.foreground, category, ceiling.foregroundSource),
),
}; };
} catch { } catch {
return null; return null;
@@ -466,6 +538,9 @@ export function generatePlan(
// Modell (Ebene) stammt statt aus einer Magie-Konstante. // Modell (Ebene) stammt statt aus einer Magie-Konstante.
const lwByCode = categoryLwMap(project.layers); const lwByCode = categoryLwMap(project.layers);
const colorByCode = categoryColorMap(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 // Freie 2D-Zeichengeometrie dieses Geschosses (wie Wände nach sichtbaren
// Kategorien + Darstellungsmodus gefiltert). Wird über der Wand-Poché // Kategorien + Darstellungsmodus gefiltert). Wird über der Wand-Poché
@@ -489,8 +564,14 @@ export function generatePlan(
); );
for (const ceiling of ceilings) { for (const ceiling of ceilings) {
const greyed = categoryDisplay(ceiling.categoryCode).greyed; const greyed = categoryDisplay(ceiling.categoryCode).greyed;
const lwMm = lwByCode.get(ceiling.categoryCode) ?? WALL_FALLBACK_MM; const category = catByCode.get(ceiling.categoryCode);
addCeilingPoche(primitives, project, ceiling, greyed, detail, lwMm); 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 // Räume (SIA-416-Flächen) dieses Geschosses: transluzente Farbfüllung + Stempel
@@ -510,14 +591,20 @@ export function generatePlan(
for (const wall of walls) { for (const wall of walls) {
const wallGreyed = categoryDisplay(wall.categoryCode).greyed; 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 // Türen (Legacy) + Öffnungen (Fenster/Türen) am Wandhost: BEIDE sparen die
// Wand-Poché aus. Aus beiden wird eine gemeinsame Lücken-Intervall-Liste. // Wand-Poché aus. Aus beiden wird eine gemeinsame Lücken-Intervall-Liste.
const doors = project.doors.filter((d) => d.hostWallId === wall.id); const doors = project.doors.filter((d) => d.hostWallId === wall.id);
const openings = openingsOfWall(project, wall.id); const openings = openingsOfWall(project, wall.id);
const gaps = wallGaps(wall, doors, openings); const gaps = wallGaps(wall, doors, openings);
const cuts = joins.get(wall.id) ?? { startCut: null, endCut: null }; 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 // Referenzlinie: dünne gestrichelte Wand-Achslinie (von der Oberleiste
// umschaltbar). Wird über der Poché gezeichnet, damit sie sichtbar bleibt. // umschaltbar). Wird über der Poché gezeichnet, damit sie sichtbar bleibt.
if (showReferenceLines) addReferenceLine(primitives, wall, wallGreyed); if (showReferenceLines) addReferenceLine(primitives, wall, wallGreyed);
@@ -557,7 +644,7 @@ export function generatePlan(
// 2D-Zeichengeometrie zuletzt (über der Poché). // 2D-Zeichengeometrie zuletzt (über der Poché).
for (const d of drawings) { for (const d of drawings) {
const greyed = categoryDisplay(d.categoryCode).greyed; 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 // Kontext-Konturen (Höhenlinien) als dezente Linien zeichnen — UNTER der
@@ -780,6 +867,17 @@ function categoryColorMap(layers: LayerCategory[]): Map<string, string> {
return 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<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 * 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 * §7.2). Farbe/Strichstärke kommen aus dem optionalen LineStyle, sonst aus der
@@ -793,15 +891,30 @@ function addDrawing2D(
greyed: boolean, greyed: boolean,
lwByCode: Map<string, number>, lwByCode: Map<string, number>,
colorByCode: Map<string, string>, colorByCode: Map<string, string>,
catByCode: Map<string, LayerCategory>,
): void { ): void {
const category = catByCode.get(d.categoryCode);
const ls = d.lineStyleId const ls = d.lineStyleId
? project.lineStyles.find((l) => l.id === d.lineStyleId) ? project.lineStyles.find((l) => l.id === d.lineStyleId)
: undefined; : undefined;
// Plan-Tinte fix dunkel: Fallback auf MONO_INK (statt hell „#d0d0d0"), damit // Plan-Tinte fix dunkel: Fallback auf MONO_INK (statt hell „#d0d0d0"), damit
// ein kategorieloses 2D-Element auf dem hellen Papier sichtbar bleibt. // ein kategorieloses 2D-Element auf dem hellen Papier sichtbar bleibt.
const color = d.color ?? ls?.color ?? colorByCode.get(d.categoryCode) ?? MONO_INK; 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; 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"). // Zickzack-Parameter aus dem LineStyle durchreichen (nur kind==="zigzag").
// Additiv: fehlt es, bleibt die Linie gerade/gestrichelt wie bisher. // Additiv: fehlt es, bleibt die Linie gerade/gestrichelt wie bisher.
const zigzag = ls?.kind === "zigzag" ? ls.zigzag : undefined; const zigzag = ls?.kind === "zigzag" ? ls.zigzag : undefined;
@@ -821,16 +934,18 @@ function addDrawing2D(
out.push({ out.push({
kind: "polygon", kind: "polygon",
pts, pts,
// Hintergrund-Override hat Vorrang vor dem Alt-Feld fillColor (beide leer = // Hintergrund-Override (Wert ?? By-Layer) hat Vorrang vor dem Alt-Feld
// transparente Fläche, nur Schraffur) — Sample bleibt identisch. // fillColor (alle leer = transparente Fläche, nur Schraffur) — Sample
fill: d.background ?? d.fillColor ?? "none", // bleibt identisch (kein Element setzt background/backgroundSource).
fill: background ?? d.fillColor ?? "none",
// Umriss zeichnen die separaten Linien-Primitive (mit Strichmuster); die // Umriss zeichnen die separaten Linien-Primitive (mit Strichmuster); die
// Füllfläche selbst bleibt randlos, um doppelte Striche zu vermeiden. // Füllfläche selbst bleibt randlos, um doppelte Striche zu vermeiden.
stroke: "none", stroke: "none",
strokeWidthMm: 0, strokeWidthMm: 0,
// Drawing2D trägt kein Bauteil → Vordergrund-Override direkt aus d.foreground // Drawing2D trägt kein Bauteil → Vordergrund/Schraffur-Override direkt aus
// (Attribut „Nach System" ⇒ undefined ⇒ HatchStyle.color-Fallback). // d.foreground/d.hatchId bzw. der Ebene (Attribut „Nach System" ⇒
hatch: d.hatchId ? resolveHatch(project, d.hatchId, undefined, d.foreground) : NO_HATCH, // undefined ⇒ HatchStyle.color-Fallback bzw. NO_HATCH).
hatch: hatchId ? resolveHatch(project, hatchId, undefined, foreground) : NO_HATCH,
greyed, greyed,
drawingId: d.id, drawingId: d.id,
}); });
@@ -929,6 +1044,7 @@ function addWallPoche(
greyed: boolean, greyed: boolean,
detail: DetailLevel, detail: DetailLevel,
wallLwMm: number, wallLwMm: number,
category: LayerCategory | undefined,
): void { ): void {
const wt = getWallType(project, wall); const wt = getWallType(project, wall);
const total = wallTypeThickness(wt); const total = wallTypeThickness(wt);
@@ -1015,11 +1131,13 @@ function addWallPoche(
for (let li = 0; li < wt.layers.length; li++) { for (let li = 0; li < wt.layers.length; li++) {
const layer = wt.layers[li]; const layer = wt.layers[li];
const comp = getComponent(project, layer.componentId); const comp = getComponent(project, layer.componentId);
const layerHatchId =
resolveHatchId(wall.hatchId, wall.hatchSource, category, comp.hatchId) ?? comp.hatchId;
const layerHatch = resolveHatch( const layerHatch = resolveHatch(
project, project,
comp.hatchId, layerHatchId,
wallAngleDeg, wallAngleDeg,
resolveForeground(comp, wall.foreground), resolveForeground(comp, wall.foreground, category, wall.foregroundSource),
); );
// SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die // SIA-Poché: neutraler Hintergrund (weiß) statt Bauteil-Albedo, die
// Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché. // Schichtschraffur liegt darüber; Vollmuster ("solid") = schwarze Poché.
@@ -1319,6 +1437,7 @@ function addCeilingPoche(
greyed: boolean, greyed: boolean,
detail: DetailLevel, detail: DetailLevel,
lwMm: number, lwMm: number,
category: LayerCategory | undefined,
): void { ): void {
const pts = ceiling.outline; const pts = ceiling.outline;
if (pts.length < 3) return; if (pts.length < 3) return;
@@ -1333,10 +1452,23 @@ function addCeilingPoche(
// NICHT die Schnitt-Schraffur (die gehört auf echte Schnittflächen, siehe // NICHT die Schnitt-Schraffur (die gehört auf echte Schnittflächen, siehe
// resolveCeilingSectionStyle/splitSlabLayers), sondern `viewHatchId`. Fehlt // resolveCeilingSectionStyle/splitSlabLayers), sondern `viewHatchId`. Fehlt
// sie (Default), bleibt die Aufsicht weiss. Im Detailgrad „grob" entfällt // 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 = const hatch =
detail !== "grob" && comp?.viewHatchId detail !== "grob" && comp && effectiveViewHatchId
? resolveHatch(project, comp.viewHatchId, undefined, resolveForeground(comp, ceiling.foreground)) ? resolveHatch(
project,
effectiveViewHatchId,
undefined,
resolveForeground(comp, ceiling.foreground, category, ceiling.foregroundSource),
)
: NO_HATCH; : NO_HATCH;
const solid = hatch.pattern === "solid"; const solid = hatch.pattern === "solid";
out.push({ out.push({
+14
View File
@@ -15,6 +15,7 @@ import {
} from "../model/types"; } from "../model/types";
import type { import type {
Component, Component,
Drawing2D,
DrawingLevel, DrawingLevel,
HatchStyle, HatchStyle,
LayerCategory, LayerCategory,
@@ -152,6 +153,13 @@ export interface ProjectSlice {
id: string, id: string,
value: string | null, value: string | null,
) => void; ) => 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 * Skaliert die Geometrie eines Elements auf Zielbreite×-höhe (Meter) um einen
* Ankerpunkt (fx/fy [0,1] relativ zur bbox; 0,0 = oben-links). * Ankerpunkt (fx/fy [0,1] relativ zur bbox; 0,0 = oben-links).
@@ -748,6 +756,12 @@ export function createProjectSlice(
setElementBackground: (kind, id, value) => setElementBackground: (kind, id, value) =>
setProject((p) => setOverrideField(p, kind, id, "background", 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) ──────────────────────────────────────────── // ── Größe (Resize um Anker) ────────────────────────────────────────────
resizeElement: (kind, id, w, h, anchor) => resizeElement: (kind, id, w, h, anchor) =>
setProject((p) => resizeElement(p, kind, id, w, h, anchor)), setProject((p) => resizeElement(p, kind, id, w, h, anchor)),
+50 -5
View File
@@ -18,6 +18,7 @@ import {
wallTypeThickness, wallTypeThickness,
} from "../model/types"; } from "../model/types";
import type { import type {
AttributeSource,
Ceiling, Ceiling,
Drawing2D, Drawing2D,
Drawing2DGeom, Drawing2DGeom,
@@ -229,6 +230,24 @@ export interface Selection {
* System". Nachfolger von `fillColor`; hat im Plan Vorrang. * System". Nachfolger von `fillColor`; hat im Plan Vorrang.
*/ */
background?: string; 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. */ /** Ob die Form geschlossen ist (rect/circle/closed polyline) — nur Drawing2D. */
closed?: boolean; closed?: boolean;
/** Achsparallele Bounding-Box in Modell-Metern. */ /** Achsparallele Bounding-Box in Modell-Metern. */
@@ -310,7 +329,10 @@ function geomBBox(g: Drawing2DGeom): SelectionBBox {
function wallSelection(project: Project, wall: Wall): Selection { function wallSelection(project: Project, wall: Wall): Selection {
const cat = findCategory(project.layers, wall.categoryCode); const cat = findCategory(project.layers, wall.categoryCode);
const color = wall.color ?? cat?.color ?? WALL_FALLBACK_COLOR; 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 minX = Math.min(wall.start.x, wall.end.x);
const minY = Math.min(wall.start.y, wall.end.y); const minY = Math.min(wall.start.y, wall.end.y);
const maxX = Math.max(wall.start.x, wall.end.x); const maxX = Math.max(wall.start.x, wall.end.x);
@@ -351,10 +373,15 @@ function wallSelection(project: Project, wall: Wall): Selection {
categoryCode: wall.categoryCode, categoryCode: wall.categoryCode,
color, color,
weightMm, weightMm,
fillHatchId: undefined, fillHatchId: wall.hatchId,
fillColor: undefined, fillColor: undefined,
foreground: wall.foreground, foreground: wall.foreground,
background: wall.background, background: wall.background,
foregroundSource: wall.foregroundSource,
backgroundSource: wall.backgroundSource,
strokeWeightOverride: wall.strokeWeight,
strokeWeightSource: wall.strokeWeightSource,
hatchSource: wall.hatchSource,
closed: undefined, closed: undefined,
bbox: { minX, minY, maxX, maxY }, bbox: { minX, minY, maxX, maxY },
wall: wallInfo, wall: wallInfo,
@@ -365,7 +392,7 @@ function wallSelection(project: Project, wall: Wall): Selection {
function ceilingSelection(project: Project, ceiling: Ceiling): Selection { function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
const cat = findCategory(project.layers, ceiling.categoryCode); const cat = findCategory(project.layers, ceiling.categoryCode);
const color = ceiling.color ?? cat?.color ?? WALL_FALLBACK_COLOR; 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 box = outlineBBox(ceiling.outline);
const wt = getCeilingType(project, ceiling); const wt = getCeilingType(project, ceiling);
const thickness = ceilingThickness(project, ceiling); const thickness = ceilingThickness(project, ceiling);
@@ -397,10 +424,15 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection {
categoryCode: ceiling.categoryCode, categoryCode: ceiling.categoryCode,
color, color,
weightMm, weightMm,
fillHatchId: undefined, fillHatchId: ceiling.hatchId,
fillColor: undefined, fillColor: undefined,
foreground: ceiling.foreground, foreground: ceiling.foreground,
background: ceiling.background, background: ceiling.background,
foregroundSource: ceiling.foregroundSource,
backgroundSource: ceiling.backgroundSource,
strokeWeightOverride: ceiling.strokeWeight,
strokeWeightSource: ceiling.strokeWeightSource,
hatchSource: ceiling.hatchSource,
closed: true, closed: true,
bbox: { minX: box.minX, minY: box.minY, maxX: box.maxX, maxY: box.maxY }, bbox: { minX: box.minX, minY: box.minY, maxX: box.maxX, maxY: box.maxY },
ceiling: ceilingInfo, ceiling: ceilingInfo,
@@ -547,7 +579,15 @@ function drawingSelection(project: Project, d: Drawing2D): Selection {
: undefined; : undefined;
const cat = findCategory(project.layers, d.categoryCode); const cat = findCategory(project.layers, d.categoryCode);
const color = d.color ?? ls?.color ?? cat?.color ?? DRAW_FALLBACK_COLOR; 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); const closed = isClosedGeom(d.geom);
return { return {
kind: "drawing2d", kind: "drawing2d",
@@ -559,6 +599,11 @@ function drawingSelection(project: Project, d: Drawing2D): Selection {
fillColor: closed ? d.fillColor ?? null : null, fillColor: closed ? d.fillColor ?? null : null,
foreground: closed ? d.foreground : undefined, foreground: closed ? d.foreground : undefined,
background: closed ? d.background : 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, closed,
bbox: geomBBox(d.geom), bbox: geomBBox(d.geom),
}; };