diff --git a/rhino/elemente.py b/rhino/elemente.py index 11881b5..b683a1d 100644 --- a/rhino/elemente.py +++ b/rhino/elemente.py @@ -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): diff --git a/rhino/oberleiste.py b/rhino/oberleiste.py index f2d4cec..d5a0828 100644 --- a/rhino/oberleiste.py +++ b/rhino/oberleiste.py @@ -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"), diff --git a/rhino/schnitte.py b/rhino/schnitte.py index 60a43c4..22f943e 100644 --- a/rhino/schnitte.py +++ b/rhino/schnitte.py @@ -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), diff --git a/src/OberleisteApp.jsx b/src/OberleisteApp.jsx index 431d364..9a62a60 100644 --- a/src/OberleisteApp.jsx +++ b/src/OberleisteApp.jsx @@ -17,6 +17,8 @@ import { setDarstellung, arrangeSelection, toggleReferenzlinien, + toggleOsnap, + setOsnapMode, toggleGridVisible, } from './lib/rhinoBridge' const PRESETS = [ @@ -706,6 +708,97 @@ export default function OberleisteApp() {
+ {/* ====== SNAP-BAR (Architektur-Osnaps + Grid) ====== + 4×2 Grid, nur Icons, schlank. Symbol-Wahl orientiert sich an + Rhinos eigenen Snap-Markern (End=Quadrat, Mid=Dreieck, Cen= + Kreis, Int=X, Perp=Winkel, Near=Plus). Ortho + Grid-Snap sind + in Rhinos Footer-Bar — hier nur was dort fehlt. + Reihe 1: Master-O | End | Mid | Int + Reihe 2: Perp | Cen | Near | Grid */} + {(() => { + const om = state.osnapModes || {} + const osnapDisabled = !state.osnap + const IconBtn = ({ icon, active, disabled, onClick, isFirst, title }) => ( + + ) + const rowStyle = { + display: 'inline-flex', width: BAR_H * 4, + height: BAR_H + 2, boxSizing: 'border-box', + border: '1px solid var(--border)', borderRadius: 999, + overflow: 'hidden', flexShrink: 0, + } + return ( +