Files
DOSSIER/rhino/schnitte.py
T
karim 059cbf8d4d Schnitt/Ansicht-Feature + Terrain-Volumen + Geschoss-Add-Dialog
Schnitt-Feature V1+V2:
- Neues rhino/schnitte.py mit Pick-Workflow, Activation (Clipping-Planes +
  Parallel-View), 2D-Plan-Symbol auf 18_Schnittlinien-Sublayer
- Doppelklick auf Symbol aktiviert den Schnitt
- Schnitt-Settings (cutAtLine/Tiefe/Höhen/Blickrichtung) im GeschossSettingsDialog
- View-Snapshot + Restore beim Wechsel Schnitt → Geschoss
- Symbol-Cleanup bei Delete via normalem Ebenen-Menü

Terrain als Volumen:
- swisstopo.volumize_terrain_object: Skirt + Bottom-Cap auf Mesh/Brep
  damit Clipping-Planes gefuellte Querschnitte erzeugen
- UI im SwisstopoApp mit Nachbearbeitung-Section + Tiefen-Eingabe

Geschoss-Add mit Dialog:
- + im GeschossManager oeffnet 3-Optionen-Picker (Geschoss/Schnitt/Zeichnung)
- Geschoss-Dialog mit Anker-Dropdown, Position über/unter, Auto-Name,
  Höhen-Prefill aus Anker

Fix: _send_state fallback — Element gilt als selektiert wenn Source ODER
Volume in der Selection ist (robust gegen Layer-Visibility wenn Referenz-
linien-Layer im aktuellen Mode versteckt ist)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 18:28:59 +02:00

630 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#! 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