Treppen 2D-Plansymbol + Pure-Transform fuer hidden Layer

Frontend:
- 2D-Plansymbol pro Treppe (Tritte/Lauflinie/Aussenlinie/Bruchsymbol)
  mit per-Treppe-Toggles in Properties-Panel
- 'Obere Stufen gestrichelt'-Toggle splittet Tritte/Aussenlinie an
  Schnittebene; Lauflinie hat zwei Pfeile bei Bruch
- Wand-Polyline-Grips fuer alle Vertices (nicht nur Enden)
- PopupMenu unterstuetzt Divider + Checkbox-Items

Backend:
- Eigener Layer 41_Treppen_2D fuer Plansymbol, Layer-Default schwarz
- Aussenlinie-Polygone folgen der Bruch-Diagonale (kein Versatz mehr)
- Linetype-Fallback laedt Dashed bei Bedarf nach
- Tritten-immer-an (Toggle entfernt), Z auf Geschoss-OKFF
- TREPPEN/RAEUME Layer-Migration auf Capital-Case (Treppen/Raeume)
- Selection-Partnership: treppe_2d_symbol pairs in axis + volume

Pure-Transform fuer Treppen-Move:
- treppe_2d_symbol + treppe_volume in VOLUME_TYPES → cascade-Support
- Phase 1.5 Volume-only-Detection: wenn Source unbewegt aber Volumes
  uniform translated → synthetisiere canonical aus Avg-Delta der
  bewegten Volumes (unbewegte rausgefiltert sonst Verzerrung)
- Hidden-inclusive ObjectEnumerator in Snapshot + Apply-Loop damit
  hidden treppe_axis auf 40_Treppen mit-transformiert wird
- Properties-Fallback im _send_state findet hidden Sources via
  expliziter Iteration → Panel zeigt Treppe auch bei 3D-Layer aus
- Dimensionen-Panel skipt on_select/idle waehrend UT_ACTIVE oder
  Partnership-Cascade → keine Flicker beim Drag mehr
