Compare commits
4 Commits
d3984ba501
...
15185568ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 15185568ce | |||
| 82bd15a074 | |||
| 0978d9fc2e | |||
| 2a75b1da93 |
+575
-115
@@ -3922,6 +3922,8 @@ def _regenerate_element_body(doc, element_id, src_obj, meta, geom, geschoss_name
|
|||||||
if meta["type"] == "wand_axis":
|
if meta["type"] == "wand_axis":
|
||||||
uk, ok = _resolve_uk_ok(doc, meta["geschoss"],
|
uk, ok = _resolve_uk_ok(doc, meta["geschoss"],
|
||||||
meta["uk_override"], meta["ok_override"])
|
meta["uk_override"], meta["ok_override"])
|
||||||
|
print("[ELEMENTE] regen wand {}: uk={:.3f} ok={:.3f} (uk_over='{}' ok_over='{}')".format(
|
||||||
|
element_id, uk, ok, meta.get("uk_override", ""), meta.get("ok_override", "")))
|
||||||
# Wand-Verbindungen: Miter-Linien aus Nachbarwand-Joints (Corner + T).
|
# Wand-Verbindungen: Miter-Linien aus Nachbarwand-Joints (Corner + T).
|
||||||
miter_start = None
|
miter_start = None
|
||||||
miter_end = None
|
miter_end = None
|
||||||
@@ -5580,7 +5582,19 @@ class ElementeBridge(panel_base.BaseBridge):
|
|||||||
oeff_sims_in=simsi_def,
|
oeff_sims_in=simsi_def,
|
||||||
oeff_glas=glas_def,
|
oeff_glas=glas_def,
|
||||||
oeff_referenz=referenz_def)
|
oeff_referenz=referenz_def)
|
||||||
new_id = doc.Objects.AddPoint(on_axis, attrs)
|
# Oeffnungs-Punkt auf UK+Brueestung-Hoehe platzieren (= visuell auf
|
||||||
|
# Unterkante Oeffnung). Constraint vergleicht spaeter pt.Z mit
|
||||||
|
# UK+brueest — wenn der Punkt am axis.Z=0 saesse, wuerde der erste
|
||||||
|
# Move die Brueest auf 0 droppen. Hier UK auflösen (Geschoss-OKFF +
|
||||||
|
# ggf. Override) und Punkt direkt auf richtige Welt-Z setzen.
|
||||||
|
try:
|
||||||
|
wall_uk, _ = _resolve_uk_ok(doc, geschoss,
|
||||||
|
wall_meta.get("uk_override", ""),
|
||||||
|
wall_meta.get("ok_override", ""))
|
||||||
|
except Exception:
|
||||||
|
wall_uk = 0.0
|
||||||
|
pt_at_brueest = rg.Point3d(on_axis.X, on_axis.Y, wall_uk + float(brueest))
|
||||||
|
new_id = doc.Objects.AddPoint(pt_at_brueest, attrs)
|
||||||
if new_id == System.Guid.Empty:
|
if new_id == System.Guid.Empty:
|
||||||
print("[ELEMENTE] AddPoint fehlgeschlagen"); return
|
print("[ELEMENTE] AddPoint fehlgeschlagen"); return
|
||||||
|
|
||||||
@@ -6736,10 +6750,21 @@ def _apply_wand_z_drag_constraint(new_obj, meta):
|
|||||||
meta["uk_override"], meta["ok_override"])
|
meta["uk_override"], meta["ok_override"])
|
||||||
new_uk = uk_cur + delta
|
new_uk = uk_cur + delta
|
||||||
new_ok = ok_cur + delta
|
new_ok = ok_cur + delta
|
||||||
|
print("[ELEMENTE] wand z-drag: uk_cur={:.3f} ok_cur={:.3f} new_uk={:.3f} new_ok={:.3f} (meta uk_over='{}' ok_over='{}')".format(
|
||||||
|
uk_cur, ok_cur, new_uk, new_ok, meta.get("uk_override", ""), meta.get("ok_override", "")))
|
||||||
attrs = new_obj.Attributes.Duplicate()
|
attrs = new_obj.Attributes.Duplicate()
|
||||||
attrs.SetUserString(_KEY_UK_OVER, "{:.6f}".format(new_uk))
|
attrs.SetUserString(_KEY_UK_OVER, "{:.6f}".format(new_uk))
|
||||||
attrs.SetUserString(_KEY_OK_OVER, "{:.6f}".format(new_ok))
|
attrs.SetUserString(_KEY_OK_OVER, "{:.6f}".format(new_ok))
|
||||||
doc.Objects.ModifyAttributes(new_obj.Id, attrs, True)
|
mod_ok = doc.Objects.ModifyAttributes(new_obj.Id, attrs, True)
|
||||||
|
# Verifikation: UK_OVER wirklich in Doc geschrieben?
|
||||||
|
verify = doc.Objects.FindId(new_obj.Id)
|
||||||
|
if verify is not None:
|
||||||
|
actual_uk = verify.Attributes.GetUserString(_KEY_UK_OVER) or "<empty>"
|
||||||
|
actual_ok = verify.Attributes.GetUserString(_KEY_OK_OVER) or "<empty>"
|
||||||
|
print("[ELEMENTE] wand z-drag ModifyAttributes returned={} → stored uk_over='{}' ok_over='{}'".format(
|
||||||
|
mod_ok, actual_uk, actual_ok))
|
||||||
|
else:
|
||||||
|
print("[ELEMENTE] wand z-drag verify: FindId returned None!")
|
||||||
# Curve auf Z=0 fixen. LineCurve: explizit beide Endpunkte (auch bei
|
# Curve auf Z=0 fixen. LineCurve: explizit beide Endpunkte (auch bei
|
||||||
# einzelnem End-Grip-Drag). Andere Curves: ueber Translation (akzeptiert
|
# einzelnem End-Grip-Drag). Andere Curves: ueber Translation (akzeptiert
|
||||||
# leichten Schraeg bei End-Grip-Drag, gleicht sich beim naechsten
|
# leichten Schraeg bei End-Grip-Drag, gleicht sich beim naechsten
|
||||||
@@ -6801,6 +6826,7 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
|||||||
|
|
||||||
parent_id = meta.get("oeff_parent")
|
parent_id = meta.get("oeff_parent")
|
||||||
parent_curve = None
|
parent_curve = None
|
||||||
|
parent_meta = None
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is not None and parent_id:
|
if doc is not None and parent_id:
|
||||||
for obj in doc.Objects:
|
for obj in doc.Objects:
|
||||||
@@ -6809,10 +6835,20 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
|||||||
cg = obj.Geometry
|
cg = obj.Geometry
|
||||||
if isinstance(cg, rg.Curve):
|
if isinstance(cg, rg.Curve):
|
||||||
parent_curve = cg
|
parent_curve = cg
|
||||||
|
parent_meta = m
|
||||||
break
|
break
|
||||||
|
|
||||||
target_x, target_y = pt_new.X, pt_new.Y
|
target_x, target_y = pt_new.X, pt_new.Y
|
||||||
if parent_curve is not None:
|
# Wenn die Wand gerade migrate'd wurde (Rotation/Reshape/XY-Move) →
|
||||||
|
# XY-Projektion HIER UEBERSPRINGEN. Migrate hat den Punkt schon per
|
||||||
|
# Bogenlaengen-Mapping auf die neue Achse gesetzt. Eine zweite XY-
|
||||||
|
# Projektion mit ClosestPoint(pt_old) auf der NEUEN Achse wuerde die
|
||||||
|
# Position wieder verschieben (Rotation: pt_old liegt nicht mehr auf
|
||||||
|
# der neuen Achse → ClosestPoint+Tangent stimmen nicht zusammen).
|
||||||
|
migrated_walls = sc.sticky.get("_dossier_migrated_walls")
|
||||||
|
skip_xy_projection = (isinstance(migrated_walls, set)
|
||||||
|
and parent_id in migrated_walls)
|
||||||
|
if parent_curve is not None and not skip_xy_projection:
|
||||||
if pt_old is not None:
|
if pt_old is not None:
|
||||||
try:
|
try:
|
||||||
rc, t_old = parent_curve.ClosestPoint(
|
rc, t_old = parent_curve.ClosestPoint(
|
||||||
@@ -6850,17 +6886,33 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
|
|||||||
cur_bruest_val = float(cur_bruest) if cur_bruest not in (None, "") else 0.9
|
cur_bruest_val = float(cur_bruest) if cur_bruest not in (None, "") else 0.9
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
cur_bruest_val = 0.9
|
cur_bruest_val = 0.9
|
||||||
# Z-Delta = Drag in Z. Der Punkt SITZT visuell auf Bruestung-Hoehe
|
# Z-Delta gegen den ERWARTETEN Welt-Z des Punktes = Wand-UK + Brueest.
|
||||||
# (siehe Geometry-Schreibung unten), daher pt_old.Z ~= alte Bruestung.
|
# Bruestung ist relativ zur Wand-UK gespeichert. Wenn die Wand
|
||||||
delta_z = (pt_new.Z - pt_old.Z) if pt_old is not None else (pt_new.Z - cur_bruest_val)
|
# hochgezogen wurde (UK_OVER += z_delta) und der Wand-Loop den
|
||||||
|
# Oeffnungs-Punkt um z_delta translatet hat, sitzt der Punkt jetzt auf
|
||||||
|
# `new_UK + cur_brueest` = `expected_pt_z`. delta_z = 0 → kein
|
||||||
|
# Bruestungs-Update (gut so, sonst doppelt). Wenn der User nur den
|
||||||
|
# Punkt allein vertikal gezogen hat (Brueestung-Drag), divergiert
|
||||||
|
# pt_new.Z vom expected_pt_z → delta_z entspricht der echten User-
|
||||||
|
# Eingabe → Bruestung wird angepasst.
|
||||||
|
wall_uk = 0.0
|
||||||
|
if parent_meta is not None:
|
||||||
|
try:
|
||||||
|
wall_uk, _ = _resolve_uk_ok(doc, parent_meta["geschoss"],
|
||||||
|
parent_meta["uk_override"],
|
||||||
|
parent_meta["ok_override"])
|
||||||
|
except Exception:
|
||||||
|
wall_uk = 0.0
|
||||||
|
expected_pt_z = wall_uk + cur_bruest_val
|
||||||
|
delta_z = pt_new.Z - expected_pt_z
|
||||||
new_bruest = cur_bruest_val
|
new_bruest = cur_bruest_val
|
||||||
if abs(delta_z) >= 1e-6:
|
if abs(delta_z) >= 1e-6:
|
||||||
new_bruest = max(0.0, cur_bruest_val + delta_z)
|
new_bruest = max(0.0, cur_bruest_val + delta_z)
|
||||||
|
|
||||||
# Punkt visuell auf Bruestungs-Hoehe (= Unterkante Oeffnung), nicht auf 0.
|
# Punkt visuell auf der Unterkante der Oeffnung in Welt-Z platzieren =
|
||||||
# So sieht der User wo die Oeffnung beginnt + Z-Drag-Delta entspricht
|
# Wand-UK + Brueest. So sieht der User wo die Oeffnung beginnt, auch
|
||||||
# direkt der Bruestungsaenderung.
|
# wenn die Wand auf einem hoeheren Geschoss steht.
|
||||||
target_z = new_bruest
|
target_z = wall_uk + new_bruest
|
||||||
geom_changed = not (
|
geom_changed = not (
|
||||||
abs(target_x - pt_new.X) < 1e-9
|
abs(target_x - pt_new.X) < 1e-9
|
||||||
and abs(target_y - pt_new.Y) < 1e-9
|
and abs(target_y - pt_new.Y) < 1e-9
|
||||||
@@ -6901,10 +6953,15 @@ def _on_object_replaced(sender, e):
|
|||||||
"""
|
"""
|
||||||
if sc.sticky.get(_REGEN_BUSY): return
|
if sc.sticky.get(_REGEN_BUSY): return
|
||||||
# Wenn ein User-Transform-Command (_Move/_Rotate/etc.) aktiv ist: GAR
|
# Wenn ein User-Transform-Command (_Move/_Rotate/etc.) aktiv ist: GAR
|
||||||
# NICHTS hier tun. Rhinos Move soll konfliktfrei durchlaufen. Nach
|
# NICHTS hier tun (Rhinos Move soll konfliktfrei durchlaufen). Erstes
|
||||||
# CommandEnd vergleichen wir Snapshot vs. aktuellen State + machen den
|
# Event = User hat geklickt → Redraw ab jetzt suppressen, sonst Mismatch-
|
||||||
# ganzen Update in einem konfliktfreien Batch.
|
# Frame zwischen Rhinos Auto-Redraw und unserem Regen.
|
||||||
if sc.sticky.get(_UT_ACTIVE_KEY): return
|
if sc.sticky.get(_UT_ACTIVE_KEY):
|
||||||
|
_suppress_redraw_until_cmd_end()
|
||||||
|
return
|
||||||
|
# Undo/Redo: Rhino restored den Zustand → wir machen NICHTS, sonst
|
||||||
|
# Regen-Storm fuer jedes restored Object.
|
||||||
|
if sc.sticky.get(_UNDO_ACTIVE_KEY): return
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
# Snapshot der aktuell selektierten IDs — damit Migrate die Objekte
|
# Snapshot der aktuell selektierten IDs — damit Migrate die Objekte
|
||||||
# skippen kann die Rhinos Move/Rotate gerade transformiert (sonst
|
# skippen kann die Rhinos Move/Rotate gerade transformiert (sonst
|
||||||
@@ -7044,12 +7101,20 @@ def _on_object_replaced_body(sender, e):
|
|||||||
print("[ELEMENTE] on_object_replaced:", ex)
|
print("[ELEMENTE] on_object_replaced:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
|
def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom, old_positions=None):
|
||||||
"""Verschiebt alle Oeffnungs-Points einer Wand mit, wenn deren Achse
|
"""Verschiebt alle Oeffnungs-Points einer Wand mit, wenn deren Achse
|
||||||
veraendert wird. Mapping ueber relative Bogenlaenge: ein Oeffnungs-
|
veraendert wird. Mapping ueber relative Bogenlaenge: ein Oeffnungs-
|
||||||
Punkt bei 30 % der alten Kurve sitzt nachher bei 30 % der neuen.
|
Punkt bei 30 % der alten Kurve sitzt nachher bei 30 % der neuen.
|
||||||
So bleiben die Oeffnungen 'sticky' an der Wand bei Verschieben,
|
So bleiben die Oeffnungen 'sticky' an der Wand bei Verschieben,
|
||||||
Drehen, Skalieren oder Reshape der Achse."""
|
Drehen, Skalieren oder Reshape der Achse.
|
||||||
|
|
||||||
|
`old_positions` (optional): {opening_id: (x, y, z)} — Pre-Transform
|
||||||
|
Snapshot der Oeffnungs-Punkte. WICHTIG bei Rotation/Move: nach Rhinos
|
||||||
|
Transform liegen die Punkte schon NICHT MEHR auf der alten Axis →
|
||||||
|
`ClosestPoint(current_pos)` an old_geom snappt zum naechsten Endpunkt
|
||||||
|
statt zur echten Bogenlaengen-Position → alle Oeffnungen landen am
|
||||||
|
selben Ende. Bei Reshape-Operationen ohne Snapshot: Fallback auf
|
||||||
|
aktuelle Geometrie (Punkt liegt dort noch auf alter Axis)."""
|
||||||
if not isinstance(old_geom, rg.Curve) or not isinstance(new_geom, rg.Curve):
|
if not isinstance(old_geom, rg.Curve) or not isinstance(new_geom, rg.Curve):
|
||||||
return
|
return
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -7059,6 +7124,29 @@ def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
|
|||||||
new_len = new_geom.GetLength()
|
new_len = new_geom.GetLength()
|
||||||
except Exception: return
|
except Exception: return
|
||||||
if old_len < 1e-9 or new_len < 1e-9: return
|
if old_len < 1e-9 or new_len < 1e-9: return
|
||||||
|
# Wand-UK aufloesen damit Oeffnungs-Punkte auf UK+Brueestung gesetzt
|
||||||
|
# werden (= visuell auf Unterkante Oeffnung). Sonst landen sie auf
|
||||||
|
# reiner Brueest-Hoehe und der nachfolgende Constraint interpretiert
|
||||||
|
# die Diskrepanz als User-Z-Drag → Brueest dropt.
|
||||||
|
wall_uk = 0.0
|
||||||
|
src = _find_axis(doc, wall_id)
|
||||||
|
if src is not None:
|
||||||
|
wm = _read_meta(src)
|
||||||
|
if wm:
|
||||||
|
try:
|
||||||
|
wall_uk, _ = _resolve_uk_ok(doc, wm["geschoss"],
|
||||||
|
wm["uk_override"],
|
||||||
|
wm["ok_override"])
|
||||||
|
except Exception:
|
||||||
|
wall_uk = 0.0
|
||||||
|
# Migrierten Wand registrieren — der Constraint soll fuer Oeffnungen
|
||||||
|
# dieser Wand die XY-Projektion ueberspringen (migrate hat XY bereits
|
||||||
|
# via Bogenlaengen-Mapping korrekt gesetzt).
|
||||||
|
migrated = sc.sticky.get("_dossier_migrated_walls")
|
||||||
|
if not isinstance(migrated, set):
|
||||||
|
migrated = set()
|
||||||
|
migrated.add(wall_id)
|
||||||
|
sc.sticky["_dossier_migrated_walls"] = migrated
|
||||||
|
|
||||||
# Selected-Snapshot vom Replace-Handler — nicht live IsSelected, weil
|
# Selected-Snapshot vom Replace-Handler — nicht live IsSelected, weil
|
||||||
# op_obj im laufenden Move-Event evtl. schon stale ist.
|
# op_obj im laufenden Move-Event evtl. schon stale ist.
|
||||||
@@ -7079,18 +7167,27 @@ def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
|
|||||||
# transform" + ganzer Regen-Undo-Record wird rollbacked.
|
# transform" + ganzer Regen-Undo-Record wird rollbacked.
|
||||||
if str(op_obj.Id) in skip_ids:
|
if str(op_obj.Id) in skip_ids:
|
||||||
continue
|
continue
|
||||||
pt_geom = op_obj.Geometry
|
# Pre-Transform Position bevorzugen — die liegt garantiert
|
||||||
if hasattr(pt_geom, 'Location'):
|
# auf der alten Axis. Aktuelle (post-transform) Position kann
|
||||||
cur_pos = pt_geom.Location
|
# bei Rotation weit weg liegen → ClosestPoint snappt zum
|
||||||
elif isinstance(pt_geom, rg.Point3d):
|
# falschen Endpunkt.
|
||||||
cur_pos = pt_geom
|
src_pos = None
|
||||||
else:
|
if old_positions is not None:
|
||||||
continue
|
src_pos = old_positions.get(op_meta["id"])
|
||||||
|
if src_pos is None:
|
||||||
|
pt_geom = op_obj.Geometry
|
||||||
|
if hasattr(pt_geom, 'Location'):
|
||||||
|
loc = pt_geom.Location
|
||||||
|
src_pos = (loc.X, loc.Y, loc.Z)
|
||||||
|
elif isinstance(pt_geom, rg.Point3d):
|
||||||
|
src_pos = (pt_geom.X, pt_geom.Y, pt_geom.Z)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
# XY-only ClosestPoint — sonst zieht eine non-zero Z-Komponente
|
# XY-only ClosestPoint — sonst zieht eine non-zero Z-Komponente
|
||||||
# (Bruestungs-Hoehe) den Parameter bei kurvigen Wand-Achsen
|
# (Bruestungs-Hoehe) den Parameter bei kurvigen Wand-Achsen
|
||||||
# leicht weg von der „echten" Position.
|
# leicht weg von der „echten" Position.
|
||||||
ok_old, t_old = old_geom.ClosestPoint(
|
ok_old, t_old = old_geom.ClosestPoint(
|
||||||
rg.Point3d(cur_pos.X, cur_pos.Y, 0.0))
|
rg.Point3d(src_pos[0], src_pos[1], 0.0))
|
||||||
if not ok_old: continue
|
if not ok_old: continue
|
||||||
# Bogenlaenge auf alter Kurve bis t_old → relative Position
|
# Bogenlaenge auf alter Kurve bis t_old → relative Position
|
||||||
sub = rg.Interval(old_geom.Domain.Min, t_old)
|
sub = rg.Interval(old_geom.Domain.Min, t_old)
|
||||||
@@ -7126,7 +7223,9 @@ def _migrate_openings_to_new_axis(wall_id, old_geom, new_geom):
|
|||||||
bruest_z = float(bruest) if bruest not in (None, "") else 0.0
|
bruest_z = float(bruest) if bruest not in (None, "") else 0.0
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
bruest_z = 0.0
|
bruest_z = 0.0
|
||||||
new_pos = rg.Point3d(new_pos.X, new_pos.Y, bruest_z)
|
# Welt-Z = Wand-UK + Brueestung (Konvention: Punkt sitzt
|
||||||
|
# visuell auf Unterkante Oeffnung).
|
||||||
|
new_pos = rg.Point3d(new_pos.X, new_pos.Y, wall_uk + bruest_z)
|
||||||
doc.Objects.Replace(op_obj.Id, rg.Point(new_pos))
|
doc.Objects.Replace(op_obj.Id, rg.Point(new_pos))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] migrate one opening:", ex)
|
print("[ELEMENTE] migrate one opening:", ex)
|
||||||
@@ -7159,6 +7258,14 @@ def _on_object_added(sender, e):
|
|||||||
neue UUID, Volume-Duplikate werden geloescht (Regen baut das neue
|
neue UUID, Volume-Duplikate werden geloescht (Regen baut das neue
|
||||||
Volumen am richtigen Ort)."""
|
Volumen am richtigen Ort)."""
|
||||||
if sc.sticky.get(_REGEN_BUSY): return
|
if sc.sticky.get(_REGEN_BUSY): return
|
||||||
|
# Waehrend Move/Rotate/Mirror/Scale: Rhino feuert intern Delete+Add fuer
|
||||||
|
# jedes transformierte Objekt. CommandEnd uebernimmt die Re-Sync —
|
||||||
|
# diese Events ignorieren, sonst laeuft die Regen-Pipeline trotz
|
||||||
|
# Pure-Translate-Skip.
|
||||||
|
if sc.sticky.get(_UT_ACTIVE_KEY):
|
||||||
|
_suppress_redraw_until_cmd_end()
|
||||||
|
return
|
||||||
|
if sc.sticky.get(_UNDO_ACTIVE_KEY): return
|
||||||
try:
|
try:
|
||||||
new_obj = e.TheObject
|
new_obj = e.TheObject
|
||||||
meta = _read_meta(new_obj)
|
meta = _read_meta(new_obj)
|
||||||
@@ -7254,6 +7361,13 @@ def _on_object_deleted(sender, e):
|
|||||||
wenn die Source mit gleicher ID zurueckkommt (= Transform, kein User-
|
wenn die Source mit gleicher ID zurueckkommt (= Transform, kein User-
|
||||||
Delete).
|
Delete).
|
||||||
"""
|
"""
|
||||||
|
# Waehrend Move/Rotate/Mirror/Scale: CommandEnd-Pfad uebernimmt das
|
||||||
|
# Re-Sync. Sonst queued der Delete-Event ueberfluessige Regen-Calls die
|
||||||
|
# den Pure-Translate-Skip wieder zunichtemachen.
|
||||||
|
if sc.sticky.get(_UT_ACTIVE_KEY):
|
||||||
|
_suppress_redraw_until_cmd_end()
|
||||||
|
return
|
||||||
|
if sc.sticky.get(_UNDO_ACTIVE_KEY): return
|
||||||
try:
|
try:
|
||||||
obj = e.TheObject
|
obj = e.TheObject
|
||||||
meta = _read_meta(obj)
|
meta = _read_meta(obj)
|
||||||
@@ -7688,8 +7802,15 @@ _USER_TRANSFORM_CMDS = frozenset((
|
|||||||
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
"Drag", "Gumball", "Orient", "Orient3Pt", "RemapCPlane", "Transform",
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# Undo/Redo: Rhino restored Objekte aus dem Undo-Stack → feuert Add/Delete-
|
||||||
|
# Events fuer ALLE betroffenen Objekte. Unsere Handler wuerden fuer jedes
|
||||||
|
# einen Regen queuen → Storm. Wir suppressen die Handler komplett; Undo hat
|
||||||
|
# den Zustand schon konsistent wiederhergestellt, kein Regen noetig.
|
||||||
|
_USER_UNDO_CMDS = frozenset(("Undo", "Redo"))
|
||||||
|
|
||||||
_UT_ACTIVE_KEY = "_dossier_user_transform_active"
|
_UT_ACTIVE_KEY = "_dossier_user_transform_active"
|
||||||
_UT_SNAPSHOT_KEY = "_dossier_pre_transform_snapshot"
|
_UT_SNAPSHOT_KEY = "_dossier_pre_transform_snapshot"
|
||||||
|
_UNDO_ACTIVE_KEY = "_dossier_undo_active"
|
||||||
|
|
||||||
|
|
||||||
def _snapshot_source_positions(doc):
|
def _snapshot_source_positions(doc):
|
||||||
@@ -7697,22 +7818,30 @@ def _snapshot_source_positions(doc):
|
|||||||
BBox-Centers. Source-Map (key=element_id) füttert Constraint+Migrate.
|
BBox-Centers. Source-Map (key=element_id) füttert Constraint+Migrate.
|
||||||
Volume-Map (key=obj.Id-string) erlaubt im CommandEnd die Pure-Translate-
|
Volume-Map (key=obj.Id-string) erlaubt im CommandEnd die Pure-Translate-
|
||||||
Detection — wir checken pro Volume ob es schon vom Rhinos Move
|
Detection — wir checken pro Volume ob es schon vom Rhinos Move
|
||||||
transformed wurde, oder noch ge-translaten werden muss."""
|
transformed wurde, oder noch ge-translaten werden muss.
|
||||||
snap = {"sources": {}, "volumes": {}}
|
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
|
if doc is None: return snap
|
||||||
for obj in doc.Objects:
|
for obj in doc.Objects:
|
||||||
try:
|
try:
|
||||||
|
snap["obj_ids"].add(str(obj.Id))
|
||||||
m = _read_meta(obj)
|
m = _read_meta(obj)
|
||||||
if not m: continue
|
if not m: continue
|
||||||
t = m.get("type")
|
t = m.get("type")
|
||||||
geom = obj.Geometry
|
geom = obj.Geometry
|
||||||
if t in SOURCE_TYPES:
|
if t in SOURCE_TYPES:
|
||||||
|
parent = m.get("oeff_parent") or ""
|
||||||
if hasattr(geom, "Location"):
|
if hasattr(geom, "Location"):
|
||||||
p = geom.Location
|
p = geom.Location
|
||||||
snap["sources"][m["id"]] = {"type": t, "pos": (p.X, p.Y, p.Z)}
|
snap["sources"][m["id"]] = {"type": t,
|
||||||
|
"oeff_parent": parent,
|
||||||
|
"pos": (p.X, p.Y, p.Z)}
|
||||||
elif isinstance(geom, rg.Curve):
|
elif isinstance(geom, rg.Curve):
|
||||||
s = geom.PointAtStart; e = geom.PointAtEnd
|
s = geom.PointAtStart; e = geom.PointAtEnd
|
||||||
snap["sources"][m["id"]] = {"type": t,
|
snap["sources"][m["id"]] = {"type": t,
|
||||||
|
"oeff_parent": parent,
|
||||||
"start": (s.X, s.Y, s.Z),
|
"start": (s.X, s.Y, s.Z),
|
||||||
"end": (e.X, e.Y, e.Z)}
|
"end": (e.X, e.Y, e.Z)}
|
||||||
elif t in VOLUME_TYPES:
|
elif t in VOLUME_TYPES:
|
||||||
@@ -7728,56 +7857,271 @@ def _snapshot_source_positions(doc):
|
|||||||
return snap
|
return snap
|
||||||
|
|
||||||
|
|
||||||
|
def _suppress_redraw_until_cmd_end():
|
||||||
|
"""Schaltet RedrawEnabled erst auf False sobald das ERSTE Object-Event
|
||||||
|
waehrend eines User-Transform-Commands feuert. Damit bleiben Rubber-
|
||||||
|
Band-Linie und Drag-Vorschau waehrend des Pickings sichtbar (Picking
|
||||||
|
feuert keine Object-Events), aber Rhinos automatischer Post-Move-
|
||||||
|
Redraw (kommt nach dem Klick, direkt nach den Replace-Events) wird
|
||||||
|
unterdrueckt. Wird im selben Command nur einmal aktiv."""
|
||||||
|
if sc.sticky.get("_dossier_cmd_redraw_suppressed"): return
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
try:
|
||||||
|
sc.sticky["_dossier_cmd_redraw_prev"] = bool(doc.Views.RedrawEnabled)
|
||||||
|
doc.Views.RedrawEnabled = False
|
||||||
|
sc.sticky["_dossier_cmd_redraw_suppressed"] = True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] suppress redraw:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _on_command_begin(sender, e):
|
def _on_command_begin(sender, e):
|
||||||
try:
|
try:
|
||||||
name = getattr(e, "CommandEnglishName", "") or ""
|
name = getattr(e, "CommandEnglishName", "") or ""
|
||||||
except Exception: name = ""
|
except Exception: name = ""
|
||||||
if name not in _USER_TRANSFORM_CMDS: return
|
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
|
# Undo/Redo: nur Flag setzen, KEIN Snapshot, KEIN Redraw-Suppress —
|
||||||
|
# Rhinos Undo verwaltet RedrawEnabled selbst. Event-Handler ignorieren
|
||||||
|
# waehrend dieser Phase alle Add/Delete/Replace-Events → kein Regen-
|
||||||
|
# Storm.
|
||||||
|
if name in _USER_UNDO_CMDS:
|
||||||
|
sc.sticky[_UNDO_ACTIVE_KEY] = name
|
||||||
|
return
|
||||||
|
if name not in _USER_TRANSFORM_CMDS: return
|
||||||
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
sc.sticky[_UT_SNAPSHOT_KEY] = _snapshot_source_positions(doc)
|
||||||
sc.sticky[_UT_ACTIVE_KEY] = name
|
sc.sticky[_UT_ACTIVE_KEY] = name
|
||||||
|
# RedrawEnabled bleibt HIER auf True. Wird erst beim ersten Object-Event
|
||||||
|
# (= nach dem Klick) via `_suppress_redraw_until_cmd_end` ausgeschaltet.
|
||||||
|
# Rubber-Band-Linie + Drag-Vorschau bleiben dadurch wahrend Picking
|
||||||
|
# sichtbar.
|
||||||
|
# Undo-Record umschliesst Rhinos Move + unseren Regen in EINEM Undo-
|
||||||
|
# Schritt. Sonst macht jedes Delete/AddBrep eine eigene Undo-Entry und
|
||||||
|
# Cmd+Z bringt nur halbe Wand zurueck → Duplikate.
|
||||||
|
try:
|
||||||
|
serial = doc.BeginUndoRecord("Element-Transform")
|
||||||
|
sc.sticky["_dossier_undo_serial"] = serial
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] cmd-begin undo record:", ex)
|
||||||
|
sc.sticky["_dossier_undo_serial"] = None
|
||||||
|
|
||||||
|
|
||||||
def _on_command_end(sender, e):
|
def _on_command_end(sender, e):
|
||||||
|
# Undo/Redo abschliessen: nur Flag clearen, kein Regen + ein Selection-
|
||||||
|
# Refresh fuers Gestaltung-Panel (Listener waren waehrend Undo aus).
|
||||||
|
if sc.sticky.get(_UNDO_ACTIVE_KEY):
|
||||||
|
sc.sticky[_UNDO_ACTIVE_KEY] = None
|
||||||
|
gb = sc.sticky.get("gestaltung_bridge")
|
||||||
|
if gb is not None:
|
||||||
|
try: gb._send_selection()
|
||||||
|
except Exception: pass
|
||||||
|
b = sc.sticky.get("elemente_bridge")
|
||||||
|
if b is not None:
|
||||||
|
try: b._send_state()
|
||||||
|
except Exception: pass
|
||||||
|
return
|
||||||
name = sc.sticky.get(_UT_ACTIVE_KEY)
|
name = sc.sticky.get(_UT_ACTIVE_KEY)
|
||||||
if not name: return
|
if not name: return
|
||||||
sc.sticky[_UT_ACTIVE_KEY] = None
|
# _UT_ACTIVE_KEY bleibt gesetzt bis am Ende der Funktion — sonst feuern
|
||||||
|
# gestaltungs Listener auf die Replace-Events die wir hier selber
|
||||||
|
# erzeugen (Pure-Translate translates Volumen via Replace; Regen-Pfad
|
||||||
|
# ersetzt Sub-Volumen). Cleanup im finally-Block am Ende.
|
||||||
snapshot = sc.sticky.get(_UT_SNAPSHOT_KEY) or {}
|
snapshot = sc.sticky.get(_UT_SNAPSHOT_KEY) or {}
|
||||||
sc.sticky[_UT_SNAPSHOT_KEY] = None
|
sc.sticky[_UT_SNAPSHOT_KEY] = None
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
if doc is None: return
|
if doc is None:
|
||||||
|
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||||
|
sc.sticky["_dossier_cmd_redraw_suppressed"] = None
|
||||||
|
sc.sticky["_dossier_cmd_redraw_prev"] = None
|
||||||
|
sc.sticky["_dossier_undo_serial"] = None
|
||||||
|
return
|
||||||
|
|
||||||
|
# RedrawEnabled wurde idR schon beim ersten Object-Event nach dem
|
||||||
|
# User-Klick auf False gesetzt (`_suppress_redraw_until_cmd_end`). Den
|
||||||
|
# gemerkten prev-Wert lesen. Falls kein Event gefeuert hat (z.B. Move
|
||||||
|
# ohne tatsaechliche Aenderung), suppressen wir jetzt selber.
|
||||||
|
if sc.sticky.get("_dossier_cmd_redraw_suppressed"):
|
||||||
|
prev_redraw_enabled = sc.sticky.get("_dossier_cmd_redraw_prev", True)
|
||||||
|
sc.sticky["_dossier_cmd_redraw_suppressed"] = None
|
||||||
|
sc.sticky["_dossier_cmd_redraw_prev"] = None
|
||||||
|
else:
|
||||||
|
prev_redraw_enabled = doc.Views.RedrawEnabled
|
||||||
|
doc.Views.RedrawEnabled = False
|
||||||
|
|
||||||
sources_snap = snapshot.get("sources", {}) if isinstance(snapshot, dict) else {}
|
sources_snap = snapshot.get("sources", {}) if isinstance(snapshot, dict) else {}
|
||||||
volumes_snap = snapshot.get("volumes", {}) 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-Translate-Detection ───────────────────────────────────────────
|
# ─── Mirror/Copy-Duplikat-Detection ─────────────────────────────────────
|
||||||
# Wenn ALLE bewegten Sources um den exakt gleichen Vektor verschoben
|
# Rhinos Mirror/Copy/Array erzeugt KOPIEN selektierter Objekte mit ihren
|
||||||
# wurden, KEIN Z-Drag (= Property-Aenderung), KEIN Rotate/Scale (= End-
|
# UserStrings (= Metadata). Resultat: Duplikat-IDs im Doc — z.B. zwei
|
||||||
# Grip-Drag) — dann reicht eine reine Translation aller noch unbewegten
|
# `wand_axis` mit `id=wall_xxx`. Unser System haelt die fuer „dasselbe
|
||||||
# Volumen + Punkte. KEIN Wand-Regen, KEIN Boolean-Diff. Geht instant.
|
# Element", was zu „verkoppelten" Elementen fuehrt und zu kaputten
|
||||||
def _source_delta(obj, old):
|
# 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-
|
||||||
|
# Anwendung auf alle noch unbewegten Volumen + Punkte. KEIN Wand-Regen,
|
||||||
|
# KEIN Boolean-Diff. Geht instant.
|
||||||
|
import math as _math
|
||||||
|
|
||||||
|
def _source_rigid_transform(obj, old):
|
||||||
|
"""Berechnet den Rigid-2D-Transform (Translation + Z-Rotation) der
|
||||||
|
alte Source-Geometrie auf die aktuelle abbildet. Returns None wenn
|
||||||
|
Z-Drag/Scale/End-Grip/Mirror erkannt."""
|
||||||
geom = obj.Geometry
|
geom = obj.Geometry
|
||||||
if hasattr(geom, "Location"):
|
|
||||||
op = old.get("pos")
|
|
||||||
if op is None: return None
|
|
||||||
p = geom.Location
|
|
||||||
return (p.X - op[0], p.Y - op[1], p.Z - op[2])
|
|
||||||
if isinstance(geom, rg.Curve):
|
if isinstance(geom, rg.Curve):
|
||||||
os_pt = old.get("start"); oe_pt = old.get("end")
|
os_pt = old.get("start"); oe_pt = old.get("end")
|
||||||
if os_pt is None or oe_pt is None: return None
|
if os_pt is None or oe_pt is None: return None
|
||||||
ns = geom.PointAtStart; ne = geom.PointAtEnd
|
ns = geom.PointAtStart; ne = geom.PointAtEnd
|
||||||
ds = (ns.X - os_pt[0], ns.Y - os_pt[1], ns.Z - os_pt[2])
|
# Z-Aenderung verbietet Pure-Transform (= Z-Drag → UK_OVER muss
|
||||||
de = (ne.X - oe_pt[0], ne.Y - oe_pt[1], ne.Z - oe_pt[2])
|
# geschrieben werden → Regen-Pfad).
|
||||||
# Beide Endpunkte muessen den gleichen Vektor haben (= Translate).
|
if (abs(ns.Z - os_pt[2]) > 1e-6 or
|
||||||
# Sonst ist's Rotate/Scale/End-Grip-Drag.
|
abs(ne.Z - oe_pt[2]) > 1e-6):
|
||||||
if (abs(ds[0]-de[0]) > 1e-6 or abs(ds[1]-de[1]) > 1e-6
|
|
||||||
or abs(ds[2]-de[2]) > 1e-6):
|
|
||||||
return None
|
return None
|
||||||
return ds
|
old_dx = oe_pt[0] - os_pt[0]; old_dy = oe_pt[1] - os_pt[1]
|
||||||
|
new_dx = ne.X - ns.X; new_dy = ne.Y - ns.Y
|
||||||
|
old_len = _math.hypot(old_dx, old_dy)
|
||||||
|
new_len = _math.hypot(new_dx, new_dy)
|
||||||
|
if old_len < 1e-9: return None
|
||||||
|
# Laengenaenderung → Scale (oder einzelner Endpunkt-Drag)
|
||||||
|
if abs(old_len - new_len) > 1e-6: return None
|
||||||
|
# Drehwinkel um Z aus Richtungsvektoren
|
||||||
|
old_angle = _math.atan2(old_dy, old_dx)
|
||||||
|
new_angle = _math.atan2(new_dy, new_dx)
|
||||||
|
angle = new_angle - old_angle
|
||||||
|
# Transform: erst um old_start zentrieren, dann rotieren, dann
|
||||||
|
# zu new_start translaten. So mappen sowohl old_start→new_start
|
||||||
|
# als auch old_end→new_end korrekt.
|
||||||
|
to_origin = rg.Transform.Translation(-os_pt[0], -os_pt[1], -os_pt[2])
|
||||||
|
rotate = rg.Transform.Rotation(angle, rg.Vector3d.ZAxis, rg.Point3d.Origin)
|
||||||
|
to_new = rg.Transform.Translation(ns.X, ns.Y, ns.Z)
|
||||||
|
return to_new * rotate * to_origin
|
||||||
|
if hasattr(geom, "Location"):
|
||||||
|
op = old.get("pos")
|
||||||
|
if op is None: return None
|
||||||
|
p = geom.Location
|
||||||
|
# Punkt: keine Orientierungs-Info → nur Translation ableitbar.
|
||||||
|
# Konsistenz mit Curve-Transform wird in Phase 2 geprueft.
|
||||||
|
return rg.Transform.Translation(p.X - op[0], p.Y - op[1], p.Z - op[2])
|
||||||
return None
|
return None
|
||||||
|
|
||||||
deltas = {}
|
def _is_identity_transform(t, tol=1e-6):
|
||||||
|
for i in range(4):
|
||||||
|
for j in range(4):
|
||||||
|
ref = 1.0 if i == j else 0.0
|
||||||
|
if abs(t[i, j] - ref) > tol: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _transforms_equal(t1, t2, tol=1e-6):
|
||||||
|
for i in range(4):
|
||||||
|
for j in range(4):
|
||||||
|
if abs(t1[i, j] - t2[i, j]) > tol: return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Phase 1: Transform pro Source berechnen, abort bei non-rigid
|
||||||
|
source_transforms = {}
|
||||||
abort_pure = False
|
abort_pure = False
|
||||||
for obj in doc.Objects:
|
for obj in doc.Objects:
|
||||||
try:
|
try:
|
||||||
@@ -7786,54 +8130,123 @@ def _on_command_end(sender, e):
|
|||||||
if m.get("type") not in SOURCE_TYPES: continue
|
if m.get("type") not in SOURCE_TYPES: continue
|
||||||
old = sources_snap.get(m["id"])
|
old = sources_snap.get(m["id"])
|
||||||
if old is None: continue
|
if old is None: continue
|
||||||
d = _source_delta(obj, old)
|
t = _source_rigid_transform(obj, old)
|
||||||
if d is None:
|
if t is None:
|
||||||
# End-Grip-Drag o.ae. → kein Pure-Translate
|
|
||||||
abort_pure = True
|
abort_pure = True
|
||||||
break
|
break
|
||||||
deltas[m["id"]] = d
|
source_transforms[m["id"]] = t
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
pure_delta = None
|
# Phase 2: moved_ids + canonical (bevorzugt Curve-Source fuer
|
||||||
if not abort_pure:
|
# Rotations-Info; Points haben nur Translation)
|
||||||
moved = [d for d in deltas.values()
|
moved_ids = {eid for eid, t in source_transforms.items()
|
||||||
if abs(d[0]) > 1e-6 or abs(d[1]) > 1e-6 or abs(d[2]) > 1e-6]
|
if not _is_identity_transform(t)}
|
||||||
if moved:
|
canonical = None
|
||||||
# Alle bewegten Sources muessen denselben Vektor haben
|
for eid in moved_ids:
|
||||||
first = moved[0]
|
old = sources_snap.get(eid)
|
||||||
same = all(abs(d[0]-first[0]) < 1e-6 and
|
if old and "start" in old:
|
||||||
abs(d[1]-first[1]) < 1e-6 and
|
canonical = source_transforms[eid]
|
||||||
abs(d[2]-first[2]) < 1e-6 for d in moved)
|
break
|
||||||
# Z-Drag erkennen: wenn Wand-Achse Z aenderung hat → Brüstungs-
|
if canonical is None and moved_ids:
|
||||||
# Mitnahme noetig → kein Pure-Translate.
|
# Keine Curve bewegt → nimm irgendeinen Point-Transform
|
||||||
has_z_drag = abs(first[2]) > 1e-6
|
for eid in moved_ids:
|
||||||
if same and not has_z_drag:
|
canonical = source_transforms[eid]
|
||||||
pure_delta = first
|
break
|
||||||
|
|
||||||
|
# Phase 3: alle bewegten Sources MUESSEN canonical erfuellen
|
||||||
|
all_consistent = True
|
||||||
|
if canonical is not None and not abort_pure:
|
||||||
|
for eid in moved_ids:
|
||||||
|
old = sources_snap.get(eid)
|
||||||
|
if old is None: continue
|
||||||
|
if "start" in old:
|
||||||
|
# Curve: Transform muss canonical sein
|
||||||
|
if not _transforms_equal(source_transforms[eid], canonical):
|
||||||
|
all_consistent = False
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
# Point: canonical applied to old_pos muss aktuelle Position sein
|
||||||
|
op = old.get("pos")
|
||||||
|
if op is None: continue
|
||||||
|
expected = rg.Point3d(op[0], op[1], op[2])
|
||||||
|
expected.Transform(canonical)
|
||||||
|
actual = None
|
||||||
|
for obj in doc.Objects:
|
||||||
|
mm = _read_meta(obj)
|
||||||
|
if mm and mm.get("id") == eid and mm.get("type") == old.get("type"):
|
||||||
|
gg = obj.Geometry
|
||||||
|
if hasattr(gg, "Location"):
|
||||||
|
actual = gg.Location
|
||||||
|
break
|
||||||
|
if actual is None: continue
|
||||||
|
if (abs(actual.X - expected.X) > 1e-6 or
|
||||||
|
abs(actual.Y - expected.Y) > 1e-6 or
|
||||||
|
abs(actual.Z - expected.Z) > 1e-6):
|
||||||
|
all_consistent = False
|
||||||
|
break
|
||||||
|
|
||||||
|
# Orphan-Oeffnung erkennen: bewegte Oeffnung deren Eltern-Wand NICHT
|
||||||
|
# mitbewegt wurde. Cutout muss regen.
|
||||||
|
orphan_opening = False
|
||||||
|
for eid in moved_ids:
|
||||||
|
old = sources_snap.get(eid)
|
||||||
|
if old and old.get("type") == "oeffnung_point":
|
||||||
|
parent = old.get("oeff_parent")
|
||||||
|
if parent and parent not in moved_ids:
|
||||||
|
orphan_opening = True
|
||||||
|
break
|
||||||
|
|
||||||
|
pure_transform = None
|
||||||
|
if abort_pure:
|
||||||
|
print("[ELEMENTE] no pure-transform: z-drag/scale/end-grip detected")
|
||||||
|
elif orphan_opening:
|
||||||
|
print("[ELEMENTE] no pure-transform: opening moved without parent wall (cutout muss regen)")
|
||||||
|
elif not all_consistent:
|
||||||
|
print("[ELEMENTE] no pure-transform: sources moved with different transforms")
|
||||||
|
elif canonical is not None:
|
||||||
|
pure_transform = canonical
|
||||||
|
|
||||||
|
if pure_transform is not None:
|
||||||
|
# PURE-TRANSFORM PFAD: Transform auf alle Geometries anwenden die
|
||||||
|
# nicht schon vom User-Move transformed wurden. Funktioniert fuer
|
||||||
|
# Translation UND Rotation. → instant feedback.
|
||||||
|
tx = pure_transform[0, 3]
|
||||||
|
ty = pure_transform[1, 3]
|
||||||
|
tz = pure_transform[2, 3]
|
||||||
|
# Rotations-Anteil aus m00/m01 (Z-Rotation der 2x2 oberen Submatrix)
|
||||||
|
rot_deg = _math.degrees(_math.atan2(pure_transform[1, 0], pure_transform[0, 0]))
|
||||||
|
print("[ELEMENTE] pure-transform: tx={:.3f} ty={:.3f} tz={:.3f} rot={:.1f}°".format(
|
||||||
|
tx, ty, tz, rot_deg))
|
||||||
|
|
||||||
|
# Eltern→Kind-Cascade: nur bewegte Sources + deren Children folgen.
|
||||||
|
def _should_follow(m):
|
||||||
|
eid = m.get("id")
|
||||||
|
if eid in moved_ids: return True
|
||||||
|
parent = m.get("oeff_parent")
|
||||||
|
if parent and parent in moved_ids: return True
|
||||||
|
return False
|
||||||
|
|
||||||
if pure_delta is not None:
|
|
||||||
# PURE-TRANSLATE PFAD: nur Geometries translaten die nicht schon vom
|
|
||||||
# User-Move transformed wurden. Keine Brep-Regeneration, kein
|
|
||||||
# Boolean-Diff. → instant feedback.
|
|
||||||
vec = rg.Vector3d(*pure_delta)
|
|
||||||
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
_was_busy = sc.sticky.get(_REGEN_BUSY, False)
|
||||||
sc.sticky[_REGEN_BUSY] = True
|
sc.sticky[_REGEN_BUSY] = True
|
||||||
prev_redraw = doc.Views.RedrawEnabled
|
|
||||||
doc.Views.RedrawEnabled = False
|
|
||||||
try:
|
try:
|
||||||
for obj in list(doc.Objects):
|
for obj in list(doc.Objects):
|
||||||
try:
|
try:
|
||||||
m = _read_meta(obj)
|
m = _read_meta(obj)
|
||||||
if not m: continue
|
if not m: continue
|
||||||
t = m.get("type")
|
t = m.get("type")
|
||||||
# Sources die nicht in deltas waren (= unbewegt) auch translaten
|
if not _should_follow(m): continue
|
||||||
if t in SOURCE_TYPES and m["id"] not in deltas:
|
# Sources die nicht bewegt wurden (= identity transform)
|
||||||
|
# transformen — nur via _should_follow erlaubt (Cascade).
|
||||||
|
if t in SOURCE_TYPES:
|
||||||
|
src_t = source_transforms.get(m["id"])
|
||||||
|
if src_t is not None and not _is_identity_transform(src_t):
|
||||||
|
continue # Rhino hat bereits transformed
|
||||||
new_geom = obj.Geometry.Duplicate()
|
new_geom = obj.Geometry.Duplicate()
|
||||||
new_geom.Translate(vec)
|
new_geom.Transform(pure_transform)
|
||||||
doc.Objects.Replace(obj.Id, new_geom)
|
doc.Objects.Replace(obj.Id, new_geom)
|
||||||
continue
|
continue
|
||||||
# Volumes: vergleiche Position vs. Snapshot. Wenn unbewegt,
|
# Volumes: bb-Center gegen Snapshot vergleichen. Unbewegt
|
||||||
# translaten. Wenn bereits transformed (durch Multi-Select-
|
# → transformen. Bereits transformed (Rhino) → skip.
|
||||||
# Move), skip.
|
|
||||||
if t in VOLUME_TYPES:
|
if t in VOLUME_TYPES:
|
||||||
vol_snap = volumes_snap.get(str(obj.Id))
|
vol_snap = volumes_snap.get(str(obj.Id))
|
||||||
if vol_snap is None: continue
|
if vol_snap is None: continue
|
||||||
@@ -7846,22 +8259,38 @@ def _on_command_end(sender, e):
|
|||||||
dy = c_now.Y - c_old[1]
|
dy = c_now.Y - c_old[1]
|
||||||
dz = c_now.Z - c_old[2]
|
dz = c_now.Z - c_old[2]
|
||||||
if (abs(dx) < 1e-6 and abs(dy) < 1e-6 and abs(dz) < 1e-6):
|
if (abs(dx) < 1e-6 and abs(dy) < 1e-6 and abs(dz) < 1e-6):
|
||||||
# noch nicht transformed → translaten
|
|
||||||
new_geom = obj.Geometry.Duplicate()
|
new_geom = obj.Geometry.Duplicate()
|
||||||
new_geom.Translate(vec)
|
new_geom.Transform(pure_transform)
|
||||||
doc.Objects.Replace(obj.Id, new_geom)
|
doc.Objects.Replace(obj.Id, new_geom)
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] pure-translate:", ex)
|
print("[ELEMENTE] pure-transform:", ex)
|
||||||
finally:
|
finally:
|
||||||
doc.Views.RedrawEnabled = prev_redraw
|
|
||||||
sc.sticky[_REGEN_BUSY] = _was_busy
|
sc.sticky[_REGEN_BUSY] = _was_busy
|
||||||
|
doc.Views.RedrawEnabled = prev_redraw_enabled
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
# Flag erst HIER cleren, nachdem alle Replace-Events durch sind —
|
||||||
|
# sonst feuert gestaltung.on_replace pro Volume.
|
||||||
|
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||||
|
# Undo-Record schliessen — alles seit BeginUndoRecord landet in
|
||||||
|
# einem einzelnen Cmd+Z-Schritt.
|
||||||
|
undo_serial = sc.sticky.get("_dossier_undo_serial")
|
||||||
|
if undo_serial:
|
||||||
|
try: doc.EndUndoRecord(undo_serial)
|
||||||
|
except Exception: pass
|
||||||
|
sc.sticky["_dossier_undo_serial"] = None
|
||||||
|
sc.sticky["_dossier_migrated_walls"] = None
|
||||||
b = sc.sticky.get("elemente_bridge")
|
b = sc.sticky.get("elemente_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_state()
|
try: b._send_state()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
# Gestaltung-Panel einmalig nachziehen — Listener waren waehrend
|
||||||
|
# des User-Transform-Commands suspendiert.
|
||||||
|
gb = sc.sticky.get("gestaltung_bridge")
|
||||||
|
if gb is not None:
|
||||||
|
try: gb._send_selection()
|
||||||
|
except Exception: pass
|
||||||
return
|
return
|
||||||
|
|
||||||
# ─── Regulärer Pfad: Constraints + Migrate + Regen (existing flow) ──────
|
# ─── Regulärer Pfad: Constraints + Migrate + Regen (existing flow) ──────
|
||||||
@@ -7877,11 +8306,10 @@ def _on_command_end(sender, e):
|
|||||||
# Regen ausloesen → mehrere Regens pro Wand. Wir machen am Schluss EINEN
|
# Regen ausloesen → mehrere Regens pro Wand. Wir machen am Schluss EINEN
|
||||||
# Regen pro affected_wall — viel schneller bei mehreren Oeffnungen.
|
# Regen pro affected_wall — viel schneller bei mehreren Oeffnungen.
|
||||||
sc.sticky["_dossier_skip_sync_regen"] = True
|
sc.sticky["_dossier_skip_sync_regen"] = True
|
||||||
# Display-Updates komplett suppressen waehrend der Batch — Rhino zeichnet
|
# RedrawEnabled wurde schon in _on_command_begin auf False gesetzt —
|
||||||
# sonst nach jedem Brep-Add/Replace neu, was bei mehreren Sub-Volumen
|
# damit unterdruecken wir auch Rhinos automatischen Post-Move-Redraw
|
||||||
# sichtbares „Aufbauen" verursacht. Ein einziger Redraw am Ende reicht.
|
# (sonst kurzer Mismatch-Frame: Oeffnung an neuer Pos, Wand-Loch noch
|
||||||
prev_redraw = doc.Views.RedrawEnabled
|
# an alter Pos).
|
||||||
doc.Views.RedrawEnabled = False
|
|
||||||
try:
|
try:
|
||||||
for obj in list(doc.Objects):
|
for obj in list(doc.Objects):
|
||||||
try:
|
try:
|
||||||
@@ -7911,7 +8339,20 @@ def _on_command_end(sender, e):
|
|||||||
old_line = rg.LineCurve(
|
old_line = rg.LineCurve(
|
||||||
rg.Point3d(os[0], os[1], os[2]),
|
rg.Point3d(os[0], os[1], os[2]),
|
||||||
rg.Point3d(oe[0], oe[1], oe[2]))
|
rg.Point3d(oe[0], oe[1], oe[2]))
|
||||||
_migrate_openings_to_new_axis(m["id"], old_line, geom)
|
# Pre-Transform Oeffnungs-Positionen aus dem
|
||||||
|
# Snapshot ziehen — Migrate braucht sie um die
|
||||||
|
# Bogenlaengen-Position auf der ALTEN Axis zu
|
||||||
|
# finden (sonst bei Rotation falscher Snap).
|
||||||
|
old_op_positions = {}
|
||||||
|
for snap_id, snap_data in sources_snap.items():
|
||||||
|
if snap_data.get("type") != "oeffnung_point":
|
||||||
|
continue
|
||||||
|
if snap_data.get("oeff_parent") != m["id"]:
|
||||||
|
continue
|
||||||
|
pos = snap_data.get("pos")
|
||||||
|
if pos: old_op_positions[snap_id] = pos
|
||||||
|
_migrate_openings_to_new_axis(
|
||||||
|
m["id"], old_line, geom, old_op_positions)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] post-cmd migrate:", ex)
|
print("[ELEMENTE] post-cmd migrate:", ex)
|
||||||
# Z-Drag detect + Brüstungs-Mitnahme. Constraint setzt
|
# Z-Drag detect + Brüstungs-Mitnahme. Constraint setzt
|
||||||
@@ -7925,26 +8366,30 @@ def _on_command_end(sender, e):
|
|||||||
except (ValueError, TypeError): z_delta = 0.0
|
except (ValueError, TypeError): z_delta = 0.0
|
||||||
sc.sticky["_elemente_wand_z_delta"] = None
|
sc.sticky["_elemente_wand_z_delta"] = None
|
||||||
if abs(z_delta) >= 1e-6:
|
if abs(z_delta) >= 1e-6:
|
||||||
# Brüstungen aller Öffnungen der Wand um delta mitnehmen
|
# OPTION A: Brueest ist RELATIV zur Wand-UK. Da UK
|
||||||
|
# in `_apply_wand_z_drag_constraint` schon um z_delta
|
||||||
|
# geaendert wurde, folgt die Oeffnung automatisch via
|
||||||
|
# Regen (cutout = new_UK + brueest = old_world_Z +
|
||||||
|
# z_delta). Wir muessen NICHT die brueest-UserString
|
||||||
|
# aktualisieren — sonst gaebe es Doppel-Addition.
|
||||||
|
# Den Oeffnungs-Punkt setzen wir auf Snapshot-Z +
|
||||||
|
# z_delta. So funktioniert es egal ob Rhino die
|
||||||
|
# Oeffnung schon mit-bewegt hat (User-Multi-Select)
|
||||||
|
# oder nicht — das End-Z ist immer das richtige.
|
||||||
for op_obj, op_meta in _find_openings_for_wall(doc, m["id"]):
|
for op_obj, op_meta in _find_openings_for_wall(doc, m["id"]):
|
||||||
cur_b = op_meta.get("oeff_brueest")
|
|
||||||
try:
|
try:
|
||||||
cur_b_val = float(cur_b) if cur_b not in (None, "") else 0.0
|
op_snap = sources_snap.get(op_meta["id"])
|
||||||
except (ValueError, TypeError):
|
if not op_snap: continue
|
||||||
cur_b_val = 0.0
|
op_pos = op_snap.get("pos")
|
||||||
new_b = max(0.0, cur_b_val + z_delta)
|
if op_pos is None: continue
|
||||||
try:
|
|
||||||
attrs = op_obj.Attributes.Duplicate()
|
|
||||||
attrs.SetUserString(_KEY_OEFF_BRUEST,
|
|
||||||
"{:.6f}".format(new_b))
|
|
||||||
doc.Objects.ModifyAttributes(op_obj.Id, attrs, True)
|
|
||||||
pt_geom = op_obj.Geometry
|
pt_geom = op_obj.Geometry
|
||||||
if hasattr(pt_geom, "Location"):
|
if not hasattr(pt_geom, "Location"): continue
|
||||||
pt = pt_geom.Location
|
pt = pt_geom.Location
|
||||||
doc.Objects.Replace(op_obj.Id,
|
target_z = op_pos[2] + z_delta
|
||||||
rg.Point(rg.Point3d(pt.X, pt.Y, new_b)))
|
doc.Objects.Replace(op_obj.Id,
|
||||||
|
rg.Point(rg.Point3d(pt.X, pt.Y, target_z)))
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] post-cmd brueest:", ex)
|
print("[ELEMENTE] post-cmd brueest pt-shift:", ex)
|
||||||
affected_walls.add(m["id"])
|
affected_walls.add(m["id"])
|
||||||
elif t == "oeffnung_point":
|
elif t == "oeffnung_point":
|
||||||
op_pos = old.get("pos")
|
op_pos = old.get("pos")
|
||||||
@@ -7962,19 +8407,34 @@ def _on_command_end(sender, e):
|
|||||||
# Sync-Regen aller betroffenen Wände — Move ist sauber abgeschlossen,
|
# Sync-Regen aller betroffenen Wände — Move ist sauber abgeschlossen,
|
||||||
# kein Konflikt mehr moeglich. EIN Regen pro Wand (nicht pro Oeffnung).
|
# kein Konflikt mehr moeglich. EIN Regen pro Wand (nicht pro Oeffnung).
|
||||||
# Display bleibt suppressed bis ALLE Wände durch sind → kein „Aufbauen".
|
# Display bleibt suppressed bis ALLE Wände durch sind → kein „Aufbauen".
|
||||||
try:
|
for wid in affected_walls:
|
||||||
for wid in affected_walls:
|
try: _regenerate_element(doc, wid)
|
||||||
try: _regenerate_element(doc, wid)
|
except Exception as ex:
|
||||||
except Exception as ex:
|
print("[ELEMENTE] post-cmd regen:", ex)
|
||||||
print("[ELEMENTE] post-cmd regen:", ex)
|
doc.Views.RedrawEnabled = prev_redraw_enabled
|
||||||
finally:
|
|
||||||
doc.Views.RedrawEnabled = prev_redraw
|
|
||||||
try: doc.Views.Redraw()
|
try: doc.Views.Redraw()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
# Flag erst HIER cleren — nach dem Regen-Pfad, der via _regenerate_element
|
||||||
|
# viele Replace-Events erzeugt die wir auch suppressen wollen.
|
||||||
|
sc.sticky[_UT_ACTIVE_KEY] = None
|
||||||
|
# Undo-Record schliessen — alles seit BeginUndoRecord landet in
|
||||||
|
# einem einzelnen Cmd+Z-Schritt.
|
||||||
|
undo_serial = sc.sticky.get("_dossier_undo_serial")
|
||||||
|
if undo_serial:
|
||||||
|
try: doc.EndUndoRecord(undo_serial)
|
||||||
|
except Exception: pass
|
||||||
|
sc.sticky["_dossier_undo_serial"] = None
|
||||||
|
sc.sticky["_dossier_migrated_walls"] = None
|
||||||
b = sc.sticky.get("elemente_bridge")
|
b = sc.sticky.get("elemente_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_state()
|
try: b._send_state()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
# Gestaltung-Panel einmalig nachziehen — Listener waren waehrend
|
||||||
|
# des Commands + des Regens suspendiert.
|
||||||
|
gb = sc.sticky.get("gestaltung_bridge")
|
||||||
|
if gb is not None:
|
||||||
|
try: gb._send_selection()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
def _install_listeners(bridge):
|
def _install_listeners(bridge):
|
||||||
|
|||||||
+38
-19
@@ -1368,6 +1368,13 @@ def _install_selection_listener(bridge):
|
|||||||
return
|
return
|
||||||
|
|
||||||
def refresh(*args):
|
def refresh(*args):
|
||||||
|
# Waehrend Move/Rotate/Mirror/Scale schweigen — Rhino oszilliert die
|
||||||
|
# Selection pro transformiertem Object mehrfach (deselect→delete→add→
|
||||||
|
# reselect). Bei 7 Objekten sind das ~100 IPC-Sends in den WebView,
|
||||||
|
# was sich als „Regen" anfuehlt. elemente._on_command_end refresht
|
||||||
|
# nach dem Command einmalig.
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
b = sc.sticky.get("gestaltung_bridge")
|
b = sc.sticky.get("gestaltung_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
try: b._send_selection()
|
try: b._send_selection()
|
||||||
@@ -1380,6 +1387,12 @@ def _install_selection_listener(bridge):
|
|||||||
- Hatch hat _FILL_OWNER_KEY (= curve_id) → Curve um den gleichen
|
- Hatch hat _FILL_OWNER_KEY (= curve_id) → Curve um den gleichen
|
||||||
Vektor mit-translaten (User hat Hatch alleine verschoben).
|
Vektor mit-translaten (User hat Hatch alleine verschoben).
|
||||||
"""
|
"""
|
||||||
|
# Waehrend User-Transform-Command: elemente uebernimmt die Geometrie-
|
||||||
|
# Synchronisation. Hatch-Re-Create laeuft hier sowieso ins Leere weil
|
||||||
|
# Rhino bei Move Delete+Add statt Replace feuert.
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
|
if sc.sticky.get("_elemente_regen_busy"): return
|
||||||
new_obj = args.NewRhinoObject
|
new_obj = args.NewRhinoObject
|
||||||
if new_obj is None or new_obj.Id in _processing:
|
if new_obj is None or new_obj.Id in _processing:
|
||||||
return
|
return
|
||||||
@@ -1457,11 +1470,13 @@ def _install_selection_listener(bridge):
|
|||||||
"""Wenn eine Curve geloescht wird, ihre gekoppelte Hatch mitloeschen.
|
"""Wenn eine Curve geloescht wird, ihre gekoppelte Hatch mitloeschen.
|
||||||
Wenn umgekehrt eine Hatch direkt geloescht wird, den Verweis auf der
|
Wenn umgekehrt eine Hatch direkt geloescht wird, den Verweis auf der
|
||||||
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
|
Curve aufraeumen damit beim naechsten Toggle keine Geister-Referenz steht."""
|
||||||
|
# Waehrend User-Transform-Command: Rhino feuert Delete+Add fuer
|
||||||
|
# transformierte Objekte. Curve→Hatch-Cascade hier wuerde die Hatch
|
||||||
|
# killen obwohl sie gleich wieder benoetigt wird.
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
|
if sc.sticky.get("_elemente_regen_busy"): return
|
||||||
obj = args.TheObject
|
obj = args.TheObject
|
||||||
try:
|
|
||||||
print("[GESTALTUNG] on_delete fired id={}".format(obj.Id if obj else None))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
if obj is None or obj.Id in _processing:
|
if obj is None or obj.Id in _processing:
|
||||||
return
|
return
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
@@ -1470,16 +1485,24 @@ def _install_selection_listener(bridge):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
|
# Schneller Bail-out: ohne Hatch-UserString interessiert uns das
|
||||||
|
# Event nicht. Vermeidet Print-Spam fuer Wand-Sub-Volumen etc.
|
||||||
try:
|
try:
|
||||||
hatch_id_str = attrs.GetUserString(_FILL_KEY)
|
hatch_id_str = attrs.GetUserString(_FILL_KEY)
|
||||||
except Exception:
|
except Exception:
|
||||||
hatch_id_str = None
|
hatch_id_str = None
|
||||||
# Fallback: Mapping in sc.sticky (UserStrings koennen nach Delete leer sein)
|
try:
|
||||||
if not hatch_id_str:
|
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
|
||||||
|
except Exception:
|
||||||
|
owner_id_str = None
|
||||||
|
if not hatch_id_str and not owner_id_str:
|
||||||
|
# UserStrings koennen nach Delete leer sein → Sticky-Fallback.
|
||||||
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
|
hatch_id_str = _lookup_hatch_for_curve(obj.Id)
|
||||||
if hatch_id_str:
|
if not hatch_id_str:
|
||||||
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
|
return
|
||||||
|
print("[GESTALTUNG] on_delete: hatch via sticky map gefunden")
|
||||||
|
|
||||||
|
# Pfad A: geloeschte Curve hatte eine Hatch -> Hatch mitloeschen
|
||||||
if hatch_id_str:
|
if hatch_id_str:
|
||||||
try:
|
try:
|
||||||
hatch_id = System.Guid(hatch_id_str)
|
hatch_id = System.Guid(hatch_id_str)
|
||||||
@@ -1504,10 +1527,6 @@ def _install_selection_listener(bridge):
|
|||||||
return # Curve-Fall fertig
|
return # Curve-Fall fertig
|
||||||
|
|
||||||
# Pfad B: geloeschte Hatch hatte einen Owner-Verweis -> Curve aufraeumen
|
# Pfad B: geloeschte Hatch hatte einen Owner-Verweis -> Curve aufraeumen
|
||||||
try:
|
|
||||||
owner_id_str = attrs.GetUserString(_FILL_OWNER_KEY)
|
|
||||||
except Exception:
|
|
||||||
owner_id_str = None
|
|
||||||
if owner_id_str:
|
if owner_id_str:
|
||||||
try:
|
try:
|
||||||
owner_id = System.Guid(owner_id_str)
|
owner_id = System.Guid(owner_id_str)
|
||||||
@@ -1532,16 +1551,17 @@ def _install_selection_listener(bridge):
|
|||||||
- Wenn das Objekt eben gerade als Teil eines Drag/Move geloescht wurde,
|
- Wenn das Objekt eben gerade als Teil eines Drag/Move geloescht wurde,
|
||||||
stellen wir die Hatch mit den gemerkten Metadaten wieder her.
|
stellen wir die Hatch mit den gemerkten Metadaten wieder her.
|
||||||
- Sonst pruefen wir ob die Ebene ein Auto-Fill konfiguriert hat."""
|
- Sonst pruefen wir ob die Ebene ein Auto-Fill konfiguriert hat."""
|
||||||
|
# Waehrend User-Transform-Command: elemente uebernimmt Geometrie-Sync.
|
||||||
|
# Auto-Fill hier wuerde unnoetige Hatches erzeugen weil das Objekt
|
||||||
|
# bereits eine geerbte Fill-UserString hat (vom Delete+Add im Move).
|
||||||
|
if sc.sticky.get("_dossier_user_transform_active"): return
|
||||||
|
if sc.sticky.get("_dossier_undo_active"): return
|
||||||
|
if sc.sticky.get("_elemente_regen_busy"): return
|
||||||
obj = args.TheObject
|
obj = args.TheObject
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return
|
return
|
||||||
try:
|
|
||||||
geom_kind = type(obj.Geometry).__name__
|
|
||||||
except Exception:
|
|
||||||
geom_kind = "?"
|
|
||||||
if obj.Id in _processing:
|
if obj.Id in _processing:
|
||||||
return
|
return
|
||||||
print("[GESTALTUNG] on_add: id={} type={}".format(obj.Id, geom_kind))
|
|
||||||
doc = Rhino.RhinoDoc.ActiveDoc
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
|
||||||
# 1) Drag-Recovery: Hatch-Metadaten wurden gerade in on_delete gespeichert?
|
# 1) Drag-Recovery: Hatch-Metadaten wurden gerade in on_delete gespeichert?
|
||||||
@@ -1566,7 +1586,6 @@ def _install_selection_listener(bridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[GESTALTUNG] on_add Exception:", ex)
|
print("[GESTALTUNG] on_add Exception:", ex)
|
||||||
return
|
return
|
||||||
print("[GESTALTUNG] on_add ok={}".format(ok))
|
|
||||||
if ok:
|
if ok:
|
||||||
b = sc.sticky.get("gestaltung_bridge")
|
b = sc.sticky.get("gestaltung_bridge")
|
||||||
if b is not None:
|
if b is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user