diff --git a/rhino/elemente.py b/rhino/elemente.py index 4f99ae7..2179366 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -7818,11 +7818,15 @@ def _snapshot_source_positions(doc): BBox-Centers. Source-Map (key=element_id) füttert Constraint+Migrate. Volume-Map (key=obj.Id-string) erlaubt im CommandEnd die Pure-Translate- Detection — wir checken pro Volume ob es schon vom Rhinos Move - transformed wurde, oder noch ge-translaten werden muss.""" - snap = {"sources": {}, "volumes": {}} + transformed wurde, oder noch ge-translaten werden muss. + obj_ids-Set: alle pre-Command Rhino-Object-IDs. Wird in CommandEnd + benutzt um Mirror/Copy-Duplikate zu erkennen (= neue Objs mit IDs die + nicht im Snapshot waren).""" + snap = {"sources": {}, "volumes": {}, "obj_ids": set()} if doc is None: return snap for obj in doc.Objects: try: + snap["obj_ids"].add(str(obj.Id)) m = _read_meta(obj) if not m: continue t = m.get("type") @@ -7946,8 +7950,115 @@ def _on_command_end(sender, e): sources_snap = snapshot.get("sources", {}) if isinstance(snapshot, dict) else {} volumes_snap = snapshot.get("volumes", {}) if isinstance(snapshot, dict) else {} + old_obj_ids = snapshot.get("obj_ids", set()) if isinstance(snapshot, dict) else set() - # ─── Pure-Transform-Detection (Translation + Z-Rotation) ──────────────── + # ─── Mirror/Copy-Duplikat-Detection ───────────────────────────────────── + # Rhinos Mirror/Copy/Array erzeugt KOPIEN selektierter Objekte mit ihren + # UserStrings (= Metadata). Resultat: Duplikat-IDs im Doc — z.B. zwei + # `wand_axis` mit `id=wall_xxx`. Unser System haelt die fuer „dasselbe + # Element", was zu „verkoppelten" Elementen fuehrt und zu kaputten + # Pure-Transform-Detections. + # + # Fix: alle NEUEN Objs (obj.Id nicht im Snapshot) deren UserString-id + # bereits im Snapshot existiert → neue UUID. Sub-Volumen und + # Oeffnungs-Parent-Refs werden konsistent umgehaengt. + _type_to_prefix = { + "wand_axis": "wall_", + "decke_outline": "decke_", + "dach_outline": "dach_", + "treppe_axis": "treppe_", + "stuetze_point": "trag_", + "traeger_axis": "trag_", + "raum_outline": "raum_", + "decke_aussparung_outline": "aussp_", + } + # Pass A: identifiziere neue Sources mit dup-IDs, sammle (obj, alte_id, neue_id) + dup_source_renames = [] # list of (obj, old_id, new_id, type) + for obj in doc.Objects: + try: + if str(obj.Id) in old_obj_ids: continue # original existed pre-command + m = _read_meta(obj) + if not m: continue + t = m.get("type") + if t not in SOURCE_TYPES: continue + old_id = m["id"] + if old_id not in sources_snap: continue # echtes neues Element + if t == "oeffnung_point": + prefix = "fenster_" if m.get("oeff_typ") == "fenster" else "tuer_" + else: + prefix = _type_to_prefix.get(t, "elem_") + new_id = prefix + uuid.uuid4().hex[:10] + dup_source_renames.append((obj, old_id, new_id, t)) + except Exception as ex: + print("[ELEMENTE] dup detection:", ex) + + # Pass B: neue Volumes mit dup-IDs identifizieren (alte UserString-id ist + # eine umbenannte Source). Mapping alte_id → neue_id zum Lookup. + elem_id_map = {old_id: new_id for (_, old_id, new_id, _) in dup_source_renames} + dup_volume_renames = [] # list of (obj, new_id, oeff_parent_old, oeff_parent_new) + for obj in doc.Objects: + try: + if str(obj.Id) in old_obj_ids: continue + m = _read_meta(obj) + if not m: continue + t = m.get("type") + if t not in VOLUME_TYPES: continue + old_vol_id = m["id"] + new_vol_id = elem_id_map.get(old_vol_id) + if not new_vol_id: continue # Volume gehoert nicht zu einem renamed Source + # oeff_parent rewire bei oeffnung_volume + old_parent = m.get("oeff_parent") or "" + new_parent = elem_id_map.get(old_parent, old_parent) + dup_volume_renames.append((obj, new_vol_id, old_parent, new_parent)) + except Exception as ex: + print("[ELEMENTE] dup volume detection:", ex) + + # Pass C: oeffnung_point's oeff_parent rewire (nicht-Volume, also Sources) + # Wenn eine Wand umbenannt wurde, alle (umbenannten) Oeffnungen die zu ihr + # gehoeren auch auf neue Wand-id umhaengen. + if elem_id_map: + # In dup_source_renames Liste: fuer oeffnung_point-Renames pruefen, ob + # ihr oeff_parent in elem_id_map ist → updaten. + for i, (obj, old_id, new_id, t) in enumerate(dup_source_renames): + if t != "oeffnung_point": continue + try: + m = _read_meta(obj) + if not m: continue + old_parent = m.get("oeff_parent") or "" + new_parent = elem_id_map.get(old_parent, old_parent) + # Tuple aktualisieren (alte vs neue parent-ID, fuer apply unten) + dup_source_renames[i] = (obj, old_id, new_id, t, new_parent) + except Exception: pass + + # Pass D: alle gesammelten Renames anwenden + n_renamed = 0 + for entry in dup_source_renames: + try: + if len(entry) == 5: + obj, old_id, new_id, t, new_parent = entry + else: + obj, old_id, new_id, t = entry + new_parent = None + attrs = obj.Attributes.Duplicate() + attrs.SetUserString(_KEY_ID, new_id) + if new_parent is not None: + attrs.SetUserString(_KEY_OEFF_PARENT, new_parent) + doc.Objects.ModifyAttributes(obj.Id, attrs, True) + n_renamed += 1 + except Exception as ex: + print("[ELEMENTE] apply source rename:", ex) + for obj, new_vol_id, old_parent, new_parent in dup_volume_renames: + try: + attrs = obj.Attributes.Duplicate() + attrs.SetUserString(_KEY_ID, new_vol_id) + if old_parent and new_parent and new_parent != old_parent: + attrs.SetUserString(_KEY_OEFF_PARENT, new_parent) + doc.Objects.ModifyAttributes(obj.Id, attrs, True) + n_renamed += 1 + except Exception as ex: + print("[ELEMENTE] apply volume rename:", ex) + if n_renamed > 0: + print("[ELEMENTE] mirror/copy-Duplikate: {} Objs neu-ID'd".format(n_renamed)) # Wenn ALLE bewegten Sources sich mit dem gleichen Rigid-2D-Transform # abbilden lassen (Translation und/oder Rotation um Z-Achse, KEIN Scale, # KEIN Z-Drag, KEIN End-Grip-Drag, KEIN Mirror), reicht eine Transform-