diff --git a/rhino/library.py b/rhino/library.py index 0d8bbdd..71a3dd6 100644 --- a/rhino/library.py +++ b/rhino/library.py @@ -148,6 +148,97 @@ def load_manifest(): return _empty_manifest() +def save_manifest(manifest): + """Schreibt das Manifest zurueck zur library.json. Items werden + normalisiert. Returns True/False.""" + ensure_library() + path = os.path.join(library_root(), _MANIFEST_FN) + try: + if not isinstance(manifest, dict): manifest = _empty_manifest() + manifest.setdefault("schemaVersion", _SCHEMA_VERSION) + manifest.setdefault("name", "Dossier-Library") + items = manifest.get("items") or [] + manifest["items"] = [_normalize_item(x) for x in items + if _normalize_item(x) is not None] + with open(path, "w") as f: + json.dump(manifest, f, indent=2, ensure_ascii=False) + return True + except Exception as ex: + print("[LIBRARY] save_manifest:", ex) + return False + + +def update_item(item_id, patch): + """Patcht ein Item im Manifest. Returns (ok, new_manifest).""" + m = load_manifest() + items = m.get("items", []) + found = False + for it in items: + if it.get("id") == item_id: + for k, v in (patch or {}).items(): + it[k] = v + found = True + break + if not found: return False, m + ok = save_manifest(m) + return ok, load_manifest() + + +def delete_item(item_id): + """Loescht ein Item aus dem Manifest. Asset-Files bleiben auf Disk + (User koennte sie noch wollen).""" + m = load_manifest() + items = m.get("items", []) + new_items = [it for it in items if it.get("id") != item_id] + if len(new_items) == len(items): return False, m + m["items"] = new_items + ok = save_manifest(m) + return ok, load_manifest() + + +def add_item(item): + """Fuegt ein neues Item zum Manifest hinzu. Returns (ok, new_manifest).""" + norm = _normalize_item(item) + if norm is None: return False, load_manifest() + m = load_manifest() + items = m.get("items", []) + # Dedupe per id + items = [it for it in items if it.get("id") != norm["id"]] + items.append(norm) + m["items"] = items + ok = save_manifest(m) + return ok, load_manifest() + + +def copy_to_assets(src_path, target_name=None): + """Kopiert eine Datei in library/assets/. target_name optional (sonst + Original-Name). Returns relativer Pfad ('assets/foo.3dm') oder None.""" + if not src_path or not os.path.isfile(src_path): + return None + ensure_library() + assets_dir = os.path.join(library_root(), "assets") + if not os.path.isdir(assets_dir): + try: os.makedirs(assets_dir) + except Exception: pass + base = target_name or os.path.basename(src_path) + target = os.path.join(assets_dir, base) + # Konflikt-Resolution: bei doppeltem Namen Nummer dran + if os.path.isfile(target): + stem, ext = os.path.splitext(base) + n = 2 + while os.path.isfile(os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext))): + n += 1 + target = os.path.join(assets_dir, "{}_{}{}".format(stem, n, ext)) + try: + import shutil + shutil.copy2(src_path, target) + rel = os.path.relpath(target, library_root()) + return rel + except Exception as ex: + print("[LIBRARY] copy_to_assets:", ex) + return None + + def _empty_manifest(): return {"schemaVersion": _SCHEMA_VERSION, "name": "Dossier-Library", "items": []} diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 9bd98b7..2d575fc 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -888,13 +888,25 @@ class EbenenBridge(panel_base.BaseBridge): }) except Exception: built_in = [] + # Library-Items initial laden (Symbole/Objekte fuer den + # Symbole-Tab; Materialien sind separat). + try: + import library + lib_manifest = library.load_manifest() + lib_items = lib_manifest.get("items", []) + lib_root = library.library_root() + except Exception: + lib_items = []; lib_root = "" params = { "defaults": current.get("defaults", {}), + "project": current.get("project", {}), "materials": current.get("materials", []), "builtinMaterials": built_in, "hatchPatterns": _hatch_pattern_names(doc), "hatchPatternsFull": _list_hatch_patterns_full(doc), "linetypes": _list_linetypes_full(doc), + "libraryItems": lib_items, + "libraryRoot": lib_root, } def on_save(updated): doc2 = Rhino.RhinoDoc.ActiveDoc @@ -992,6 +1004,14 @@ class EbenenBridge(panel_base.BaseBridge): self._delete_hatch(p) elif t == "IMPORT_HATCH_FILE": self._import_hatch_file() + elif t == "LIST_LIBRARY_ITEMS": + self._send_library() + elif t == "ADD_LIBRARY_FILE": + self._add_library_file(p) + elif t == "UPDATE_LIBRARY_ITEM": + self._update_library_item(p) + elif t == "DELETE_LIBRARY_ITEM": + self._delete_library_item(p) def _pick_texture(self, payload): slot = payload.get("slot") or "diffuse" try: @@ -1157,6 +1177,117 @@ class EbenenBridge(panel_base.BaseBridge): "linetypes": _list_linetypes_full(d), "hatchPatternsFull": _list_hatch_patterns_full(d), }) + + # ---- Library-Item CRUD ---- + def _send_library(self): + """Sendet aktuelle Library-Items ans Frontend + (LIBRARY_ITEMS-Message).""" + try: + import library + m = library.load_manifest() + self.send("LIBRARY_ITEMS", { + "items": m.get("items", []), + "libraryRoot": library.library_root(), + }) + except Exception as ex: + print("[PROJECT-SETTINGS] _send_library:", ex) + + def _add_library_file(self, payload): + """Datei-Picker → kopieren in library/assets/ → Item zum + Manifest hinzufuegen. Payload: {variant: '2d'|'3d', + targetId: ?, itemType: 'symbol'|'object'}. + Wenn targetId gesetzt: bestehendes Item updaten (Datei + wird seinem files2d/3d zugewiesen). Sonst: neues Item.""" + try: + import library + import Eto.Forms as forms + variant = (payload.get("variant") or "2d").lower() + if variant not in ("2d", "3d"): variant = "2d" + target_id = (payload.get("targetId") or "").strip() or None + item_type = payload.get("itemType") or "object" + if item_type not in ("symbol", "object"): + item_type = "object" + dlg = forms.OpenFileDialog() + dlg.Title = ("Datei waehlen ({})".format(variant.upper())) + dlg.MultiSelect = False + dlg.Filters.Add(forms.FileFilter( + "Rhino 3DM", ".3dm")) + dlg.Filters.Add(forms.FileFilter( + "Andere CAD-Formate", ".dwg", ".obj", ".fbx", ".dae", ".stl")) + dlg.Filters.Add(forms.FileFilter("Alle", ".*")) + parent = bridge_holder.get("form") + res = dlg.ShowDialog(parent) if parent else dlg.ShowDialog(None) + if str(res) != "Ok": + return + path = dlg.FileName or "" + if not path: return + ext = (path.split(".")[-1] if "." in path else "").lower() + if ext != "3dm": + # TODO: konvertieren via temporaerer RhinoDoc-Import + # Phase 1: nur .3dm direkt unterstuetzt + print("[PROJECT-SETTINGS] Format '.{}' wird in dieser " + "Version noch nicht konvertiert — bitte in Rhino " + "oeffnen + als .3dm speichern".format(ext)) + self.send("LIBRARY_ERROR", { + "msg": "Format .{} noch nicht unterstuetzt. " + "Konvertiere in Rhino zu .3dm.".format(ext), + }) + return + rel = library.copy_to_assets(path) + if not rel: + print("[PROJECT-SETTINGS] copy_to_assets failed") + return + if target_id: + # Bestehendes Item updaten + m = library.load_manifest() + for it in m.get("items", []): + if it.get("id") == target_id: + key = "files" + variant + it[key] = [rel] + if not it.get("type"): it["type"] = item_type + break + library.save_manifest(m) + else: + # Neues Item + import os + base = os.path.basename(path) + stem = os.path.splitext(base)[0] + import uuid as _uuid + new_id = "obj-" + _uuid.uuid4().hex[:10] + item = { + "id": new_id, + "type": item_type, + "name": stem, + "version": 1, + "tags": [], + "files" + variant: [rel], + } + library.add_item(item) + self._send_library() + except Exception as ex: + print("[PROJECT-SETTINGS] _add_library_file:", ex) + + def _update_library_item(self, payload): + """Patcht ein Item per id. payload: {id, patch: {...}}""" + try: + import library + item_id = (payload.get("id") or "").strip() + patch = payload.get("patch") or {} + if not item_id or not isinstance(patch, dict): return + library.update_item(item_id, patch) + self._send_library() + except Exception as ex: + print("[PROJECT-SETTINGS] _update_library_item:", ex) + + def _delete_library_item(self, payload): + try: + import library + item_id = (payload.get("id") or "").strip() + if not item_id: return + library.delete_item(item_id) + self._send_library() + except Exception as ex: + print("[PROJECT-SETTINGS] _delete_library_item:", ex) b = _ProjectSettingsBridge() bridge_holder["form"] = panel_base.open_satellite_window( "project_settings", diff --git a/src/components/ProjectSettingsDialog.jsx b/src/components/ProjectSettingsDialog.jsx index 58d7bfc..023c611 100644 --- a/src/components/ProjectSettingsDialog.jsx +++ b/src/components/ProjectSettingsDialog.jsx @@ -5,6 +5,7 @@ import { openLibrary, pickTextureFile, onMessage, renameLinetype, deleteLinetype, loadLinetypeDefaults, importLinetypeFile, renameHatch, deleteHatch, importHatchFile, + listLibraryItems, addLibraryFile, updateLibraryItem, deleteLibraryItem, } from '../lib/rhinoBridge' /* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */ @@ -638,6 +639,9 @@ export default function ProjectSettingsDialog({ const [hatches, setHatches] = useState(initial.hatchPatternsFull || []) const [selLt, setSelLt] = useState(null) const [selHt, setSelHt] = useState(null) + const [libItems, setLibItems] = useState(initial.libraryItems || []) + const [libRoot, setLibRoot] = useState(initial.libraryRoot || '') + const [selLib, setSelLib] = useState(null) const builtin = initial.builtinMaterials || [] // Aktuell ausgewaehltes Material aus Selection ableiten @@ -668,6 +672,13 @@ export default function ProjectSettingsDialog({ if (lts) setLinetypes(lts) if (hps) setHatches(hps) }) + onMessage('LIBRARY_ITEMS', ({ items, libraryRoot }) => { + if (Array.isArray(items)) setLibItems(items) + if (libraryRoot) setLibRoot(libraryRoot) + }) + onMessage('LIBRARY_ERROR', ({ msg }) => { + if (msg) alert(msg) + }) }, []) // Backend-File-Picker-Antwort: aktualisiert das Slot im aktuell @@ -741,6 +752,7 @@ export default function ProjectSettingsDialog({ { key: 'materials', label: 'Materialien' }, { key: 'linetypes', label: 'Linientypen' }, { key: 'hatches', label: 'Schraffuren' }, + { key: 'symbols', label: 'Symbole' }, ]} active={tab} onChange={setTab} /> {/* Body */} @@ -1074,6 +1086,195 @@ export default function ProjectSettingsDialog({ )} + + {tab === 'symbols' && ( +