// Inhalts-Panel „Attribute" — die Attribut-Palette (Vectorworks-Stil) für die // aktuelle Auswahl. Zeigt die EFFEKTIV aufgelösten Stift-/Füll-Eigenschaften des // 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 // echten Setter (Linienstil) werden read-only/informativ gezeigt; Felder, die // das Modell gar nicht trägt (Deckkraft, Caps, Schlagschatten), werden bewusst // weggelassen, um die Palette ehrlich und schlank zu halten. // // DOSSIER-Tabellen-Look: KEINE wiederholten Feld-Labels pro Zeile — ein kompaktes // 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"; import { Dropdown } from "../ui/Dropdown"; import { ColorHexField } from "../ui/ColorHexField"; /** 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; const { project } = host; // Wandtyp-/Deckentyp-Wahl für das NEUE Bauteil (was gezeichnet wird) — früher // in der Werkzeug-Sidebar (ToolsPanel), seit deren Entfernung hier. Erscheint // nur bei aktivem Wand-/Decken-Werkzeug (unabhängig von einer Auswahl); Wand und // Decke teilen sich denselben Aufbau-Typ (WallType) + Host-State. const drawingComponent = host.activeTool === "wall" || host.activeTool === "ceiling"; const typeSection = drawingComponent ? ( <>
{t("attr.newComponent")}
{t(host.activeTool === "wall" ? "tool.wallType" : "tool.ceilingType")} host.onActiveWallTypeId(v)} options={project.wallTypes.map((wt) => ({ value: wt.id, label: wt.name }))} />
) : null; // Leerzustand: der Typ-Picker (falls ein Bauteil-Werkzeug aktiv ist), sonst ein // kompakter Hinweis. if (sel === null) { return (
{typeSection} {!typeSection &&
{t("attr.empty")}
}
); } const isDrawing = sel.kind === "drawing2d"; // 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 = sel.kind === "wall" || 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). const sourceSelect = ( editable: boolean, ui: UiSource, onChange: (next: UiSource) => void, ) => ( onChange(v as UiSource)} options={[ { value: "layer", label: t("attr.source.layer") }, { 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) => ( ); // 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, ) => ( <> {t(labelKey)} {ui === "custom" && valueEditor} {sourceSelect(editable, ui, onSource)} ); // 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 ? project.drawings2d.find((d) => d.id === sel.id) : undefined; const lineStyle = drawing?.lineStyleId ? project.lineStyles.find((l) => l.id === drawing.lineStyleId) : undefined; const bboxW = sel.bbox.maxX - sel.bbox.minX; const bboxH = sel.bbox.maxY - sel.bbox.minY; return (
{/* Typ-Picker für das neue Bauteil (nur bei aktivem Wand-/Decken-Werkzeug). */} {typeSection} {/* EIN durchgehendes Grid über alle Sektionen — dieselben zwei Spalten (Label | Wert) für Stift/Füllung/Maße, damit die Wertfelder überall bündig in EINER Spalte stehen. Sektion-Balken spannen beide Spalten. */}
{/* ── Stift/Strich ──────────────────────────────────────────── */}
{t("attr.stroke")}
{/* Farbe — gilt für Wand UND Drawing2D (Kontrakt setzt beide). */} {t("attr.color")} {/* 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, <> { const v = Number(e.target.value); if (Number.isFinite(v)) host.onSetSelectionWeight(v); }} /> {PEN_WEIGHTS.map((w) => ( , )} {/* Linienstil — nur Drawing2D trägt ihn (Wand/Decke/etc. read-only). */} {t("attr.lineStyle")} {isDrawing ? ( host.onSetSelectionLineStyle(v === "__none__" ? undefined : v) } options={[ { value: "__none__", label: t("attr.lineStyle.none") }, ...project.lineStyles.map((l) => ({ value: l.id, label: l.name })), ]} /> ) : ( {lineStyle ? lineStyle.name : "—"} )} {/* ── Füllung (Vordergrund/Hintergrund + Schraffur) ─────────── */} {/* Vordergrund = Muster-/Schraffurfarbe, Hintergrund = Füllfarbe/ Poché. Jedes Feld: Nach Ebene (LayerCategory erzwingt den Wert) / Nach Bauteil (erbt vom Component-Manager, Default) / eigener Wert. */}
{t("attr.fill")}
{sourceRow( "attr.foreground", pocheEditable, fgUi, onForegroundSourceChange, colorValue(sel.foreground, host.onSetSelectionForeground), )} {sourceRow( "attr.background", pocheEditable, bgUi, onBackgroundSourceChange, colorValue(sel.background, host.onSetSelectionBackground), )} {sourceRow( "attr.hatch", fillEditable, hatchUi, onHatchSourceChange, host.onSetSelectionFill(v || null)} options={[ { value: "", label: t("attr.none") }, ...project.hatches.map((h) => ({ value: h.id, label: h.name })), ]} />, )} {/* ── Maße (informativ, aus der bbox) ───────────────────────── */}
{t("attr.size")}
{t("attr.width")} {formatM(bboxW)} {t("attr.height")} {formatM(bboxH)}
); }