This commit is contained in:
2026-05-28 00:41:05 +02:00
parent d9589e99f5
commit bcf7d557b1
5 changed files with 1172 additions and 107 deletions
+13
View File
@@ -586,6 +586,10 @@ def _install_listeners(bridge):
# tick_idle iteriert alle Doc-Objekte, das ist Overhead bei jedem
# Tick zwischen den einzelnen Deletes. CommandEnd refresht.
if sc.sticky.get("_dossier_bulk_op_active"): return
# Waehrend Gumball/Move/Rotate: nicht pollen. Geometrie ist gerade
# in Transit (Live-Replace pro Frame), Werte wuerden mit ~5/s
# zwischen Frames flickern. CommandEnd triggert finalen _send_state.
if sc.sticky.get("_dossier_user_transform_active"): return
b = sc.sticky.get("dimensionen_bridge")
if b is not None:
try: b.tick_idle()
@@ -595,6 +599,15 @@ def _install_listeners(bridge):
# Swisstopo-Import feuert tausende Selection-Events → bail.
if sc.sticky.get("dossier_swisstopo_busy"): return
if sc.sticky.get("_dossier_bulk_op_active"): return
# Waehrend elemente.py's Partnership-Cascade (Klick auf Wand/Treppe
# → 30+ Partner selektiert in einem Rutsch): NICHT pro Event ein
# _send_state feuern. Sonst rauscht das Dimensionen-Panel mit 30+
# Re-Renders durch und die Werte/Auswahl-Anzeige flickert wild.
# Der Idle-Tick holt die finale Selektion eh ~5/s nach.
if sc.sticky.get("_elemente_select_busy"): return
# Waehrend User-Transform (Gumball/Move/Rotate): kein Re-Send, sonst
# rauscht Replace-Storm durch und der Frontend-State zappelt.
if sc.sticky.get("_dossier_user_transform_active"): return
b = sc.sticky.get("dimensionen_bridge")
if b is not None:
try: b._send_state(force=True)
+1014 -21
View File
File diff suppressed because it is too large Load Diff
+87 -81
View File
@@ -96,53 +96,44 @@ def _find_axis_for_obj(doc, obj):
return None
def _curve_endpoints(curve):
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer
LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben
PointAtStart/PointAtEnd. Bei degenerierten Curves None."""
if curve is None: return None, None
def _axis_vertices(geom):
"""Liefert die Vertices der wand_axis-Curve als Liste.
- PolylineCurve: alle Vertices
- LineCurve / sonstige Curve: [Start, End] (zwei-Vertex-Faelle)
Returnt [] bei degeneriertem Input."""
if geom is None: return []
try:
return curve.PointAtStart, curve.PointAtEnd
if isinstance(geom, rg.PolylineCurve):
poly = geom.ToPolyline()
if poly is None or poly.Count < 2: return []
return list(poly)
p_start = geom.PointAtStart
p_end = geom.PointAtEnd
return [p_start, p_end]
except Exception:
return None, None
return []
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt):
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der
wand_axis-Curve gegen new_pt. Geht intelligent um mit:
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt
- PolylineCurve: ersetze ersten/letzten Vertex, Rest bleibt
- andere Curve-Typen: aktuell nur Line-Fallback (Erst/Letzt-Vertex
rekonstruieren)
Setzt die neue Geometrie via Objects.Replace — das feuert
def _replace_axis_vertex(doc, axis_obj, vertex_idx, new_pt):
"""Tauscht den Vertex an Index `vertex_idx` der wand_axis-Curve gegen
new_pt. Funktioniert fuer Linien (idx 0/1) und Polylinien (alle idx).
Setzt die neue Geometrie via Objects.Replace — feuert
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
if axis_obj is None or axis_obj.IsDeleted: return False
geom = axis_obj.Geometry
if geom is None: return False
try:
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen
if isinstance(geom, rg.PolylineCurve):
poly = geom.ToPolyline()
if poly is None or poly.Count < 2: return False
pts = list(poly)
if kind == "start":
pts[0] = new_pt
else:
pts[-1] = new_pt
new_poly = rg.Polyline(pts)
new_curve = rg.PolylineCurve(new_poly)
pts = _axis_vertices(geom)
if not pts: return False
if vertex_idx < 0 or vertex_idx >= len(pts): return False
pts[vertex_idx] = new_pt
if len(pts) == 2:
new_curve = rg.LineCurve(pts[0], pts[1])
else:
# LineCurve oder unbekannter Typ → reduziere auf Line zwischen
# neuem + altem fixen Punkt.
p_start, p_end = _curve_endpoints(geom)
if p_start is None or p_end is None: return False
if kind == "start":
new_curve = rg.LineCurve(new_pt, p_end)
else:
new_curve = rg.LineCurve(p_start, new_pt)
new_curve = rg.PolylineCurve(rg.Polyline(pts))
return doc.Objects.Replace(axis_obj.Id, new_curve)
except Exception as ex:
print("[WAND_GRIPS] replace endpoint:", ex)
print("[WAND_GRIPS] replace vertex:", ex)
return False
@@ -155,15 +146,17 @@ class _EndpointConduit(rd.DisplayConduit):
def __init__(self):
rd.DisplayConduit.__init__(self)
self.hot_key = None # (axis_id_str, kind) — fuer Hover
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint
self.hot_key = None # (axis_id_str, vidx) — fuer Hover
self.drag_key = None # (axis_id_str, vidx) — waehrend aktivem Drag
self.drag_preview = None # Liste von rg.Line — Live-Vorschau (Linien
# zu Nachbar-Vertices waehrend GetPoint)
def _collect_endpoints(self, doc):
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier-
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek-
tiert sind)."""
def _collect_grip_points(self, doc):
"""Liefert Liste von (axis_obj, vertex_idx, world_pt) fuer ALLE
Vertices aller selektierten Waende — fuer Polyline-Waende ist
jeder Knick ein eigener Grip. Iteriert die Selektion + dedupli-
ziert Achsen (jede Wand erscheint nur einmal, auch wenn mehrere
Volumen mit-selektiert sind)."""
out = []
seen_axis = set()
try:
@@ -175,24 +168,21 @@ class _EndpointConduit(rd.DisplayConduit):
aid = str(axis.Id)
if aid in seen_axis: continue
seen_axis.add(aid)
p_start, p_end = _curve_endpoints(axis.Geometry)
if p_start is not None:
out.append((axis, "start", p_start))
if p_end is not None:
out.append((axis, "end", p_end))
for i, pt in enumerate(_axis_vertices(axis.Geometry)):
out.append((axis, i, pt))
return out
def DrawForeground(self, e):
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
for axis, kind, pt in self._collect_endpoints(doc):
for axis, vidx, pt in self._collect_grip_points(doc):
aid = str(axis.Id)
# Skip den gerade gezogenen Marker — der wird via
# drag_preview separat dargestellt.
if self.drag_key and self.drag_key == (aid, kind):
if self.drag_key and self.drag_key == (aid, vidx):
continue
is_hot = self.hot_key and self.hot_key == (aid, kind)
is_hot = self.hot_key and self.hot_key == (aid, vidx)
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
@@ -204,11 +194,12 @@ class _EndpointConduit(rd.DisplayConduit):
# Fallback fuer aeltere Rhino-Versionen: einfacher
# DrawDot mit Label "●"
e.Display.DrawDot(pt, "", fill, _MARKER_BORDER)
# Drag-Preview-Linie waehrend GetPoint aktiv ist
if self.drag_preview is not None:
try:
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
except Exception: pass
# Drag-Preview-Linien waehrend GetPoint aktiv ist
if self.drag_preview:
for line in self.drag_preview:
try:
e.Display.DrawLine(line, _MARKER_HOVER, 2)
except Exception: pass
except Exception as ex:
print("[WAND_GRIPS] DrawForeground:", ex)
@@ -226,21 +217,21 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
def _hit_test(self, view, screen_pt):
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint-
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste."""
"""Liefert (axis, vertex_idx, world_pt) wenn screen_pt nahe eines
Vertex-Markers liegt, sonst None."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return None
try:
vp = view.ActiveViewport
except Exception: return None
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
for axis, kind, world_pt in self.conduit._collect_endpoints(doc):
for axis, vidx, world_pt in self.conduit._collect_grip_points(doc):
try:
s = vp.WorldToClient(world_pt)
dx = s.X - screen_pt.X
dy = s.Y - screen_pt.Y
if (dx * dx + dy * dy) <= thresh2:
return axis, kind, world_pt
return axis, vidx, world_pt
except Exception: continue
return None
@@ -274,44 +265,59 @@ class _EndpointMouseHandler(Rhino.UI.MouseCallback):
# Default-Klick (Selection) abwuergen — wir uebernehmen
try: e.Cancel = True
except Exception: pass
axis, kind, world_pt = hit
self._start_drag(view.Document, axis, kind, world_pt)
axis, vidx, world_pt = hit
self._start_drag(view.Document, axis, vidx, world_pt)
except Exception as ex:
print("[WAND_GRIPS] OnMouseDown:", ex)
def _start_drag(self, doc, axis, kind, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint
gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc.
wie bei _Move."""
def _start_drag(self, doc, axis, vertex_idx, anchor_pt):
"""Startet eine Rhino-GetPoint-Interaktion um den Vertex zu
verschieben. BasePoint-Strategie:
- End-Vertex (idx 0 oder letzter): gegenueberliegender End-Vertex
→ User bekommt Tracking-Linie + Wand-Laenge wie bei _Move
- Mittel-Vertex (Polyline-Knick): Vertex selbst, plus Live-Preview
zu beiden Nachbar-Vertices damit beide Segmente sichtbar mit-
schwingen."""
if doc is None: return
geom = axis.Geometry
if geom is None: return
p_start, p_end = _curve_endpoints(geom)
if p_start is None or p_end is None: return
fixed_pt = p_end if kind == "start" else p_start
# Conduit-State: drag-Marker hervorheben + Preview-Linie
self.conduit.drag_key = (str(axis.Id), kind)
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt)
pts = _axis_vertices(geom)
if not pts or vertex_idx < 0 or vertex_idx >= len(pts): return
is_first = vertex_idx == 0
is_last = vertex_idx == len(pts) - 1
prev_pt = pts[vertex_idx - 1] if not is_first else None
next_pt = pts[vertex_idx + 1] if not is_last else None
if is_first: base_pt = next_pt
elif is_last: base_pt = prev_pt
else: base_pt = anchor_pt
# Conduit-State: drag-Marker hervorheben + Preview-Linien
self.conduit.drag_key = (str(axis.Id), vertex_idx)
self.conduit.drag_preview = []
if prev_pt is not None:
self.conduit.drag_preview.append(rg.Line(prev_pt, anchor_pt))
if next_pt is not None:
self.conduit.drag_preview.append(rg.Line(next_pt, anchor_pt))
self._busy = True
try:
gp = Rhino.Input.Custom.GetPoint()
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(fixed_pt, True)
gp.DrawLineFromPoint(fixed_pt, True)
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
# Endpunkt sich bewegt.
gp.SetCommandPrompt("Wand-Vertex: neuer Punkt (Esc=Abbruch)")
gp.SetBasePoint(base_pt, True)
gp.DrawLineFromPoint(base_pt, True)
def _on_mouse_move(sender, args):
try:
self.conduit.drag_preview = rg.Line(fixed_pt, args.Point)
preview = []
if prev_pt is not None:
preview.append(rg.Line(prev_pt, args.Point))
if next_pt is not None:
preview.append(rg.Line(next_pt, args.Point))
self.conduit.drag_preview = preview
except Exception: pass
try: gp.MouseMove += _on_mouse_move
except Exception: pass
res = gp.Get()
if res == Rhino.Input.GetResult.Point:
new_pt = gp.Point()
_replace_axis_endpoint(doc, axis, kind, new_pt)
_replace_axis_vertex(doc, axis, vertex_idx, new_pt)
except Exception as ex:
print("[WAND_GRIPS] _start_drag:", ex)
finally: