diff --git a/rhino/library.py b/rhino/library.py index 71a3dd6..f17ad95 100644 --- a/rhino/library.py +++ b/rhino/library.py @@ -210,6 +210,91 @@ def add_item(item): return ok, load_manifest() +def save_selection_to_asset(doc, target_name): + """Speichert die aktuelle Selection aus dem Doc als eigene .3dm-Datei + in library/assets/.3dm. Returns relativer Pfad oder None. + Geometry wird relativ zum BoundingBox-Min platziert damit der Block- + Origin am Ursprung sitzt — sinnvoll fuer Symbol-Insert.""" + if doc is None or not target_name: return None + try: + sel = list(doc.Objects.GetSelectedObjects(False, False)) + except Exception: + sel = [] + if not sel: + print("[LIBRARY] save_selection: keine Auswahl") + return None + if Rhino is None: 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 + # Safe filename + safe = "".join(c if (c.isalnum() or c in "-_") else "_" for c in target_name) + if not safe.endswith(".3dm"): safe += ".3dm" + target = os.path.join(assets_dir, safe) + if os.path.isfile(target): + stem, ext = os.path.splitext(safe) + 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)) + # File3dm aufbauen + Selection rein + try: + from Rhino.FileIO import File3dm, File3dmWriteOptions + import Rhino.Geometry as rg + # BoundingBox sammeln um geometrie an Ursprung zu verschieben + bbox = rg.BoundingBox.Empty + geoms = [] + for o in sel: + g = o.Geometry + if g is None: continue + try: + bb = g.GetBoundingBox(True) + if bb.IsValid: bbox.Union(bb) + except Exception: pass + geoms.append((g, o.Attributes)) + if not geoms: + return None + if not bbox.IsValid: + origin = rg.Point3d(0, 0, 0) + else: + origin = bbox.Min + offset = rg.Transform.Translation(-origin.X, -origin.Y, -origin.Z) + f3 = File3dm() + for g, a in geoms: + try: + g2 = g.Duplicate() + try: g2.Transform(offset) + except Exception: pass + # Generischer Add fuer alle GeometryBase + try: f3.Objects.Add(g2, a) + except Exception: + # Fallback: typ-spezifisch + if isinstance(g2, rg.Brep): f3.Objects.AddBrep(g2) + elif isinstance(g2, rg.Curve): f3.Objects.AddCurve(g2) + elif isinstance(g2, rg.Mesh): f3.Objects.AddMesh(g2) + elif isinstance(g2, rg.Point): f3.Objects.AddPoint(g2.Location) + except Exception as ex: + print("[LIBRARY] save_selection add:", ex) + # Write + try: + opts = File3dmWriteOptions() + opts.Version = 8 + f3.Write(target, opts) + except Exception: + try: f3.Write(target, 8) + except Exception as ex: + print("[LIBRARY] save_selection write:", ex) + return None + rel = os.path.relpath(target, library_root()) + print("[LIBRARY] save_selection: {} objs → {}".format(len(geoms), rel)) + return rel + except Exception as ex: + print("[LIBRARY] save_selection:", ex) + return None + + 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.""" diff --git a/rhino/rhinopanel.py b/rhino/rhinopanel.py index 2d575fc..2c8cb9c 100644 --- a/rhino/rhinopanel.py +++ b/rhino/rhinopanel.py @@ -1012,6 +1012,8 @@ class EbenenBridge(panel_base.BaseBridge): self._update_library_item(p) elif t == "DELETE_LIBRARY_ITEM": self._delete_library_item(p) + elif t == "SAVE_SELECTION_AS_LIBRARY": + self._save_selection_as_library(p) def _pick_texture(self, payload): slot = payload.get("slot") or "diffuse" try: @@ -1288,6 +1290,71 @@ class EbenenBridge(panel_base.BaseBridge): self._send_library() except Exception as ex: print("[PROJECT-SETTINGS] _delete_library_item:", ex) + + def _save_selection_as_library(self, payload): + """Aktuelle Selection im Doc als Library-Item speichern. + payload: {variant: '2d'|'3d', targetId?: str, itemType?: str}. + Wenn targetId gesetzt: bestehendes Item updaten. Sonst: + Rhino-Prompt nach Name + neues Item anlegen.""" + d = Rhino.RhinoDoc.ActiveDoc + if d is None: return + try: + sel = list(d.Objects.GetSelectedObjects(False, False)) + except Exception: sel = [] + if not sel: + self.send("LIBRARY_ERROR", { + "msg": "Bitte erst Objekte in Rhino selektieren."}) + return + import library + 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" + # Name: bei bestehendem Item den Item-Name nehmen, sonst + # Rhino-Prompt + name = "" + if target_id: + m = library.load_manifest() + for it in m.get("items", []): + if it.get("id") == target_id: + name = it.get("name") or "item" + break + else: + try: + import Rhino.Input.Custom as ric + from Rhino.Input import GetResult + gs = ric.GetString() + gs.SetCommandPrompt("Name fuer neues Library-Item") + gs.AcceptNothing(True) + if gs.Get() == GetResult.String: + name = (gs.StringResult() or "").strip() + except Exception as ex: + print("[PROJECT-SETTINGS] save_selection prompt:", ex) + if not name: name = "Neues_Item" + # Speichern + rel = library.save_selection_to_asset(d, name + "_" + variant) + if not rel: + self.send("LIBRARY_ERROR", { + "msg": "Konnte Selection nicht speichern."}) + return + if target_id: + library.update_item(target_id, { + ("files" + variant): [rel], + }) + else: + import uuid as _uuid + new_id = "obj-" + _uuid.uuid4().hex[:10] + item = { + "id": new_id, + "type": item_type, + "name": name, + "version": 1, + "tags": [], + ("files" + variant): [rel], + } + library.add_item(item) + self._send_library() 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 023c611..56db889 100644 --- a/src/components/ProjectSettingsDialog.jsx +++ b/src/components/ProjectSettingsDialog.jsx @@ -6,6 +6,7 @@ import { renameLinetype, deleteLinetype, loadLinetypeDefaults, importLinetypeFile, renameHatch, deleteHatch, importHatchFile, listLibraryItems, addLibraryFile, updateLibraryItem, deleteLibraryItem, + saveSelectionAsLibrary, } from '../lib/rhinoBridge' /* Field — Stack-Layout fuer komplexe Inputs (mehrere Felder nebeneinander) */ @@ -1150,12 +1151,15 @@ export default function ProjectSettingsDialog({ ) })} -
- addLibraryFile('2d', null, 'object')} - title="Datei waehlen, kopiert in Library + neues Item" /> + title=".3dm-Datei waehlen → kopiert in Library + neues Item" /> + saveSelectionAsLibrary('2d', null, 'object')} + title="Selektierte Objekte in Rhino als Library-Item speichern" />
@@ -1221,8 +1225,13 @@ export default function ProjectSettingsDialog({ {(it.files2d || []).join(', ')} )} - addLibraryFile('2d', it.id, it.type)} /> +
+ addLibraryFile('2d', it.id, it.type)} /> + saveSelectionAsLibrary('2d', it.id, it.type)} + title="Selektierte Objekte als 2D-Variante speichern" /> +
@@ -1240,8 +1249,13 @@ export default function ProjectSettingsDialog({ {(it.files3d || []).join(', ')} )} - addLibraryFile('3d', it.id, it.type)} /> +
+ addLibraryFile('3d', it.id, it.type)} /> + saveSelectionAsLibrary('3d', it.id, it.type)} + title="Selektierte Objekte als 3D-Variante speichern" /> +
diff --git a/src/lib/rhinoBridge.js b/src/lib/rhinoBridge.js index 482a764..1c05241 100644 --- a/src/lib/rhinoBridge.js +++ b/src/lib/rhinoBridge.js @@ -217,6 +217,16 @@ export function updateLibraryItem(id, patch) { send('UPDATE_LIBRARY_ITEM', { id, patch }) } export function deleteLibraryItem(id) { send('DELETE_LIBRARY_ITEM', { id }) } +// Aktuelle Rhino-Selection als Library-Item speichern. variant='2d'|'3d'. +// Wenn targetId gesetzt: bestehendes Item aktualisieren. Sonst neues +// Item (Backend fragt nach Namen via Rhino-Prompt). +export function saveSelectionAsLibrary(variant, targetId, itemType) { + send('SAVE_SELECTION_AS_LIBRARY', { + variant: variant || '2d', + targetId: targetId || null, + itemType: itemType || 'object', + }) +} // Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt // eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie. // opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }