#! python3 # -*- coding: utf-8 -*- """ schnitte.py Schnitte + Ansichten als Zeichnungsebenen-Typ. Datenmodell: in dossier_zeichnungsebenen-JSON ergaenzt jeder Schnitt einen Eintrag mit type="schnitt" + linePts/dirSign/depthBack/cutAtLine/heightMin/ heightMax. Geschoss-Eintraege haben type="geschoss" (oder fehlend = legacy). Aktivierung (vom Ebenen-Panel via SET_ACTIVE getriggert): - 1 Clipping-Plane (Ansicht) oder 2 Clipping-Planes (Schnitt) erzeugen - View auf Parallel-Projektion umstellen, Kamera senkrecht zur Linie - Zoom auf BBox (linePts + heightMin/Max + depthBack) 2D-Plan-Symbol: Linie + Pfeile an den Enden + Beschriftung — bleibt im Grundriss sichtbar wenn man wieder im Geschoss ist. """ import math import json import uuid import Rhino import Rhino.Geometry as rg import System import scriptcontext as sc # UserStrings auf Clipping-Plane- und Symbol-Objekten — fuer Wiederfinden + # Cleanup beim Wechsel der Zeichnungsebene. _KEY_SCHNITT_CLIP = "dossier_schnitt_clip" # "1" auf Clipping-Plane-Objekten _KEY_SCHNITT_ROLE = "dossier_schnitt_role" # "cut" | "back" _KEY_SCHNITT_SYMBOL = "dossier_schnitt_symbol" # "1" auf 2D-Symbol-Curves _KEY_SCHNITT_ID = "dossier_schnitt_id" # zugehoerige Schnitt-Id def _line_vectors(p1, p2, dir_sign): """Liefert (line_dir, view_dir, mid). view_dir = perp zur Linie in XY, Richtung definiert durch dir_sign (+1 = CCW-perp, -1 = CW-perp). Das ist die Blickrichtung — wohin der Pfeil im Plan zeigt.""" line = rg.Vector3d(p2.X - p1.X, p2.Y - p1.Y, 0) if line.Length < 1e-6: return None, None, None line.Unitize() perp = rg.Vector3d(-line.Y, line.X, 0) if dir_sign < 0: perp = -perp mid = rg.Point3d((p1.X + p2.X) * 0.5, (p1.Y + p2.Y) * 0.5, (p1.Z + p2.Z) * 0.5) return line, perp, mid def make_schnitt_symbol(p1, p2, dir_sign, name="A"): """Erzeugt die 2D-Plan-Markierung: Hauptlinie + 2 Endpfeile in view_dir-Richtung. Liefert Liste von Curves auf p1.Z.""" line_dir, view_dir, _ = _line_vectors(p1, p2, dir_sign) if line_dir is None: return [] out = [] # Hauptlinie out.append(rg.LineCurve(p1, p2)) # Pfeile an beiden Enden — Stiel (perp zur Linie, in view_dir) + 2 Spitzen arrow_stem = 0.5 # Meter (Doc-Units) — kompromiss zwischen sichtbar bei 1:200 und nicht zu gross bei 1:50 arrow_head = 0.15 for ep in (p1, p2): tip = rg.Point3d(ep.X + view_dir.X * arrow_stem, ep.Y + view_dir.Y * arrow_stem, ep.Z) out.append(rg.LineCurve(ep, tip)) # zwei Spitzen am Pfeil-Tip, gegen view_dir zurueck, seitlich gespreizt h1 = rg.Point3d( tip.X - view_dir.X * arrow_head + line_dir.X * arrow_head, tip.Y - view_dir.Y * arrow_head + line_dir.Y * arrow_head, tip.Z) h2 = rg.Point3d( tip.X - view_dir.X * arrow_head - line_dir.X * arrow_head, tip.Y - view_dir.Y * arrow_head - line_dir.Y * arrow_head, tip.Z) out.append(rg.LineCurve(tip, h1)) out.append(rg.LineCurve(tip, h2)) return out def _collect_viewport_ids(doc): """Alle Modell-Viewports — die Clipping-Plane soll in jedem schneiden, sonst sieht der User beim View-Wechsel das geclippte Modell nur in einem.""" ids = [] seen = set() try: for view in doc.Views: try: vid = view.ActiveViewport.Id k = str(vid) if k not in seen and vid != System.Guid.Empty: seen.add(k); ids.append(vid) except Exception: pass except Exception: pass return ids def find_schnitt_clip_objects(doc): """Findet alle Clipping-Plane-Objekte die zu einem aktiven Schnitt gehoeren (UserString _KEY_SCHNITT_CLIP gesetzt).""" out = [] try: for obj in doc.Objects: if obj is None or obj.IsDeleted: continue try: if obj.Attributes.GetUserString(_KEY_SCHNITT_CLIP) == "1": out.append(obj) except Exception: pass except Exception: pass return out def clear_schnitt_clipping(doc): """Loescht alle Schnitt-Clipping-Planes. Wird beim Wechsel weg vom Schnitt-Modus aufgerufen (auf Geschoss oder anderen Schnitt).""" n = 0 for obj in find_schnitt_clip_objects(doc): try: doc.Objects.Delete(obj.Id, True) n += 1 except Exception as ex: print("[SCHNITT] clear: {}".format(ex)) if n: print("[SCHNITT] {} Clipping-Plane(s) entfernt".format(n)) def _add_clipping_plane(doc, plane, du, dv, vp_ids, role): """Wrapper: legt eine Clipping-Plane mit dem Schnitt-UserString an.""" try: gid = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids) if gid is None or gid == System.Guid.Empty: print("[SCHNITT] AddClippingPlane lieferte Empty Guid") return None obj = doc.Objects.FindId(gid) if obj is None: return None attrs = obj.Attributes.Duplicate() attrs.SetUserString(_KEY_SCHNITT_CLIP, "1") attrs.SetUserString(_KEY_SCHNITT_ROLE, role) # Locked-Mode: User kann die Plane nicht versehentlich greifen. # Mac Rhino rendert Locked-Planes teilweise nur als blasse Edge — # das ist ok, wir wollen sie eh unauffaellig. attrs.Mode = Rhino.DocObjects.ObjectMode.Locked doc.Objects.ModifyAttributes(obj, attrs, True) return obj except Exception as ex: print("[SCHNITT] AddClippingPlane Fehler ({}):".format(role), ex) return None def activate_schnitt(doc, z): """Hauptfunktion: setzt Clipping-Planes + View fuer einen Schnitt- oder Ansicht-Eintrag. Plane-Logik: - view_dir = senkrecht zur Linie in XY, Richtung = dir_sign (Pfeil zeigt in view_dir = Blickrichtung weg vom Betrachter zum Subjekt) - Cut-Plane (nur bei cutAtLine=True, also Schnitt): liegt auf Schnittlinie, Normal = +view_dir → visible Seite = +view_dir (Subjekt), -view_dir (Betrachter) wird geclippt - Back-Plane: parallel, depthBack in +view_dir entfernt, Normal = -view_dir → visible Seite = -view_dir (Subjekt-Zone), alles dahinter weg View-Logik: Parallel-Projektion, Kamera bei mid - view_dir * dist, target bei mid. Zoom auf bbox. """ if z is None: return pts = z.get("linePts") or [] if len(pts) < 2: print("[SCHNITT] '{}' hat keine linePts".format(z.get("name"))) return try: p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0) p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0) except Exception as ex: print("[SCHNITT] linePts ungueltig:", ex) return dir_sign = 1 if int(z.get("dirSign", 1) or 1) >= 0 else -1 depth_back = max(0.5, float(z.get("depthBack", 8.0) or 8.0)) cut_at_line = bool(z.get("cutAtLine", True)) h_min = float(z.get("heightMin", -1.0) or -1.0) h_max = float(z.get("heightMax", 12.0) or 12.0) if h_max <= h_min: h_max = h_min + 3.0 line_dir, view_dir, mid = _line_vectors(p1, p2, dir_sign) if line_dir is None: print("[SCHNITT] '{}' hat zu kurze Linie".format(z.get("name"))) return line_len = p1.DistanceTo(p2) # Clipping-Planes vorher aufraeumen (Re-Aktivierung mit neuen Werten) clear_schnitt_clipping(doc) # Plane-Dimensionen — gross genug fuer typische Architekturmodelle margin = 5.0 du = line_len + margin * 2 dv = (h_max - h_min) + margin * 2 plane_z = (h_min + h_max) * 0.5 vp_ids = _collect_viewport_ids(doc) if not vp_ids: print("[SCHNITT] keine Viewports — Plane wuerde nichts schneiden") return n_planes = 0 # Cut-Plane (nur bei echtem Schnitt) — sitzt AUF der Linie, schneidet # alles vor der Linie (Betrachter-Seite) weg if cut_at_line: # Plane.Origin = mid auf Schnittlinie + Hoehen-Mitte # X-Axis = line_dir (entlang Linie) # Y-Axis = +Z (vertikal) # → Normal = X × Y = line_dir × Z cut_origin = rg.Point3d(mid.X, mid.Y, plane_z) cut_plane = rg.Plane(cut_origin, line_dir, rg.Vector3d(0, 0, 1)) # Pruefen ob Normal in +view_dir zeigt (sonst flippen via -X-Axis) actual_n = rg.Vector3d.CrossProduct(line_dir, rg.Vector3d(0, 0, 1)) if actual_n * view_dir < 0: cut_plane = rg.Plane(cut_origin, -line_dir, rg.Vector3d(0, 0, 1)) obj = _add_clipping_plane(doc, cut_plane, du, dv, vp_ids, "cut") if obj is not None: n_planes += 1 # Back-Plane (bei BEIDEN: Schnitt UND Ansicht) — sitzt depthBack hinter # der Schnittlinie in view_dir-Richtung, Normal zeigt zurueck (-view_dir) back_origin = rg.Point3d( mid.X + view_dir.X * depth_back, mid.Y + view_dir.Y * depth_back, plane_z) back_plane = rg.Plane(back_origin, line_dir, rg.Vector3d(0, 0, 1)) actual_nb = rg.Vector3d.CrossProduct(line_dir, rg.Vector3d(0, 0, 1)) # Wir wollen Normal = -view_dir, also flippen wenn actual zu +view_dir zeigt if actual_nb * view_dir > 0: back_plane = rg.Plane(back_origin, -line_dir, rg.Vector3d(0, 0, 1)) 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 try: view = doc.Views.ActiveView if view is None: for v in doc.Views: view = v; break if view is not None: vp = view.ActiveViewport cam_dist = max(50.0, depth_back * 3 + line_len) cam_pos = rg.Point3d( mid.X - view_dir.X * cam_dist, mid.Y - view_dir.Y * cam_dist, plane_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) vp.SetCameraLocations(target, cam_pos) vp.CameraUp = rg.Vector3d(0, 0, 1) # Zoom auf Schnitt-BoundingBox + etwas Rand bb = rg.BoundingBox( rg.Point3d(min(p1.X, p2.X) - margin, min(p1.Y, p2.Y) - margin, h_min - margin), rg.Point3d(max(p1.X, p2.X) + margin + view_dir.X * depth_back, max(p1.Y, p2.Y) + margin + view_dir.Y * depth_back, h_max + margin)) vp.ZoomBoundingBox(bb) view.Redraw() except Exception as ex: print("[SCHNITT] view setup:", ex) kind = "Schnitt" if cut_at_line else "Ansicht" print("[SCHNITT] {} '{}' aktiviert: {} Plane(s), depthBack={:.1f}m".format( kind, z.get("name"), n_planes, depth_back)) def find_symbol_objects_for(doc, schnitt_id): """Findet alle 2D-Symbol-Curves zu einer Schnitt-ID.""" out = [] try: for obj in doc.Objects: if obj is None or obj.IsDeleted: continue try: if obj.Attributes.GetUserString(_KEY_SCHNITT_SYMBOL) != "1": continue if obj.Attributes.GetUserString(_KEY_SCHNITT_ID) != schnitt_id: continue out.append(obj) except Exception: pass except Exception: pass return out _STICKY_PRE_VIEW = "_dossier_pre_schnitt_view" def save_pre_schnitt_view(doc): """Snapshot der aktiven Viewport-Stellung in sc.sticky. Wird genau EINMAL pro Schnitt-Sitzung aufgerufen: beim Wechsel von einem Geschoss auf einen Schnitt. Verhindert dass Schnitt→Schnitt-Wechsel den Original-Plan-View ueberschreibt. Pro Doc separate Key (RuntimeSerialNumber). Speichert Projection-Mode, Kamera-Pos/Target, CameraUp — alles was nach dem Schnitt restored werden muss.""" if doc is None: return try: view = doc.Views.ActiveView if view is None: return vp = view.ActiveViewport snap = { "is_parallel": bool(vp.IsParallelProjection), "cam_pos": (vp.CameraLocation.X, vp.CameraLocation.Y, vp.CameraLocation.Z), "target": (vp.CameraTarget.X, vp.CameraTarget.Y, vp.CameraTarget.Z), "cam_up": (vp.CameraUp.X, vp.CameraUp.Y, vp.CameraUp.Z), } try: key = _STICKY_PRE_VIEW + "_" + str(doc.RuntimeSerialNumber) except Exception: key = _STICKY_PRE_VIEW + "_default" sc.sticky[key] = snap except Exception as ex: print("[SCHNITT] save view:", ex) def restore_pre_schnitt_view(doc): """Restored den letzten Pre-Schnitt-View (Plan-Top etc.) und entfernt den Snapshot aus sticky. No-op wenn kein Snapshot da. Wird beim Wechsel von einem Schnitt zurueck auf ein Geschoss aufgerufen.""" if doc is None: return False try: key = _STICKY_PRE_VIEW + "_" + str(doc.RuntimeSerialNumber) except Exception: key = _STICKY_PRE_VIEW + "_default" snap = sc.sticky.get(key) if not snap: return False try: view = doc.Views.ActiveView if view is None: return False vp = view.ActiveViewport if snap.get("is_parallel"): vp.ChangeToParallelProjection(True) else: vp.ChangeToPerspectiveProjection(True, 50.0) pos = rg.Point3d(*snap["cam_pos"]) tgt = rg.Point3d(*snap["target"]) up = rg.Vector3d(*snap["cam_up"]) vp.SetCameraLocations(tgt, pos) vp.CameraUp = up view.Redraw() try: del sc.sticky[key] except Exception: pass print("[SCHNITT] Pre-Schnitt-View restored") return True except Exception as ex: print("[SCHNITT] restore view:", ex) return False def is_schnitt_id(doc, z_id): """True wenn die gegebene Zeichnungsebenen-Id ein Schnitt-Eintrag ist.""" if not z_id or doc is None: return False try: raw = doc.Strings.GetValue("dossier_zeichnungsebenen") if not raw: return False for z in json.loads(raw): if isinstance(z, dict) and z.get("id") == z_id: return z.get("type") == "schnitt" except Exception: pass return False def cleanup_schnitt_artifacts(doc, schnitt_id, active_id=None): """Loescht alle Doc-Artefakte eines Schnitts: 2D-Plan-Symbole UND Clipping-Planes (falls der Schnitt aktuell aktiv ist). Beruehrt `dossier_zeichnungsebenen` NICHT — das macht der Caller (oder ist schon passiert im _apply-Pfad). Idempotent: doppeltes Cleanup ist harmlos.""" if not schnitt_id: return 0 n = 0 for obj in find_symbol_objects_for(doc, schnitt_id): try: if doc.Objects.Delete(obj.Id, True): n += 1 except Exception: pass # Wenn der geloeschte Schnitt aktiv war: Clipping-Planes auch weg if active_id and active_id == schnitt_id: try: clear_schnitt_clipping(doc) except Exception: pass return n def delete_schnitt_entry(doc, schnitt_id): """Loescht einen Schnitt-Eintrag komplett: aus dossier_zeichnungsebenen + alle 2D-Symbol-Curves + Clipping-Planes (falls aktiv).""" active_id = "" try: active_id = doc.Strings.GetValue("dossier_active_id") or "" except Exception: pass try: raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]" lst = json.loads(raw) if not isinstance(lst, list): return False new_lst = [e for e in lst if not (isinstance(e, dict) and e.get("id") == schnitt_id and e.get("type") == "schnitt")] if len(new_lst) == len(lst): return False doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(new_lst, ensure_ascii=False)) except Exception as ex: print("[SCHNITT] delete entry:", ex) return False cleanup_schnitt_artifacts(doc, schnitt_id, active_id=active_id) return True def schnitt_ids_in_list(z_list): """Liefert die set der Schnitt-Ids in einer dossier_zeichnungsebenen-Liste. Helper fuer Cleanup-Detection im _apply-Pfad (alt vs neu vergleichen).""" out = set() if not isinstance(z_list, list): return out for z in z_list: if isinstance(z, dict) and z.get("type") == "schnitt" and z.get("id"): out.add(z["id"]) return out def create_schnitt_entry(doc, name, p1, p2, dir_sign=1, depth_back=8.0, cut_at_line=True, height_min=-1.0, height_max=12.0, symbol_layer_idx=-1): """Erzeugt einen neuen Schnitt-Eintrag: appended an dossier_zeichnungsebenen + erzeugt 2D-Plan-Symbol. Liefert die Schnitt-Id (str). Caller sollte broadcast_state + ggf. SET_ACTIVE auf die neue Id triggern damit das Panel den Eintrag sieht und ihn auto-aktiviert.""" schnitt_id = "schnitt_" + uuid.uuid4().hex[:10] entry = { "id": schnitt_id, "name": name or "Schnitt", "type": "schnitt", "linePts": [[float(p1.X), float(p1.Y)], [float(p2.X), float(p2.Y)]], "dirSign": int(1 if dir_sign >= 0 else -1), "depthBack": float(depth_back), "cutAtLine": bool(cut_at_line), "heightMin": float(height_min), "heightMax": float(height_max), "visible": True, "locked": False, "isGeschoss": False, } raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]" try: lst = json.loads(raw) except Exception: lst = [] if not isinstance(lst, list): lst = [] lst.append(entry) try: doc.Strings.SetString("dossier_zeichnungsebenen", json.dumps(lst, ensure_ascii=False)) except Exception as ex: print("[SCHNITT] persist entry:", ex) return None # 2D-Symbol auf Plan curves = make_schnitt_symbol(p1, p2, dir_sign, name) for crv in curves: try: attrs = Rhino.DocObjects.ObjectAttributes() if symbol_layer_idx >= 0: attrs.LayerIndex = symbol_layer_idx attrs.SetUserString(_KEY_SCHNITT_SYMBOL, "1") attrs.SetUserString(_KEY_SCHNITT_ID, schnitt_id) doc.Objects.AddCurve(crv, attrs) except Exception as ex: print("[SCHNITT] add symbol curve:", ex) return schnitt_id def activate_schnitt_by_id(doc, schnitt_id): """Aktiviert einen Schnitt anhand seiner Id. Routet ueber die EbenenBridge damit der ganze _set_active_zeichnungsebene-Pfad durchlaeuft (View-Snapshot, Clipping-Setup, broadcast). Liefert True bei Erfolg. Wird vom Doppelklick-Handler genutzt damit der User vom Plan-Symbol direkt in die Section springen kann ohne den Umweg ueber den Geschoss-Switcher.""" if not schnitt_id or doc is None: return False try: raw = doc.Strings.GetValue("dossier_zeichnungsebenen") if not raw: return False z_list = json.loads(raw) z = next((x for x in z_list if isinstance(x, dict) and x.get("id") == schnitt_id and x.get("type") == "schnitt"), None) if z is None: return False eb = sc.sticky.get("ebenen_bridge_ref") \ or sc.sticky.get("zeichnungsebenen_bridge_ref") if eb is None: # Fallback: direkt aktivieren ohne broadcast print("[SCHNITT] keine EbenenBridge — direkt aktivieren") activate_schnitt(doc, z) return True eb._set_active_zeichnungsebene(z) return True except Exception as ex: print("[SCHNITT] activate_by_id:", ex) return False class _SchnittDoubleClickHandler(Rhino.UI.MouseCallback): """MouseCallback: erkennt Doppelklick auf ein 2D-Schnittsymbol und aktiviert den zugehoerigen Schnitt. Erkennung via UserString dossier_schnitt_symbol=1 + dossier_schnitt_id. Wichtig: die Klicks selektieren das Curve vorab (Rhino-Default), wir pruefen also einfach die aktuelle Selection. Bei Treffer wird der Schnitt aktiviert + e.Cancel=True gesetzt damit Rhinos default Edit-Modus nicht zusaetzlich aufpoppt.""" def OnMouseDoubleClick(self, e): try: view = e.View if view is None: return doc = view.Document if doc is None: return sel = list(doc.Objects.GetSelectedObjects(False, False)) if not sel: return for obj in sel: try: sym = obj.Attributes.GetUserString("dossier_schnitt_symbol") sid = obj.Attributes.GetUserString("dossier_schnitt_id") if sym == "1" and sid: if activate_schnitt_by_id(doc, sid): try: e.Cancel = True except Exception: pass return except Exception: pass except Exception as ex: print("[SCHNITT] OnMouseDoubleClick:", ex) def install_double_click_handler(): """Registriert den Schnittsymbol-Doppelklick-Handler global. Idempotent via sticky-Flag — bei Modul-Reload wird der alte Handler erst disabled, dann neu erstellt. Sonst wuerde nach jedem _reset_panels eine zweite Instanz mit-feuern.""" try: old = sc.sticky.get("_dossier_schnitt_dblclick_handler") if old is not None: try: old.Enabled = False except Exception: pass h = _SchnittDoubleClickHandler() h.Enabled = True sc.sticky["_dossier_schnitt_dblclick_handler"] = h print("[SCHNITT] Doppelklick-Handler aktiv") except Exception as ex: print("[SCHNITT] install_double_click_handler:", ex) def pick_schnitt_interactive(doc, defaults=None): """Interaktiver Pick: 2 Punkte + Dir-Pfeil + Defaults aus settings. Liefert die neue Schnitt-Id oder None bei Abbruch. defaults: {depthBack, heightMin, heightMax, cutAtLine, namePrefix}""" defaults = defaults or {} name_prefix = defaults.get("namePrefix", "S") depth_back = float(defaults.get("depthBack", 8.0)) h_min = float(defaults.get("heightMin", -1.0)) h_max = float(defaults.get("heightMax", 12.0)) cut_at_line = bool(defaults.get("cutAtLine", True)) # Pick Punkt 1 gp = Rhino.Input.Custom.GetPoint() gp.SetCommandPrompt("Schnittlinie: Startpunkt") gp.Get() if gp.CommandResult() != Rhino.Commands.Result.Success: return None p1 = gp.Point() p1 = rg.Point3d(p1.X, p1.Y, 0) # Pick Punkt 2 mit Vorschau-Linie gp2 = Rhino.Input.Custom.GetPoint() gp2.SetCommandPrompt("Schnittlinie: Endpunkt") gp2.SetBasePoint(p1, True) gp2.DrawLineFromPoint(p1, True) gp2.Get() if gp2.CommandResult() != Rhino.Commands.Result.Success: return None p2 = gp2.Point() p2 = rg.Point3d(p2.X, p2.Y, 0) if p1.DistanceTo(p2) < 0.01: print("[SCHNITT] Linie zu kurz") return None # Pick Blickrichtung (welche Seite ist "hinten") gp3 = Rhino.Input.Custom.GetPoint() gp3.SetCommandPrompt("Blickrichtung: Punkt auf der Subjekt-Seite klicken") mid = rg.Point3d((p1.X + p2.X) * 0.5, (p1.Y + p2.Y) * 0.5, 0) gp3.SetBasePoint(mid, True) gp3.DrawLineFromPoint(mid, True) gp3.Get() if gp3.CommandResult() != Rhino.Commands.Result.Success: return None p3 = gp3.Point() # dir_sign aus Klick-Position: ist p3 auf der +perp oder -perp Seite? line = rg.Vector3d(p2.X - p1.X, p2.Y - p1.Y, 0) perp_default = rg.Vector3d(-line.Y, line.X, 0) perp_default.Unitize() to_p3 = rg.Vector3d(p3.X - mid.X, p3.Y - mid.Y, 0) dir_sign = 1 if (perp_default * to_p3) >= 0 else -1 # Auto-Name: zaehle existierende Schnitte raw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]" try: existing = [e for e in json.loads(raw) if isinstance(e, dict) and e.get("type") == "schnitt"] n = len(existing) + 1 except Exception: n = 1 auto_name = "{}-{}".format(name_prefix, n) # Symbol-Layer ermitteln: '18_Schnittlinien' unter dem aktiven Geschoss. # Fallback auf Default-Layer wenn nichts resolvbar. symbol_layer_idx = -1 try: import elemente as _el active_id = doc.Strings.GetValue("dossier_active_id") or "" geschoss = _el._geschoss_by_id(doc, active_id) if active_id else None if geschoss is None: # erstes Geschoss in der Liste als Fallback for g in _el._load_geschosse(doc): if isinstance(g, dict) and g.get("isGeschoss"): geschoss = g; break if geschoss and geschoss.get("name"): sym_path = _el._layer_path_schnittlinie(doc, geschoss["name"]) symbol_layer_idx = _el._ensure_layer(doc, sym_path) except Exception as ex: print("[SCHNITT] symbol-layer resolve:", ex) sid = create_schnitt_entry(doc, auto_name, p1, p2, dir_sign=dir_sign, depth_back=depth_back, cut_at_line=cut_at_line, height_min=h_min, height_max=h_max, symbol_layer_idx=symbol_layer_idx) return sid