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:
@@ -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"))
|
||||
Reference in New Issue
Block a user