import { useState, useEffect } from 'react' import Icon from './components/Icon' import ContextMenu from './components/ContextMenu' import { onMessage, notifyReady, setOverridesEnabled, addRule, updateRule, deleteRule, reorderRules, duplicateRule, reapplyOverrides, clearOverrideRules, savePreset, loadPreset, deletePreset, saveRuleTemplate, addFromTemplate, deleteRuleTemplate, } from './lib/rhinoBridge' const COND_TYPES = [ { value: 'layer_name', label: 'Layer-Name' }, { value: 'user_string', label: 'UserString' }, { value: 'object_name', label: 'Objekt-Name' }, ] const OPS = [ { value: 'equals', label: '=' }, { value: 'not_equals', label: '≠' }, { value: 'contains', label: 'enthält' }, { value: 'starts_with', label: 'beginnt mit' }, { value: 'ends_with', label: 'endet mit' }, ] const labelXs = { fontSize: 9, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.06em', fontWeight: 600, } // --------------------------------------------------------------------------- function ConditionLeaf({ cond, layers, onChange, onRemove, canRemove }) { const t = cond?.type || 'layer_name' const op = cond?.operator || 'equals' return (
{canRemove && ( )}
{t === 'user_string' && ( onChange({ ...cond, key: e.target.value })} style={{ width: '100%' }} /> )}
{t === 'layer_name' ? ( ) : ( onChange({ ...cond, value: e.target.value })} style={{ flex: 1, minWidth: 0 }} /> )}
) } function ConditionsEditor({ rule, layers, onChange }) { const conds = rule.conditions && rule.conditions.length > 0 ? rule.conditions : (rule.condition ? [rule.condition] : [{ type: 'layer_name', operator: 'equals', value: '' }]) const logic = (rule.conditionsLogic || 'and').toLowerCase() const update = (i, newCond) => { const next = conds.slice() next[i] = newCond onChange({ ...rule, conditions: next, condition: undefined }) } const remove = (i) => { const next = conds.slice() next.splice(i, 1) onChange({ ...rule, conditions: next.length ? next : [{ type: 'layer_name', operator: 'equals', value: '' }], condition: undefined }) } const add = () => { const next = conds.slice() next.push({ type: 'layer_name', operator: 'equals', value: '' }) onChange({ ...rule, conditions: next, condition: undefined }) } const setLogic = (l) => onChange({ ...rule, conditionsLogic: l }) return (
{conds.length > 1 && (
Logik:
)} {conds.map((c, i) => ( update(i, nc)} onRemove={() => remove(i)} canRemove={conds.length > 1} /> ))}
) } function ActionRow({ label, icon, active, onToggle, children }) { return (
{active && (
{children}
)}
) } function ActionsEditor({ actions, linetypes, hatchPatterns, onChange }) { const a = actions || {} const setProp = (key, val) => { const next = { ...a } if (val === '' || val === null || val === undefined) { delete next[key] } else { next[key] = val } onChange(next) } return (
setProp('color', e.target.checked ? (a.color || '#888888') : '')} > setProp('color', e.target.value)} style={{ width: 36, height: 26, padding: 2, flexShrink: 0 }} /> setProp('color', e.target.value)} style={{ flex: 1, minWidth: 0 }} /> setProp('lineweight', e.target.checked ? (a.lineweight ?? 0.25) : '')} > setProp('lineweight', parseFloat(e.target.value) || 0)} style={{ width: 80 }} /> mm setProp('linetype', e.target.checked ? (a.linetype || 'Continuous') : '')} > setProp('hatchPattern', e.target.checked ? (a.hatchPattern || 'Solid') : '')} > setProp('hatchScale', e.target.checked ? (a.hatchScale ?? 1.0) : '')} > setProp('hatchScale', parseFloat(e.target.value) || 1.0)} style={{ width: 80 }} />
Hatch-Override modifiziert nur existierende Schraffuren. Curves ohne Hatch bleiben unverändert.
) } function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatch, onDelete, onDuplicate, onMoveUp, onMoveDown, onContextMenu }) { const [open, setOpen] = useState(false) const summarize = () => { const conds = (rule.conditions && rule.conditions.length > 0) ? rule.conditions : (rule.condition ? [rule.condition] : []) const logic = (rule.conditionsLogic || 'and').toUpperCase() const a = rule.actions || {} const parts = [] const condTexts = conds.map(c => { if (c.type === 'layer_name') return `Layer ${c.operator} "${c.value || '?'}"` if (c.type === 'user_string') return `${c.key || '?'} ${c.operator} "${c.value || '?'}"` if (c.type === 'object_name') return `Name ${c.operator} "${c.value || '?'}"` return '' }).filter(Boolean) if (condTexts.length === 1) parts.push(condTexts[0]) else if (condTexts.length > 1) parts.push(condTexts.join(` ${logic} `)) const acts = Object.keys(a) if (acts.length) parts.push('→ ' + acts.join(', ')) return parts.join(' ') } return (
{ if (onContextMenu) { ev.preventDefault(); onContextMenu(ev) } }} style={{ border: '1px solid var(--border)', borderRadius: 'var(--r-lg)', background: 'var(--bg-section)', padding: 8, marginBottom: 8, opacity: rule.enabled === false ? 0.5 : 1, }}> {/* Row 1: index + checkbox + name + edit-toggle */}
#{index + 1} onPatch({ ...rule, enabled: e.target.checked })} title="Regel aktiv" style={{ flexShrink: 0, width: 'auto', height: 'auto', padding: 0 }} /> onPatch({ ...rule, name: e.target.value })} style={{ flex: 1, minWidth: 0 }} />
{!open && (
{summarize() || '(leer)'}
)} {open && ( <>
Bedingungen
Überschreibungen
onPatch({ ...rule, actions: a })} />
)}
) } // --------------------------------------------------------------------------- export default function OverridesApp() { const [state, setState] = useState({ enabled: false, rules: [], layers: [], linetypes: [], hatchPatterns: [], presets: [], activePreset: null, ruleTemplates: [], }) const [selectedPreset, setSelectedPreset] = useState('') const [selectedTemplate, setSelectedTemplate] = useState('') const [ctxMenu, setCtxMenu] = useState(null) // {x, y, ruleId} useEffect(() => { onMessage('STATE', (s) => { setState((prev) => ({ ...prev, ...s })) // Dropdown synct sich auf das Backend-activePreset nur wenn der wert // gesetzt ist (z.B. nachdem Topbar eine Kombination geladen hat). // Wenn activePreset null wird (Rules wurden gerade editiert -> variant C), // BEHALTEN wir die lokale Auswahl — sonst weiss der Save-Button nicht // mehr in welche Kombination der User gerade editiert. if (s && s.activePreset) { setSelectedPreset(s.activePreset) } }) notifyReady() }, []) const onPatch = (rule) => updateRule(rule.id, rule) const onMove = (id, delta) => { const ids = state.rules.map(r => r.id) const i = ids.indexOf(id) const j = i + delta if (i < 0 || j < 0 || j >= ids.length) return const next = ids.slice() next.splice(j, 0, next.splice(i, 1)[0]) reorderRules(next) } // Kontextmenue fuer eine Regel — analog Ausschnitte/Ebenen. const ruleCtxItems = (ruleId) => { const rule = (state.rules || []).find(r => r.id === ruleId) if (!rule) return [] const i = state.rules.findIndex(r => r.id === ruleId) const enabled = rule.enabled !== false return [ { label: enabled ? 'Deaktivieren' : 'Aktivieren', icon: enabled ? 'visibility_off' : 'visibility', onClick: () => updateRule(ruleId, { ...rule, enabled: !enabled }) }, { divider: true }, { label: 'Prio hoeher (nach oben)', icon: 'arrow_upward', disabled: i <= 0, onClick: () => onMove(ruleId, -1) }, { label: 'Prio tiefer (nach unten)', icon: 'arrow_downward', disabled: i >= state.rules.length - 1, onClick: () => onMove(ruleId, +1) }, { divider: true }, { label: 'Duplizieren', icon: 'content_copy', onClick: () => duplicateRule(ruleId) }, { label: 'Als Vorlage speichern…', icon: 'bookmark_add', onClick: () => { const def = rule.name || 'Vorlage' const name = window.prompt('Name für Vorlage:', def) if (!name || !name.trim()) return saveRuleTemplate(name.trim(), rule) } }, { divider: true }, { label: 'Löschen', icon: 'delete', danger: true, onClick: () => { if (window.confirm(`Regel "${rule.name || '(ohne Name)'}" löschen?`)) deleteRule(ruleId) } }, ] } return (
{/* Override-Kombinationen — Dropdown plus kontextabhaengiger Save. Globaler AN/AUS-Toggle ist jetzt in der Oberleiste, hier ueber- fluessig. Reapply-Button raus: Backend re-applied automatisch bei jeder Aenderung. */}
Override-Kombinationen
{/* Neue Regel: leere Regel ODER aus Vorlage. */}
Regeln sind additiv. Bei Konflikt gewinnt die oberste.
{/* Rule list */}
{(state.rules || []).length === 0 && (
Noch keine Regeln.
Oben klicken um eine neue Regel zu erstellen.
)} {(state.rules || []).map((r, i) => ( deleteRule(r.id)} onDuplicate={() => duplicateRule(r.id)} onMoveUp={() => onMove(r.id, -1)} onMoveDown={() => onMove(r.id, +1)} onContextMenu={(ev) => setCtxMenu({ x: ev.clientX, y: ev.clientY, ruleId: r.id })} /> ))}
{ctxMenu && ( setCtxMenu(null)} /> )}
) }