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:
@@ -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):
|
||||
|
||||
+96
-10
@@ -722,17 +722,95 @@ def _set_display_mode(name):
|
||||
|
||||
# --- Snap / Ortho via ModelAidSettings --------------------------------------
|
||||
|
||||
# Architekten-relevante Osnap-Modes — UI-Key → OsnapModes-Flag.
|
||||
# Vollstaendige Liste in Rhino: End, Near, Focus, Center, Vertex, Knot,
|
||||
# Quadrant, Midpoint, Intersection, Perpendicular, Tangent, Point.
|
||||
# Architektur-Workflow nutzt v.a. die ersten 6.
|
||||
def _osnap_flag_map():
|
||||
try:
|
||||
OM = Rhino.ApplicationSettings.OsnapModes
|
||||
return {
|
||||
"end": OM.End,
|
||||
"mid": OM.Midpoint,
|
||||
"int": OM.Intersection,
|
||||
"perp": OM.Perpendicular,
|
||||
"cen": OM.Center,
|
||||
"near": OM.Near,
|
||||
}
|
||||
except Exception:
|
||||
return {}
|
||||
|
||||
|
||||
def _get_osnap_modes_dict():
|
||||
flags = _osnap_flag_map()
|
||||
if not flags: return {}
|
||||
try:
|
||||
cur = int(Rhino.ApplicationSettings.ModelAidSettings.OsnapModes)
|
||||
return {k: bool(cur & int(f)) for k, f in flags.items()}
|
||||
except Exception:
|
||||
return {k: False for k in flags}
|
||||
|
||||
|
||||
def _set_osnap_mode(key, enabled):
|
||||
flags = _osnap_flag_map()
|
||||
flag = flags.get(key)
|
||||
if flag is None: return
|
||||
try:
|
||||
s = Rhino.ApplicationSettings.ModelAidSettings
|
||||
cur = int(s.OsnapModes)
|
||||
flag_i = int(flag)
|
||||
new = (cur | flag_i) if enabled else (cur & ~flag_i)
|
||||
s.OsnapModes = Rhino.ApplicationSettings.OsnapModes(new)
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] _set_osnap_mode:", ex)
|
||||
|
||||
|
||||
def _is_grid_visible():
|
||||
try:
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return True
|
||||
view = doc.Views.ActiveView
|
||||
if view is None: return True
|
||||
vp = view.ActiveViewport
|
||||
try: return bool(vp.ConstructionGridVisible)
|
||||
except Exception:
|
||||
try: return bool(vp.ShowConstructionGrid)
|
||||
except Exception: return True
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
|
||||
def _set_grid_visible(visible):
|
||||
"""Schaltet Konstruktions-Grid in ALLEN Modell-Viewports an/aus."""
|
||||
doc = Rhino.RhinoDoc.ActiveDoc
|
||||
if doc is None: return
|
||||
v_in = bool(visible)
|
||||
for v in doc.Views:
|
||||
try:
|
||||
vp = v.ActiveViewport
|
||||
try: vp.ConstructionGridVisible = v_in
|
||||
except Exception:
|
||||
try: vp.ShowConstructionGrid = v_in
|
||||
except Exception: pass
|
||||
except Exception: pass
|
||||
try: doc.Views.Redraw()
|
||||
except Exception: pass
|
||||
|
||||
|
||||
def _get_snap_state():
|
||||
try:
|
||||
s = Rhino.ApplicationSettings.ModelAidSettings
|
||||
return {
|
||||
"ortho": bool(s.Ortho),
|
||||
"gridSnap": bool(s.GridSnap),
|
||||
"osnap": bool(s.UseHorizontalDialog) if False else bool(getattr(s, "Osnap", False)) or False,
|
||||
"planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)),
|
||||
"ortho": bool(s.Ortho),
|
||||
"gridSnap": bool(s.GridSnap),
|
||||
"osnap": bool(getattr(s, "Osnap", False)) or bool(getattr(s, "OsnapEnabled", False)),
|
||||
"planar": bool(getattr(s, "ProjectOsnapsToCPlane", False)),
|
||||
"gridVisible": _is_grid_visible(),
|
||||
"osnapModes": _get_osnap_modes_dict(),
|
||||
}
|
||||
except Exception:
|
||||
return {"ortho": False, "gridSnap": False, "osnap": False, "planar": False}
|
||||
return {"ortho": False, "gridSnap": False, "osnap": False,
|
||||
"planar": False, "gridVisible": True, "osnapModes": {}}
|
||||
|
||||
|
||||
def _set_ortho(v):
|
||||
@@ -753,11 +831,11 @@ def _set_osnap_master(v):
|
||||
"""Master-Toggle fuer Object-Snap (alle aktiven Snaps)."""
|
||||
try:
|
||||
s = Rhino.ApplicationSettings.ModelAidSettings
|
||||
if hasattr(s, "Osnap"):
|
||||
s.Osnap = bool(v)
|
||||
elif hasattr(s, "UsePoints"):
|
||||
# Fallback: einzelne Modi durch
|
||||
pass
|
||||
# Verschiedene Rhino-Versionen — beide Properties probieren
|
||||
for attr in ("Osnap", "OsnapEnabled"):
|
||||
if hasattr(s, attr):
|
||||
setattr(s, attr, bool(v))
|
||||
return
|
||||
except Exception as ex:
|
||||
print("[OBERLEISTE] _set_osnap_master:", ex)
|
||||
|
||||
@@ -1018,6 +1096,12 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
elif t == "TOGGLE_OSNAP":
|
||||
_set_osnap_master(bool(p.get("enabled")))
|
||||
self._send_state(force=True)
|
||||
elif t == "SET_OSNAP_MODE":
|
||||
_set_osnap_mode(p.get("key") or "", bool(p.get("enabled")))
|
||||
self._send_state(force=True)
|
||||
elif t == "TOGGLE_GRID_VISIBLE":
|
||||
_set_grid_visible(bool(p.get("visible")))
|
||||
self._send_state(force=True)
|
||||
|
||||
# --- Graphical Overrides ----------------------------------------
|
||||
elif t == "TOGGLE_OVERRIDES":
|
||||
@@ -1464,6 +1548,8 @@ class OberleisteBridge(panel_base.BaseBridge):
|
||||
info.get("viewMode"),
|
||||
info.get("displayMode"),
|
||||
info.get("ortho"), info.get("gridSnap"), info.get("osnap"),
|
||||
info.get("gridVisible"),
|
||||
tuple(sorted((info.get("osnapModes") or {}).items())),
|
||||
info.get("showLineweights"),
|
||||
info["overridesEnabled"], info["overridesCount"],
|
||||
info.get("overridesActivePreset"),
|
||||
|
||||
+31
-6
@@ -247,8 +247,23 @@ def activate_schnitt(doc, z, skip_view=False):
|
||||
obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back")
|
||||
if obj is not None: n_planes += 1
|
||||
|
||||
# View setzen: Parallel-Projektion, Kamera senkrecht zur Linie.
|
||||
# Bei skip_view=True (Grip-Drag-Re-Activate) komplett ueberspringen.
|
||||
# Projektion: 'parallel' (klassischer Schnitt) oder 'perspective'
|
||||
# (Schnittperspektive — perspektivische Section mit gleicher Cut-
|
||||
# Logik). Bei perspective wird Kamera leicht naeher geholt + FOV
|
||||
# gesetzt; Cut-Planes sind identisch.
|
||||
projection = (z.get("projection") or "parallel").strip().lower()
|
||||
if projection not in ("parallel", "perspective"): projection = "parallel"
|
||||
|
||||
# Kamera-Z fuer Perspektive: explizit ueber cameraHeight setzbar,
|
||||
# sonst Default = Mitte der Hoehenrange (= plane_z). Bei Parallel
|
||||
# ignoriert weil Kamera-Z in Orthoprojektion das Bild nicht aendert.
|
||||
try:
|
||||
cam_z = float(z.get("cameraHeight")) if z.get("cameraHeight") is not None else plane_z
|
||||
except Exception:
|
||||
cam_z = plane_z
|
||||
|
||||
# View setzen — Kamera senkrecht zur Linie. Bei skip_view=True
|
||||
# (Grip-Drag-Re-Activate) komplett ueberspringen.
|
||||
if not skip_view:
|
||||
try:
|
||||
view = doc.Views.ActiveView
|
||||
@@ -257,18 +272,28 @@ def activate_schnitt(doc, z, skip_view=False):
|
||||
if view is not None:
|
||||
vp = view.ActiveViewport
|
||||
cam_dist = max(50.0, depth_back * 3 + line_len)
|
||||
# Bei Perspektive: Kamera + Target auf cam_z. Bei Parallel:
|
||||
# plane_z (Mitte Hoehenrange) — Z spielt eh keine Rolle
|
||||
# fuers Bild, aber sauber gesetzt fuer konsistente
|
||||
# Kamera-Ausrichtung.
|
||||
view_z = cam_z if projection == "perspective" else plane_z
|
||||
cam_pos = rg.Point3d(
|
||||
mid.X - view_dir.X * cam_dist,
|
||||
mid.Y - view_dir.Y * cam_dist,
|
||||
plane_z)
|
||||
view_z)
|
||||
target = rg.Point3d(
|
||||
mid.X + view_dir.X * (depth_back * 0.5),
|
||||
mid.Y + view_dir.Y * (depth_back * 0.5),
|
||||
plane_z)
|
||||
vp.ChangeToParallelProjection(True)
|
||||
view_z)
|
||||
if projection == "perspective":
|
||||
vp.ChangeToPerspectiveProjection(True, 50.0)
|
||||
else:
|
||||
vp.ChangeToParallelProjection(True)
|
||||
vp.SetCameraLocations(target, cam_pos)
|
||||
vp.CameraUp = rg.Vector3d(0, 0, 1)
|
||||
# Zoom auf Schnitt-BoundingBox + etwas Rand
|
||||
# Zoom auf Schnitt-BoundingBox + etwas Rand. Bei Perspektive
|
||||
# macht ZoomBoundingBox auch Sinn — Rhino passt das FOV-Frame
|
||||
# entsprechend an.
|
||||
bb = rg.BoundingBox(
|
||||
rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin,
|
||||
h_min - margin),
|
||||
|
||||
Reference in New Issue
Block a user