Snapshot: Wand/Öffnung Multi-Surface-Select + Z-Drag + Brüstungs-Mitnahme

Stable working state after a long iteration session. The plugin now supports:
- Multi-Surface-Select für alle Element-Typen (Türen/Fenster/Treppen/Tragwerk)
- Wand-Z-Drag → unbound mode (UK/OK-Override, Wand vom Geschoss entkoppelt)
- Wand-Z-Drag nimmt verknüpfte Öffnungen mit (Brüstung += delta_z via Idle-Pfad)
- Öffnungs-XY-Drag snapt direktional auf Wand-Tangente
- Öffnungs-Z-Drag passt Brüstung an (Fenster sofort sync, Tür deferred)
- Wand-Delete kaskadiert Öffnungen (deferred via Idle, robust gegen _Rotate/_Move)
- Source-Cascade beim Öffnungs-Delete (deferred analog Wand-Kaskade)
- Listener-Cleanup robust gegen _reset_panels.py Reload (Refs in
  _dossier_runtime_event_refs gespeichert, vor Re-Install deregistriert)
- _count_same_id_type filtert IsDeleted (verhindert Source-Duplikat-Bug bei Move)
- Frontend: Brüstungs-Slider für Tür ("Schwelle"), Flügel-Block nur bei Fenster

Plus aus früherer Phase dieser Session:
- Dossier-Launcher Auto-Load via Rhinos StartupCommands-XML
- Default-Pfad zeigt auf gebundeltes startup.py (out-of-the-box für neue User)
- Splash-Window beim Plugin-Load mit native macOS rounded corners
- Diverse Launcher-Verbesserungen (Brüstungs-Default, tauri.conf, capabilities)

