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>
This commit is contained in:
@@ -0,0 +1,629 @@
|
||||
#! 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
|
||||
Reference in New Issue
Block a user