Files
DOSSIER/rhino/ausschnitte.py
T
karim 0c5f8055a5 Fenster/Tueren LoD + Stile + Phase-3-Ausschnitt-Darstellung + UI-Konsistenz
Fenster/Tueren:
- 3-stufige SIA-400-Darstellung pro Element: einfach (1:100, flache
  Scheibe ohne Tiefe in Wand-Mittelebene), standard (1:50, Rahmen +
  Glas + Sims), detail (1:20, Doppelverglasung).
- Aussenseite-Flag mit Auto-Detection aus der Click-Richtung beim
  Setzen — Sim sitzt automatisch aussen. Im Panel als Umkehren-Toggle.
- Tueren-Rahmen-Typ Zarge|Block — Blockrahmen ragt seitlich raus.
- Rahmen-Offset (m von Wand-Innenseite) ersetzt das 3-Preset Lage-
  Feld. Wirkt auch in der einfachen Darstellung (Pane sitzt auf der
  Rahmen-Mittelebene, nicht in Wand-Mitte).
- Sims nur AUSSEN. Innen entfaellt — der Sim ist gleichzeitig der
  visuelle Indikator fuer die Aussenseite.
- Oeffnungs-Stile: list/save/delete-API mit 6 Default-Presets
  (Fenster Standard/Gross/Bandlage, Tuer Innen/Eingang/Verglast).
  Style-ID per UserString am Objekt persistiert. Im Panel BarCombo
  mit "Aktuelle als Stil speichern…". Beim Rhino-Command "Stil"-
  Option zum Picken vor dem Klick.

Ausschnitt-Darstellung (Phase 3):
- Doc-Level Override dossier_aktive_darstellung gewinnt vor per-
  Object-Setting. Wechsel triggert Regen aller Oeffnungen via neuer
  regenerate_all_oeffnungen-API.
- Ausschnitt-Capture speichert die Darstellung mit, Restore wendet
  sie an und regeneriert.
- Oberleiste-Quick-Switch BarCombo mit 4 Optionen.
- AusschnittSettings-Dialog: Darstellungs-Dropdown.

Gestaltung (SectionStyle Phase 2):
- _set_section_style schreibt per-Object SectionHatchIndex/Scale/
  Rotation/Color mit Multi-Fallback (Property-Namen varieren je
  Rhino-Build). _selection_summary liest die selben zurueck.
- HatchEditor als shared Component fuer Fill + Section.
- geometryKind ignoriert DOSSIER-Source-Curves damit Wand-Selektion
  (Axis + Volume) als 3D klassifiziert wird.

UI-Konsistenz Panels:
- Ebenenkombi zurueck als eigene Section oben im Ebenen-Panel,
  Modelldarstellung-Dropdown an die freigewordene Position in der
  Oberleiste (Row 1 Col 2 im 2x2-Preset-Block).
- BarCombo erweitert: stretch-Prop (Pill waechst auf Container-
  Breite), onSecond/secondIcon/secondTitle fuer 2. Trailing-Button,
  gearIcon-Prop. Plus-Slot immer ganz aussen rechts, Settings-Slot
  direkt nach dem Caret.
- Ebenen + Zeichnungsebenen visuell kohaerent: identisches Padding
  (1px 12px 1px 0), Chevron/Spacer-Slot 12px, Master-Row mit Eye
  16x16 + Lock 14x14, gleiche Border + Borderfarbe. Eye-Icons in
  beiden Panels untereinander ausgerichtet.
- Properties-Container ohne Border (war zuvor accent-gruen, dann
  border — User wollte gar nichts mehr).
- ElementList raus aus dem Elemente-Panel (Uebersicht via Tree-
  Window erreichbar). NeuesElement bleibt voll sichtbar bei
  Selektion (kein Collapse), Properties oben.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 12:34:15 +02:00

887 lines
35 KiB
Python

