diff --git a/src/model/types.ts b/src/model/types.ts index f791f92..72bd7e2 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -831,12 +831,14 @@ export interface 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`). + * (`color`/`lw`/`hatch`). Das ist auch der DEFAULT, wenn das Source-Feld + * fehlt (`undefined`) — ein frisch erzeugtes Element (Source-Feld nie + * gesetzt) folgt also der Ebene, nicht dem Bauteil. * • "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. + * bisheriger Fallback-Kette. Muss aktiv im Attribut-Panel gewählt werden + * (nur bei Wand/Decke anwendbar — ein Drawing2D hat kein Bauteil). * Ein gesetzter expliziter Wert (z. B. `foreground`) gewinnt IMMER, unabhängig - * von der Source (s. Resolve-Reihenfolge in `plan/generatePlan.ts`). + * von der Source (s. Resolve-Reihenfolge in `plan/generatePlan/shared.ts`). */ export type AttributeSource = "layer" | "object"; diff --git a/src/panels/AttributesPanel.tsx b/src/panels/AttributesPanel.tsx index e340fed..743d4af 100644 --- a/src/panels/AttributesPanel.tsx +++ b/src/panels/AttributesPanel.tsx @@ -51,9 +51,10 @@ import { 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). */ + * Wert"), sonst das rohe Source-Feld (`undefined` ⇒ „Nach Ebene", Default — + * s. AttributeSource in model/types.ts). */ function uiSourceOf(hasValue: boolean, raw: AttributeSource | undefined): UiSource { - return hasValue ? "custom" : raw ?? "object"; + return hasValue ? "custom" : raw ?? "layer"; } export function AttributesPanel() { @@ -131,13 +132,21 @@ export function AttributesPanel() { sel.kind === "ceiling" || (isDrawing && sel.closed === true); - // 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). + // Ein Drawing2D hat KEIN eigenes Bauteil (Component) — „Nach Bauteil" wäre + // dort bedeutungslos (resolveForeground & Co. fallen dann auf gar keinen + // Wert zurück, s. selectionInfo.ts `noComponent`). Nur Wand/Decke bieten + // die Option daher an. + const allowObjectSource = !isDrawing; + + // Der Quellen-Dropdown (Nach Ebene/Nach Bauteil/eigener Wert), gemeinsam für + // alle vier Felder. Deaktiviert, wenn das Feld für diese Elementart gar + // nicht gilt (z. B. Strichstärke bei einer Treppe); „Nach Bauteil" fehlt bei + // Drawing2D (s. o.). const sourceSelect = ( editable: boolean, ui: UiSource, onChange: (next: UiSource) => void, + allowObject: boolean, ) => ( onChange(v as UiSource)} options={[ { value: "layer", label: t("attr.source.layer") }, - { value: "object", label: t("attr.source.object") }, + ...(allowObject ? [{ value: "object", label: t("attr.source.object") }] : []), { value: "custom", label: t("attr.source.custom") }, ]} /> ); - // 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) => ( - - ); + // Farb-Swatch — IMMER sichtbar (Nutzer-Wunsch: den aktuell EFFEKTIVEN Wert + // (welche Farbe auch immer Ebene/Bauteil/Override gerade liefert) jederzeit + // sehen, nicht nur bei „eigener Wert"). Ändert man ihn direkt, wird + // automatisch „eigener Wert": der Setter schreibt den Override, wodurch + // `uiSourceOf` beim nächsten Render „custom" ableitet (kein separater + // Umschalt-Schritt nötig). + const colorValue = ( + value: string | undefined, + onSet: (color: string) => void, + editable: boolean, + ) => ; - // Eine Attribut-Zeile: Label + Quellen-Dropdown + (nur bei „eigener Wert") - // das passende Eingabe-Element. + // Eine Attribut-Zeile: Label + IMMER sichtbares Eingabe-Element (zeigt den + // effektiven Wert) + Quellen-Dropdown. const sourceRow = ( labelKey: string, editable: boolean, ui: UiSource, onSource: (next: UiSource) => void, valueEditor: ReactNode, + allowObject: boolean = allowObjectSource, ) => ( <> {t(labelKey)} - {ui === "custom" && valueEditor} - {sourceSelect(editable, ui, onSource)} + {valueEditor} + {sourceSelect(editable, ui, onSource, allowObject)} ); @@ -187,12 +202,17 @@ export function AttributesPanel() { sel.hatchSource, ); + // Wechsel auf „eigener Wert" per Dropdown (nicht durchs Editieren des Felds + // selbst, s. colorValue-Kommentar oben) sät den Override mit dem gerade + // ANGEZEIGTEN effektiven Wert — sonst würde die Farbe beim Umschalten + // unerwartet springen (z. B. auf sel.color statt der tatsächlich sichtbaren + // Ebenen-/Bauteilfarbe). const onForegroundSourceChange = (next: UiSource) => { - if (next === "custom") host.onSetSelectionForeground(sel.foreground ?? sel.color); + if (next === "custom") host.onSetSelectionForeground(sel.effectiveForeground ?? sel.color); else host.onSetSelectionForegroundSource(next); }; const onBackgroundSourceChange = (next: UiSource) => { - if (next === "custom") host.onSetSelectionBackground(sel.background ?? sel.color); + if (next === "custom") host.onSetSelectionBackground(sel.effectiveBackground ?? sel.color); else host.onSetSelectionBackgroundSource(next); }; const onWeightSourceChange = (next: UiSource) => { @@ -200,8 +220,11 @@ export function AttributesPanel() { else host.onSetSelectionStrokeWeightSource(next); }; const onHatchSourceChange = (next: UiSource) => { - if (next === "custom") host.onSetSelectionFill(project.hatches[0]?.id ?? null); - else host.onSetSelectionHatchSource(next); + if (next === "custom") { + host.onSetSelectionFill(sel.effectiveHatchId ?? project.hatches[0]?.id ?? null); + } else { + host.onSetSelectionHatchSource(next); + } }; // Effektiver Linienstil rein informativ (kein Setter im Kontrakt → kein Fake). @@ -272,6 +295,7 @@ export function AttributesPanel() { type="number" step={0.01} min={0} + disabled={!weightEditable} list="attr-pen-weights" value={sel.weightMm} onChange={(e) => { @@ -352,7 +376,7 @@ export function AttributesPanel() { pocheEditable, fgUi, onForegroundSourceChange, - colorValue(sel.foreground, host.onSetSelectionForeground), + colorValue(sel.effectiveForeground, host.onSetSelectionForeground, pocheEditable), )} {sourceRow( @@ -360,7 +384,7 @@ export function AttributesPanel() { pocheEditable, bgUi, onBackgroundSourceChange, - colorValue(sel.background, host.onSetSelectionBackground), + colorValue(sel.effectiveBackground, host.onSetSelectionBackground, pocheEditable), )} {sourceRow( @@ -369,7 +393,8 @@ export function AttributesPanel() { hatchUi, onHatchSourceChange, host.onSetSelectionFill(v || null)} options={[ { value: "", label: t("attr.none") }, diff --git a/src/plan/generatePlan/shared.resolve.test.ts b/src/plan/generatePlan/shared.resolve.test.ts new file mode 100644 index 0000000..3c4e193 --- /dev/null +++ b/src/plan/generatePlan/shared.resolve.test.ts @@ -0,0 +1,81 @@ +/** + * `resolveForeground`/`resolveBackground`/`resolveHatchId`/`resolveStrokeWeight` + * — By-Layer/By-Object-Auflösung. Nutzer-Report: ein neu erzeugtes Element + * (Source-Feld nie gesetzt) zeigte im Attribut-Panel „Nach Bauteil" als + * Default, obwohl „Nach Ebene" der gewünschte Standard ist. Fix: `source` + * fehlend/`"layer"` liefert jetzt die Kategorie, NUR ein explizites + * `"object"` fällt auf das Bauteil zurück (vorher umgekehrt). + */ + +import { describe, it, expect } from "vitest"; +import { + resolveForeground, + resolveBackground, + resolveHatchId, + resolveStrokeWeight, +} from "./shared"; +import type { Component, LayerCategory } from "../../model/types"; + +const comp: Component = { + id: "c1", + name: "Beton", + color: "#888888", + foreground: "#111111", + background: "#222222", + hatchId: "sia-concrete", + joinPriority: 10, +}; + +const category: LayerCategory = { + code: "20", + name: "Wände", + color: "#0a0a0a", + lw: 0.5, + visible: true, + locked: false, + hatch: "sia-wall", +}; + +describe("resolveForeground/resolveBackground — Default fehlender Source ist jetzt 'Nach Ebene'", () => { + it("kein Override, Source fehlt: liefert die Kategorie-Farbe (nicht mehr die Bauteilfarbe)", () => { + expect(resolveForeground(comp, undefined, category, undefined)).toBe(category.color); + expect(resolveBackground(comp, undefined, category, undefined)).toBe(category.color); + }); + + it("kein Override, Source explizit 'layer': unverändert die Kategorie-Farbe", () => { + expect(resolveForeground(comp, undefined, category, "layer")).toBe(category.color); + expect(resolveBackground(comp, undefined, category, "layer")).toBe(category.color); + }); + + it("kein Override, Source explizit 'object': fällt weiterhin auf die Bauteilfarbe zurück", () => { + expect(resolveForeground(comp, undefined, category, "object")).toBe(comp.foreground); + expect(resolveBackground(comp, undefined, category, "object")).toBe(comp.background); + }); + + it("ein gesetzter Override gewinnt immer, unabhängig von der Source", () => { + expect(resolveForeground(comp, "#ff00ff", category, "object")).toBe("#ff00ff"); + expect(resolveForeground(comp, "#ff00ff", category, undefined)).toBe("#ff00ff"); + }); + + it("ohne Kategorie fällt 'Nach Ebene' auf die Bauteilfarbe zurück (kein Crash)", () => { + expect(resolveForeground(comp, undefined, undefined, undefined)).toBe(comp.foreground); + }); +}); + +describe("resolveHatchId — Default fehlender Source ist jetzt 'Nach Ebene'", () => { + it("kein Override, Source fehlt: liefert die Kategorie-Schraffur", () => { + expect(resolveHatchId(undefined, undefined, category, comp.hatchId)).toBe(category.hatch); + }); + it("kein Override, Source explizit 'object': liefert den Fallback (Bauteil-Schraffur)", () => { + expect(resolveHatchId(undefined, "object", category, comp.hatchId)).toBe(comp.hatchId); + }); +}); + +describe("resolveStrokeWeight — Default fehlender Source ist jetzt 'Nach Ebene'", () => { + it("kein Override, Source fehlt: liefert die Kategorie-Strichstärke", () => { + expect(resolveStrokeWeight(undefined, undefined, category, 0.13)).toBe(category.lw); + }); + it("kein Override, Source explizit 'object': liefert den Fallback", () => { + expect(resolveStrokeWeight(undefined, "object", category, 0.13)).toBe(0.13); + }); +}); diff --git a/src/plan/generatePlan/shared.ts b/src/plan/generatePlan/shared.ts index 4e905e4..1f51344 100644 --- a/src/plan/generatePlan/shared.ts +++ b/src/plan/generatePlan/shared.ts @@ -419,12 +419,12 @@ export function resolveHatch( * Kollabiert die Vordergrund-Farbkette (Muster-/Schraffurlinienfarbe) eines * 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`) ?? + * (Attribut-Override `override`) ?? (`source!=="object"` ⇒ `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). + * `source` fehlt/`"layer"` ⇒ „Nach Ebene" (DEFAULT — ein neu erzeugtes Element + * ohne explizites Source-Feld folgt der Ebene, nicht dem Bauteil; „Nach + * Bauteil" muss aktiv gewählt werden). Nur ein EXPLIZITES `"object"` fällt auf + * die Bauteil-Kette zurück. */ export function resolveForeground( comp: Component, @@ -433,14 +433,14 @@ export function resolveForeground( source?: AttributeSource, ): string | undefined { if (override != null) return override; - if (source === "layer") return category?.color ?? comp.foreground; + if (source !== "object") return category?.color ?? comp.foreground; return comp.foreground; } /** * Kollabiert die Hintergrund-/Füllfarbkette (Poché-Override) eines Bauteils zu * einem einzelnen Wert, exakt analog zu {@link resolveForeground}: - * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.color`) ?? + * (Attribut-Override `override`) ?? (`source!=="object"` ⇒ `category.color`) ?? * (`Component.background`) ?? `undefined`. * `undefined` als Endergebnis heißt „kein Override gesetzt" — Aufrufer fallen * dann auf ihren eigenen Default zurück (bei Wand/Decke die neutrale SIA- @@ -455,7 +455,7 @@ export function resolveBackground( source?: AttributeSource, ): string | undefined { if (override != null) return override; - if (source === "layer") return category?.color ?? comp.background; + if (source !== "object") return category?.color ?? comp.background; return comp.background; } @@ -463,10 +463,10 @@ export function resolveBackground( * 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`) ?? + * (Attribut-Override `override`) ?? (`source!=="object"` ⇒ `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). + * `source` fehlt/`"layer"` ⇒ „Nach Ebene" (DEFAULT, s. {@link resolveForeground}). * Ergebnis kann `undefined` sein — der Aufrufer fällt dann auf `NO_HATCH` zurück. */ export function resolveHatchId( @@ -476,17 +476,17 @@ export function resolveHatchId( fallback?: string, ): string | undefined { if (override != null) return override; - if (source === "layer") return category?.hatch ?? fallback; + if (source !== "object") 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`) ?? + * (Attribut-Override `override`) ?? (`source!=="object"` ⇒ `category.lw`) ?? * `fallback` (bisherige Bauteil-/LineStyle-/Kategorie-Default-Kette des * Aufrufers — heutiges Verhalten). - * `source` fehlt/`"object"` ⇒ „Nach Bauteil" (heutiges Verhalten, unverändert). + * `source` fehlt/`"layer"` ⇒ „Nach Ebene" (DEFAULT, s. {@link resolveForeground}). */ export function resolveStrokeWeight( override: number | undefined, @@ -495,7 +495,7 @@ export function resolveStrokeWeight( fallback: number, ): number { if (override != null) return override; - if (source === "layer") return category?.lw ?? fallback; + if (source !== "object") return category?.lw ?? fallback; return fallback; } diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index b5ccb2f..a0b1adb 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -43,6 +43,7 @@ import type { WallReferenceLine, } from "../model/types"; import { localToLv95 } from "../io/lv95"; +import { resolveForeground, resolveBackground, resolveHatchId } from "../plan/generatePlan/shared"; import { columnVerticalExtent } from "../model/wall"; import { columnFootprint } from "../geometry/column"; import { evaluateRoom, polygonArea } from "../geometry/roomArea"; @@ -413,6 +414,18 @@ export interface Selection { strokeWeightSource?: AttributeSource; /** Quelle der Schraffur, wenn kein `fillHatchId` gesetzt ist. */ hatchSource?: AttributeSource; + /** + * EFFEKTIV aufgelöster Vordergrund (Override ?? Ebene ?? Bauteil, s. + * `resolveForeground` — dieselbe Kette wie beim Rendern), für die IMMER + * sichtbare Vorschau im Attribut-Panel (Nutzer-Wunsch: „man sollte immer + * sehen welche Farbe [gilt]"), unabhängig von der aktuellen Quelle. + * `undefined` = kein Wert an keiner Stelle der Kette (echtes „Nach System"). + */ + effectiveForeground?: string; + /** Effektiv aufgelöster Hintergrund, analog zu {@link Selection.effectiveForeground}. */ + effectiveBackground?: string; + /** Effektiv aufgelöste Schraffur-ID, analog zu {@link Selection.effectiveForeground}. */ + effectiveHatchId?: string | null; /** Ob die Form geschlossen ist (rect/circle/closed polyline) — nur Drawing2D. */ closed?: boolean; /** @@ -533,6 +546,26 @@ function wallSelection(project: Project, wall: Wall): Selection { // Wand-Attribute auflösen (Referenzlinie, Aufbau, Referenzgeschoss, UK/OK). const wt = getWallType(project, wall); const thickness = wallTypeThickness(wt); + // Effektive Vordergrund-/Hintergrund-/Schraffur-Vorschau fürs Attribut-Panel + // (Nutzer-Wunsch: immer den TATSÄCHLICHEN Wert sehen, nicht nur ein leeres + // Override-Feld) — dieselbe Resolve-Kette wie beim Rendern (resolveForeground + // & Co.), als Bauteil-Repräsentant die ÄUSSERSTE Schicht (wie `abbrevOf` + // unten). Bei mehrschichtigem Aufbau nur eine Annäherung (jede Schicht kann + // im Schnitt ihre EIGENE Bauteilfarbe zeigen) — für die Panel-Vorschau + // ausreichend, der Override selbst wirkt ohnehin einheitlich auf alle Schichten. + const outerComponent = getComponent(project, wt.layers[0]?.componentId); + const effectiveForeground = outerComponent + ? resolveForeground(outerComponent, wall.foreground, cat, wall.foregroundSource) + : wall.foreground; + const effectiveBackground = outerComponent + ? resolveBackground(outerComponent, wall.background, cat, wall.backgroundSource) + : wall.background; + const effectiveHatchId = resolveHatchId( + wall.hatchId, + wall.hatchSource, + cat, + outerComponent?.hatchId, + ); const floor = project.drawingLevels.find((z) => z.id === wall.floorId); const { zBottom, zTop } = wallVerticalExtent(project, wall); const above = nextFloorAbove(project, wall); @@ -606,6 +639,9 @@ function wallSelection(project: Project, wall: Wall): Selection { background: wall.background, foregroundSource: wall.foregroundSource, backgroundSource: wall.backgroundSource, + effectiveForeground, + effectiveBackground, + effectiveHatchId, strokeWeightOverride: wall.strokeWeight, strokeWeightSource: wall.strokeWeightSource, hatchSource: wall.hatchSource, @@ -623,6 +659,21 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection { const box = outlineBBox(ceiling.outline); const wt = getCeilingType(project, ceiling); const thickness = ceilingThickness(project, ceiling); + // Effektive Vordergrund-/Hintergrund-/Schraffur-Vorschau — s. Kommentar in + // wallSelection (dieselbe Kette, äusserste Schicht als Bauteil-Repräsentant). + const outerComponent = getComponent(project, wt.layers[0]?.componentId); + const effectiveForeground = outerComponent + ? resolveForeground(outerComponent, ceiling.foreground, cat, ceiling.foregroundSource) + : ceiling.foreground; + const effectiveBackground = outerComponent + ? resolveBackground(outerComponent, ceiling.background, cat, ceiling.backgroundSource) + : ceiling.background; + const effectiveHatchId = resolveHatchId( + ceiling.hatchId, + ceiling.hatchSource, + cat, + outerComponent?.hatchId, + ); const floor = project.drawingLevels.find((z) => z.id === ceiling.floorId); const { zBottom, zTop } = ceilingVerticalExtent(project, ceiling); const ceilingInfo: CeilingInfo = { @@ -666,6 +717,9 @@ function ceilingSelection(project: Project, ceiling: Ceiling): Selection { background: ceiling.background, foregroundSource: ceiling.foregroundSource, backgroundSource: ceiling.backgroundSource, + effectiveForeground, + effectiveBackground, + effectiveHatchId, strokeWeightOverride: ceiling.strokeWeight, strokeWeightSource: ceiling.strokeWeightSource, hatchSource: ceiling.hatchSource, @@ -998,15 +1052,32 @@ function drawingSelection(project: Project, d: Drawing2D): Selection { const cat = findCategory(project.layers, d.categoryCode); const color = d.color ?? ls?.color ?? cat?.color ?? DRAW_FALLBACK_COLOR; // Effektive Strichstärke, dieselbe Kette wie generatePlan/addDrawing2D - // (resolveStrokeWeight): Override zuerst, "layer" erzwingt die Kategorie - // (umgeht das LineStyle-Gewicht), sonst die heutige LineStyle-/Kategorie-Kette. + // (resolveStrokeWeight): Override zuerst, Source ≠ "object" (also "layer" + // ODER fehlend, s. AttributeSource-Default) erzwingt die Kategorie (umgeht + // das LineStyle-Gewicht), sonst die LineStyle-/Kategorie-Kette. const weightMm = d.weightMm ?? - (d.strokeWeightSource === "layer" ? cat?.lw : undefined) ?? + (d.strokeWeightSource !== "object" ? cat?.lw : undefined) ?? ls?.weight ?? cat?.lw ?? WALL_FALLBACK_MM; const closed = isClosedGeom(d.geom); + // Effektive Vordergrund-/Hintergrund-/Schraffur-Vorschau — s. Kommentar in + // wallSelection. Ein Drawing2D hat KEIN eigenes Bauteil (daher auch keine + // „Nach Bauteil"-Option im Panel, s. AttributesPanel.tsx) — als Platzhalter + // ein leeres Bauteil, dessen foreground/background/hatchId immer fehlen, + // sodass die Kette bei einem (aus Alt-Daten) expliziten `source:"object"` + // korrekt auf „kein Wert" statt einer erfundenen Bauteilfarbe fällt. + const noComponent = { id: "", name: "", color, hatchId: "", joinPriority: 0 }; + const effectiveForeground = closed + ? resolveForeground(noComponent, d.foreground, cat, d.foregroundSource) + : undefined; + const effectiveBackground = closed + ? resolveBackground(noComponent, d.background, cat, d.backgroundSource) + : undefined; + const effectiveHatchId = closed + ? resolveHatchId(d.hatchId, d.hatchSource, cat, undefined) + : null; return { kind: "drawing2d", id: d.id, @@ -1020,6 +1091,9 @@ function drawingSelection(project: Project, d: Drawing2D): Selection { background: closed ? d.background : undefined, foregroundSource: closed ? d.foregroundSource : undefined, backgroundSource: closed ? d.backgroundSource : undefined, + effectiveForeground, + effectiveBackground, + effectiveHatchId, strokeWeightOverride: d.weightMm, strokeWeightSource: d.strokeWeightSource, hatchSource: closed ? d.hatchSource : undefined,