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:
+59
-4
@@ -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
@@ -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
@@ -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",
|
||||
|
||||
@@ -500,6 +500,19 @@ export interface LayerCategory {
|
||||
children?: LayerCategory[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Quelle eines vererbbaren Attributs (Vordergrund/Hintergrund/Strichstärke/
|
||||
* Schraffur), wenn KEIN expliziter Wert am Element gesetzt ist:
|
||||
* • "layer" — „Nach Ebene": die LayerCategory des Elements erzwingt den Wert
|
||||
* (`color`/`lw`/`hatch`).
|
||||
* • "object" — „Nach Bauteil": erbt vom Bauteil (Component) bzw. dessen
|
||||
* bisheriger Fallback-Kette. Das ist auch der Default, wenn das Source-Feld
|
||||
* fehlt (`undefined`) — damit bleibt das heutige Verhalten unverändert.
|
||||
* Ein gesetzter expliziter Wert (z. B. `foreground`) gewinnt IMMER, unabhängig
|
||||
* von der Source (s. Resolve-Reihenfolge in `plan/generatePlan.ts`).
|
||||
*/
|
||||
export type AttributeSource = "layer" | "object";
|
||||
|
||||
/**
|
||||
* Lage der Wandachse (Referenzlinie) über die Dicke der Wand — analog
|
||||
* Vectorworks. Gemessen relativ zur Laufrichtung (start→end) mit der
|
||||
@@ -553,6 +566,37 @@ export interface Wall {
|
||||
* `undefined` = „Nach System" (erben → Component.background → Component.color).
|
||||
*/
|
||||
background?: string;
|
||||
/**
|
||||
* Attribut-Override der Strichstärke (mm Papier) DIESER Wand-Instanz (Umriss/
|
||||
* Schichtfugen). `undefined` = „Nach System" (erben → `strokeWeightSource`).
|
||||
*/
|
||||
strokeWeight?: number;
|
||||
/**
|
||||
* Attribut-Override der Schnitt-Schraffur (Hatch Manager) DIESER Wand-Instanz;
|
||||
* überschreibt die Schraffur ALLER Schichten einheitlich. `undefined` =
|
||||
* „Nach System" (erben → `hatchSource`).
|
||||
*/
|
||||
hatchId?: string;
|
||||
/**
|
||||
* Quelle des Vordergrunds, wenn kein expliziter `foreground`-Wert gesetzt ist:
|
||||
* "layer" erzwingt die Ebenenfarbe (LayerCategory.color), "object"/`undefined`
|
||||
* (Default) erbt vom Bauteil (heutiges Verhalten).
|
||||
*/
|
||||
foregroundSource?: AttributeSource;
|
||||
/** Quelle des Hintergrunds, analog zu {@link Wall.foregroundSource}. */
|
||||
backgroundSource?: AttributeSource;
|
||||
/**
|
||||
* Quelle der Strichstärke, wenn kein explizites `strokeWeight` gesetzt ist:
|
||||
* "layer" erzwingt `LayerCategory.lw`, "object"/`undefined` (Default) fällt auf
|
||||
* die bisherige Kategorie-/Fallback-Strichstärke zurück (heutiges Verhalten).
|
||||
*/
|
||||
strokeWeightSource?: AttributeSource;
|
||||
/**
|
||||
* Quelle der Schraffur, wenn kein explizites `hatchId` gesetzt ist: "layer"
|
||||
* erzwingt `LayerCategory.hatch`, "object"/`undefined` (Default) erbt vom
|
||||
* Bauteil (heutiges Verhalten).
|
||||
*/
|
||||
hatchSource?: AttributeSource;
|
||||
/**
|
||||
* Lage der Wandachse über die Dicke. Fehlt sie, gilt "center" (= heutiges
|
||||
* Verhalten: Schichten symmetrisch −T/2 … +T/2 um die Achse).
|
||||
@@ -625,6 +669,25 @@ export interface Ceiling {
|
||||
* `undefined` = „Nach System" (erben → Component.background → Component.color).
|
||||
*/
|
||||
background?: string;
|
||||
/**
|
||||
* Attribut-Override der Strichstärke (mm Papier) DIESER Decken-Instanz.
|
||||
* `undefined` = „Nach System" (erben → `strokeWeightSource`).
|
||||
*/
|
||||
strokeWeight?: number;
|
||||
/**
|
||||
* Attribut-Override der Schraffur (Hatch Manager) DIESER Decken-Instanz;
|
||||
* überschreibt sowohl die Schnitt- als auch die Ansichts-Schraffur des
|
||||
* Bauteils. `undefined` = „Nach System" (erben → `hatchSource`).
|
||||
*/
|
||||
hatchId?: string;
|
||||
/** Quelle des Vordergrunds, analog zu {@link Wall.foregroundSource}. */
|
||||
foregroundSource?: AttributeSource;
|
||||
/** Quelle des Hintergrunds, analog zu {@link Wall.backgroundSource}. */
|
||||
backgroundSource?: AttributeSource;
|
||||
/** Quelle der Strichstärke, analog zu {@link Wall.strokeWeightSource}. */
|
||||
strokeWeightSource?: AttributeSource;
|
||||
/** Quelle der Schraffur, analog zu {@link Wall.hatchSource}. */
|
||||
hatchSource?: AttributeSource;
|
||||
/**
|
||||
* Vertikale Bindung der OBERKANTE (OK). Fehlt sie, sitzt die OK an der
|
||||
* Oberkante des Geschosses (baseElevation + floorHeight).
|
||||
@@ -914,6 +977,25 @@ export interface Drawing2D {
|
||||
* vor dem LineStyle-Gewicht und der Kategorie-Strichstärke.
|
||||
*/
|
||||
weightMm?: number;
|
||||
/**
|
||||
* Quelle des Vordergrunds (Schraffur-Musterfarbe), wenn kein explizites
|
||||
* `foreground` gesetzt ist: "layer" erzwingt die Ebenenfarbe
|
||||
* (LayerCategory.color), "object"/`undefined` (Default) = heutiges Verhalten
|
||||
* (kein Bauteil-Bezug bei Drawing2D → HatchStyle.color-Fallback).
|
||||
*/
|
||||
foregroundSource?: AttributeSource;
|
||||
/** Quelle des Hintergrunds (Füllfarbe), analog zu `foregroundSource`. */
|
||||
backgroundSource?: AttributeSource;
|
||||
/**
|
||||
* Quelle der Strichstärke, wenn kein explizites `weightMm` gesetzt ist:
|
||||
* "layer" erzwingt `LayerCategory.lw` (unter Umgehung des LineStyle-Gewichts),
|
||||
* "object"/`undefined` (Default) = heutige Kette (LineStyle.weight ?? Kategorie).
|
||||
*/
|
||||
strokeWeightSource?: AttributeSource;
|
||||
/** Quelle der Schraffur, wenn kein explizites `hatchId` gesetzt ist: "layer"
|
||||
* erzwingt `LayerCategory.hatch`, "object"/`undefined` (Default) = heutiges
|
||||
* Verhalten (ohne `hatchId` keine Schraffur). */
|
||||
hatchSource?: AttributeSource;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1190,6 +1272,16 @@ export const flattenCategories = (cats: LayerCategory[]): LayerCategory[] => {
|
||||
return out;
|
||||
};
|
||||
|
||||
/**
|
||||
* Liefert die LayerCategory zu einem Kategorie-Code (Baum durchsucht), oder
|
||||
* `undefined`, falls der Code auf keine Ebene verweist (verwaister
|
||||
* `categoryCode`). Nicht-werfend, damit die Attribut-Resolve-Kette
|
||||
* (`plan/generatePlan.ts`) robust auf einen fehlenden Ebenen-Wert zurückfallen
|
||||
* kann (→ Bauteil-Fallback).
|
||||
*/
|
||||
export const getLayerCategory = (project: Project, code: string): LayerCategory | undefined =>
|
||||
flattenCategories(project.layers).find((c) => c.code === code);
|
||||
|
||||
/** Menge aller Codes sichtbarer Kategorien (Baum berücksichtigt). */
|
||||
export const collectVisibleCodes = (layers: LayerCategory[]): Set<string> => {
|
||||
const codes = new Set<string>();
|
||||
|
||||
+157
-78
@@ -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
@@ -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
@@ -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({
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user