Known issue: bei Multi-Select-Move mit vielen Sub-Volumen kann sporadisch
"Unable to transform" auftreten (Rhinos Move-Operation kollidiert mit Wand-
Regen). Tür-spezifischer Defer-Pfad mildert das, Fenster läuft sync.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-18 01:50:45 +02:00
parent 1180d7bedf
commit 961b3c0396
52 changed files with 10760 additions and 765 deletions
+263 -35
View File
@@ -1,4 +1,4 @@
# ! python3
#! python 3
# -*- coding: utf-8 -*-
"""
layer_builder.py
@@ -68,6 +68,150 @@ def _add_layer(doc, name, parent_id=None, color=None, lw=None):
return doc.Layers.Add(layer)
def _find_hatch_pattern_index(doc, name):
"""Sucht einen Hatch-Pattern-Index per Name (case-insensitive). -1 wenn nicht da."""
if not name or name == "None":
return -1
target = name.strip().lower()
try:
for i in range(doc.HatchPatterns.Count):
hp = doc.HatchPatterns[i]
if hp is None or hp.IsDeleted: continue
if hp.Name and hp.Name.strip().lower() == target:
return i
except Exception as ex:
print("[EBENEN] hatch lookup:", ex)
return -1
def _find_linetype_index(doc, name):
"""Sucht einen Linetype-Index per Name. -1 = ByLayer."""
if not name or name in ("byLayer", "by_layer", "ByLayer"):
return -1
target = name.strip().lower()
try:
for i in range(doc.Linetypes.Count):
lt = doc.Linetypes[i]
if lt is None or lt.IsDeleted: continue
if lt.Name and lt.Name.strip().lower() == target:
return i
except Exception: pass
return -1
def _apply_section_style(doc, layer, section_cfg, layer_color):
"""Setzt einen Custom-SectionStyle auf den Layer aus dem Dossier-section-dict.
Nutzt Rhino-8's Python-3-API (Rhino.DocObjects.SectionStyle +
Layer.SetCustomSectionStyle / RemoveCustomSectionStyle). In IPy 2.7
sind diese Methoden nicht exponiert — dort no-op (mit Print-Warnung).
"""
if not section_cfg or not isinstance(section_cfg, dict):
return
has_setter = hasattr(layer, "SetCustomSectionStyle")
has_remover = hasattr(layer, "RemoveCustomSectionStyle")
if not has_setter:
# IPy-2.7-Pfad: API nicht da, leise raus.
return
try:
SS = Rhino.DocObjects.SectionStyle
except Exception as ex:
print("[EBENEN] SectionStyle-Klasse nicht da:", ex); return
# Wenn alles "leer/Default": Custom-Style abschalten
pat = (section_cfg.get("hatchPattern") or "None").strip()
show = section_cfg.get("boundaryShow", True)
if pat == "None" and not show and has_remover:
try: layer.RemoveCustomSectionStyle()
except Exception: pass
return
style = SS()
# --- Hatch ---
if pat and pat != "None":
hp_idx = _find_hatch_pattern_index(doc, pat)
if hp_idx >= 0:
# Property-Name probieren — Rhino-8 hat HatchIndex
for prop in ("HatchIndex", "HatchPatternIndex"):
if hasattr(style, prop):
try: setattr(style, prop, hp_idx); break
except Exception: pass
# Hatch-Scale
for prop in ("HatchScale", "HatchPatternScale"):
if hasattr(style, prop):
try: setattr(style, prop, float(section_cfg.get("hatchScale") or 1.0)); break
except Exception: pass
# Hatch-Rotation (Rhino erwartet Radians — wir bekommen Grad)
import math
rot_deg = float(section_cfg.get("hatchRotation") or 0)
for prop in ("HatchRotation", "HatchAngle"):
if hasattr(style, prop):
try:
setattr(style, prop, math.radians(rot_deg))
break
except Exception: pass
# Hatch-Color (null = ByObject = nicht setzen)
hatch_color = section_cfg.get("hatchColor")
if hatch_color:
for prop in ("HatchColor", "FillColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(hatch_color)); break
except Exception: pass
# Background
bg = section_cfg.get("background")
if bg in ("object", "byObject"):
for prop in ("BackgroundColorUsage", "FillBackground"):
if hasattr(style, prop):
# Enum-Werte sind versioniert; wir versuchen via int
try: setattr(style, prop, 1); break
except Exception: pass
# --- Boundary ---
if hasattr(style, "BoundaryVisible"):
try: style.BoundaryVisible = bool(show)
except Exception: pass
elif hasattr(style, "ShowBoundary"):
try: style.ShowBoundary = bool(show)
except Exception: pass
if show:
# Boundary color
bc = section_cfg.get("boundaryColor")
if bc:
for prop in ("BoundaryColor", "OutlineColor", "EdgeColor"):
if hasattr(style, prop):
try: setattr(style, prop, _color(bc)); break
except Exception: pass
# Boundary width scale
ws = float(section_cfg.get("boundaryWidthScale") or 1.0)
for prop in ("BoundaryWidthScale", "EdgeWidthScale", "OutlineWidthScale"):
if hasattr(style, prop):
try: setattr(style, prop, ws); break
except Exception: pass
# Linetype
lt = section_cfg.get("boundaryLinetype")
if lt and lt not in ("byLayer", "ByLayer"):
lt_idx = _find_linetype_index(doc, lt)
for prop in ("BoundaryLinetypeIndex", "EdgeLinetypeIndex"):
if hasattr(style, prop):
try: setattr(style, prop, lt_idx); break
except Exception: pass
# Section open objects
soo = bool(section_cfg.get("sectionOpenObjects", True))
for prop in ("SectionOpenObjects", "ClipOpenObjects"):
if hasattr(style, prop):
try: setattr(style, prop, soo); break
except Exception: pass
# Style auf Layer setzen
try:
layer.SetCustomSectionStyle(style)
except Exception as ex:
print("[EBENEN] SetCustomSectionStyle({}): {}".format(layer.Name, ex))
def build_layers(doc, zeichnungsebenen, ebenen):
"""
Stellt sicher dass fuer jede Zeichnungsebene ein Parent-Layer existiert
@@ -113,6 +257,13 @@ def build_layers(doc, zeichnungsebenen, ebenen):
sub.PlotWeight = lw
sub.SetUserString("dossier_code", e["code"])
# Section Style anwenden (Py3-only — IPy 2.7 no-op)
try:
_apply_section_style(doc, doc.Layers[sub_idx],
e.get("section"), e.get("color"))
except Exception as ex:
print("[EBENEN] section-style apply ({}): {}".format(sub_name, ex))
doc.Views.Redraw()
print("[EBENEN] {} Zeichnungsebenen x {} Ebenen aktualisiert".format(
len(zeichnungsebenen), len(ebenen)))
@@ -241,48 +392,125 @@ def update_clipping_plane(doc, active_z, enabled):
"""
Erstellt/aktualisiert/entfernt die DOSSIER-Clipping-Plane an OKFF + Schnitthoehe
des aktiven Geschosses. Plane zeigt nach +Z, schneidet alles oberhalb weg.
Diagnostik via [CLIP]-Praefix in den Print-Ausgaben — bei Problemen die
Rhino-Konsole nach 'CLIP' filtern.
"""
import Rhino.Geometry as rg
z_name = (active_z or {}).get("name") if active_z else None
print("[CLIP] update: enabled={} active='{}' isGeschoss={} okff={} sh={}".format(
enabled,
z_name,
bool(active_z and active_z.get("isGeschoss")),
(active_z or {}).get("okff"),
(active_z or {}).get("schnitthoehe"),
))
existing = _find_clipping_plane(doc)
print("[CLIP] existing: {}".format(existing.Id if existing else "none"))
is_geschoss = bool(active_z and active_z.get("isGeschoss") and active_z.get("okff") is not None)
if (not enabled) or (not is_geschoss):
if existing is not None:
try:
doc.Objects.Delete(existing.Id, True)
except Exception:
pass
return
okff = float(active_z.get("okff", 0.0))
sh = float(active_z.get("schnitthoehe", 1.0))
cut_z = okff + sh
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d.ZAxis)
du, dv = 50000.0, 50000.0
# IMMER vorhandene Plane loeschen — bei Re-Enable wollen wir frische
# vp_ids (alte koennten leer/falsch sein, dann clippt das Replace zwar
# die Geometrie aber keinen Viewport).
if existing is not None:
try:
new_surf = rg.PlaneSurface(plane, rg.Interval(-du/2.0, du/2.0), rg.Interval(-dv/2.0, dv/2.0))
doc.Objects.Replace(existing.Id, new_surf)
doc.Objects.Delete(existing.Id, True)
print("[CLIP] alte Plane geloescht")
except Exception as ex:
print("[EBENEN] Clip-Update:", ex)
else:
vp_ids = []
for view in doc.Views:
try:
vp_ids.append(view.ActiveViewportID)
except Exception:
try: vp_ids.append(view.ActiveViewport.Id)
except Exception: pass
print("[CLIP] Delete fehlgeschlagen:", ex)
if (not enabled) or (not is_geschoss):
print("[CLIP] disabled — fertig (enabled={}, isGeschoss={})".format(enabled, is_geschoss))
doc.Views.Redraw()
return
# dict.get(k, default) liefert default NUR wenn Key fehlt — bei
# Key-vorhanden-aber-None gibt's None zurueck. float(None) crasht.
# Daher explizit None-faangen:
okff_raw = active_z.get("okff")
sh_raw = active_z.get("schnitthoehe")
okff = float(okff_raw) if okff_raw is not None else 0.0
sh = float(sh_raw) if sh_raw is not None else 1.0
cut_z = okff + sh
print("[CLIP] cut_z={} (okff={}, schnitthoehe={})".format(cut_z, okff, sh))
# Normal nach -Z = sichtbar bleibt UNTERHALB der Plane (Grundriss-Schnitt:
# man steht auf der Boden-Seite, alles darueber wird weggeschnitten).
# Mit +Z waere es genau umgekehrt (Decke + Rest oben sichtbar).
plane = rg.Plane(rg.Point3d(0.0, 0.0, cut_z), rg.Vector3d(0.0, 0.0, -1.0))
du, dv = 50000.0, 50000.0
# Viewport-IDs sammeln — wir wollen ALLE Modell-Viewports clippen,
# nicht nur den gerade aktiven. Sammeln aus mehreren Quellen +
# dedupen damit die Plane in Top/Front/Right/Perspective gleichzeitig
# wirkt.
vp_ids = []
seen = set()
def _add(vpid):
if vpid is None: return
try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id != System.Guid.Empty:
obj = doc.Objects.FindId(new_id)
if obj is not None:
attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1")
attrs.Mode = Rhino.DocObjects.ObjectMode.Locked
doc.Objects.ModifyAttributes(obj, attrs, True)
print("[EBENEN] Clipping-Plane bei Z={} erstellt".format(cut_z))
except Exception as ex:
print("[EBENEN] Clip-Create:", ex)
key = str(vpid)
except Exception: return
if key in seen or vpid == System.Guid.Empty: return
seen.add(key); vp_ids.append(vpid)
# Methode 1: GetViewList — alle Modell-Views (kein Page-Layout)
try:
views = doc.Views.GetViewList(True, False)
for v in views:
try: _add(v.ActiveViewport.Id)
except Exception: pass
try: _add(v.MainViewport.Id)
except Exception: pass
except Exception as ex:
print("[CLIP] GetViewList Fehler:", ex)
# Methode 2: Iteration ueber Views (Fallback falls GetViewList anders)
try:
for view in doc.Views:
try: _add(view.ActiveViewport.Id)
except Exception: pass
try: _add(view.MainViewport.Id)
except Exception: pass
try: _add(view.ActiveViewportID)
except Exception: pass
except Exception as ex:
print("[CLIP] doc.Views iteration Fehler:", ex)
# Namen fuer Debug-Output sammeln
vp_names = []
try:
for view in doc.Views:
try: vp_names.append(view.ActiveViewport.Name)
except Exception: pass
except Exception: pass
print("[CLIP] {} Viewport-ID(s) gesammelt: {}".format(
len(vp_ids), ", ".join(vp_names) or "(keine Namen)"))
if not vp_ids:
print("[CLIP] WARNUNG: keine Viewports — Plane wuerde nichts schneiden")
try:
new_id = doc.Objects.AddClippingPlane(plane, du, dv, vp_ids)
if new_id == System.Guid.Empty:
print("[CLIP] AddClippingPlane lieferte Empty Guid — Fehler")
return
obj = doc.Objects.FindId(new_id)
if obj is None:
print("[CLIP] FindId nach Erstellung lieferte None — Object weg")
return
attrs = obj.Attributes.Duplicate()
attrs.SetUserString(_CLIP_KEY, "1")
# Mode = Normal damit die Plane in Mac Rhino voll sichtbar ist.
# Locked-Mode rendert auf Mac oft nur ein blasses Edge. Wer den
# Plane-Boundary nicht selektieren will, kann via Layer locken.
attrs.Mode = Rhino.DocObjects.ObjectMode.Normal
doc.Objects.ModifyAttributes(obj, attrs, True)
print("[CLIP] Plane erstellt: Z={}, ID={}, du/dv={}/{}".format(
cut_z, new_id, du, dv))
except Exception as ex:
print("[CLIP] AddClippingPlane Fehler:", ex)
doc.Views.Redraw()