Overrides-Fenster aufgeräumt + Rule-Templates
UX-Cleanup:
- Globaler AN/AUS-Toggle entfernt — den gibt's bereits in der
Oberleiste, doppelt war redundant.
- Reload/Refresh-Button entfernt — Backend re-applied automatisch
bei jeder Regel-Aenderung, manuelles Reload nicht noetig.
- + (Neue Regel) wurde aus dem Header in eine neue Sektion
UNTER der Kombinationen-Card verschoben.
Neues Feature: Rule-Templates (einzelne wiederverwendbare Regeln)
- Storage: ~/Library/.../override_rule_templates.json (cross-doc,
parallel zu den Kombinationen-Presets)
- API in overrides.py: list/save/load/delete_rule_template
- Bridge-Messages: SAVE_RULE_TEMPLATE, DELETE_RULE_TEMPLATE,
ADD_FROM_TEMPLATE
- State enthaelt jetzt ruleTemplates: [{name, rule}]
UI:
- Neuer Bereich "Neue Regel" unter Kombinationen: [+ leer] +
[+ Aus Vorlage ▼ dropdown]
- Vorlage waehlen → insert auf hoechste Prio (gleich wie addRule)
- Im Dropdown unten: "🗑 <name> loeschen" zum Entfernen einer Vorlage
- Im Rule-Kontextmenue: neuer Eintrag "Als Vorlage speichern…"
fragt nach Name, speichert die Regel cross-doc
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,9 @@ _STORE_KEY = "dossier_overrides"
|
|||||||
# Globale Presets (cross-doc) — Datei im User-Home
|
# Globale Presets (cross-doc) — Datei im User-Home
|
||||||
_PRESETS_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel")
|
_PRESETS_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel")
|
||||||
_PRESETS_PATH = os.path.join(_PRESETS_DIR, "override_presets.json")
|
_PRESETS_PATH = os.path.join(_PRESETS_DIR, "override_presets.json")
|
||||||
|
# Rule-Templates: einzelne wiederverwendbare Regeln (cross-doc). Andere
|
||||||
|
# Datei damit User Combo-Presets und Einzel-Templates separat verwalten kann.
|
||||||
|
_RULE_TPL_PATH = os.path.join(_PRESETS_DIR, "override_rule_templates.json")
|
||||||
|
|
||||||
# UserString-Keys fuer Original-Backups (pro Objekt)
|
# UserString-Keys fuer Original-Backups (pro Objekt)
|
||||||
_ORIG_COLOR_SRC = "dossier_or_csrc"
|
_ORIG_COLOR_SRC = "dossier_or_csrc"
|
||||||
@@ -183,6 +186,72 @@ def delete_preset(name):
|
|||||||
return _write_presets_file(new)
|
return _write_presets_file(new)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Rule-Templates: einzelne wiederverwendbare Regeln (cross-doc) ----------
|
||||||
|
|
||||||
|
def _read_rule_templates():
|
||||||
|
if not os.path.isfile(_RULE_TPL_PATH): return []
|
||||||
|
try:
|
||||||
|
with open(_RULE_TPL_PATH, "rb") as f:
|
||||||
|
data = json.loads(f.read().decode("utf-8"))
|
||||||
|
if isinstance(data, list): return data
|
||||||
|
if isinstance(data, dict) and "templates" in data:
|
||||||
|
return data.get("templates") or []
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OVERRIDES] read_rule_templates:", ex)
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _write_rule_templates(templates):
|
||||||
|
try:
|
||||||
|
if not os.path.isdir(_PRESETS_DIR):
|
||||||
|
os.makedirs(_PRESETS_DIR)
|
||||||
|
with open(_RULE_TPL_PATH, "wb") as f:
|
||||||
|
f.write(json.dumps(templates or [], ensure_ascii=False, indent=2).encode("utf-8"))
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OVERRIDES] write_rule_templates:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def list_rule_templates():
|
||||||
|
"""Liefert Liste von {name, rule} fuer alle gespeicherten Templates."""
|
||||||
|
out = []
|
||||||
|
for t in _read_rule_templates():
|
||||||
|
if not isinstance(t, dict): continue
|
||||||
|
out.append({"name": t.get("name", "(ohne Name)"),
|
||||||
|
"rule": t.get("rule") or {}})
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def save_rule_template(name, rule):
|
||||||
|
"""Speichert/ueberschreibt eine Regel als Template unter name."""
|
||||||
|
if not name or not isinstance(name, str): return False
|
||||||
|
name = name.strip()
|
||||||
|
if not name or not isinstance(rule, dict): return False
|
||||||
|
templates = _read_rule_templates()
|
||||||
|
for i, t in enumerate(templates):
|
||||||
|
if isinstance(t, dict) and t.get("name") == name:
|
||||||
|
templates[i] = {"name": name, "rule": rule}
|
||||||
|
return _write_rule_templates(templates)
|
||||||
|
templates.append({"name": name, "rule": rule})
|
||||||
|
return _write_rule_templates(templates)
|
||||||
|
|
||||||
|
|
||||||
|
def load_rule_template(name):
|
||||||
|
"""Liefert die Rule eines Templates oder None."""
|
||||||
|
for t in _read_rule_templates():
|
||||||
|
if isinstance(t, dict) and t.get("name") == name:
|
||||||
|
return json.loads(json.dumps(t.get("rule") or {}))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def delete_rule_template(name):
|
||||||
|
templates = _read_rule_templates()
|
||||||
|
new = [t for t in templates if not (isinstance(t, dict) and t.get("name") == name)]
|
||||||
|
if len(new) == len(templates): return False
|
||||||
|
return _write_rule_templates(new)
|
||||||
|
|
||||||
|
|
||||||
def set_active_preset(doc, name):
|
def set_active_preset(doc, name):
|
||||||
"""Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config
|
"""Aktiviert ein gespeichertes Preset: kopiert dessen Rules ins Doc-Config
|
||||||
und markiert es als activePreset. Wenn name leer/None: aktives Preset
|
und markiert es als activePreset. Wenn name leer/None: aktives Preset
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ def _payload(doc):
|
|||||||
"hatchPatterns": _list_hatch_patterns(doc),
|
"hatchPatterns": _list_hatch_patterns(doc),
|
||||||
"presets": overrides.list_presets(),
|
"presets": overrides.list_presets(),
|
||||||
"activePreset": cfg.get("activePreset"),
|
"activePreset": cfg.get("activePreset"),
|
||||||
|
"ruleTemplates": overrides.list_rule_templates(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -210,6 +211,36 @@ class OverridesBridge(panel_base.BaseBridge):
|
|||||||
overrides.delete_preset(name)
|
overrides.delete_preset(name)
|
||||||
self._send_state()
|
self._send_state()
|
||||||
|
|
||||||
|
# --- Rule-Templates (cross-doc, einzelne Regeln) ----------------
|
||||||
|
elif t == "SAVE_RULE_TEMPLATE":
|
||||||
|
name = (p.get("name") or "").strip()
|
||||||
|
rule = p.get("rule") or {}
|
||||||
|
if name and isinstance(rule, dict):
|
||||||
|
# ID/Name aus dem Template-Rule herausschneiden (template hat
|
||||||
|
# eigenen Namen, ID wird beim Insert neu generiert)
|
||||||
|
clean = {k: v for k, v in rule.items() if k not in ("id",)}
|
||||||
|
overrides.save_rule_template(name, clean)
|
||||||
|
self._send_state()
|
||||||
|
elif t == "DELETE_RULE_TEMPLATE":
|
||||||
|
name = (p.get("name") or "").strip()
|
||||||
|
if name:
|
||||||
|
overrides.delete_rule_template(name)
|
||||||
|
self._send_state()
|
||||||
|
elif t == "ADD_FROM_TEMPLATE":
|
||||||
|
name = (p.get("name") or "").strip()
|
||||||
|
if not name: return
|
||||||
|
tpl = overrides.load_rule_template(name)
|
||||||
|
if not tpl: return
|
||||||
|
cfg = overrides.load_config(doc)
|
||||||
|
new_rule = dict(tpl)
|
||||||
|
new_rule["id"] = "rule_" + uuid.uuid4().hex[:8]
|
||||||
|
new_rule.setdefault("enabled", True)
|
||||||
|
new_rule.setdefault("name", name)
|
||||||
|
rules = cfg.get("rules") or []
|
||||||
|
rules.insert(0, new_rule)
|
||||||
|
overrides.update_rules(doc, rules, cfg.get("enabled"))
|
||||||
|
self._send_state()
|
||||||
|
|
||||||
|
|
||||||
def _ensure_listeners_once():
|
def _ensure_listeners_once():
|
||||||
"""Overrides-Listener nur EINMAL global installieren (statt bei jedem
|
"""Overrides-Listener nur EINMAL global installieren (statt bei jedem
|
||||||
|
|||||||
+61
-28
@@ -6,6 +6,7 @@ import {
|
|||||||
setOverridesEnabled, addRule, updateRule, deleteRule,
|
setOverridesEnabled, addRule, updateRule, deleteRule,
|
||||||
reorderRules, duplicateRule, reapplyOverrides, clearOverrideRules,
|
reorderRules, duplicateRule, reapplyOverrides, clearOverrideRules,
|
||||||
savePreset, loadPreset, deletePreset,
|
savePreset, loadPreset, deletePreset,
|
||||||
|
saveRuleTemplate, addFromTemplate, deleteRuleTemplate,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const COND_TYPES = [
|
const COND_TYPES = [
|
||||||
@@ -383,9 +384,10 @@ function RuleCard({ rule, index, total, layers, linetypes, hatchPatterns, onPatc
|
|||||||
export default function OverridesApp() {
|
export default function OverridesApp() {
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
enabled: false, rules: [], layers: [], linetypes: [], hatchPatterns: [], presets: [],
|
enabled: false, rules: [], layers: [], linetypes: [], hatchPatterns: [], presets: [],
|
||||||
activePreset: null,
|
activePreset: null, ruleTemplates: [],
|
||||||
})
|
})
|
||||||
const [selectedPreset, setSelectedPreset] = useState('')
|
const [selectedPreset, setSelectedPreset] = useState('')
|
||||||
|
const [selectedTemplate, setSelectedTemplate] = useState('')
|
||||||
const [ctxMenu, setCtxMenu] = useState(null) // {x, y, ruleId}
|
const [ctxMenu, setCtxMenu] = useState(null) // {x, y, ruleId}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -435,6 +437,14 @@ export default function OverridesApp() {
|
|||||||
{ label: 'Duplizieren',
|
{ label: 'Duplizieren',
|
||||||
icon: 'content_copy',
|
icon: 'content_copy',
|
||||||
onClick: () => duplicateRule(ruleId) },
|
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 },
|
{ divider: true },
|
||||||
{ label: 'Löschen',
|
{ label: 'Löschen',
|
||||||
icon: 'delete', danger: true,
|
icon: 'delete', danger: true,
|
||||||
@@ -454,30 +464,10 @@ export default function OverridesApp() {
|
|||||||
background: 'var(--bg-base)',
|
background: 'var(--bg-base)',
|
||||||
boxSizing: 'border-box',
|
boxSizing: 'border-box',
|
||||||
}}>
|
}}>
|
||||||
{/* Header — globaler Toggle + Refresh + FAB neue Regel */}
|
{/* Override-Kombinationen — Dropdown plus kontextabhaengiger Save.
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
Globaler AN/AUS-Toggle ist jetzt in der Oberleiste, hier ueber-
|
||||||
<button
|
fluessig. Reapply-Button raus: Backend re-applied automatisch
|
||||||
onClick={() => setOverridesEnabled(!state.enabled)}
|
bei jeder Aenderung. */}
|
||||||
className={state.enabled ? 'btn-contained' : 'btn-outlined'}
|
|
||||||
style={{ flex: 1 }}
|
|
||||||
title="Overrides global an/aus"
|
|
||||||
>
|
|
||||||
<Icon name={state.enabled ? 'visibility' : 'visibility_off'} size={14} />
|
|
||||||
<span>{state.enabled ? 'Overrides AN' : 'Overrides AUS'}</span>
|
|
||||||
</button>
|
|
||||||
<button onClick={reapplyOverrides} className="btn-icon-tonal" title="Regeln neu anwenden">
|
|
||||||
<Icon name="refresh" size={14} />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => addRule({})}
|
|
||||||
className="btn-add"
|
|
||||||
title="Neue Regel oben einfügen (höchste Priorität)"
|
|
||||||
>
|
|
||||||
<Icon name="add" size={16} />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Override-Kombinationen — Dropdown plus kontextabhaengiger Save. */}
|
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex', flexDirection: 'column', gap: 6,
|
display: 'flex', flexDirection: 'column', gap: 6,
|
||||||
padding: 10,
|
padding: 10,
|
||||||
@@ -494,9 +484,6 @@ export default function OverridesApp() {
|
|||||||
if (v) {
|
if (v) {
|
||||||
loadPreset(v, 'replace')
|
loadPreset(v, 'replace')
|
||||||
} else {
|
} else {
|
||||||
// "— neu / keine —" → Editor wirklich leeren, sonst bleiben
|
|
||||||
// die Regeln der vorigen Kombination stehen und der User
|
|
||||||
// baut versehentlich auf altem Stand weiter.
|
|
||||||
clearOverrideRules()
|
clearOverrideRules()
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -549,6 +536,52 @@ export default function OverridesApp() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Neue Regel: leere Regel ODER aus Vorlage. */}
|
||||||
|
<div style={{
|
||||||
|
display: 'flex', alignItems: 'center', gap: 6,
|
||||||
|
padding: '8px 10px',
|
||||||
|
background: 'var(--bg-section)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
borderRadius: 'var(--r-lg)',
|
||||||
|
}}>
|
||||||
|
<button
|
||||||
|
onClick={() => addRule({})}
|
||||||
|
className="btn-add"
|
||||||
|
title="Leere Regel oben einfuegen"
|
||||||
|
>
|
||||||
|
<Icon name="add" size={16} />
|
||||||
|
</button>
|
||||||
|
<select
|
||||||
|
value={selectedTemplate}
|
||||||
|
onChange={(e) => {
|
||||||
|
const v = e.target.value
|
||||||
|
if (!v) { setSelectedTemplate(''); return }
|
||||||
|
if (v === '__delete__') {
|
||||||
|
if (!selectedTemplate) return
|
||||||
|
if (!window.confirm(`Vorlage "${selectedTemplate}" dauerhaft loeschen?`)) return
|
||||||
|
deleteRuleTemplate(selectedTemplate)
|
||||||
|
setSelectedTemplate('')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
addFromTemplate(v)
|
||||||
|
setSelectedTemplate(v)
|
||||||
|
}}
|
||||||
|
style={{ flex: 1, minWidth: 0 }}
|
||||||
|
title="Regel aus Vorlage einfuegen"
|
||||||
|
>
|
||||||
|
<option value="">+ Aus Vorlage…</option>
|
||||||
|
{(state.ruleTemplates || []).map(t => (
|
||||||
|
<option key={t.name} value={t.name}>{t.name}</option>
|
||||||
|
))}
|
||||||
|
{selectedTemplate && (state.ruleTemplates || []).some(t => t.name === selectedTemplate) && (
|
||||||
|
<>
|
||||||
|
<option disabled>──────────</option>
|
||||||
|
<option value="__delete__">🗑 "{selectedTemplate}" loeschen</option>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic', lineHeight: 1.4 }}>
|
<div style={{ fontSize: 10, color: 'var(--text-muted)', fontStyle: 'italic', lineHeight: 1.4 }}>
|
||||||
Regeln sind additiv. Bei Konflikt gewinnt die <b>oberste</b>.
|
Regeln sind additiv. Bei Konflikt gewinnt die <b>oberste</b>.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -192,6 +192,10 @@ export function clearOverrideRules() { send('CLEAR_RULES', {}) }
|
|||||||
export function savePreset(name) { send('SAVE_PRESET', { name }) }
|
export function savePreset(name) { send('SAVE_PRESET', { name }) }
|
||||||
export function loadPreset(name, mode) { send('LOAD_PRESET', { name, mode: mode || 'replace' }) }
|
export function loadPreset(name, mode) { send('LOAD_PRESET', { name, mode: mode || 'replace' }) }
|
||||||
export function deletePreset(name) { send('DELETE_PRESET', { name }) }
|
export function deletePreset(name) { send('DELETE_PRESET', { name }) }
|
||||||
|
// Rule-Templates: einzelne Regel speichern/anwenden/loeschen
|
||||||
|
export function saveRuleTemplate(name, rule) { send('SAVE_RULE_TEMPLATE', { name, rule }) }
|
||||||
|
export function addFromTemplate(name) { send('ADD_FROM_TEMPLATE', { name }) }
|
||||||
|
export function deleteRuleTemplate(name) { send('DELETE_RULE_TEMPLATE', { name }) }
|
||||||
|
|
||||||
// --- Dimensionen-Panel ---
|
// --- Dimensionen-Panel ---
|
||||||
export function setRefPoint(x, y, z) { send('SET_REF_POINT', { x, y, z }) }
|
export function setRefPoint(x, y, z) { send('SET_REF_POINT', { x, y, z }) }
|
||||||
|
|||||||
Reference in New Issue
Block a user