// Tab „Overrides" (grafische Overrides, Regel-Engine A1) — Master-Detail wie // Linien/Wandstile: Liste links = Regeln in PRIORITÄTS-Reihenfolge (oben // gewinnt pro Aktions-Feld) mit Aktiv-Checkbox, Auf/Ab und Löschen; Detail // rechts = Bedingung (Kriterium/Operator/Wert) + optionale Aktionen (Farbe/ // Strichstärke/Linienstil). Reines Rendering-Overlay — die Auswertung liegt in // src/overrides/engine.ts, der Einhängepunkt in plan/generatePlan.ts; // Elementdaten bleiben unverändert. import { useState } from "react"; import type { OverrideConditionType, OverrideOperator, OverrideRule, Project } from "../../model/types"; import { PEN_WEIGHTS } from "../../model/types"; import { t } from "../../i18n"; import { ColorField, DeleteButton, FieldRow, NumberField, SelectField, TextField } from "./fields"; /** Bedingungs-Typen (UI-Beschriftung übersetzt, Werte englisch). */ const OVERRIDE_TYPE_OPTIONS: { value: OverrideConditionType; labelKey: string }[] = [ { value: "layer_name", labelKey: "resources.overrides.condType.layer" }, { value: "object_name", labelKey: "resources.overrides.condType.object" }, ]; /** Vergleichs-Operatoren (UI-Beschriftung übersetzt, Werte englisch). */ const OVERRIDE_OP_OPTIONS: { value: OverrideOperator; labelKey: string }[] = [ { value: "equals", labelKey: "resources.overrides.op.equals" }, { value: "contains", labelKey: "resources.overrides.op.contains" }, { value: "starts_with", labelKey: "resources.overrides.op.startsWith" }, { value: "not_equals", labelKey: "resources.overrides.op.notEquals" }, ]; /** Kompakte Bedingungs-Zusammenfassung für die Listenzeile. */ function overrideConditionSummary(rule: OverrideRule): string { const type = OVERRIDE_TYPE_OPTIONS.find((o) => o.value === rule.condition.type); const op = OVERRIDE_OP_OPTIONS.find((o) => o.value === rule.condition.operator); return `${type ? t(type.labelKey) : rule.condition.type} ${ op ? t(op.labelKey) : rule.condition.operator } „${rule.condition.value}"`; } /** Optionale Aktion: Checkbox (an/aus) + Steuerelement, solange sie an ist. */ function OptionalAction({ label, active, onToggle, children, }: { label: string; active: boolean; onToggle: (on: boolean) => void; children?: React.ReactNode; }) { return (
{active ? children : –}
); } /** Detail-Panel EINER Override-Regel (rechte Seite des Master-Detail-Layouts). */ function OverrideRuleDetail({ rule, project, onPatch, onDelete, }: { rule: OverrideRule; project: Project; onPatch: (patch: Partial) => void; onDelete: () => void; }) { const patchCondition = (patch: Partial) => onPatch({ condition: { ...rule.condition, ...patch } }); const patchActions = (patch: Partial) => onPatch({ actions: { ...rule.actions, ...patch } }); // Sentinel „—": kein Linienstil-Override. const NONE = "__none__"; const lineOptions = [ { value: NONE, label: t("resources.overrides.linetype.none") }, ...project.lineStyles.map((l) => ({ value: l.id, label: l.name })), ]; return (
onPatch({ name: e.target.value })} />
{t("resources.overrides.condition")}
patchCondition({ type })} options={OVERRIDE_TYPE_OPTIONS.map((o) => ({ value: o.value, label: t(o.labelKey), }))} /> patchCondition({ operator })} options={OVERRIDE_OP_OPTIONS.map((o) => ({ value: o.value, label: t(o.labelKey), }))} /> patchCondition({ value })} />
{t("resources.overrides.actions")}
{/* Vorschlagswerte für die Strichstärke (Standard-Stiftbreiten in mm). */} {PEN_WEIGHTS.map((w) => ( patchActions({ color: on ? "#808080" : undefined })} > patchActions({ color })} /> patchActions({ lineweight: on ? 0.35 : undefined })} > patchActions({ lineweight: Math.max(0, lineweight) })} step={0.05} min={0} list="ov-pen-weights" /> patchActions({ linetypeId: on ? project.lineStyles[0]?.id : undefined, }) } > patchActions({ linetypeId: v === NONE ? undefined : v }) } options={lineOptions} />
); } /** Tab „Overrides": Regelliste (Priorität von oben nach unten) + Detail. */ export function OverridesTab({ project, onSetOverrideRules, }: { project: Project; onSetOverrideRules?: (rules: OverrideRule[]) => void; }) { const rules = project.overrideRules ?? []; const [selectedId, setSelectedId] = useState(null); const selected = rules.find((r) => r.id === selectedId) ?? rules[0]; const setRules = (next: OverrideRule[]) => onSetOverrideRules?.(next); const patchRule = (id: string, patch: Partial) => setRules(rules.map((r) => (r.id === id ? { ...r, ...patch } : r))); const addRule = () => { const rule: OverrideRule = { id: `ov-${Date.now()}`, name: t("resources.overrides.defaultName"), enabled: true, condition: { type: "layer_name", operator: "contains", value: "" }, actions: {}, }; setRules([...rules, rule]); setSelectedId(rule.id); }; const deleteRule = (id: string) => setRules(rules.filter((r) => r.id !== id)); const moveRule = (id: string, dir: -1 | 1) => { const i = rules.findIndex((r) => r.id === id); const j = i + dir; if (i < 0 || j < 0 || j >= rules.length) return; const next = rules.slice(); [next[i], next[j]] = [next[j], next[i]]; setRules(next); }; return (
{t("resources.overrides.hint")}
{rules.map((r) => (
setSelectedId(r.id)} onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") setSelectedId(r.id); }} > e.stopPropagation()} onChange={(e) => patchRule(r.id, { enabled: e.target.checked })} /> {r.name || t("resources.overrides.placeholder")} {overrideConditionSummary(r)}
))} {rules.length === 0 && (
{t("resources.overrides.empty")}
)}
{selected ? ( patchRule(selected.id, patch)} onDelete={() => deleteRule(selected.id)} /> ) : (
{t("resources.overrides.empty")}
)}
); }