Mirror/Copy: Duplikat-IDs in CommandEnd umbenennen statt verkoppelt zu lassen

Rhinos Mirror/Copy/Array kopiert selektierte Objekte mit ihren UserString-
Metadaten → Duplikat-IDs im Doc (z.B. zwei `wand_axis` mit gleicher
`id=wall_xxx`). Resultat: unser System sieht beide als „dasselbe Element",
fasst sie verkoppelt an, Pure-Transform wird konfus, Original wand_volume
wandert mit weil bb-snapshot matched.

Fix in `_on_command_end`, BEVOR Pure-Transform-Detection laeuft:

1. Snapshot speichert jetzt `obj_ids`-Set aller pre-Command Rhino-Object-Ids.
2. Pass A: alle neuen Sources (obj.Id nicht im Snapshot) deren UserString-id
   bereits in `sources_snap` existiert → identifiziert als Mirror/Copy-
   Duplikat, neue UUID generiert (gleicher Prefix wie bei Original-Erzeugung).
3. Pass B: alle neuen Volumes mit id = alter-renamed-Source → bekommen die
   neue ID + `oeff_parent` wird umgehaengt wenn ihre Eltern-Wand renamed.
4. Pass C: neue oeffnung_points kriegen `oeff_parent` auf renamed Wand
   umgehaengt.
5. Pass D: alle gesammelten Renames atomar via ModifyAttributes anwenden.

Resultat: Mirror-Kopie ist nach CommandEnd ein vollstaendig eigenstaendiges
Element mit eigenen IDs + intakter Parent-Cascade. Pure-Transform sieht
saubere Snapshot-vs-aktuell-Bilanz (Originale=Identity, Kopien außerhalb
des Snapshots → keine Action erforderlich, Rhino hat sie schon geometrisch
korrekt platziert).

Funktioniert generisch fuer Mirror, Copy, Array — alle dup-id-erzeugenden
Operationen. Im Log: `[ELEMENTE] mirror/copy-Duplikate: N Objs neu-ID'd`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 23:17:39 +02:00
parent 82bd15a074
commit 15185568ce
+114 -3
View File
@@ -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-