Snap-Bar + Drag-Reorder + Schnittperspektive + Top-View Z-Guard + Multi-Geschoss-Clipping

Snap-Bar (Oberleiste):
- 4x2 Icon-Grid mit architektonischen Osnap-Modi (End/Mid/Int/Perp/Cen/Near)
- Master-O Toggle + Grid-Sichtbarkeit
- Symbol-Wahl angelehnt an Rhinos eigene Snap-Marker
- Ortho + Grid-Snap raus (in Rhino-Footer)
- Backend: _osnap_flag_map + _get/_set_osnap_modes + _set_grid_visible

Drag-to-Reorder (GeschossManager):
- HTML5-Drag auf jeder Zeile
- Drop-Indikator (accent-Border oben/unten je nach Cursor-Position)
- Gedraggte Row faded auf opacity 0.4
- Array-Reorder + onChange triggert recalcOkff -> OKFFs konsistent

Schnittperspektive:
- projection: 'parallel' | 'perspective' im Schnitt-Settings-Dialog
- Augenhoehe (cameraHeight) nur bei Perspektive sichtbar
- activate_schnitt mit ChangeToPerspectiveProjection(50 FOV)
- skip_view=True bei Grip-Drag-Re-Activate damit View nicht ploetzlich
  in Section springt

Top-View Z-Guard:
- _is_active_view_top_like + _suppress_z_drift_if_top_view in
  _on_object_replaced — bei Plan-View wird Z-Drift einer source-curve
  automatisch zurueckgerollt (gegen ungewolltes Snappen auf z!=0 oder
  Gumball-Z)

Multi-Geschoss-Clipping-UX:
- Klick auf cut-Icon einer nicht-aktiven Geschoss-Zeile aktiviert das
  Geschoss mit + toggelt Clipping → Plane erscheint sofort

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-23 23:44:12 +02:00
parent 736325fba1
commit ee01c7ebdc
7 changed files with 426 additions and 17 deletions
+84
View File
@@ -4815,6 +4815,7 @@ def _make_treppe_volume(axis_curve, breite, referenz, n_stufen, uk, ok,
return None
def _regenerate_element(doc, element_id):
"""Regeneriert das Volumen eines Elements (Wand oder Decke) anhand
seines Source-Objekts (Achse bzw. Outline)."""
@@ -9733,6 +9734,81 @@ def _apply_oeffnung_constraint(new_obj, meta, old_obj=None):
return geom_changed
def _is_active_view_top_like():
"""True wenn der aktive Viewport von oben/unten reinschaut (Plan-
Ansicht). Erkennt Top + Bottom + custom Views deren Kamera-Richtung
ueberwiegend vertikal ist."""
try:
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return False
view = doc.Views.ActiveView
if view is None: return False
vp = view.ActiveViewport
if vp is None: return False
cam = vp.CameraDirection
if cam.Length < 1e-9: return False
cam_n = rg.Vector3d(cam.X, cam.Y, cam.Z)
cam_n.Unitize()
# Vertikal = |Z-Komponente| nahe 1.0
return abs(cam_n.Z) > 0.95
except Exception:
return False
def _suppress_z_drift_if_top_view(e):
"""Bei Move in Top-View: Z-Komponente des Drifts zurueckrollen.
Vergleicht Z der OldRhinoObject-Geometry mit der NewRhinoObject-
Geometry; wenn signifikanter Z-Drift, translatiert die neue Geometry
in -Z zurueck.
Wird NUR fuer SOURCE-Curves gerufen (Caller filtert vorher). Smart-
Elements sollen in Plan-Ansicht 2D-Verhalten haben Z-Drift via
Z-Snap oder Gumball-Z ist meist ungewollt.
Wenn der User WIRKLICH Z aendern will: einfach in eine andere View
wechseln (Front, Right, Perspective)."""
if not _is_active_view_top_like(): return
try:
old_obj = e.OldRhinoObject
new_obj = e.NewRhinoObject
if old_obj is None or new_obj is None: return
old_geom = old_obj.Geometry
new_geom = new_obj.Geometry
if old_geom is None or new_geom is None: return
# BBox-Min-Z als Referenz nehmen — robust gegen unterschiedliche
# Curve-Typen (Line, Polyline, Point, Curve).
try: bb_old = old_geom.GetBoundingBox(True)
except Exception: return
try: bb_new = new_geom.GetBoundingBox(True)
except Exception: return
if not bb_old.IsValid or not bb_new.IsValid: return
z_old = bb_old.Min.Z
z_new = bb_new.Min.Z
dz = z_new - z_old
# Schwellwert 0.001 Doc-Units (≈ 1mm wenn Doc in m) damit Floating-
# Point-Mikro-Drift nicht panisch korrigiert wird.
if abs(dz) < 0.001: return
# Korrigiere: new geometry um -dz in Z verschieben.
corrected = new_geom.Duplicate()
try:
corrected.Transform(rg.Transform.Translation(0, 0, -dz))
except Exception: return
# Replace — _REGEN_BUSY setzen damit der Replace-Event nicht
# rekursiv wieder unseren Guard triggert.
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
_was = sc.sticky.get(_REGEN_BUSY, False)
sc.sticky[_REGEN_BUSY] = True
try:
doc.Objects.Replace(new_obj.Id, corrected)
print("[ELEMENTE] Top-View Z-Guard: Δz={:.4f} → 0 fuer {}".format(
dz, new_obj.Attributes.GetUserString(_KEY_ID) or "?"))
finally:
sc.sticky[_REGEN_BUSY] = _was
except Exception as ex:
print("[ELEMENTE] _suppress_z_drift_if_top_view:", ex)
def _on_object_replaced(sender, e):
"""Wenn eine Source (Wand-Achse/Decke-Outline/etc.) veraendert wird
Regeneration queuen (debounct ueber Idle, 50 ms Ruhe).
@@ -9792,6 +9868,14 @@ def _on_object_replaced_body(sender, e):
except Exception: pass
if meta is None or meta.get("type") not in SOURCE_TYPES:
return
# Top-View Z-Guard: in Plan-Ansicht soll keine Z-Verschiebung
# passieren. User bewegt Wand/Decke via _Move oder Gumball und
# erwartet 2D-Verhalten — Z-Drift durch versehentliches Snappen
# auf Z!=0 Objekte oder Gumball-Z wird zurueckgenommen.
try:
_suppress_z_drift_if_top_view(e)
except Exception as ex:
print("[ELEMENTE] z-guard:", ex)
try:
new_obj = e.NewRhinoObject
if new_obj and not _read_meta(new_obj):