Korrektur: Wand/Decke bleiben bei "Nach Bauteil" als Default, nur

Drawing2D wechselt auf "Nach Ebene"

Nutzer-Korrektur zum vorigen Commit: "eine Wand usw soll weiterhin
nach Bauteil haben und die weisser Grund und schwarzer Vordergrund
haben. Also nach Bauteil. 2D Elemente haben aber bei Attribute kein
nach Bauteil!!!" -- der vorige Commit hatte den Default global (auch
für Wand/Decke) auf "Nach Ebene" umgestellt, was die neutrale SIA-
Poché-Konvention (weisser Grund/schwarzer Vordergrund über die
Bauteil-Kette) durch die rohe Ebenenfarbe ersetzt hätte.

resolveForeground/resolveBackground/resolveHatchId/resolveStrokeWeight
(plan/generatePlan/shared.ts) sind zurückgesetzt auf ihr ursprüngliches
Verhalten: `source === "layer"` (fehlend/"object" ⇒ weiterhin Bauteil-
Kette, DEFAULT bei Wand/Decke). Der elementart-abhängige Default sitzt
jetzt an den AUFRUFERN statt im generischen Resolver:
  • Wand/Decke (selectionInfo.ts): rohes Source-Feld unverändert
    durchgereicht -- Default bleibt "Nach Bauteil".
  • Drawing2D (selectionInfo.ts drawingSelection): `d.foregroundSource
    ?? "layer"` usw. VOR dem Resolver -- Default wird dort explizit
    "Nach Ebene" (kein eigenes Bauteil, "Nach Bauteil" bietet das Panel
    für 2D-Elemente ohnehin nicht mehr an, s. vorletzter Commit).
  • AttributesPanel.tsx uiSourceOf() bekommt einen isDrawing-Parameter
    für denselben elementart-abhängigen Default in der Dropdown-
    Anzeige.

+Tests in shared.resolve.test.ts auf die jetzt korrekten Erwartungen
umgeschrieben (Default bleibt Bauteil, explizites "layer" liefert die
Kategorie, Drawing2D-Aufrufer-Mapping separat geprüft). tsc/vitest
934/934 grün.
This commit is contained in:
2026-08-22 01:05:20 +02:00
parent bfc064796f
commit 85be7173e1
5 changed files with 103 additions and 64 deletions
+11 -5
View File
@@ -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`).
*/
+25 -11
View File
@@ -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
+38 -29
View File
@@ -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,
);
});
});
+22 -15
View File
@@ -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;
}
+7 -4
View File
@@ -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",