Initial commit — Dossier Rhino 8 Plugin

OpenStudio-Suite Architektur-Plugin fuer Rhino 8 (Mac):
- Smart-Elemente: Wand, Decke, Dach (Pult/Sattel/Walm/Mansarde),
  Oeffnungen (Fenster/Tueren mit Rahmen + Sims + Glas + Fluegel),
  Treppen (gerade · L · Wendel mit Schrittmass-Validierung)
- Live-Previews mit Step-Lines + Soll-Range-Clamping
- Bidirektionale Selection-Sync zwischen Source-Linie und Volume
- Geschoss-/Ebenen-Verwaltung mit OKFF-Persistenz
- Layouts mit PDF-Export
- Ausschnitte / Massstab / Override-Regeln
- Petrol-Gruen Theme (Rapport-konform)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 04:27:41 +02:00
commit 9dc191be4f
145 changed files with 32629 additions and 0 deletions
+748
View File
@@ -0,0 +1,748 @@
# ! python3
# -*- coding: utf-8 -*-
"""
layouts.py
LAYOUTS-Panel: Layout-Pages erstellen + Details mit Ausschnitten bestuecken.
Phase 1 — Snapshot-Mode: Ausschnitt wird beim Zuweisen auf das Detail angewendet,
Re-Sync per Knopf. Live-Link und Masterlayouts kommen spaeter.
"""
import os
import sys
import json
import Rhino
import Rhino.Geometry as rg
import System
import scriptcontext as sc
_HERE = os.path.dirname(os.path.abspath(__file__))
if _HERE not in sys.path:
sys.path.insert(0, _HERE)
import panel_base
PANEL_GUID_STR = "4e5d6c7b-8a9f-4e3d-c4b5-a6b7c8d9e0f1"
# UserString-Key auf jedem Detail — speichert die Ausschnitt-Bindung.
_BIND_KEY = "dossier_bound_ausschnitt"
# Doc-Strings fuer Layout-Folder-Organisation (analog Ausschnitte-Ordner).
_FOLDER_LIST_KEY = "dossier_layout_folders" # JSON-Array: ["A","B",...]
_FOLDER_MAP_KEY = "dossier_layout_folder_map" # JSON-Dict: {pageId: "A"}
# Vordefinierte Papierformate in Millimetern (Welt-Einheit wird beim Erstellen
# umgerechnet, falls das Doc nicht auf mm steht).
PAPER_SIZES_MM = {
"A0": (841, 1189),
"A1": (594, 841),
"A2": (420, 594),
"A3": (297, 420),
"A4": (210, 297),
"Letter": (216, 279),
}
def _page_unit_system(doc):
"""Rhino hat ein eigenes Unit-System fuer Layouts (PageUnitSystem), das
sich vom Modell-Unit-System unterscheiden kann (z.B. Modell in Meter,
Plan in Millimeter — Standard bei Architektur). AddPageView und
SetPageSize erwarten Werte in PageUnitSystem, NICHT in ModelUnitSystem."""
try:
return doc.PageUnitSystem
except Exception:
return doc.ModelUnitSystem
def _mm_to_page(doc):
"""Faktor: mm -> Page-Unit. Wird fuer AddPageView/SetPageSize benutzt."""
try:
return Rhino.RhinoMath.UnitScale(Rhino.UnitSystem.Millimeters,
_page_unit_system(doc))
except Exception:
return 1.0
def _page_to_mm(doc):
"""Faktor: Page-Unit -> mm. Wird beim PDF-Export gebraucht."""
try:
return Rhino.RhinoMath.UnitScale(_page_unit_system(doc),
Rhino.UnitSystem.Millimeters)
except Exception:
return 1.0
def _page_size_in_doc(doc, fmt, landscape):
"""Liefert (width, height) in Page-Units."""
if fmt not in PAPER_SIZES_MM: return None
w_mm, h_mm = PAPER_SIZES_MM[fmt]
if landscape:
w_mm, h_mm = h_mm, w_mm
f = _mm_to_page(doc)
return (w_mm * f, h_mm * f)
def _load_folder_list(doc):
"""Liefert die Liste explizit angelegter Ordner (Reihenfolge bleibt)."""
raw = doc.Strings.GetValue(_FOLDER_LIST_KEY)
if not raw: return []
try:
data = json.loads(raw)
return [n for n in data if isinstance(n, str) and n]
except Exception:
return []
def _save_folder_list(doc, names):
try:
doc.Strings.SetString(_FOLDER_LIST_KEY, json.dumps(list(names), ensure_ascii=False))
except Exception as ex:
print("[LAYOUTS] _save_folder_list:", ex)
def _load_folder_map(doc):
"""page_id -> folder_name."""
raw = doc.Strings.GetValue(_FOLDER_MAP_KEY)
if not raw: return {}
try:
data = json.loads(raw)
if isinstance(data, dict):
return {k: v for k, v in data.items() if isinstance(v, str) and v}
except Exception:
pass
return {}
def _save_folder_map(doc, m):
try:
doc.Strings.SetString(_FOLDER_MAP_KEY, json.dumps(m, ensure_ascii=False))
except Exception as ex:
print("[LAYOUTS] _save_folder_map:", ex)
def _list_layouts(doc):
"""Liefert dict-Liste aller PageView-Layouts inkl. Ordner-Zuweisung.
Groessen werden ZUSAETZLICH in mm geliefert, damit das Frontend ohne
Unit-Kenntnis formatieren kann."""
fmap = _load_folder_map(doc)
pu_to_mm = _page_to_mm(doc)
out = []
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
try:
pid = str(v.MainViewport.Id)
w = float(v.PageWidth)
h = float(v.PageHeight)
out.append({
"id": pid,
"name": v.PageName or "",
"width": w,
"height": h,
"widthMm": w * pu_to_mm,
"heightMm": h * pu_to_mm,
"detailCount": len(v.GetDetailViews() or []),
"folder": fmap.get(pid, ""),
})
except Exception as ex:
print("[LAYOUTS] list_layouts:", ex)
return out
def _find_page_by_id(doc, page_id):
"""page_id ist die MainViewport.Id (str). Liefert RhinoPageView oder None."""
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
try:
if str(v.MainViewport.Id) == page_id:
return v
except Exception:
pass
return None
def _find_detail_by_id(doc, page, detail_id):
"""Detail-Object auf einer Page anhand seiner Detail-ID."""
try:
for d in page.GetDetailViews():
if str(d.Id) == detail_id:
return d
except Exception as ex:
print("[LAYOUTS] find_detail:", ex)
return None
def _get_detail_binding(detail):
"""Liest gebundene Ausschnitt-ID aus dem Detail-UserString."""
try:
v = detail.Attributes.GetUserString(_BIND_KEY)
return v if v else None
except Exception:
return None
def _set_detail_binding(detail, snap_id):
"""Schreibt/loescht die Ausschnitt-ID auf das Detail-UserString."""
try:
if snap_id:
detail.Attributes.SetUserString(_BIND_KEY, snap_id)
else:
detail.Attributes.SetUserString(_BIND_KEY, "") # leerer String = "kein Binding"
detail.CommitChanges()
return True
except Exception as ex:
print("[LAYOUTS] set_binding:", ex)
return False
def _detail_dict(detail, snap_lookup):
"""Serialisiert ein Detail fuer das Frontend."""
bbox = detail.Geometry.GetBoundingBox(True) # in Welt-Koordinaten der Page
bound_id = _get_detail_binding(detail)
bound_name = (snap_lookup.get(bound_id, {}) or {}).get("name") if bound_id else None
try:
vp_name = detail.Viewport.Name
except Exception:
vp_name = ""
return {
"id": str(detail.Id),
"name": vp_name,
"x": float(bbox.Min.X),
"y": float(bbox.Min.Y),
"width": float(bbox.Max.X - bbox.Min.X),
"height": float(bbox.Max.Y - bbox.Min.Y),
"boundAusschnitt": bound_id,
"boundAusschnittName": bound_name,
}
def _snap_lookup(doc):
"""Map snap_id -> snap dict. Wird fuer Detail-Display gebraucht."""
out = {}
try:
raw = doc.Strings.GetValue("dossier_ausschnitte")
if raw:
data = json.loads(raw)
if isinstance(data, list):
for s in data:
if isinstance(s, dict) and s.get("id"):
out[s["id"]] = s
except Exception:
pass
return out
def _slim_snaps(doc):
"""Schlanke Liste von Snapshots fuer Frontend-Dropdown."""
out = []
try:
raw = doc.Strings.GetValue("dossier_ausschnitte")
if not raw: return []
data = json.loads(raw)
if not isinstance(data, list): return []
for s in data:
if isinstance(s, dict) and s.get("id"):
out.append({
"id": s.get("id"),
"name": s.get("name"),
"folder": s.get("folder", ""),
"scale": s.get("scale", ""),
})
except Exception:
pass
return out
# --- Bridge -----------------------------------------------------------------
class LayoutsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "layouts")
def _on_ready(self):
self._send_state()
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if not isinstance(p, dict): p = {}
if t == "READY": self._on_ready()
elif t == "LIST": self._send_state()
elif t == "NEW_LAYOUT": self._new_layout(p)
elif t == "DELETE_LAYOUT": self._delete_layout(p.get("id"))
elif t == "RENAME_LAYOUT": self._rename_layout(p.get("id"), p.get("name"))
elif t == "SET_PAGE_SIZE": self._set_page_size(p)
elif t == "ACTIVATE_LAYOUT": self._activate_layout(p.get("id"))
elif t == "EXPORT_PDF": self._export_pdf(p)
elif t == "ADD_DETAIL": self._add_detail(p)
elif t == "DELETE_DETAIL": self._delete_detail(p.get("pageId"), p.get("detailId"))
elif t == "BIND_AUSSCHNITT": self._bind_ausschnitt(p)
elif t == "SYNC_DETAIL": self._sync_detail(p.get("pageId"), p.get("detailId"))
elif t == "SYNC_LAYOUT": self._sync_layout(p.get("id"))
# Ordner-Management
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "SET_FOLDER": self._set_folder(p.get("id"), p.get("folder") or "")
# --- State-Snapshot -----------------------------------------------------
def _send_state(self):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
self.send("STATE", {"layouts": [], "snapshots": [], "details": {}, "folders": []})
return
layouts = _list_layouts(doc)
snaps = _slim_snaps(doc)
snap_lookup = _snap_lookup(doc)
# Ordner: explizite Liste + alle in Layouts referenzierten
explicit_folders = _load_folder_list(doc)
for l in layouts:
f = l.get("folder")
if f and f not in explicit_folders:
explicit_folders.append(f)
# Pro Layout die Details mitgeben
details = {}
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
try:
pid = str(v.MainViewport.Id)
details[pid] = [_detail_dict(d, snap_lookup) for d in v.GetDetailViews()]
except Exception as ex:
print("[LAYOUTS] details for page:", ex)
self.send("STATE", {
"layouts": layouts,
"snapshots": snaps,
"details": details,
"folders": explicit_folders,
})
# --- Ordner -------------------------------------------------------------
def _add_folder(self, name):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = _load_folder_list(doc)
if name not in folders:
folders.append(name)
_save_folder_list(doc, folders)
self._send_state()
def _remove_folder(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = [f for f in _load_folder_list(doc) if f != name]
_save_folder_list(doc, folders)
# Layouts aus diesem Ordner herausnehmen (zurueck auf Root)
m = _load_folder_map(doc)
m = {k: v for k, v in m.items() if v != name}
_save_folder_map(doc, m)
self._send_state()
def _set_folder(self, page_id, folder):
if not page_id: return
doc = Rhino.RhinoDoc.ActiveDoc
m = _load_folder_map(doc)
if folder:
m[page_id] = folder
# Sicherstellen dass der Ordner-Name in der expliziten Liste ist
folders = _load_folder_list(doc)
if folder not in folders:
folders.append(folder)
_save_folder_list(doc, folders)
else:
if page_id in m: del m[page_id]
_save_folder_map(doc, m)
self._send_state()
# --- Layouts ------------------------------------------------------------
def _resolve_size(self, doc, p):
"""Bestimmt (w, h) in Page-Units aus Payload — Format-Name ODER
customWidth/customHeight in mm."""
fmt = p.get("format")
if fmt == "custom":
try:
wmm = float(p.get("customWidth"))
hmm = float(p.get("customHeight"))
except Exception:
return None
if wmm <= 0 or hmm <= 0: return None
f = _mm_to_page(doc)
return (wmm * f, hmm * f)
if fmt in PAPER_SIZES_MM:
return _page_size_in_doc(doc, fmt, bool(p.get("landscape", True)))
return None
def _new_layout(self, p):
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
name = (p.get("name") or "").strip()
size = self._resolve_size(doc, p)
if size is None:
print("[LAYOUTS] ungueltige Groesse:", p); return
w, h = size
if not name:
name = "Layout {}".format(len(_list_layouts(doc)) + 1)
try:
page = doc.Views.AddPageView(name, w, h)
if page is None:
print("[LAYOUTS] AddPageView fehlgeschlagen"); return
print("[LAYOUTS] '{}' angelegt ({}x{})".format(name, w, h))
except Exception as ex:
print("[LAYOUTS] AddPageView Fehler:", ex)
self._send_state()
def _set_page_size(self, p):
"""Aendert die Groesse einer bestehenden Layout-Seite via
RhinoPageView.SetPageSize (Rhino 8). Faellt zurueck auf Property-
Setter, falls die Methode nicht existiert."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, p.get("id"))
if page is None: return
size = self._resolve_size(doc, p)
if size is None:
print("[LAYOUTS] ungueltige Groesse fuer set_page_size:", p); return
w, h = size
done = False
# 1) SetPageSize(double, double) — Rhino 8
if hasattr(page, "SetPageSize"):
try:
page.SetPageSize(float(w), float(h))
done = True
print("[LAYOUTS] SetPageSize -> {}x{}".format(w, h))
except Exception as ex:
print("[LAYOUTS] SetPageSize fehlgeschlagen:", ex)
# 2) Fallback: Properties (haengt von Rhino-Version ab)
if not done:
try:
page.PageWidth = float(w)
page.PageHeight = float(h)
done = True
print("[LAYOUTS] PageWidth/Height-Properties -> {}x{}".format(w, h))
except Exception as ex:
print("[LAYOUTS] Property-Setter fehlgeschlagen:", ex)
if not done:
print("[LAYOUTS] Konnte Seiten-Groesse nicht setzen — bitte ueber Rhinos Layout-Dialog aendern")
try: page.Redraw()
except Exception: pass
self._send_state()
def _export_pdf(self, p):
"""Exportiert Layouts als ein gemeinsames PDF. Akzeptiert:
- "ids": Liste von Layout-IDs (Multi-Export)
- "id": einzelne Layout-ID
- sonst alle Layouts.
Save-Dialog via Eto.Forms, Inhalt via FilePdf API mit
EXPLIZITER Pixel-Groesse aus Page-Dimensionen (sonst wird's ein
Mini-Bildchen)."""
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
ids = p.get("ids")
if not ids and p.get("id"): ids = [p.get("id")]
dpi = float(p.get("dpi") or 300)
targets = []
if ids:
for pid in ids:
page = _find_page_by_id(doc, pid)
if page is not None: targets.append(page)
else:
for v in doc.Views:
if isinstance(v, Rhino.Display.RhinoPageView):
targets.append(v)
if not targets:
print("[LAYOUTS] kein Layout zu exportieren"); return
path = self._pick_save_path(doc, targets)
if not path:
print("[LAYOUTS] Export abgebrochen")
return
# PageWidth/PageHeight sind in Page-Units — fuer die Pixel-Berechnung
# rechnen wir in mm um (1 inch = 25.4 mm).
mm_per_pu = _page_to_mm(doc)
# Remember current active view to restore afterwards
prev_view = None
try: prev_view = doc.Views.ActiveView
except Exception: pass
try:
pdf = Rhino.FileIO.FilePdf.Create()
n_added = 0
for page in targets:
try:
# Page muss kurz aktiv sein — sonst rendert ViewCapture
# leer (vor allem auf macOS). Kurzer Idle-Wait gibt dem
# Renderer Zeit, sonst kommt's beim ERSTEN Page-Wechsel
# zu Race-Bedingungen.
try:
page.SetPageAsActive()
doc.Views.ActiveView = page
page.Redraw()
doc.Views.Redraw()
Rhino.RhinoApp.Wait() # einen Idle-Tick verarbeiten
except Exception: pass
# Page-Size in mm -> Pixel @dpi
w_mm = float(page.PageWidth) * mm_per_pu
h_mm = float(page.PageHeight) * mm_per_pu
if w_mm <= 0 or h_mm <= 0:
print("[LAYOUTS] Page '{}' hat ungueltige Groesse: {}x{} mm".format(
page.PageName, w_mm, h_mm))
continue
px_w = max(1, int(round(w_mm / 25.4 * dpi)))
px_h = max(1, int(round(h_mm / 25.4 * dpi)))
# Settings mit expliziter Groesse — die Drei-Argument-
# Variante ist die zuverlaessige fuer PDF-Export.
settings = None
try:
size = System.Drawing.Size(px_w, px_h)
settings = Rhino.Display.ViewCaptureSettings(page, size, dpi)
except Exception as ex:
print("[LAYOUTS] 3-arg Settings:", ex)
if settings is None:
settings = Rhino.Display.ViewCaptureSettings(page, dpi)
# Vector-Output — sonst wird's gerastert und klein
try: settings.RasterMode = False
except Exception: pass
pdf.AddPage(settings)
n_added += 1
print("[LAYOUTS] add page '{}': {}x{}mm -> {}x{}px".format(
page.PageName, w_mm, h_mm, px_w, px_h))
except Exception as ex:
print("[LAYOUTS] add_page '{}': {}".format(page.PageName, ex))
if n_added == 0:
print("[LAYOUTS] Keine Seiten konnten hinzugefuegt werden")
else:
pdf.Write(path)
print("[LAYOUTS] PDF geschrieben: {} ({} Seite(n))".format(path, n_added))
except Exception as ex:
print("[LAYOUTS] PDF-Export fehlgeschlagen:", ex)
finally:
# Vorherige View wieder aktivieren
if prev_view is not None:
try: doc.Views.ActiveView = prev_view
except Exception: pass
def _pick_save_path(self, doc, targets):
"""Eto.Forms SaveFileDialog — Default: Doc-Folder + erster Layout-Name."""
try:
import Eto.Forms as forms
dlg = forms.SaveFileDialog()
dlg.Filters.Add(forms.FileFilter("PDF", ".pdf"))
try: dlg.CurrentFilterIndex = 0
except Exception: pass
# Default-Filename — Layout-Name oder Doc-Name
if len(targets) == 1:
base = targets[0].PageName or "Layout"
else:
base = "Layouts"
if doc.Path:
base = os.path.splitext(os.path.basename(doc.Path))[0] + "_Layouts"
dlg.FileName = "{}.pdf".format(base)
# Default-Folder — neben der .3dm wenn vorhanden
if doc.Path:
try: dlg.Directory = System.Uri(os.path.dirname(doc.Path))
except Exception: pass
try:
import Rhino.UI as RhinoUI
parent = RhinoUI.RhinoEtoApp.MainWindow
except Exception:
parent = None
result = dlg.ShowDialog(parent)
if str(result) != "Ok":
return None
path = dlg.FileName
if path and not path.lower().endswith(".pdf"):
path += ".pdf"
return path
except Exception as ex:
print("[LAYOUTS] SaveFileDialog:", ex)
return None
def _delete_layout(self, page_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None:
print("[LAYOUTS] delete: page not found", page_id); return
name = page.PageName
# Andere View aktivieren — Rhino verweigert oft, die aktive Page zu loeschen
try:
for v in doc.Views:
if v is not page and not isinstance(v, Rhino.Display.RhinoPageView):
doc.Views.ActiveView = v
break
else:
# Nur PageViews da — irgendeine andere aktivieren
for v in doc.Views:
if v is not page:
doc.Views.ActiveView = v
break
except Exception as ex:
print("[LAYOUTS] activate other view:", ex)
done = False
# Methode 1: doc.Views.Remove
try:
r = doc.Views.Remove(page)
print("[LAYOUTS] doc.Views.Remove returned:", r)
# Verify
if _find_page_by_id(doc, page_id) is None:
done = True
except Exception as ex:
print("[LAYOUTS] doc.Views.Remove failed:", ex)
# Methode 2: Close + Delete via RunScript-Fallback
if not done:
try:
Rhino.RhinoApp.RunScript('_-CommandHistory _Hide _Enter', False)
except Exception: pass
try:
# Rhino 8 hat _-Layout _Delete <name>
Rhino.RhinoApp.RunScript('_-Layout _Delete "{}" _Enter'.format(name), False)
if _find_page_by_id(doc, page_id) is None:
done = True
print("[LAYOUTS] geloescht via _-Layout _Delete")
except Exception as ex:
print("[LAYOUTS] _-Layout _Delete failed:", ex)
if not done:
print("[LAYOUTS] Konnte Layout '{}' nicht loeschen — bitte manuell ueber Layout-Tab".format(name))
# Folder-Mapping aufraeumen
try:
m = _load_folder_map(doc)
if page_id in m:
del m[page_id]
_save_folder_map(doc, m)
except Exception: pass
self._send_state()
def _rename_layout(self, page_id, name):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None or not name: return
try:
page.PageName = name.strip()
except Exception as ex:
print("[LAYOUTS] Rename page:", ex)
self._send_state()
def _activate_layout(self, page_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
try:
page.SetPageAsActive()
doc.Views.ActiveView = page
except Exception as ex:
print("[LAYOUTS] activate page:", ex)
# --- Details ------------------------------------------------------------
def _add_detail(self, p):
"""Neues Detail auf einer Seite anlegen. Optional gleich an einen
Ausschnitt binden (= Snapshot anwenden)."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, p.get("pageId"))
if page is None: return
# Bounding-Box auf der Seite: default 80% der Seitenflaeche, zentriert
try:
pw = page.PageWidth
ph = page.PageHeight
margin_x = pw * 0.1
margin_y = ph * 0.1
c0 = rg.Point2d(margin_x, margin_y)
c1 = rg.Point2d(pw - margin_x, ph - margin_y)
# Erlaubt Override per Payload
for k_min, default in (("x", margin_x), ("y", margin_y)):
v = p.get(k_min)
if isinstance(v, (int, float)):
if k_min == "x": c0 = rg.Point2d(float(v), c0.Y)
if k_min == "y": c0 = rg.Point2d(c0.X, float(v))
for k_max, default in (("x2", pw - margin_x), ("y2", ph - margin_y)):
v = p.get(k_max)
if isinstance(v, (int, float)):
if k_max == "x2": c1 = rg.Point2d(float(v), c1.Y)
if k_max == "y2": c1 = rg.Point2d(c1.X, float(v))
proj = Rhino.Display.DefinedViewportProjection.Top
detail = page.AddDetailView("Detail", c0, c1, proj)
if detail is None:
print("[LAYOUTS] AddDetailView gab None"); return
page.Redraw()
# Optional Ausschnitt binden + anwenden
snap_id = p.get("ausschnittId")
if snap_id:
_set_detail_binding(detail, snap_id)
try:
import ausschnitte
ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id)
except Exception as ex:
print("[LAYOUTS] initial apply:", ex)
except Exception as ex:
print("[LAYOUTS] AddDetailView:", ex)
self._send_state()
def _delete_detail(self, page_id, detail_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
detail = _find_detail_by_id(doc, page, detail_id)
if detail is None: return
try:
doc.Objects.Delete(detail.Id, True)
page.Redraw()
except Exception as ex:
print("[LAYOUTS] Delete detail:", ex)
self._send_state()
def _bind_ausschnitt(self, p):
"""Setzt die Binding und wendet den Ausschnitt sofort an (Snapshot-Mode)."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, p.get("pageId"))
if page is None: return
detail = _find_detail_by_id(doc, page, p.get("detailId"))
if detail is None: return
snap_id = p.get("ausschnittId") or None
_set_detail_binding(detail, snap_id)
if snap_id:
try:
import ausschnitte
ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id)
except Exception as ex:
print("[LAYOUTS] apply on bind:", ex)
self._send_state()
def _sync_detail(self, page_id, detail_id):
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
detail = _find_detail_by_id(doc, page, detail_id)
if detail is None: return
snap_id = _get_detail_binding(detail)
if not snap_id:
print("[LAYOUTS] sync: kein Binding auf diesem Detail")
return
try:
import ausschnitte
ausschnitte.apply_snapshot_to_detail(doc, detail, snap_id)
except Exception as ex:
print("[LAYOUTS] sync:", ex)
self._send_state()
def _sync_layout(self, page_id):
"""Alle Details der Page mit ihren Bindings re-applien."""
doc = Rhino.RhinoDoc.ActiveDoc
page = _find_page_by_id(doc, page_id)
if page is None: return
try:
import ausschnitte
for d in page.GetDetailViews():
snap_id = _get_detail_binding(d)
if snap_id:
ausschnitte.apply_snapshot_to_detail(doc, d, snap_id)
page.Redraw()
except Exception as ex:
print("[LAYOUTS] sync layout:", ex)
self._send_state()
def _bridge_factory():
b = LayoutsBridge()
sc.sticky["layouts_bridge"] = b
return b
panel_base.register_and_open("layouts", "LAYOUTS", PANEL_GUID_STR,
_bridge_factory, icon_spec=("L", "#7a5fa8"))