0c5f8055a5
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>
887 lines
35 KiB
Python
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"))
|