Projekt-Settings-Dialog + Library Phase A + Material-Merger

- Project-Settings-Dialog (Voreinstellungen Geschoss/Schnitt + Material-Editor)
  ueber Zahnrad-Icon in Oberleiste; Defaults werden in schnitte.pick_schnitt
  + GeschossManager als Vorgabe genommen, pro-Element-Werte unangetastet
- Dossier-Library Phase A (lokal, read-only): rhino/library.py + LibraryBrowser
  Satellite; Seed-Manifest unter ~/Library/Application Support/Dossier/library/
- Material-Merger: _get_all_materials(doc) merged builtin _MATERIAL_LIBRARY
  mit Projekt-Settings-Materialien (inkl. Library-Imports); Wand-Erstellung,
  Sub-Layer-Anlage + Elemente-Material-Dropdown ziehen jetzt aus dem Merge

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 02:19:09 +02:00
parent ee01c7ebdc
commit a308ba62d2
13 changed files with 1087 additions and 56 deletions
+200
View File
@@ -96,6 +96,7 @@ def _broadcast_state(doc=None, hatch_patterns=None):
"zeichnungsebenen": json.loads(z_raw) if z_raw else None,
"ebenen": json.loads(e_raw) if e_raw else None,
"projectZeroMum": zero_mum,
"projectSettings": load_project_settings(doc),
"hatchPatterns": hatch_patterns if hatch_patterns is not None
else _hatch_pattern_names(doc),
"layerCombinations": list_layer_preset_names(doc),
@@ -120,6 +121,83 @@ def _broadcast_state(doc=None, hatch_patterns=None):
_PRESETS_KEY = "dossier_layer_presets"
_ACTIVE_COMB_KEY = "dossier_layer_active_comb"
# Projekt-Einstellungen: zentrale Voreinstellungen die beim Erstellen
# neuer Geschosse / Schnitte / etc. als Default genommen werden. Pro-
# Element-Werte ueberschreiben das natuerlich — das hier ist nur die
# Voreinstellung. Persistiert pro Doc.
_PROJECT_SETTINGS_KEY = "dossier_project_settings"
_PROJECT_SETTINGS_DEFAULTS = {
"defaults": {
"geschossHoehe": 3.0,
"schnitthoehe": 1.0,
"schnittDepthBack": 8.0,
"schnittHeightMin": -1.0,
"schnittHeightMax": 12.0,
},
"materials": [], # User-erweiterte Materialien (zusaetzlich zur
# hardcoded _MATERIAL_LIBRARY in elemente.py)
}
def _normalize_material(m):
"""Garantiert Material-Schema: name + color + hatch + scale + source
+ libraryId. source: 'local' | 'library' | 'builtin'. libraryId: UUID
wenn aus Library, sonst null. Felder fehlen bei alten Eintraegen — wir
setzen Defaults damit Frontend nicht null-checken muss."""
if not isinstance(m, dict): return None
return {
"name": m.get("name") or "Unbenannt",
"color": m.get("color") or "#888888",
"hatch": m.get("hatch") or "Solid",
"scale": float(m.get("scale", 1.0) or 1.0),
"source": m.get("source") or "local",
"libraryId": m.get("libraryId"), # None wenn nicht aus Library
}
def load_project_settings(doc):
"""Liefert die Project-Settings als dict — mit Defaults-Merge wenn
Felder fehlen. Garantiert dass `defaults` und `materials` immer da
sind, und Materialien normalisiert (source + libraryId)."""
raw = None
try: raw = doc.Strings.GetValue(_PROJECT_SETTINGS_KEY) if doc else None
except Exception: raw = None
out = {
"defaults": dict(_PROJECT_SETTINGS_DEFAULTS["defaults"]),
"materials": list(_PROJECT_SETTINGS_DEFAULTS["materials"]),
}
if not raw: return out
try:
data = json.loads(raw)
if isinstance(data, dict):
d = data.get("defaults")
if isinstance(d, dict):
for k, v in d.items():
out["defaults"][k] = v
m = data.get("materials")
if isinstance(m, list):
out["materials"] = [
_normalize_material(x) for x in m
if _normalize_material(x) is not None
]
except Exception as ex:
print("[PROJECT-SETTINGS] load:", ex)
return out
def save_project_settings(doc, settings):
"""Persistiert Settings in doc.Strings. settings: dict mit
'defaults' + 'materials'. Caller broadcastet ggf. selbst."""
if doc is None or not isinstance(settings, dict): return False
try:
doc.Strings.SetString(_PROJECT_SETTINGS_KEY,
json.dumps(settings, ensure_ascii=False))
return True
except Exception as ex:
print("[PROJECT-SETTINGS] save:", ex)
return False
def load_layer_presets(doc):
raw = doc.Strings.GetValue(_PRESETS_KEY)
@@ -531,9 +609,131 @@ class EbenenBridge(panel_base.BaseBridge):
try: open_layer_combinations_window()
except Exception as ex:
print("[EBENEN] open layer-combinations:", ex)
elif t == "OPEN_PROJECT_SETTINGS":
try: self._open_project_settings()
except Exception as ex:
print("[EBENEN] open project-settings:", ex)
elif t == "OPEN_LIBRARY":
try: self._open_library()
except Exception as ex:
print("[EBENEN] open library:", ex)
# ---- Helpers ----
def _open_project_settings(self):
"""Oeffnet React-Satellite mit Projekt-Voreinstellungen (Geschoss-/
Schnitt-Defaults + Material-Library). Werte werden nur als
Voreinstellung beim Erstellen neuer Elemente genutzt — pro-Element
editierte Werte bleiben davon unberuehrt."""
doc = Rhino.RhinoDoc.ActiveDoc
current = load_project_settings(doc)
# Hardcoded Material-Library aus elemente.py mit-laden damit
# Frontend die Defaults zeigt + User sie ggf. anpassen kann.
try:
import elemente
built_in = []
for name, m in elemente._MATERIAL_LIBRARY.items():
built_in.append({
"name": name,
"color": m.get("color", "#888888"),
"hatch": m.get("hatch", "Solid"),
"scale": float(m.get("scale", 1.0)),
"source": "builtin",
"libraryId": None,
"builtin": True, # legacy flag fuer aelteren UI-Code
})
except Exception:
built_in = []
params = {
"defaults": current.get("defaults", {}),
"materials": current.get("materials", []),
"builtinMaterials": built_in,
"hatchPatterns": _hatch_pattern_names(doc),
}
def on_save(updated):
doc2 = Rhino.RhinoDoc.ActiveDoc
if doc2 is None: return
new_settings = {
"defaults": updated.get("defaults", {}),
"materials": updated.get("materials", []),
}
save_project_settings(doc2, new_settings)
_broadcast_state(doc2)
try:
import scriptcontext as sc
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state()
except Exception as ex:
print("[PROJECT-SETTINGS] elemente refresh:", ex)
panel_base.open_satellite_window(
"project_settings",
params=params,
title="Projekt-Einstellungen",
size=(440, 540),
on_save=on_save)
def _open_library(self):
"""Oeffnet den Library-Browser als Satellite. Bridge bleibt offen
damit User mehrere Items hintereinander importieren kann; nach
jedem Import wird der Material-Status zurueckgemeldet damit das
UI 'importiert' anzeigt."""
import library
bridge_holder = {"form": None}
class _LibraryBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "library")
def _on_ready(self):
self._send_state()
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
manifest = library.load_manifest()
imported_ids = set()
if doc is not None:
settings = load_project_settings(doc)
for m in settings.get("materials", []):
lid = m.get("libraryId")
if lid: imported_ids.add(lid)
self.send("LIBRARY_STATE", {
"manifest": manifest,
"importedIds": list(imported_ids),
"libraryRoot": library.library_root(),
})
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "IMPORT_ITEM":
doc = Rhino.RhinoDoc.ActiveDoc
ok, msg = library.import_item(doc, p.get("id"))
print("[LIBRARY] import {}: {} ({})".format(
p.get("id"), ok, msg))
if ok:
_broadcast_state(doc)
# Elemente-Panel-Dropdown muss neu laden (materials)
try:
import scriptcontext as sc
eb = sc.sticky.get("elemente_bridge")
if eb is not None: eb._send_state()
except Exception as ex:
print("[LIBRARY] elemente refresh:", ex)
self._send_state()
elif t == "RELOAD":
self._send_state()
elif t == "CLOSE":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _LibraryBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"library",
params={},
title="Dossier-Library",
size=(640, 560),
bridge=b)
def _open_geschoss_settings(self, geschoss):
"""Oeffnet ein echtes Rhino-Fenster (Eto.Form mit WebView) mit dem
GeschossSettingsDialog. Save updated den Eintrag in doc.Strings +