9dc191be4f
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>
749 lines
28 KiB
Python
749 lines
28 KiB
Python
# ! 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"))
|