#! python 3
# -*- coding: utf-8 -*-
"""
ausschnitte.py
AUSSCHNITTE-Panel: speichert Viewport-Ausschnitte mit Kamera, Display-Mode,
Layer-Sichtbarkeit, DOSSIER-State. Anwendbar im Model-Space und auf Layout-Details.
"""
import os
import sys
import math
import json
import uuid
import Rhino
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 = "5c9e4f3f-6d0e-4f1f-b3c4-d5e6f7081a2b"
_STORE_KEY = "dossier_ausschnitte"
_FOLDERS_KEY = "dossier_ausschnitt_folders"
_PRESETS_KEY = "dossier_layer_presets"
def _orientation_from_camera(loc, tgt, parallel=True):
"""Bestimmt Orientierung:
- perspective = perspektivische Projektion (kein orthogonaler Schnitt)
- horizontal = parallele Projektion mit Blick weitgehend nach unten/oben (Grundriss)
- vertical = parallele Projektion mit Blick weitgehend seitlich (Schnitt/Ansicht)
"""
if not parallel:
return "perspective"
try:
dx = tgt[0] - loc[0]
dy = tgt[1] - loc[1]
dz = tgt[2] - loc[2]
m = math.sqrt(dx * dx + dy * dy + dz * dz)
if m <= 0: return "vertical"
return "horizontal" if (abs(dz) / m) > 0.7 else "vertical"
except Exception:
return "vertical"
def _parse_scale(scale_str):
"""Parse '1:50' / '1=50' / '50' → (page, model). Gibt None zurueck wenn nicht parsebar."""
if not scale_str:
return None
s = scale_str.strip()
for sep in (":", "=", "/"):
if sep in s:
try:
a, b = s.split(sep, 1)
pa = float(a.strip())
pb = float(b.strip())
if pa > 0 and pb > 0:
return (pa, pb)
except Exception:
pass
break
# nur Zahl: 1:N angenommen
try:
n = float(s)
if n > 0:
return (1.0, n)
except Exception:
pass
return None
# --- Capture / Apply Helpers ------------------------------------------------
def _capture_camera(vp):
dm = vp.DisplayMode
dm_id = str(dm.Id) if dm else None
dm_nm = dm.LocalName if dm else None
loc = [vp.CameraLocation.X, vp.CameraLocation.Y, vp.CameraLocation.Z]
tgt = [vp.CameraTarget.X, vp.CameraTarget.Y, vp.CameraTarget.Z]
# Frustum-Breite mitsichern: bei Parallelprojektion bestimmt sie den Zoom.
# Ohne sie laesst sich der gespeicherte Ausschnitt nicht rekonstruieren.
frustum_w = None
frustum_h = None
try:
ok, l, r, b, t, n, f = vp.GetFrustum()
if ok:
frustum_w = float(r - l)
frustum_h = float(t - b)
except Exception:
pass
return {
"viewName": vp.Name,
"location": loc,
"target": tgt,
"up": [vp.CameraUp.X, vp.CameraUp.Y, vp.CameraUp.Z],
"lens": float(vp.Camera35mmLensLength) if vp.Camera35mmLensLength else 50.0,
"parallel": bool(vp.IsParallelProjection),
"displayMode": dm_id,
"displayModeName": dm_nm,
"orientation": _orientation_from_camera(loc, tgt, bool(vp.IsParallelProjection)),
"frustumWidth": frustum_w,
"frustumHeight": frustum_h,
}
def _apply_camera(vp, cam):
if not cam: return
try:
loc = Rhino.Geometry.Point3d(*cam["location"])
tgt = Rhino.Geometry.Point3d(*cam["target"])
up = Rhino.Geometry.Vector3d(*cam["up"]) if cam.get("up") else None
if cam.get("parallel"):
vp.ChangeToParallelProjection(True)
else:
vp.ChangeToPerspectiveProjection(True, float(cam.get("lens", 50)))
vp.SetCameraLocations(tgt, loc)
if up:
try: vp.CameraUp = up
except Exception: pass
dm_id = cam.get("displayMode")
if dm_id:
try:
mode = Rhino.Display.DisplayModeDescription.GetDisplayMode(System.Guid(dm_id))
if mode is not None: vp.DisplayMode = mode
except Exception:
pass
# Zoom (Frustum-Breite) rekonstruieren — nur bei Parallelprojektion sinnvoll.
# Bei Perspective bestimmt die Lens-Length den Bildausschnitt, dort
# waere ein Magnify kontraproduktiv.
fw = cam.get("frustumWidth")
if fw and fw > 0 and cam.get("parallel"):
try:
ok, l, r, b, t, n, f = vp.GetFrustum()
if ok:
cur_w = float(r - l)
if cur_w > 0:
factor = cur_w / float(fw)
if 1e-9 < factor < 1e9:
try:
vp.Magnify(float(factor), False)
except Exception:
try:
vp.Magnify(float(factor))
except Exception:
Rhino.RhinoApp.RunScript(
"_-Zoom _Factor {:.6f} _Enter".format(factor), False)
except Exception as ex:
print("[AUSSCHNITTE] Frustum-Apply:", ex)
except Exception as ex:
print("[AUSSCHNITTE] Camera-Apply:", ex)
def _capture_layers(doc):
out = []
for layer in doc.Layers:
if layer.IsDeleted: continue
out.append({
"id": str(layer.Id),
"visible": bool(layer.IsVisible),
"locked": bool(layer.IsLocked),
})
return out
def _apply_layers_global(doc, layers):
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
for ls in layers:
layer = by_id.get(ls.get("id"))
if layer is None: continue
if layer.IsVisible != ls.get("visible", True):
layer.IsVisible = ls.get("visible", True)
if layer.IsLocked != ls.get("locked", False):
layer.IsLocked = ls.get("locked", False)
def _apply_layers_per_viewport(doc, layers, vp_id):
"""Setzt Sichtbarkeit pro Viewport (fuer Layout-Details)."""
by_id = {}
for layer in doc.Layers:
if not layer.IsDeleted:
by_id[str(layer.Id)] = layer
for ls in layers:
layer = by_id.get(ls.get("id"))
if layer is None: continue
try:
layer.SetPerViewportVisible(vp_id, ls.get("visible", True))
except Exception:
pass
_DOSSIER_KEYS = (
"dossier_zeichnungsebenen",
"dossier_ebenen",
"dossier_active_id",
"dossier_active_code",
)
def _capture_dossier_state(doc):
out = {}
for k in _DOSSIER_KEYS:
v = doc.Strings.GetValue(k)
if v is not None:
out[k] = v
return out
def _apply_dossier_state(doc, state):
for k, v in (state or {}).items():
if v is not None:
doc.Strings.SetString(k, v)
def _find_selected_detail(doc):
"""Sucht nach einem aktuell selektierten Detail-Viewport-Objekt."""
for obj in doc.Objects.GetSelectedObjects(False, False):
if isinstance(obj, Rhino.DocObjects.DetailViewObject):
return obj
return None
def _load_snapshots(doc):
"""Modul-interne Snapshot-Liste (cross-modul nutzbar)."""
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def apply_snapshot_to_detail(doc, detail, snap_id):
"""Wendet einen Ausschnitt auf ein konkretes Detail-Object an. Wird vom
Layouts-Modul benutzt, um Ausschnitt-Detail-Bindings zu synchronisieren.
Liefert True bei Erfolg."""
snap = next((s for s in _load_snapshots(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] apply_to_detail: snap nicht gefunden", snap_id)
return False
# Page-View ermitteln (fuer SetActiveDetail/SetPageAsActive)
page_view = None
try:
for view in doc.Views:
if isinstance(view, Rhino.Display.RhinoPageView):
try:
if any(d.Id == detail.Id for d in view.GetDetailViews()):
page_view = view
break
except Exception:
continue
except Exception as ex:
print("[AUSSCHNITTE] page-view-suche:", ex)
# Detail muss aktiv sein, damit Kamera-Aenderungen anschlagen
was_active = False
try: was_active = detail.IsActive
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetActiveDetail(detail.Id)
except Exception as ex: print("[AUSSCHNITTE] SetActiveDetail:", ex)
# Kamera + Layer + Name
vp = detail.Viewport
_apply_camera(vp, snap.get("camera"))
_apply_layers_per_viewport(doc, snap.get("layers", []), vp.Id)
try:
new_name = snap.get("name")
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Detail-Rename:", ex)
# Massstab
ratio = _parse_scale(snap.get("scale", ""))
if ratio is not None:
page_v, model_v = ratio
for label, setter in (
("DetailGeometry.SetScale", lambda: detail.DetailGeometry.SetScale(model_v, page_v)),
("Detail.SetScale", lambda: detail.SetScale(model_v, page_v)),
):
try:
setter()
break
except Exception:
continue
# Commit + Deaktivieren
try: detail.CommitViewportChanges()
except Exception:
try: detail.CommitChanges()
except Exception: pass
if page_view is not None and not was_active:
try: page_view.SetPageAsActive()
except Exception: pass
try:
(page_view or doc.Views).Redraw()
except Exception:
doc.Views.Redraw()
print("[AUSSCHNITTE] '{}' auf Detail {} angewendet".format(snap.get("name"), detail.Id))
return True
# --- Bridge -----------------------------------------------------------------
class AusschnittBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitte")
def _on_ready(self):
self._send_list()
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_list()
elif t == "SAVE": self._save(p.get("name", "Ausschnitt"))
elif t == "UPDATE": self._update(p.get("id"))
elif t == "RESTORE": self._restore(p.get("id"))
elif t == "APPLY_TO_DETAIL":self._apply_to_detail(p.get("id"))
elif t == "RENAME": self._rename(p.get("id"), p.get("name"))
elif t == "DELETE": self._delete(p.get("id"))
elif t == "SET_FOLDER": self._set_field(p.get("id"), "folder", p.get("folder") or "")
elif t == "SET_SCALE": self._set_field(p.get("id"), "scale", p.get("scale") or "")
elif t == "SET_DARSTELLUNG": self._set_field(p.get("id"), "darstellung",
p.get("darstellung") or "")
elif t == "DUPLICATE": self._duplicate(p.get("id"))
elif t == "ADD_FOLDER": self._add_folder(p.get("name"))
elif t == "REMOVE_FOLDER": self._remove_folder(p.get("name"))
elif t == "GET_LAYERS": self._send_layers(p.get("id"))
elif t == "UPDATE_LAYERS": self._update_layers(p.get("id"), p.get("layers") or [])
elif t == "SAVE_PRESET": self._save_preset(p.get("name"), p.get("layers") or [])
elif t == "DELETE_PRESET": self._delete_preset(p.get("name"))
elif t == "OPEN_SETTINGS": self._open_settings_window(p.get("id"))
def _load(self, doc):
raw = doc.Strings.GetValue(_STORE_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store(self, doc, snaps):
doc.Strings.SetString(_STORE_KEY, json.dumps(snaps, ensure_ascii=False))
def _load_folders(self, doc):
raw = doc.Strings.GetValue(_FOLDERS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_folders(self, doc, folders):
doc.Strings.SetString(_FOLDERS_KEY, json.dumps(folders, ensure_ascii=False))
def _load_presets(self, doc):
raw = doc.Strings.GetValue(_PRESETS_KEY)
if not raw: return []
try:
data = json.loads(raw)
return data if isinstance(data, list) else []
except Exception:
return []
def _store_presets(self, doc, presets):
doc.Strings.SetString(_PRESETS_KEY, json.dumps(presets, ensure_ascii=False))
def _send_list(self):
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
explicit_folders = self._load_folders(doc)
# Aus Snapshots zusaetzliche Ordner ableiten (falls Snap auf nicht-existenten Ordner zeigt)
for s in snaps:
f = s.get("folder", "")
if f and f not in explicit_folders:
explicit_folders.append(f)
slim = []
for s in snaps:
cam = s.get("camera", {}) or {}
orient = cam.get("orientation")
if not orient:
loc = cam.get("location") or [0, 0, 0]
tgt = cam.get("target") or [0, 0, 0]
orient = _orientation_from_camera(loc, tgt, bool(cam.get("parallel", True)))
slim.append({
"id": s.get("id"),
"name": s.get("name"),
"folder": s.get("folder", ""),
"scale": s.get("scale", ""),
"orientation": orient,
"displayModeName": cam.get("displayModeName"),
"parallel": cam.get("parallel", False),
})
preset_summary = [{"name": p.get("name"), "count": len(p.get("layers") or [])}
for p in self._load_presets(doc)]
self.send("LIST", {
"snapshots": slim,
"folders": explicit_folders,
"presets": preset_summary,
})
def _add_folder(self, name):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
folders = self._load_folders(doc)
if name not in folders:
folders.append(name)
self._store_folders(doc, folders)
self._send_list()
def _remove_folder(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
# Aus Folder-Liste entfernen
folders = [f for f in self._load_folders(doc) if f != name]
self._store_folders(doc, folders)
# Snapshots aus diesem Ordner herausnehmen (auf root)
snaps = self._load(doc)
for s in snaps:
if s.get("folder") == name:
s["folder"] = ""
self._store(doc, snaps)
self._send_list()
def _duplicate(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
src = next((s for s in snaps if s.get("id") == snap_id), None)
if not src: return
# Tiefe Kopie via JSON
copy = json.loads(json.dumps(src, ensure_ascii=False))
copy["id"] = "snap_" + uuid.uuid4().hex[:8]
copy["name"] = (src.get("name", "Ausschnitt") + " Kopie")
# Direkt nach Original einfuegen
idx = snaps.index(src)
snaps.insert(idx + 1, copy)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' dupliziert".format(src.get("name")))
def _set_field(self, snap_id, field, value):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
for s in snaps:
if s.get("id") == snap_id:
s[field] = value
break
self._store(doc, snaps)
self._send_list()
def _capture(self, doc, name, existing_id=None, prior_scale=""):
view = doc.Views.ActiveView
if view is None:
print("[AUSSCHNITTE] Keine aktive View")
return None
vp = view.ActiveViewport
# Aktuelle Skala vom MASSSTAB-Modul holen — nur sinnvoll bei Parallel-
# projektion. In Perspective bleibt scale leer (Fallback: prior_scale).
scale_str = ""
try:
import massstab
# Bewusst der EINGESTELLTE Wert (User-Intent), nicht der live aus
# dem Viewport berechnete. Letzterer drifted bei Pan/Zoom.
ratio = massstab.get_applied_scale_ratio()
if ratio is not None and ratio > 0:
if ratio >= 10:
scale_str = "1:{:.0f}".format(ratio)
else:
scale_str = "1:{:.1f}".format(ratio)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala lesen:", ex)
# Fallback: wenn kein Massstab gepinnt war, die aus dem Frustum
# berechnete Live-Skala speichern. So bleibt das Massstab-Dropdown
# nach Restore konsistent (auch wenn der eigentliche Zoom-Restore
# bereits ueber frustumWidth in _apply_camera laeuft).
if not scale_str:
try:
import massstab
live = massstab.get_current_scale_ratio()
if live is not None and live > 0:
scale_str = "1:{:.0f}".format(live) if live >= 10 else "1:{:.1f}".format(live)
except Exception as ex:
print("[AUSSCHNITTE] Live-Skala (Fallback):", ex)
if not scale_str and prior_scale:
scale_str = prior_scale # Perspective -> alten Wert nicht ueberschreiben
# Darstellungs-Override aus dem aktuellen Doc-Setting uebernehmen.
# Leer = "kein Override, per-Object respektieren".
darst = ""
try:
import elemente
darst = elemente.get_aktive_darstellung(doc) or ""
except Exception: pass
return {
"id": existing_id or "snap_" + uuid.uuid4().hex[:8],
"name": name,
"scale": scale_str,
"camera": _capture_camera(vp),
"layers": _capture_layers(doc),
"dossier": _capture_dossier_state(doc),
"darstellung": darst,
}
def _save(self, name):
doc = Rhino.RhinoDoc.ActiveDoc
snap = self._capture(doc, name)
if snap is None: return
snaps = self._load(doc)
snaps.append(snap)
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' gespeichert".format(name))
def _update(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
target = next((s for s in snaps if s.get("id") == snap_id), None)
if not target: return
updated = self._capture(doc, target.get("name", "Ausschnitt"),
existing_id=snap_id,
prior_scale=target.get("scale", ""))
if updated is None: return
for i, s in enumerate(snaps):
if s.get("id") == snap_id:
snaps[i] = updated
break
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] '{}' aktualisiert".format(target.get("name")))
def _restore(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap: return
view = doc.Views.ActiveView
if view is None: return
vp = view.ActiveViewport
_apply_camera(vp, snap.get("camera"))
# Layer-Sichtbarkeit: bevorzugt die referenzierte Ebenenkombi (live —
# zeigt aktuelle Kombi-Definition). Fallback: snap.layers (per-snap
# eingefrorener Zustand).
kombi = (snap.get("layerCombination") or "").strip()
if kombi:
try:
import rhinopanel
rhinopanel.apply_layer_preset_by_name(doc, kombi)
except Exception as ex:
print("[AUSSCHNITTE] kombi-apply '{}':".format(kombi), ex)
_apply_layers_global(doc, snap.get("layers", []))
else:
_apply_layers_global(doc, snap.get("layers", []))
# Eigene Sichtbarkeit → active_comb_name clearen
try:
import rhinopanel
rhinopanel.set_active_comb_name(doc, None)
rhinopanel._notify_oberleiste_combs()
except Exception: pass
_apply_dossier_state(doc, snap.get("dossier") or snap.get("pause") or {})
# Darstellung anwenden + Oeffnungen regenerieren
try:
import elemente
new_darst = snap.get("darstellung") or ""
cur_darst = elemente.get_aktive_darstellung(doc) or ""
if new_darst != cur_darst:
elemente.set_aktive_darstellung(doc, new_darst)
elemente.regenerate_all_oeffnungen(doc)
# Oberleiste-Topbar muss neuen Wert spiegeln
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None: b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] darstellung apply:", ex)
# Overrides: nur anwenden wenn das Snap "applyOverrides" gesetzt hat.
# Sonst bleibt der aktuelle User-Override-State unangetastet.
if snap.get("applyOverrides"):
try:
import overrides
overrides.set_enabled(doc, bool(snap.get("overridesEnabled")))
overrides.set_active_preset(doc, snap.get("overridesPreset") or None)
# Oberleiste-Cache invalidieren damit Topbar das neue Preset zeigt
try:
b = sc.sticky.get("oberleiste_bridge")
if b is not None:
b._cached_overrides = None
b._send_state(force=True)
except Exception: pass
except Exception as ex:
print("[AUSSCHNITTE] overrides-apply:", ex)
# Viewport ZUERST umbenennen — der per-Viewport-Massstab in massstab.py
# wird unter vp.Name geschluesselt. Erst nach dem Rename schreibt
# _apply_scale unter dem neuen Namen, sonst landet der Wert beim alten
# Ausschnitt und der neue zeigt "1:?".
try:
new_name = snap.get("name")
if new_name and vp.Name != new_name:
vp.Name = new_name
except Exception as ex:
print("[AUSSCHNITTE] Rename:", ex)
# Gespeicherten Massstab anwenden (z.B. "1:50") — falls vorhanden und
# Viewport parallel ist (in Perspective ignoriert massstab._apply_scale).
try:
scale_str = (snap.get("scale") or "").strip()
if scale_str:
ratio = _parse_scale(scale_str)
if ratio:
_, model_v = ratio # (page=1, model=N) -> N
import massstab
massstab._apply_scale(doc, vp, float(model_v))
print("[AUSSCHNITTE] Massstab gesetzt auf 1:{} (applied={})".format(
model_v, massstab.get_applied_scale_ratio()))
# Andere Panels (Massstab, Oberleiste) sofort ueber den
# neuen appliedScale informieren — sonst zeigt das Dropdown
# noch den vorherigen Wert bis zum naechsten Idle-Tick mit
# Aenderung an der Live-Skala.
for key in ("massstab_bridge", "oberleiste_bridge"):
try:
b = sc.sticky.get(key)
print("[AUSSCHNITTE] force-send via {}: {}".format(key, "OK" if b is not None else "MISSING"))
if b is not None:
b._send_state(force=True)
except Exception as e:
print("[AUSSCHNITTE] force-send {} failed: {}".format(key, e))
except Exception as ex:
print("[AUSSCHNITTE] Massstab-Restore:", ex)
view.Redraw()
print("[AUSSCHNITTE] '{}' wiederhergestellt".format(snap.get("name")))
def _apply_to_detail(self, snap_id):
doc = Rhino.RhinoDoc.ActiveDoc
# 1) Detail aus Selektion oder aktiver PageView ermitteln
detail = _find_selected_detail(doc)
if detail is None:
try:
av = doc.Views.ActiveView
if isinstance(av, Rhino.Display.RhinoPageView):
for d in av.GetDetailViews():
if d.IsActive:
detail = d
break
except Exception as ex:
print("[AUSSCHNITTE] Active-Detail-Suche:", ex)
if detail is None:
print("[AUSSCHNITTE] Kein Detail ausgewaehlt — bitte:")
print(" 1) ins Layout wechseln")
print(" 2) Detail-Rahmen einmal anklicken (so dass er hervorgehoben ist)")
print(" 3) erneut 'Auf Detail anwenden' waehlen")
return
# 2) Delegieren an den oeffentlichen Helper
apply_snapshot_to_detail(doc, detail, snap_id)
def _send_layers(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] Snap nicht gefunden:", snap_id)
return
snap_by_id = {}
for ls in (snap.get("layers") or []):
snap_by_id[ls.get("id")] = ls
layers = []
for layer in doc.Layers:
if layer.IsDeleted: continue
lid = str(layer.Id)
ls = snap_by_id.get(lid, {})
layers.append({
"id": lid,
"name": layer.Name,
"fullPath": layer.FullPath,
"color": "#%02x%02x%02x" % (layer.Color.R, layer.Color.G, layer.Color.B),
"visible": bool(ls.get("visible", layer.IsVisible)),
"locked": bool(ls.get("locked", layer.IsLocked)),
})
layers.sort(key=lambda x: x["fullPath"])
presets = self._load_presets(doc)
self.send("LAYERS_DATA", {
"id": snap_id,
"name": snap.get("name"),
"layers": layers,
"presets": presets,
})
def _update_layers(self, snap_id, layers):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
target = next((s for s in snaps if s.get("id") == snap_id), None)
if not target: return
new_list = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
new_list.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
target["layers"] = new_list
self._store(doc, snaps)
self._send_list()
print("[AUSSCHNITTE] Ebenen-Sichtbarkeit von '{}' aktualisiert".format(target.get("name")))
def _save_preset(self, name, layers):
if not name: return
name = name.strip()
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = self._load_presets(doc)
clean = []
for ls in layers:
lid = ls.get("id")
if not lid: continue
clean.append({
"id": lid,
"visible": bool(ls.get("visible", True)),
"locked": bool(ls.get("locked", False)),
})
existing = next((p for p in presets if p.get("name") == name), None)
if existing is not None:
existing["layers"] = clean
else:
presets.append({"name": name, "layers": clean})
self._store_presets(doc, presets)
self._send_list()
print("[AUSSCHNITTE] Ebenenkombination '{}' gespeichert ({} Ebenen)".format(name, len(clean)))
def _delete_preset(self, name):
if not name: return
doc = Rhino.RhinoDoc.ActiveDoc
presets = [p for p in self._load_presets(doc) if p.get("name") != name]
self._store_presets(doc, presets)
self._send_list()
def _rename(self, snap_id, name):
if not snap_id or not name: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = self._load(doc)
for s in snaps:
if s.get("id") == snap_id:
s["name"] = name
break
self._store(doc, snaps)
self._send_list()
def _delete(self, snap_id):
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snaps = [s for s in self._load(doc) if s.get("id") != snap_id]
self._store(doc, snaps)
self._send_list()
def _open_settings_window(self, snap_id):
"""Oeffnet ein Satelliten-Fenster (Eto.Form + WebView) mit dem
Ausschnittseinstellungen-Dialog. Lets User editieren: Massstab,
Display-Mode, Overrides, Ebenenkombi."""
if not snap_id: return
doc = Rhino.RhinoDoc.ActiveDoc
snap = next((s for s in self._load(doc) if s.get("id") == snap_id), None)
if not snap:
print("[AUSSCHNITTE] open_settings: snap nicht gefunden", snap_id)
return
outer = self
bridge_holder = {"form": None, "id": snap_id}
def _payload():
d = Rhino.RhinoDoc.ActiveDoc
sn = next((s for s in outer._load(d) if s.get("id") == bridge_holder["id"]), None)
if sn is None: sn = {}
# Listen fuer Dropdowns
display_modes = []
try:
import oberleiste
display_modes = oberleiste._list_display_modes()
except Exception as ex:
print("[AUSSCHNITTE] display_modes:", ex)
overrides_presets = []
try:
import overrides
overrides_presets = [item.get("name") for item in overrides.list_presets() if item.get("name")]
except Exception as ex:
print("[AUSSCHNITTE] overrides_presets:", ex)
layer_kombis = []
try:
import rhinopanel
layer_kombis = rhinopanel.list_layer_preset_names(d)
except Exception as ex:
print("[AUSSCHNITTE] layer_kombis:", ex)
cam = sn.get("camera") or {}
return {
"snap": {
"id": sn.get("id"),
"name": sn.get("name"),
"scale": sn.get("scale", ""),
"displayMode": cam.get("displayMode"),
"displayModeName": cam.get("displayModeName"),
"applyOverrides": bool(sn.get("applyOverrides", False)),
"overridesEnabled": bool(sn.get("overridesEnabled", False)),
"overridesPreset": sn.get("overridesPreset") or "",
"layerCombination": sn.get("layerCombination") or "",
"darstellung": sn.get("darstellung") or "",
},
"displayModes": display_modes,
"overridesPresets": overrides_presets,
"layerKombis": layer_kombis,
}
def _persist(settings):
d = Rhino.RhinoDoc.ActiveDoc
snaps = outer._load(d)
sid = bridge_holder["id"]
target = next((s for s in snaps if s.get("id") == sid), None)
if target is None:
print("[AUSSCHNITTE] persist settings: snap weg"); return
# Massstab
sc_val = (settings.get("scale") or "").strip()
target["scale"] = sc_val
# Display Mode in camera nested
cam = target.get("camera") or {}
dm_id = settings.get("displayMode")
dm_nm = settings.get("displayModeName")
if dm_id is not None: cam["displayMode"] = dm_id or None
if dm_nm is not None: cam["displayModeName"] = dm_nm or None
target["camera"] = cam
# Overrides
target["applyOverrides"] = bool(settings.get("applyOverrides"))
target["overridesEnabled"] = bool(settings.get("overridesEnabled"))
target["overridesPreset"] = (settings.get("overridesPreset") or "").strip()
# Ebenenkombi
target["layerCombination"] = (settings.get("layerCombination") or "").strip()
# Darstellung (SIA-400 LoD Override fuer diesen Ausschnitt)
darst = (settings.get("darstellung") or "").strip()
target["darstellung"] = darst if darst in ("einfach", "standard", "detail") else ""
outer._store(d, snaps)
outer._send_list()
print("[AUSSCHNITTE] Settings fuer '{}' aktualisiert".format(target.get("name")))
class _AusschnittSettingsBridge(panel_base.BaseBridge):
def __init__(self):
panel_base.BaseBridge.__init__(self, "ausschnitt_settings")
def _on_ready(self):
self._send_state()
def _send_state(self):
self.send("AUSSCHNITT_SETTINGS_STATE", _payload())
def handle(self, data):
if not isinstance(data, dict): return
t = data.get("type", "")
p = data.get("payload") or {}
if t == "READY":
self._on_ready()
elif t == "SAVE":
_persist(p.get("settings") or {})
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
elif t == "CANCEL":
try:
f = bridge_holder.get("form")
if f is not None: f.Close()
except Exception: pass
b = _AusschnittSettingsBridge()
bridge_holder["form"] = panel_base.open_satellite_window(
"ausschnitt_settings",
params=_payload(),
title="Ausschnitt: {}".format(snap.get("name", "")),
size=(420, 540),
bridge=b)
panel_base.register_and_open("ausschnitte", "Ausschnitte", PANEL_GUID_STR, AusschnittBridge,
icon_spec=("crop", "#c87050"))