diff --git a/src/model/types.ts b/src/model/types.ts index 72bd7e2..7291cf5 100644 --- a/src/model/types.ts +++ b/src/model/types.ts @@ -831,12 +831,18 @@ 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`). 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. + * (`color`/`lw`/`hatch`). * • "object" — „Nach Bauteil": erbt vom Bauteil (Component) bzw. dessen - * bisheriger Fallback-Kette. Muss aktiv im Attribut-Panel gewählt werden - * (nur bei Wand/Decke anwendbar — ein Drawing2D hat kein Bauteil). + * bisheriger Fallback-Kette (bei Wand/Decke u. a. die neutrale SIA-Poché- + * Konvention, s. `resolveBackground`). Das ist auch der DEFAULT bei Wand/ + * Decke, wenn das Source-Feld fehlt (`undefined`) — bewusst NICHT „Nach + * Ebene": eine frisch erzeugte Wand soll die neutrale Bauteil-Konvention + * zeigen (weisser Grund, schwarzer Vordergrund), nicht die rohe + * Ebenenfarbe. + * Ein Drawing2D hat KEIN eigenes Bauteil — „Nach Bauteil" ist dort im + * Attribut-Panel gar nicht erst wählbar, ein fehlendes Source-Feld wird dort + * vom Aufrufer (`selectionInfo.ts`) auf „Nach Ebene" gemappt (die einzig + * sinnvolle Alternative zum expliziten Wert bei einem bauteillosen Element). * Ein gesetzter expliziter Wert (z. B. `foreground`) gewinnt IMMER, unabhängig * von der Source (s. Resolve-Reihenfolge in `plan/generatePlan/shared.ts`). */ diff --git a/src/panels/AttributesPanel.tsx b/src/panels/AttributesPanel.tsx index 743d4af..3e240b8 100644 --- a/src/panels/AttributesPanel.tsx +++ b/src/panels/AttributesPanel.tsx @@ -50,11 +50,19 @@ import { * 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 Ebene", Default — - * s. AttributeSource in model/types.ts). */ -function uiSourceOf(hasValue: boolean, raw: AttributeSource | undefined): UiSource { - return hasValue ? "custom" : raw ?? "layer"; +/** + * Leitet den UI-Quellen-Zustand ab: ein gesetzter Wert gewinnt immer („eigener + * Wert"), sonst das rohe Source-Feld — dessen DEFAULT unterscheidet sich nach + * Elementart (s. AttributeSource in model/types.ts): Wand/Decke „Nach Bauteil" + * (neutrale SIA-Poché), Drawing2D „Nach Ebene" (kein eigenes Bauteil, „Nach + * Bauteil" wird dort im Dropdown gar nicht erst angeboten). + */ +function uiSourceOf( + hasValue: boolean, + raw: AttributeSource | undefined, + isDrawing: boolean, +): UiSource { + return hasValue ? "custom" : raw ?? (isDrawing ? "layer" : "object"); } export function AttributesPanel() { @@ -191,15 +199,21 @@ export function AttributesPanel() { ); - // 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); + // Vordergrund/Hintergrund: „eigener Wert" ⇒ auf den elementart-abhängigen + // Default zurückfallen (Wand/Decke „Nach Bauteil", Drawing2D „Nach Ebene", + // s. uiSourceOf); „Nach Ebene"/„Nach Bauteil" schreiben die Quelle direkt + // (der Host löscht dabei den expliziten Wert). + const fgUi = uiSourceOf(sel.foreground !== undefined, sel.foregroundSource, isDrawing); + const bgUi = uiSourceOf(sel.background !== undefined, sel.backgroundSource, isDrawing); + const weightUi = uiSourceOf( + sel.strokeWeightOverride !== undefined, + sel.strokeWeightSource, + isDrawing, + ); const hatchUi = uiSourceOf( sel.fillHatchId !== undefined && sel.fillHatchId !== null, sel.hatchSource, + isDrawing, ); // Wechsel auf „eigener Wert" per Dropdown (nicht durchs Editieren des Felds diff --git a/src/plan/generatePlan/shared.resolve.test.ts b/src/plan/generatePlan/shared.resolve.test.ts index 3c4e193..1ed6ad3 100644 --- a/src/plan/generatePlan/shared.resolve.test.ts +++ b/src/plan/generatePlan/shared.resolve.test.ts @@ -1,10 +1,13 @@ /** * `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). + * — By-Layer/By-Object-Auflösung. Der Resolver selbst bleibt „Nach Bauteil" + * (Component) als Default, wenn `source` fehlt/`"object"` ist — das liefert + * bei Wand/Decke die neutrale SIA-Poché-Konvention (weisser Grund, schwarzer + * Vordergrund), NICHT die rohe Ebenenfarbe (Nutzer-Bestätigung: „eine Wand + * usw soll weiterhin nach Bauteil haben"). Ein Drawing2D hat kein eigenes + * Bauteil und bietet „Nach Bauteil" im Attribut-Panel gar nicht an — DORT + * mappt der Aufrufer (`selectionInfo.ts`) ein fehlendes Source-Feld explizit + * auf `"layer"`, bevor er diese Resolver aufruft (separat unten geprüft). */ import { describe, it, expect } from "vitest"; @@ -36,46 +39,52 @@ const category: LayerCategory = { 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); +describe("resolveForeground/resolveBackground — Default bleibt 'Nach Bauteil'", () => { + it("kein Override, Source fehlt: liefert weiterhin die Bauteilfarbe (Default unverändert)", () => { + expect(resolveForeground(comp, undefined, category, undefined)).toBe(comp.foreground); + expect(resolveBackground(comp, undefined, category, undefined)).toBe(comp.background); }); - 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", () => { + it("kein Override, Source explizit 'object': unverändert die Bauteilfarbe", () => { expect(resolveForeground(comp, undefined, category, "object")).toBe(comp.foreground); expect(resolveBackground(comp, undefined, category, "object")).toBe(comp.background); }); + it("kein Override, Source explizit 'layer': liefert die Kategorie-Farbe", () => { + expect(resolveForeground(comp, undefined, category, "layer")).toBe(category.color); + expect(resolveBackground(comp, undefined, category, "layer")).toBe(category.color); + }); + it("ein gesetzter Override gewinnt immer, unabhängig von der Source", () => { - expect(resolveForeground(comp, "#ff00ff", category, "object")).toBe("#ff00ff"); + expect(resolveForeground(comp, "#ff00ff", category, "layer")).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 bleibt 'Nach Bauteil'", () => { + it("kein Override, Source fehlt: liefert den Fallback (Bauteil-Schraffur)", () => { + expect(resolveHatchId(undefined, undefined, category, comp.hatchId)).toBe(comp.hatchId); + }); + it("kein Override, Source explizit 'layer': liefert die Kategorie-Schraffur", () => { + expect(resolveHatchId(undefined, "layer", category, comp.hatchId)).toBe(category.hatch); }); }); -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); +describe("resolveStrokeWeight — Default bleibt 'Nach Bauteil'", () => { + it("kein Override, Source fehlt: liefert den Fallback", () => { + expect(resolveStrokeWeight(undefined, undefined, category, 0.13)).toBe(0.13); }); - it("kein Override, Source explizit 'object': liefert den Fallback (Bauteil-Schraffur)", () => { - expect(resolveHatchId(undefined, "object", category, comp.hatchId)).toBe(comp.hatchId); + it("kein Override, Source explizit 'layer': liefert die Kategorie-Strichstärke", () => { + expect(resolveStrokeWeight(undefined, "layer", category, 0.13)).toBe(category.lw); }); }); -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); +describe("Drawing2D-Panel-Vorschau (selectionInfo.ts) — mappt fehlende Source explizit auf 'layer'", () => { + it("simuliert das Aufrufer-Mapping `d.foregroundSource ?? \"layer\"`: liefert die Kategorie-Farbe für ein Drawing2D ohne Bauteil", () => { + const noComponent: Component = { id: "", name: "", color: "#000", hatchId: "", joinPriority: 0 }; + const drawingSource: "layer" | undefined = undefined; // wie ein frisches Drawing2D + expect(resolveForeground(noComponent, undefined, category, drawingSource ?? "layer")).toBe( + category.color, + ); }); }); diff --git a/src/plan/generatePlan/shared.ts b/src/plan/generatePlan/shared.ts index 1f51344..92c9824 100644 --- a/src/plan/generatePlan/shared.ts +++ b/src/plan/generatePlan/shared.ts @@ -419,12 +419,15 @@ 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!=="object"` ⇒ `category.color`) ?? + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.color`) ?? * (`Component.foreground`) ?? `undefined`. - * `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. + * `source` fehlt/`"object"` ⇒ „Nach Bauteil" — bei WAND/DECKE bewusst der + * DEFAULT (die Bauteil-Kette liefert die neutrale SIA-Poché, s. + * {@link resolveBackground}). Ein Drawing2D hat KEIN eigenes Bauteil und + * bietet „Nach Bauteil" im Attribut-Panel gar nicht erst an — dort wird ein + * fehlendes Source-Feld vom AUFRUFER (`selectionInfo.ts`) explizit auf + * `"layer"` gemappt, bevor er hier ankommt (dieser Resolver selbst bleibt + * Bauteil-Default, unabhängig von der Elementart). */ export function resolveForeground( comp: Component, @@ -433,20 +436,22 @@ export function resolveForeground( source?: AttributeSource, ): string | undefined { if (override != null) return override; - if (source !== "object") return category?.color ?? comp.foreground; + if (source === "layer") 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!=="object"` ⇒ `category.color`) ?? + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `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- * Poché-Farbe HATCH_INK/HATCH_PAPER, NICHT `Component.color` — die reale * Bauteilfarbe soll die Schnitt-Konvention nicht überschreiben, nur ein - * bewusster Override). + * bewusster Override). Genau DIESE neutrale Poché ist bei Wand/Decke der + * gewünschte Default („weisser Grund, schwarzer Vordergrund") — deshalb + * bleibt „Nach Bauteil" dort der Default, nicht „Nach Ebene". */ export function resolveBackground( comp: Component, @@ -455,7 +460,7 @@ export function resolveBackground( source?: AttributeSource, ): string | undefined { if (override != null) return override; - if (source !== "object") return category?.color ?? comp.background; + if (source === "layer") return category?.color ?? comp.background; return comp.background; } @@ -463,10 +468,11 @@ 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!=="object"` ⇒ `category.hatch`) ?? + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.hatch`) ?? * `fallback` (Bauteil-Schraffur, z. B. `Component.hatchId`/`viewHatchId`, oder * `undefined` bei Drawing2D ohne Bauteil-Bezug). - * `source` fehlt/`"layer"` ⇒ „Nach Ebene" (DEFAULT, s. {@link resolveForeground}). + * `source` fehlt/`"object"` ⇒ „Nach Bauteil" (DEFAULT bei Wand/Decke, s. + * {@link resolveForeground}). * Ergebnis kann `undefined` sein — der Aufrufer fällt dann auf `NO_HATCH` zurück. */ export function resolveHatchId( @@ -476,17 +482,18 @@ export function resolveHatchId( fallback?: string, ): string | undefined { if (override != null) return override; - if (source !== "object") return category?.hatch ?? fallback; + 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!=="object"` ⇒ `category.lw`) ?? + * (Attribut-Override `override`) ?? (`source==="layer"` ⇒ `category.lw`) ?? * `fallback` (bisherige Bauteil-/LineStyle-/Kategorie-Default-Kette des * Aufrufers — heutiges Verhalten). - * `source` fehlt/`"layer"` ⇒ „Nach Ebene" (DEFAULT, s. {@link resolveForeground}). + * `source` fehlt/`"object"` ⇒ „Nach Bauteil" (DEFAULT bei Wand/Decke, s. + * {@link resolveForeground}). */ export function resolveStrokeWeight( override: number | undefined, @@ -495,7 +502,7 @@ export function resolveStrokeWeight( fallback: number, ): number { if (override != null) return override; - if (source !== "object") return category?.lw ?? fallback; + if (source === "layer") return category?.lw ?? fallback; return fallback; } diff --git a/src/state/selectionInfo.ts b/src/state/selectionInfo.ts index a0b1adb..adbabbd 100644 --- a/src/state/selectionInfo.ts +++ b/src/state/selectionInfo.ts @@ -1067,16 +1067,19 @@ function drawingSelection(project: Project, d: Drawing2D): Selection { // „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. + // korrekt auf „kein Wert" statt einer erfundenen Bauteilfarbe fällt. Anders + // als bei Wand/Decke (Default „Nach Bauteil") gilt bei Drawing2D „Nach + // Ebene" als Default — daher hier explizit `?? "layer"` VOR dem Resolver + // (der selbst weiterhin generisch „Nach Bauteil" als Default kennt). const noComponent = { id: "", name: "", color, hatchId: "", joinPriority: 0 }; const effectiveForeground = closed - ? resolveForeground(noComponent, d.foreground, cat, d.foregroundSource) + ? resolveForeground(noComponent, d.foreground, cat, d.foregroundSource ?? "layer") : undefined; const effectiveBackground = closed - ? resolveBackground(noComponent, d.background, cat, d.backgroundSource) + ? resolveBackground(noComponent, d.background, cat, d.backgroundSource ?? "layer") : undefined; const effectiveHatchId = closed - ? resolveHatchId(d.hatchId, d.hatchSource, cat, undefined) + ? resolveHatchId(d.hatchId, d.hatchSource ?? "layer", cat, undefined) : null; return { kind: "drawing2d",