961b3c0396
Stable working state after a long iteration session. The plugin now supports:
- Multi-Surface-Select für alle Element-Typen (Türen/Fenster/Treppen/Tragwerk)
- Wand-Z-Drag → unbound mode (UK/OK-Override, Wand vom Geschoss entkoppelt)
- Wand-Z-Drag nimmt verknüpfte Öffnungen mit (Brüstung += delta_z via Idle-Pfad)
- Öffnungs-XY-Drag snapt direktional auf Wand-Tangente
- Öffnungs-Z-Drag passt Brüstung an (Fenster sofort sync, Tür deferred)
- Wand-Delete kaskadiert Öffnungen (deferred via Idle, robust gegen _Rotate/_Move)
- Source-Cascade beim Öffnungs-Delete (deferred analog Wand-Kaskade)
- Listener-Cleanup robust gegen _reset_panels.py Reload (Refs in
_dossier_runtime_event_refs gespeichert, vor Re-Install deregistriert)
- _count_same_id_type filtert IsDeleted (verhindert Source-Duplikat-Bug bei Move)
- Frontend: Brüstungs-Slider für Tür ("Schwelle"), Flügel-Block nur bei Fenster
Plus aus früherer Phase dieser Session:
- Dossier-Launcher Auto-Load via Rhinos StartupCommands-XML
- Default-Pfad zeigt auf gebundeltes startup.py (out-of-the-box für neue User)
- Splash-Window beim Plugin-Load mit native macOS rounded corners
- Diverse Launcher-Verbesserungen (Brüstungs-Default, tauri.conf, capabilities)
Known issue: bei Multi-Select-Move mit vielen Sub-Volumen kann sporadisch
"Unable to transform" auftreten (Rhinos Move-Operation kollidiert mit Wand-
Regen). Tür-spezifischer Defer-Pfad mildert das, Fenster läuft sync.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1097 lines
39 KiB
Python
1097 lines
39 KiB
Python
#! python 3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
massstab.py
|
|
MASSSTAB-Panel: zeigt + setzt den aktuellen Massstab des aktiven Viewports.
|
|
Funktioniert nur in Parallelprojektion. In Perspective wird "—" gemeldet.
|
|
|
|
Maesstabs-Mathematik:
|
|
frustum_width_in_doc_units = vp.GetFrustum() -> (right - left)
|
|
frustum_width_mm = frustum_width_in_doc_units * mm_per_doc_unit
|
|
screen_width_mm = pixel_width * 25.4 / dpi
|
|
N = frustum_width_mm / screen_width_mm (Skala 1:N)
|
|
|
|
DPI kann pro Doc kalibriert werden (Default 96), gespeichert in doc.Strings.
|
|
"""
|
|
import os
|
|
import sys
|
|
import math
|
|
import json
|
|
import Rhino
|
|
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 = "5c8e4f3f-6d0e-4f1a-a3d4-e5f607182941"
|
|
|
|
# DPI ist eine Hardware-Eigenschaft (Bildschirm) — NICHT pro Dokument speichern.
|
|
# Wir legen sie in einer Config-Datei im Home des Users ab.
|
|
_DOC_DPI_KEY = "dossier_dpi" # Legacy, fuer Migration
|
|
|
|
# Pro Viewport-Name der zuletzt vom User explizit gesetzte Massstab
|
|
# (Dropdown/Input/100%-Button/Ausschnitt-Restore). NICHT der Live-Zoom — der
|
|
# drifted bei Pan/Zoom. Wird von Ausschnitten beim Speichern als "der
|
|
# eingestellte Massstab" gelesen. Per-doc persistiert in doc.Strings als
|
|
# JSON-Dict, damit ein Wechsel zurueck auf einen frueher gesetzten Viewport
|
|
# den korrekten Wert wieder rausgibt — auch nach Restart.
|
|
_user_set_scales = {} # {viewport_name: float}
|
|
_user_set_scales_loaded = False # lazy load aus doc.Strings beim ersten Zugriff
|
|
_DOC_USER_SCALES_KEY = "dossier_user_scales" # JSON-Dict {vp_name: ratio}
|
|
_DOC_USER_SCALE_KEY = "dossier_user_scale" # Legacy: globaler letzter Wert,
|
|
# wird fuer Plotweight/Hatch-Rescale
|
|
# weiter doc-weit genutzt.
|
|
_CONFIG_DIR = os.path.expanduser("~/Library/Application Support/RhinoPanel")
|
|
_CONFIG_PATH = os.path.join(_CONFIG_DIR, "config.json")
|
|
_DEFAULT_DPI = 96.0
|
|
|
|
# Mac WKWebView verschluckt schnelle ExecuteScript-Bursts -> wir limitieren
|
|
# das Live-Update auf alle N Idle-Ticks (~5-10/s).
|
|
_IDLE_THROTTLE = 6
|
|
|
|
|
|
def _mm_per_doc_unit(doc):
|
|
"""Faktor: doc-Einheit -> Millimeter."""
|
|
us = doc.ModelUnitSystem
|
|
UnitSystem = Rhino.UnitSystem
|
|
if us == UnitSystem.Millimeters: return 1.0
|
|
if us == UnitSystem.Centimeters: return 10.0
|
|
if us == UnitSystem.Meters: return 1000.0
|
|
if us == UnitSystem.Kilometers: return 1000000.0
|
|
if us == UnitSystem.Inches: return 25.4
|
|
if us == UnitSystem.Feet: return 304.8
|
|
if us == UnitSystem.Yards: return 914.4
|
|
if us == UnitSystem.Miles: return 1609344.0
|
|
# Fallback ueber Rhinos UnitScale
|
|
try:
|
|
return Rhino.RhinoMath.UnitScale(us, UnitSystem.Millimeters)
|
|
except Exception:
|
|
return 1.0
|
|
|
|
|
|
_DETECT_JXA = (
|
|
'ObjC.import("CoreGraphics");'
|
|
'var id=$.CGMainDisplayID();'
|
|
'var sz=$.CGDisplayScreenSize(id);'
|
|
'var m=$.CGDisplayCopyDisplayMode(id);'
|
|
'JSON.stringify({mm:sz.width,mh:sz.height,'
|
|
'px:$.CGDisplayModeGetPixelWidth(m),' # echte/physische Pixel
|
|
'py:$.CGDisplayModeGetPixelHeight(m),'
|
|
'lpx:$.CGDisplayPixelsWide(id),' # logische Pixel (zum Vergleich)
|
|
'lpy:$.CGDisplayPixelsHigh(id)});'
|
|
)
|
|
|
|
|
|
|
|
def _detect_dpi():
|
|
"""Automatische DPI-Erkennung auf macOS via CoreGraphics.
|
|
|
|
Ruft `osascript -l JavaScript` mit einem CoreGraphics-Snippet auf:
|
|
CGDisplayScreenSize(mainDisplay) -> physische Groesse in mm
|
|
CGDisplayPixelsWide -> logische Aufloesung in px
|
|
|
|
DPI = px * 25.4 / mm. Funktioniert auf Intel- und Apple-Silicon-Macs
|
|
und passt sich automatisch an macOS-Scaling-Aenderungen an.
|
|
|
|
Implementiert ueber System.Diagnostics.Process (IronPython auf .NET
|
|
unterstuetzt kein `subprocess.check_output`).
|
|
"""
|
|
try:
|
|
from System.Diagnostics import Process, ProcessStartInfo
|
|
except Exception as ex:
|
|
print("[MASSSTAB] auto-detect: .NET Process nicht verfuegbar:", ex)
|
|
return None
|
|
if not os.path.isfile("/usr/bin/osascript"):
|
|
# Vermutlich nicht macOS -> nichts zu detecten
|
|
return None
|
|
# JXA-Snippet in eine temp-Datei schreiben, damit wir uns das
|
|
# Shell-Quoting fuer Argumente sparen koennen.
|
|
script_path = None
|
|
try:
|
|
import tempfile
|
|
fd, script_path = tempfile.mkstemp(suffix=".js", prefix="rhinopanel_dpi_")
|
|
try:
|
|
os.write(fd, _DETECT_JXA.encode("utf-8"))
|
|
finally:
|
|
os.close(fd)
|
|
psi = ProcessStartInfo()
|
|
psi.FileName = "/usr/bin/osascript"
|
|
psi.Arguments = '-l JavaScript "' + script_path + '"'
|
|
psi.RedirectStandardOutput = True
|
|
psi.RedirectStandardError = True
|
|
psi.UseShellExecute = False
|
|
psi.CreateNoWindow = True
|
|
p = Process.Start(psi)
|
|
out = p.StandardOutput.ReadToEnd()
|
|
err = p.StandardError.ReadToEnd()
|
|
# Timeout-Schutz: maximal 2s warten
|
|
try:
|
|
finished = p.WaitForExit(2000)
|
|
except Exception:
|
|
finished = True
|
|
if not finished:
|
|
try: p.Kill()
|
|
except Exception: pass
|
|
print("[MASSSTAB] auto-detect: osascript timeout")
|
|
return None
|
|
if p.ExitCode != 0:
|
|
print("[MASSSTAB] auto-detect osascript ExitCode={}:".format(p.ExitCode), err)
|
|
return None
|
|
import json as _json
|
|
data = _json.loads((out or "").strip())
|
|
mm = float(data.get("mm") or 0)
|
|
# Rhinos vp.Size auf Mac liefert PHYSISCHE Pixel (Retina-Backing),
|
|
# daher physische Pixelzahl fuer die DPI verwenden — sonst sind
|
|
# die Modelle nach Skala-Anwendung halb so gross.
|
|
px = float(data.get("px") or 0)
|
|
lpx = float(data.get("lpx") or 0)
|
|
if mm <= 0 or px <= 0:
|
|
return None
|
|
dpi = px * 25.4 / mm
|
|
if dpi < 30.0 or dpi > 600.0:
|
|
print("[MASSSTAB] auto-detect: DPI {:.1f} ausserhalb 30..600 -> ignoriert".format(dpi))
|
|
return None
|
|
print("[MASSSTAB] DPI auto-detected: {:.1f} physisch (Bildschirm {:.0f}x{:.0f}px / {:.1f}x{:.1f}mm, logisch {:.0f}x{:.0f})".format(
|
|
dpi, px, float(data.get("py") or 0),
|
|
mm, float(data.get("mh") or 0),
|
|
lpx, float(data.get("lpy") or 0)))
|
|
return dpi
|
|
except Exception as ex:
|
|
print("[MASSSTAB] auto-detect fehlgeschlagen:", ex)
|
|
return None
|
|
finally:
|
|
if script_path:
|
|
try: os.remove(script_path)
|
|
except Exception: pass
|
|
|
|
|
|
_config_cache = None # in-memory Cache fuer die Config-Datei (DPI etc.)
|
|
|
|
|
|
def _read_config():
|
|
"""Liest die Config-Datei nur einmal, danach aus Memory-Cache.
|
|
Wird via _compute_scale/_get_dpi sehr haeufig (jeden Idle-Tick) aufgerufen
|
|
-> File-IO darf nicht jeden Tick passieren."""
|
|
global _config_cache
|
|
if _config_cache is not None:
|
|
return _config_cache
|
|
cfg = {}
|
|
try:
|
|
if os.path.isfile(_CONFIG_PATH):
|
|
with open(_CONFIG_PATH, "rb") as f:
|
|
data = json.loads(f.read().decode("utf-8"))
|
|
if isinstance(data, dict):
|
|
cfg = data
|
|
except Exception as ex:
|
|
print("[MASSSTAB] config lesen:", ex)
|
|
_config_cache = cfg
|
|
return cfg
|
|
|
|
|
|
def _write_config(cfg):
|
|
"""Schreibt + invalidiert den Cache."""
|
|
global _config_cache
|
|
try:
|
|
if not os.path.isdir(_CONFIG_DIR):
|
|
os.makedirs(_CONFIG_DIR)
|
|
with open(_CONFIG_PATH, "wb") as f:
|
|
f.write(json.dumps(cfg, ensure_ascii=False, indent=2).encode("utf-8"))
|
|
_config_cache = cfg # Cache mit dem geschriebenen Stand aktualisieren
|
|
return True
|
|
except Exception as ex:
|
|
print("[MASSSTAB] config schreiben:", ex)
|
|
return False
|
|
|
|
|
|
def _get_dpi(doc):
|
|
"""Liefert den aktuell gueltigen DPI-Wert aus der Config. Loest KEINEN
|
|
Subprocess aus — Auto-Detection passiert nur explizit (bei READY,
|
|
Button-Klick oder via _force_redetect_dpi)."""
|
|
cfg = _read_config()
|
|
# Legacy-Migration: alter doc.Strings Wert -> einmalig in Config
|
|
if doc is not None and "dpi" not in cfg:
|
|
try:
|
|
legacy = doc.Strings.GetValue(_DOC_DPI_KEY)
|
|
if legacy:
|
|
v = float(legacy)
|
|
if 30.0 <= v <= 600.0:
|
|
cfg["dpi"] = v
|
|
cfg["dpi_source"] = "manual"
|
|
_write_config(cfg)
|
|
return v
|
|
except Exception:
|
|
pass
|
|
try:
|
|
v = float(cfg.get("dpi"))
|
|
if 30.0 <= v <= 600.0:
|
|
return v
|
|
except Exception:
|
|
pass
|
|
return _DEFAULT_DPI
|
|
|
|
|
|
def _bootstrap_dpi():
|
|
"""Beim Panel-Start einmal aufgerufen. Verhalten:
|
|
- Keine Config: detected + speichert
|
|
- Source 'manual': unangetastet (User hat bewusst gewaehlt)
|
|
- Source 'auto'/sonst: detected, ueberschreibt
|
|
(so faengt eine neue Detection-Logik auch alte Werte ein und der
|
|
User reagiert auf Display-Scaling-Wechsel automatisch)
|
|
Subprocess laeuft hier nur einmal pro Panel-Open."""
|
|
cfg = _read_config()
|
|
if cfg.get("dpi_source") == "manual":
|
|
return # User-Override respektieren
|
|
auto = _detect_dpi()
|
|
if auto is None:
|
|
return
|
|
cfg["dpi"] = auto
|
|
cfg["dpi_source"] = "auto"
|
|
_write_config(cfg)
|
|
|
|
|
|
def _set_dpi(doc, value, source="manual"):
|
|
try:
|
|
v = float(value)
|
|
except Exception:
|
|
return False
|
|
if not (30.0 <= v <= 600.0):
|
|
return False
|
|
cfg = _read_config()
|
|
cfg["dpi"] = v
|
|
cfg["dpi_source"] = source
|
|
if not _write_config(cfg):
|
|
return False
|
|
print("[MASSSTAB] DPI={:.1f} ({}) -> {}".format(v, source, _CONFIG_PATH))
|
|
return True
|
|
|
|
|
|
def _force_redetect_dpi():
|
|
"""Manueller "Auto-Detect"-Trigger via Button. Ueberschreibt auch
|
|
einen manuellen Override. Liefert den neuen Wert oder None."""
|
|
auto = _detect_dpi()
|
|
if auto is None:
|
|
return None
|
|
cfg = _read_config()
|
|
cfg["dpi"] = auto
|
|
cfg["dpi_source"] = "auto"
|
|
_write_config(cfg)
|
|
return auto
|
|
|
|
|
|
def _active_vp():
|
|
doc = Rhino.RhinoDoc.ActiveDoc
|
|
if doc is None: return None, None
|
|
view = doc.Views.ActiveView
|
|
if view is None: return doc, None
|
|
try:
|
|
# Page-Views (Layouts) haben einen eigenen Viewport — fuer den
|
|
# Massstab interessiert uns dort das aktive Detail.
|
|
if isinstance(view, Rhino.Display.RhinoPageView):
|
|
for d in view.GetDetailViews():
|
|
if d.IsActive:
|
|
return doc, d.Viewport
|
|
return doc, view.ActiveViewport
|
|
except Exception:
|
|
pass
|
|
return doc, view.ActiveViewport
|
|
|
|
|
|
def _get_frustum_width(vp):
|
|
"""Liefert die sichtbare Welt-Breite (right - left) oder None."""
|
|
try:
|
|
ok, l, r, b, t, n, f = vp.GetFrustum()
|
|
if not ok: return None, None
|
|
return (r - l), (t - b)
|
|
except Exception:
|
|
return None, None
|
|
|
|
|
|
def _viewport_pixels(vp):
|
|
try:
|
|
sz = vp.Size
|
|
return float(sz.Width), float(sz.Height)
|
|
except Exception:
|
|
return None, None
|
|
|
|
|
|
def _compute_scale(doc, vp):
|
|
"""Liefert dict mit Scale-Info fuer aktuellen Viewport.
|
|
Felder: viewName, parallel, scale (1:N float), pixelWidth, pixelHeight,
|
|
unitSystem (string), dpi.
|
|
scale=None bei Perspective oder unbekannten Groessen.
|
|
"""
|
|
cfg = _read_config()
|
|
info = {
|
|
"viewName": None,
|
|
"parallel": False,
|
|
"scale": None,
|
|
"appliedScale": None,
|
|
"pixelWidth": None,
|
|
"pixelHeight": None,
|
|
"unitSystem": "?",
|
|
"dpi": _get_dpi(doc) if doc else _DEFAULT_DPI,
|
|
"dpiSource": cfg.get("dpi_source", "default"),
|
|
"showLineweights": _get_lineweights_enabled(doc),
|
|
}
|
|
if vp is None or doc is None:
|
|
return info
|
|
try:
|
|
info["viewName"] = vp.Name
|
|
info["parallel"] = bool(vp.IsParallelProjection)
|
|
info["unitSystem"] = str(doc.ModelUnitSystem)
|
|
except Exception:
|
|
pass
|
|
# appliedScale pro Viewport. Map ist gefuettert durch _apply_scale und
|
|
# Ausschnitt-Restore — wenn ein anderer Viewport aktiv ist als beim letzten
|
|
# Setzen, kommt entweder dessen frueher gesetzter Wert oder None zurueck.
|
|
# Niemals auf die Live-Skala mappen — das Dropdown soll STATISCH sein.
|
|
# Wichtig: nur bei Parallelprojektion zurueckgeben. In Perspective ist ein
|
|
# Massstab konzeptionell unsinnig — selbst wenn der gleiche Viewport vorher
|
|
# parallel mit Massstab war, soll das Dropdown auf "-" springen, sobald die
|
|
# Projektion auf perspective wechselt.
|
|
try:
|
|
if info.get("parallel"):
|
|
v = _get_applied_scale_for_vp(doc, info["viewName"])
|
|
if v is not None and v > 0:
|
|
info["appliedScale"] = v
|
|
except Exception:
|
|
pass
|
|
if not info["parallel"]:
|
|
return info
|
|
fw, fh = _get_frustum_width(vp)
|
|
pw, ph = _viewport_pixels(vp)
|
|
info["pixelWidth"] = pw
|
|
info["pixelHeight"] = ph
|
|
if fw is None or pw is None or pw <= 0:
|
|
return info
|
|
mm_per_u = _mm_per_doc_unit(doc)
|
|
dpi = info["dpi"]
|
|
if dpi <= 0:
|
|
return info
|
|
frustum_mm = fw * mm_per_u
|
|
screen_mm = pw * 25.4 / dpi
|
|
if screen_mm <= 0:
|
|
return info
|
|
info["scale"] = frustum_mm / screen_mm
|
|
return info
|
|
|
|
|
|
_LW_KEY = "dossier_show_lineweights"
|
|
_LW_ORIG_KEY = "dossier_plotweight_orig" # per-objekt/layer Original-Wert
|
|
_HATCH_ORIG_KEY = "dossier_hatch_scale_orig" # auf Hatch-Attrs: original PatternScale
|
|
|
|
|
|
def _apply_scaled_lineweights(doc, enabled, scale_n):
|
|
"""Skaliert die PlotWeights aller Layer + Objekte mit scale_n wenn
|
|
enabled, sonst stellt Originalwerte wieder her. Originals werden in
|
|
UserStrings persistiert -> idempotent, ueberlebt Restart.
|
|
|
|
Default-Werte (0 oder -1 = "by parent") werden nicht angefasst — sonst
|
|
wuerden eigentlich-default-Objekte plotweight-fixiert.
|
|
"""
|
|
if doc is None: return
|
|
if scale_n is None or scale_n <= 0: scale_n = 1.0
|
|
factor = float(scale_n) if enabled else 1.0
|
|
n_layer = 0
|
|
n_obj = 0
|
|
|
|
# -- Layer ---------------------------------------------------------------
|
|
try:
|
|
for layer in doc.Layers:
|
|
if layer is None or layer.IsDeleted:
|
|
continue
|
|
try:
|
|
stored = layer.GetUserString(_LW_ORIG_KEY)
|
|
if stored:
|
|
orig = float(stored)
|
|
else:
|
|
orig = float(layer.PlotWeight) if layer.PlotWeight else 0.0
|
|
# Nur sichern wenn ueberhaupt was anzuspielen ist
|
|
if orig > 0:
|
|
layer.SetUserString(_LW_ORIG_KEY, "{:.6f}".format(orig))
|
|
if orig <= 0:
|
|
continue # default-Layer (hairline) — nicht anfassen
|
|
new = orig * factor
|
|
if abs(float(layer.PlotWeight) - new) > 1e-6:
|
|
layer.PlotWeight = new
|
|
n_layer += 1
|
|
except Exception as ex:
|
|
print("[MASSSTAB] LW scale layer '{}': {}".format(layer.Name, ex))
|
|
except Exception as ex:
|
|
print("[MASSSTAB] LW scale layers:", ex)
|
|
|
|
# -- Objekte -------------------------------------------------------------
|
|
try:
|
|
for obj in doc.Objects:
|
|
if obj is None or obj.IsDeleted:
|
|
continue
|
|
try:
|
|
a = obj.Attributes
|
|
stored = a.GetUserString(_LW_ORIG_KEY)
|
|
if stored:
|
|
orig = float(stored)
|
|
elif a.PlotWeight and a.PlotWeight > 0:
|
|
orig = float(a.PlotWeight)
|
|
new_a = a.Duplicate()
|
|
new_a.SetUserString(_LW_ORIG_KEY, "{:.6f}".format(orig))
|
|
doc.Objects.ModifyAttributes(obj, new_a, True)
|
|
else:
|
|
continue # PlotWeightSource ist "by layer" — nichts zu tun
|
|
new = orig * factor
|
|
if abs(float(a.PlotWeight) - new) > 1e-6:
|
|
new_a = a.Duplicate()
|
|
new_a.PlotWeight = new
|
|
doc.Objects.ModifyAttributes(obj, new_a, True)
|
|
n_obj += 1
|
|
except Exception:
|
|
pass
|
|
except Exception as ex:
|
|
print("[MASSSTAB] LW scale objects:", ex)
|
|
|
|
try: doc.Views.Redraw()
|
|
except Exception: pass
|
|
print("[MASSSTAB] PlotWeight-Skalierung x{:.1f}: {} Layer, {} Objekte angepasst".format(
|
|
factor, n_layer, n_obj))
|
|
# Diagnose: zeige die ersten paar Layer mit ihren echten PlotWeights
|
|
try:
|
|
shown = 0
|
|
for layer in doc.Layers:
|
|
if layer.IsDeleted or not layer.PlotWeight: continue
|
|
stored = layer.GetUserString(_LW_ORIG_KEY) or "-"
|
|
print("[MASSSTAB] Layer '{}' PlotWeight={:.3f}mm (orig={})".format(
|
|
layer.Name, float(layer.PlotWeight), stored))
|
|
shown += 1
|
|
if shown >= 5: break
|
|
except Exception: pass
|
|
|
|
# (Hatch-Skalierung ist in apply_scaled_hatches ausgelagert — die laeuft
|
|
# IMMER, unabhaengig vom Print-Mode, weil Hatches sich am Massstab
|
|
# orientieren sollten unabhaengig von Plot-Weight-Anzeige.)
|
|
|
|
|
|
def write_plotweight(doc, target, value):
|
|
"""Setze PlotWeight auf target (Layer oder ObjectAttributes-Duplicate),
|
|
Print-Mode-aware. value = "echter" Wert in mm wie er auf Papier landet.
|
|
|
|
Speichert value als Original-UserString. Wenn Print-Mode aktiv ist wird
|
|
PlotWeight = value * scale gesetzt damit die Anzeige direkt skaliert.
|
|
|
|
Aufrufer ist verantwortlich fuer ModifyAttributes / Doc-Refresh."""
|
|
if target is None: return
|
|
try:
|
|
v = float(value) if value is not None else 0.0
|
|
except Exception:
|
|
v = 0.0
|
|
try:
|
|
if v > 0:
|
|
target.SetUserString(_LW_ORIG_KEY, "{:.6f}".format(v))
|
|
else:
|
|
target.SetUserString(_LW_ORIG_KEY, "")
|
|
except Exception:
|
|
pass
|
|
print_on = _get_lineweights_enabled(doc) if doc is not None else False
|
|
factor = _read_user_scale(doc, default=1.0) if print_on else 1.0
|
|
try:
|
|
target.PlotWeight = v * factor
|
|
except Exception as ex:
|
|
print("[MASSSTAB] write_plotweight set:", ex)
|
|
|
|
|
|
def apply_scaled_hatches(doc, scale_n):
|
|
"""Skaliert alle Hatches im Doc gemaess Massstab scale_n.
|
|
|
|
Formel: factor = sqrt(N) / 10 — moderate Skalierung mit 1:100 als
|
|
Referenz-Punkt (factor = 1.0).
|
|
1:1 -> 0.10
|
|
1:50 -> 0.71
|
|
1:100 -> 1.0 (Referenz)
|
|
1:500 -> 2.24
|
|
1:1000 -> 3.16
|
|
|
|
Originale werden in Hatch-Attributes UserString gesichert.
|
|
Returns: (n_gefunden, n_skaliert)
|
|
"""
|
|
import math
|
|
if doc is None: return (0, 0)
|
|
if not scale_n or scale_n <= 0: scale_n = 1.0
|
|
factor = math.sqrt(float(scale_n)) / 10.0
|
|
# IDs vorab sammeln (sonst invalidiert Replace die Iteration)
|
|
hatch_ids = []
|
|
try:
|
|
settings = Rhino.DocObjects.ObjectEnumeratorSettings()
|
|
settings.ObjectTypeFilter = Rhino.DocObjects.ObjectType.Hatch
|
|
settings.NormalObjects = True
|
|
settings.LockedObjects = True
|
|
settings.HiddenObjects = True
|
|
settings.IncludeLights = False
|
|
settings.IncludeGrips = False
|
|
for obj in doc.Objects.GetObjectList(settings):
|
|
if obj is not None and not obj.IsDeleted:
|
|
hatch_ids.append(obj.Id)
|
|
except Exception:
|
|
try:
|
|
for obj in doc.Objects:
|
|
if obj is None or obj.IsDeleted: continue
|
|
try:
|
|
if obj.ObjectType == Rhino.DocObjects.ObjectType.Hatch:
|
|
hatch_ids.append(obj.Id)
|
|
except Exception: pass
|
|
except Exception: pass
|
|
|
|
n_scaled = 0
|
|
for hid in hatch_ids:
|
|
try:
|
|
obj = doc.Objects.FindId(hid)
|
|
if obj is None or obj.IsDeleted: continue
|
|
g = obj.Geometry
|
|
a = obj.Attributes
|
|
stored = a.GetUserString(_HATCH_ORIG_KEY)
|
|
if stored:
|
|
try: orig = float(stored)
|
|
except Exception: orig = 0.0
|
|
else:
|
|
cur = float(g.PatternScale) if g.PatternScale else 0.0
|
|
if cur > 0:
|
|
orig = cur
|
|
new_a = a.Duplicate()
|
|
new_a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(orig))
|
|
doc.Objects.ModifyAttributes(obj, new_a, True)
|
|
else:
|
|
continue
|
|
if orig <= 0: continue
|
|
new_val = orig * factor
|
|
if abs(float(g.PatternScale) - new_val) > 1e-6:
|
|
new_g = g.Duplicate()
|
|
try:
|
|
new_g.PatternScale = new_val
|
|
if doc.Objects.Replace(hid, new_g):
|
|
n_scaled += 1
|
|
except Exception as ex:
|
|
print("[MASSSTAB] hatch set PatternScale:", ex)
|
|
except Exception as ex:
|
|
print("[MASSSTAB] hatch iter:", ex)
|
|
if n_scaled or hatch_ids:
|
|
print("[MASSSTAB] Hatch-Skalierung: {} gefunden, {} mit Faktor x{:.2f} angepasst".format(
|
|
len(hatch_ids), n_scaled, factor))
|
|
try: doc.Views.Redraw()
|
|
except Exception: pass
|
|
return (len(hatch_ids), n_scaled)
|
|
|
|
|
|
def post_create_hatch_scale(doc, hatch_obj, user_scale):
|
|
"""Nach AddHatch oder Replace einer Hatch aufrufen.
|
|
Speichert user_scale als Original-UserString und skaliert die Geometrie
|
|
mit sqrt(N) (siehe apply_scaled_hatches)."""
|
|
import math
|
|
if doc is None or hatch_obj is None: return
|
|
try:
|
|
u = float(user_scale)
|
|
except Exception:
|
|
return
|
|
if u <= 0: return
|
|
try:
|
|
a = hatch_obj.Attributes.Duplicate()
|
|
a.SetUserString(_HATCH_ORIG_KEY, "{:.6f}".format(u))
|
|
doc.Objects.ModifyAttributes(hatch_obj, a, True)
|
|
except Exception as ex:
|
|
print("[MASSSTAB] post_create_hatch_scale orig:", ex)
|
|
# Mit aktuellem Massstab skalieren (sqrt-Formel /10, siehe apply_scaled_hatches)
|
|
scale_n = _read_user_scale(doc, default=1.0)
|
|
if not scale_n or scale_n <= 0: scale_n = 1.0
|
|
if abs(scale_n - 1.0) < 1e-6: return
|
|
factor = math.sqrt(float(scale_n)) / 10.0
|
|
try:
|
|
h2 = doc.Objects.FindId(hatch_obj.Id)
|
|
if h2 is None: return
|
|
new_g = h2.Geometry.Duplicate()
|
|
new_g.PatternScale = u * factor
|
|
doc.Objects.Replace(h2.Id, new_g)
|
|
except Exception as ex:
|
|
print("[MASSSTAB] post_create_hatch_scale rescale:", ex)
|
|
|
|
|
|
def read_plotweight(target):
|
|
"""Lese den "echten" PlotWeight (vor Print-Mode-Skalierung). Faellt auf
|
|
das aktuelle PlotWeight zurueck wenn kein Original gespeichert ist."""
|
|
if target is None: return 0.0
|
|
try:
|
|
s = target.GetUserString(_LW_ORIG_KEY)
|
|
if s:
|
|
return float(s)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
v = float(target.PlotWeight) if target.PlotWeight else 0.0
|
|
return v if v > 0 else 0.0
|
|
except Exception:
|
|
return 0.0
|
|
|
|
|
|
def _get_lineweights_enabled(doc):
|
|
"""Print-View / Strichstaerken-Anzeige. Default OFF — Rhino zeigt sonst
|
|
Linien als Hairlines was beim Zeichnen praktischer ist."""
|
|
if doc is None: return False
|
|
try:
|
|
v = doc.Strings.GetValue(_LW_KEY)
|
|
if v is None: return False
|
|
return v == "1"
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
def _set_lineweights_enabled(doc, enabled):
|
|
"""Togglet Rhinos Print-Display (zeigt PlotWeights mit ihren echten
|
|
Strichstaerken anstatt Hairlines), persistiert den State im Doc UND
|
|
skaliert die PlotWeights mit dem aktuellen Massstab.
|
|
|
|
Auf Mac heisst der relevante Befehl _PrintDisplay (nicht _LineWeights —
|
|
das oeffnet einen Dialog). Wir feuern beide Varianten als Fallback."""
|
|
if doc is None: return False
|
|
flag = "1" if enabled else "0"
|
|
try:
|
|
doc.Strings.SetString(_LW_KEY, flag)
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _set_lineweights_enabled persist:", ex)
|
|
# Print-Display togglen — primaerer Befehl auf Mac Rhino
|
|
on_off = "_On" if enabled else "_Off"
|
|
yes_no = "_Yes" if enabled else "_No"
|
|
cmds = [
|
|
"_-PrintDisplay _State {} _Enter".format(on_off),
|
|
"_-LineweightsDisplay _State {} _Enter".format(on_off),
|
|
"_-LineWeights _DisplayLineweights {} _Enter".format(yes_no),
|
|
]
|
|
for c in cmds:
|
|
try:
|
|
Rhino.RhinoApp.RunScript(c, False)
|
|
except Exception:
|
|
pass
|
|
# PlotWeight-Skalierung mit aktuellem Massstab
|
|
try:
|
|
scale_n = _read_user_scale(doc, default=1.0)
|
|
_apply_scaled_lineweights(doc, enabled, scale_n)
|
|
except Exception as ex:
|
|
print("[MASSSTAB] PlotWeight-Scale:", ex)
|
|
try:
|
|
for v in doc.Views: v.Redraw()
|
|
except Exception: pass
|
|
print("[MASSSTAB] Print-Display:", "AN (Strichstaerken sichtbar)" if enabled else "AUS")
|
|
return True
|
|
|
|
|
|
def _read_user_scale(doc, default=1.0):
|
|
"""Persistierter eingestellter Massstab oder default. Setze default=None
|
|
um "nie gesetzt" zu erkennen."""
|
|
if doc is None: return default
|
|
try:
|
|
raw = doc.Strings.GetValue(_DOC_USER_SCALE_KEY)
|
|
if raw:
|
|
v = float(raw)
|
|
if v > 0: return v
|
|
except Exception:
|
|
pass
|
|
return default
|
|
|
|
|
|
def _write_user_scale(doc, ratio):
|
|
if doc is None: return
|
|
try:
|
|
doc.Strings.SetString(_DOC_USER_SCALE_KEY, "{:.6f}".format(float(ratio)))
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _write_user_scale:", ex)
|
|
|
|
|
|
def _ensure_user_scales_loaded(doc):
|
|
"""Liest die persistierte per-Viewport-Map einmal pro Session aus doc.Strings."""
|
|
global _user_set_scales_loaded
|
|
if _user_set_scales_loaded or doc is None:
|
|
return
|
|
try:
|
|
raw = doc.Strings.GetValue(_DOC_USER_SCALES_KEY)
|
|
if raw:
|
|
data = json.loads(raw)
|
|
if isinstance(data, dict):
|
|
for k, v in data.items():
|
|
try:
|
|
f = float(v)
|
|
if f > 0 and k:
|
|
_user_set_scales[str(k)] = f
|
|
except Exception:
|
|
pass
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _ensure_user_scales_loaded:", ex)
|
|
_user_set_scales_loaded = True
|
|
|
|
|
|
def _write_user_scales(doc):
|
|
if doc is None: return
|
|
try:
|
|
doc.Strings.SetString(_DOC_USER_SCALES_KEY,
|
|
json.dumps(_user_set_scales, ensure_ascii=False))
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _write_user_scales:", ex)
|
|
|
|
|
|
def _get_applied_scale_for_vp(doc, vp_name):
|
|
"""Eingestellter Massstab fuer einen Viewport, oder None."""
|
|
if not vp_name: return None
|
|
_ensure_user_scales_loaded(doc)
|
|
return _user_set_scales.get(vp_name)
|
|
|
|
|
|
def _set_applied_scale_for_vp(doc, vp_name, ratio):
|
|
if not vp_name: return
|
|
_ensure_user_scales_loaded(doc)
|
|
_user_set_scales[vp_name] = float(ratio)
|
|
_write_user_scales(doc)
|
|
|
|
|
|
def _rescale_doc_patterns(doc, factor):
|
|
"""Multipliziert alle Hatch-PatternScales und per-objekt Linetype-Scales
|
|
um factor. Iteriert das gesamte Doc — laeuft auch bei 1000+ Objekten
|
|
in unter 100ms (standard CAD-Verhalten).
|
|
|
|
Globale Linetype-Skala wird auch versucht zu setzen (Rhino-API
|
|
inkonsistent ueber Versionen — wir probieren mehrere Property-Namen)."""
|
|
if factor is None or factor <= 0 or abs(factor - 1.0) < 1e-9:
|
|
return # nichts zu tun
|
|
n_h = 0
|
|
n_l = 0
|
|
HatchT = Rhino.Geometry.Hatch
|
|
try:
|
|
for obj in doc.Objects:
|
|
if obj is None or obj.IsDeleted:
|
|
continue
|
|
g = None
|
|
try: g = obj.Geometry
|
|
except Exception: g = None
|
|
# Hatch-Geometrie -> PatternScale skalieren
|
|
if g is not None and isinstance(g, HatchT):
|
|
try:
|
|
g2 = g.Duplicate()
|
|
cur = float(g2.PatternScale) if g2.PatternScale else 1.0
|
|
g2.PatternScale = cur * factor
|
|
doc.Objects.Replace(obj.Id, g2)
|
|
n_h += 1
|
|
except Exception as ex:
|
|
print("[MASSSTAB] hatch rescale:", ex)
|
|
# Per-Objekt Linetype-Scale (Rhino 8 Attribut)
|
|
try:
|
|
a = obj.Attributes
|
|
for prop in ("LinetypePatternLengthScale", "LinetypeScale"):
|
|
if hasattr(a, prop):
|
|
cur = getattr(a, prop)
|
|
if cur and cur > 0 and abs(cur - 1.0) > 1e-9:
|
|
# Nur Objekte mit explizit gesetzter Skala anfassen
|
|
# (Default=1.0 ueberlassen wir dem globalen Multiplikator).
|
|
new_a = a.Duplicate()
|
|
setattr(new_a, prop, cur * factor)
|
|
doc.Objects.ModifyAttributes(obj, new_a, True)
|
|
n_l += 1
|
|
break
|
|
except Exception:
|
|
pass
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _rescale_doc_patterns:", ex)
|
|
|
|
# Globale Linetype-Pattern-Length-Skala (Rhino-doc-Setting) versuchen.
|
|
# Property-Namen variieren je nach Version — wir probieren.
|
|
set_global = False
|
|
for prop in ("LinetypeAndPatternScale",
|
|
"LinetypeAndPatternLengthScale",
|
|
"ModelSpaceLinetypeScale"):
|
|
try:
|
|
if hasattr(doc, prop):
|
|
cur = float(getattr(doc, prop))
|
|
if cur > 0:
|
|
setattr(doc, prop, cur * factor)
|
|
set_global = True
|
|
break
|
|
except Exception:
|
|
pass
|
|
try: doc.Views.Redraw()
|
|
except Exception: pass
|
|
print("[MASSSTAB] Rescale x{:.4f}: {} Hatches, {} per-obj Linetypes{}".format(
|
|
factor, n_h, n_l, ", global Linetype-Scale" if set_global else ""))
|
|
|
|
|
|
def get_current_massstab_factor(doc=None):
|
|
"""Multiplier fuer Pattern-Skalen — aktuell deaktiviert (Faktor 1.0).
|
|
Massstab beeinflusst aktuell NUR den Viewport-Zoom, nicht die Pattern-
|
|
Skalen oder Strichstaerken."""
|
|
return 1.0
|
|
|
|
|
|
def _apply_scale(doc, vp, ratio):
|
|
"""Setzt den Frustum so dass 1mm Bildschirm == ratio doc-Einheiten in mm.
|
|
ratio = 1:N -> ratio_value = N. Liefert True bei Erfolg.
|
|
"""
|
|
if vp is None or doc is None: return False
|
|
try:
|
|
if not vp.IsParallelProjection:
|
|
print("[MASSSTAB] Viewport ist nicht parallel — Skala nicht setzbar")
|
|
return False
|
|
except Exception:
|
|
return False
|
|
pw, ph = _viewport_pixels(vp)
|
|
if pw is None or pw <= 0:
|
|
return False
|
|
dpi = _get_dpi(doc)
|
|
mm_per_u = _mm_per_doc_unit(doc)
|
|
if mm_per_u <= 0 or dpi <= 0:
|
|
return False
|
|
screen_mm = pw * 25.4 / dpi
|
|
new_frustum_mm = screen_mm * float(ratio)
|
|
new_frustum_u = new_frustum_mm / mm_per_u # in doc-units
|
|
try:
|
|
ok, l, r, b, t, n, f = vp.GetFrustum()
|
|
if not ok: return False
|
|
cur_w = (r - l)
|
|
if cur_w <= 0: return False
|
|
# RhinoViewport.SetFrustum existiert nicht — wir benutzen Magnify(factor).
|
|
# factor > 1 zoomt rein (kleineres Frustum). factor = cur_w / new_w.
|
|
factor = cur_w / new_frustum_u
|
|
if factor <= 0 or not (factor < 1e9 and factor > 1e-9):
|
|
print("[MASSSTAB] _apply_scale: ungueltiger Faktor", factor)
|
|
return False
|
|
applied = False
|
|
# Verschiedene API-Signaturen je nach Rhino-Version durchprobieren.
|
|
# 1) Magnify(factor, mode) — mode=False -> mittig
|
|
try:
|
|
vp.Magnify(float(factor), False)
|
|
applied = True
|
|
except Exception as ex1:
|
|
# 2) Magnify(factor)
|
|
try:
|
|
vp.Magnify(float(factor))
|
|
applied = True
|
|
except Exception as ex2:
|
|
# 3) Camera-Skript-Fallback (zoom faktorisch via _Zoom Factor)
|
|
try:
|
|
Rhino.RhinoApp.RunScript("_-Zoom _Factor {:.6f} _Enter".format(factor), False)
|
|
applied = True
|
|
except Exception as ex3:
|
|
print("[MASSSTAB] _apply_scale alle Varianten fehlgeschlagen:",
|
|
ex1, ex2, ex3)
|
|
if not applied:
|
|
return False
|
|
# PlotWeights nur skalieren wenn Print-Mode aktiv ist.
|
|
try:
|
|
if _get_lineweights_enabled(doc):
|
|
_apply_scaled_lineweights(doc, True, float(ratio))
|
|
except Exception as ex:
|
|
print("[MASSSTAB] LW-Rescale:", ex)
|
|
# Hatches mit sqrt(N) skalieren — moderate Anpassung.
|
|
try:
|
|
apply_scaled_hatches(doc, float(ratio))
|
|
except Exception as ex:
|
|
print("[MASSSTAB] Hatch-Rescale:", ex)
|
|
# Neuen Wert persistieren — sowohl per-Viewport (fuer das Dropdown,
|
|
# damit jeder Viewport seinen eigenen Massstab behaelt) als auch als
|
|
# globaler "letzter Wert" (Legacy-Key; wird von Plotweight/Hatch-Rescale
|
|
# doc-weit benutzt — dort ist nur EIN Faktor sinnvoll).
|
|
_write_user_scale(doc, ratio)
|
|
try:
|
|
_set_applied_scale_for_vp(doc, vp.Name, float(ratio))
|
|
except Exception as ex:
|
|
print("[MASSSTAB] per-vp scale write:", ex)
|
|
try: doc.Views.Redraw()
|
|
except Exception: pass
|
|
print("[MASSSTAB] Skala 1:{:.2f} gesetzt (Faktor {:.4f}, soll-frustum {:.4f} {})".format(
|
|
ratio, factor, new_frustum_u, str(doc.ModelUnitSystem)))
|
|
return True
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _apply_scale:", ex)
|
|
return False
|
|
|
|
|
|
def _zoom_extents(doc, vp, selected_only=False):
|
|
if vp is None or doc is None: return False
|
|
try:
|
|
if selected_only:
|
|
objs = list(doc.Objects.GetSelectedObjects(False, False))
|
|
if not objs:
|
|
print("[MASSSTAB] Keine Selektion fuer Zoom-Selection")
|
|
return False
|
|
bbox = Rhino.Geometry.BoundingBox.Empty
|
|
for o in objs:
|
|
try:
|
|
b = o.Geometry.GetBoundingBox(True)
|
|
if bbox.IsValid:
|
|
bbox.Union(b)
|
|
else:
|
|
bbox = b
|
|
except Exception:
|
|
continue
|
|
if not bbox.IsValid:
|
|
return False
|
|
# Etwas Padding (10%)
|
|
d = bbox.Diagonal
|
|
pad = max(d.X, d.Y, d.Z) * 0.05
|
|
if pad > 0:
|
|
bbox.Inflate(pad, pad, pad)
|
|
try:
|
|
vp.ZoomBoundingBox(bbox)
|
|
except Exception:
|
|
# Fallback ueber Rhino-Skript
|
|
Rhino.RhinoApp.RunScript("_-Zoom _Selected _Enter", False)
|
|
else:
|
|
try:
|
|
vp.ZoomExtents()
|
|
except Exception:
|
|
Rhino.RhinoApp.RunScript("_-Zoom _All _Extents _Enter", False)
|
|
try: doc.Views.Redraw()
|
|
except Exception: pass
|
|
return True
|
|
except Exception as ex:
|
|
print("[MASSSTAB] _zoom_extents:", ex)
|
|
return False
|
|
|
|
|
|
# --- Bridge -----------------------------------------------------------------
|
|
|
|
class MassstabBridge(panel_base.BaseBridge):
|
|
def __init__(self):
|
|
panel_base.BaseBridge.__init__(self, "massstab")
|
|
self._idle_counter = 0
|
|
self._last_info = None
|
|
|
|
def _on_ready(self):
|
|
# Einmalige Bootstrap-Detection falls noch keine DPI in der Config.
|
|
try: _bootstrap_dpi()
|
|
except Exception as ex: print("[MASSSTAB] bootstrap:", ex)
|
|
self._send_state(force=True)
|
|
|
|
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 == "REQUEST_STATE":
|
|
self._send_state(force=True)
|
|
elif t == "SET_SCALE":
|
|
doc, vp = _active_vp()
|
|
try:
|
|
ratio = float(p.get("ratio"))
|
|
except Exception:
|
|
return
|
|
if ratio <= 0: return
|
|
if _apply_scale(doc, vp, ratio):
|
|
self._send_state(force=True)
|
|
elif t == "ZOOM_ONE_TO_ONE":
|
|
doc, vp = _active_vp()
|
|
if _apply_scale(doc, vp, 1.0):
|
|
self._send_state(force=True)
|
|
elif t == "ZOOM_EXTENTS":
|
|
doc, vp = _active_vp()
|
|
_zoom_extents(doc, vp, selected_only=False)
|
|
self._send_state(force=True)
|
|
elif t == "ZOOM_SELECTION":
|
|
doc, vp = _active_vp()
|
|
_zoom_extents(doc, vp, selected_only=True)
|
|
self._send_state(force=True)
|
|
elif t == "SET_DPI":
|
|
doc, _ = _active_vp()
|
|
if _set_dpi(doc, p.get("dpi"), source="manual"):
|
|
self._send_state(force=True)
|
|
elif t == "DETECT_DPI":
|
|
v = _force_redetect_dpi()
|
|
if v is None:
|
|
print("[MASSSTAB] Auto-Detect: keine Bildschirminfo verfuegbar")
|
|
self._send_state(force=True)
|
|
elif t == "SET_LINEWEIGHTS":
|
|
doc, _ = _active_vp()
|
|
_set_lineweights_enabled(doc, bool(p.get("enabled")))
|
|
self._send_state(force=True)
|
|
|
|
def _send_state(self, force=False):
|
|
doc, vp = _active_vp()
|
|
info = _compute_scale(doc, vp)
|
|
# Vergleich gegen letzten Stand — Nachrichten sparen
|
|
if not force and info == self._last_info:
|
|
return
|
|
self._last_info = info
|
|
self.send("STATE", info)
|
|
|
|
def tick_idle(self):
|
|
self._idle_counter += 1
|
|
if self._idle_counter < _IDLE_THROTTLE:
|
|
return
|
|
self._idle_counter = 0
|
|
self._send_state(force=False)
|
|
|
|
|
|
# --- Listener fuer Live-Updates ---------------------------------------------
|
|
|
|
def _install_listeners(bridge):
|
|
flag = "massstab_listeners"
|
|
sc.sticky["massstab_bridge"] = bridge
|
|
if sc.sticky.get(flag):
|
|
return
|
|
|
|
def on_idle(s, e):
|
|
b = sc.sticky.get("massstab_bridge")
|
|
if b is not None:
|
|
try: b.tick_idle()
|
|
except Exception: pass
|
|
|
|
def on_view_change(*args):
|
|
b = sc.sticky.get("massstab_bridge")
|
|
if b is not None:
|
|
try: b._send_state(force=True)
|
|
except Exception: pass
|
|
|
|
Rhino.RhinoApp.Idle += on_idle
|
|
Rhino.RhinoDoc.ActiveDocumentChanged += on_view_change
|
|
sc.sticky[flag] = True
|
|
print("[MASSSTAB] Listener aktiv (Idle-Poll + Doc-Change)")
|
|
|
|
|
|
def get_current_scale_ratio():
|
|
"""Aktuelle LIVE-Skala 1:N als Float (aus Viewport-Frustum berechnet).
|
|
Drifted bei Pan/Zoom mit. None bei Perspective."""
|
|
doc, vp = _active_vp()
|
|
info = _compute_scale(doc, vp)
|
|
return info.get("scale")
|
|
|
|
|
|
def get_applied_scale_ratio():
|
|
"""Eingestellter Massstab fuer den aktuell aktiven Viewport, oder None.
|
|
Wird vom Ausschnitt-Capture als "der gerade gepinnte Massstab" gelesen."""
|
|
doc, vp = _active_vp()
|
|
if doc is None or vp is None:
|
|
return None
|
|
try:
|
|
v = _get_applied_scale_for_vp(doc, vp.Name)
|
|
if v is not None and v > 0:
|
|
return v
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def _bridge_factory():
|
|
b = MassstabBridge()
|
|
_install_listeners(b)
|
|
return b
|
|
|
|
|
|
# Hinweis: das eigenstaendige MASSSTAB-Panel wird nicht mehr automatisch
|
|
# registriert — OBERLEISTE enthaelt seit der Refactor alle Funktionen.
|
|
# Das Modul bleibt als Library bestehen (massstab._compute_scale,
|
|
# get_applied_scale_ratio, write_plotweight, read_plotweight, ...).
|
|
# Falls du das Panel doch wieder als separaten Tab willst, einfach
|
|
# register_standalone_panel() aufrufen oder die Zeile darunter auskommentieren.
|
|
|
|
def register_standalone_panel():
|
|
panel_base.register_and_open("massstab", "MASSSTAB", PANEL_GUID_STR, _bridge_factory,
|
|
icon_spec=("straighten", "#c87050"))
|
|
|
|
# register_standalone_panel() # auskommentiert — Standard ist OBERLEISTE
|