Wand-Grips + Schnitt-Grips + Referenz-Sublayer pro Bauteil + Print-Auto-Hide
Custom-Grip-Overlays via DisplayConduit + MouseCallback: - wand_grips.py: dicke klickbare Marker an wand_axis-Endpunkten, auch wenn die Referenz-Layer ausgeblendet ist. GetPoint mit fixem Anker. - schnitt_grips.py: 3 Marker pro Schnitt (P1, P2, Mid). Mid translatiert ganze Linie, P1/P2 verschieben Endpunkt. Hide Clipping-Planes waehrend GetPoint damit kein Verbots-Cursor durch Locked-Edges erscheint. skip_view=True bei Re-Activate damit Drag nicht in Section springt. Referenz-Architektur umgebaut: - wand_axis + oeffnung_point liegen jetzt unter <Geschoss>::20_Waende:: 20r_Referenz statt eigener top-level 19_Referenzlinien-Ebene. - Migration v4 zieht existierende Sources auf den neuen Pfad. - Toggle in Oberleiste keyword-driven: findet alle 'Referenz'-Sub-Ebenen rekursiv, toggelt alle Praefixe gemeinsam. Bauteil-uebergreifend. Oberleiste-Layout: - Druck-Ansicht-Button hoch neben Massstab-Dropdown (Reihe 1). - Referenzlinien-Toggle in Reihe 2 neben Zoom-Pill, symmetrisch zum Druck-Button. Zoom-Pill auf 3 Buttons reduziert. - Print-View AN → Referenz-Layer automatisch ausblenden, Snapshot restored beim Ausschalten. Fix: clear_schnitt_clipping respektiert Mode=Locked nicht — vor Delete auf Normal-Mode wechseln + Modify damit's persistiert. Schnitt-Loeschen raeumt Clipping-Planes jetzt sauber auf. Fix: Schnitt-Doppelklick-Handler aktiviert nur bei expliziter Schnitt- Auswahl, ignoriert andere Selektionen. Fix: _send_state Selection-Detection mit Source-ODER-Volume-Fallback — Fenster-Properties erscheinen jetzt auch wenn oeffnung_point auf hidden Referenz-Layer liegt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+91
-14
@@ -645,16 +645,77 @@ def _layer_path_volume(doc, geschoss_name):
|
|||||||
return _layer_path_axis(doc, geschoss_name)
|
return _layer_path_axis(doc, geschoss_name)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_referenz_child_in_doc(doc, parent_code, parent_keywords,
|
||||||
|
parent_default_name):
|
||||||
|
"""Stellt sicher dass unter einer Bauteil-Parent-Ebene (z.B. WAENDE,
|
||||||
|
Code 20) ein 'Referenz'-Sub-Sub-Layer existiert. Code ist
|
||||||
|
parent_code + 'r' (z.B. '20r'). Liefert (parent_sub_name, child_sub_name)
|
||||||
|
fuer Pfad-Konstruktion.
|
||||||
|
|
||||||
|
Idempotent: legt nur an wenn nicht vorhanden, triggert dann
|
||||||
|
build_layers + broadcast_state."""
|
||||||
|
parent_sub = _find_ebene_sublayer_name(
|
||||||
|
doc, parent_keywords, parent_code, parent_default_name,
|
||||||
|
default_color="#0a0a0a", default_lw=0.50)
|
||||||
|
# parent_sub = "20_Wände" oder vom User customisiert — Code aus Prefix
|
||||||
|
parent_real_code = parent_sub.split("_", 1)[0] if "_" in parent_sub else parent_code
|
||||||
|
ref_code = parent_real_code + "r"
|
||||||
|
ref_child_sub = "{}_Referenz".format(ref_code)
|
||||||
|
|
||||||
|
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
|
try: ebenen = json.loads(raw) if raw else []
|
||||||
|
except Exception: ebenen = []
|
||||||
|
if not isinstance(ebenen, list): return parent_sub, ref_child_sub
|
||||||
|
|
||||||
|
# Parent-Eintrag im Tree finden (rekursiv falls nested)
|
||||||
|
def _find_parent(lst):
|
||||||
|
for e in lst:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
if e.get("code") == parent_real_code: return e
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list):
|
||||||
|
r = _find_parent(kids)
|
||||||
|
if r is not None: return r
|
||||||
|
return None
|
||||||
|
parent_e = _find_parent(ebenen)
|
||||||
|
if parent_e is None: return parent_sub, ref_child_sub
|
||||||
|
|
||||||
|
if not isinstance(parent_e.get("children"), list):
|
||||||
|
parent_e["children"] = []
|
||||||
|
have = any(isinstance(c, dict) and c.get("code") == ref_code
|
||||||
|
for c in parent_e["children"])
|
||||||
|
if not have:
|
||||||
|
parent_e["children"].append({
|
||||||
|
"code": ref_code, "name": "Referenz",
|
||||||
|
"color": "#a0a0a0", "lw": 0.13,
|
||||||
|
"visible": True, "locked": False,
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString("dossier_ebenen",
|
||||||
|
json.dumps(ebenen, ensure_ascii=False))
|
||||||
|
import layer_builder
|
||||||
|
z_raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
zlist = json.loads(z_raw) if z_raw else []
|
||||||
|
if zlist: layer_builder.build_layers(doc, zlist, ebenen)
|
||||||
|
import rhinopanel
|
||||||
|
rhinopanel._broadcast_state(doc)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] _ensure_referenz_child:", ex)
|
||||||
|
return parent_sub, ref_child_sub
|
||||||
|
|
||||||
|
|
||||||
def _layer_path_referenz(doc, geschoss_name):
|
def _layer_path_referenz(doc, geschoss_name):
|
||||||
"""Sublayer 'Referenzlinien' (Code 19) — eigene Ebene fuer wand_axis +
|
"""Sub-Sub-Layer '<parent_code>r_Referenz' unter der WAENDE-Ebene fuer
|
||||||
oeffnung_point Source-Objekte. Getrennt vom Wand-Volumen-Layer (20)
|
wand_axis + oeffnung_point Source-Objekte. Bauteil-konsistent: die
|
||||||
damit der User die Konstruktions-Referenzen ein-/ausblenden kann ohne
|
Referenz-Linie gehoert konzeptuell zur Wand, also nested unter WAENDE
|
||||||
die Volumen-Sichtbarkeit zu verlieren. Wird automatisch im Ebenen-
|
statt als eigene top-level Ebene wie frueher (alt: 19_Referenzlinien).
|
||||||
Panel sichtbar (auto-add via _find_ebene_sublayer_name)."""
|
|
||||||
sub = _find_ebene_sublayer_name(doc, ["referenz", "referenzlinie"],
|
Toggle in der Oberleiste findet alle 'Referenz'-Sub-Sub-Layer ueber
|
||||||
"19", "Referenzlinien",
|
alle Bauteile keyword-basiert — falls Decken/Dach/Tragwerk spaeter
|
||||||
default_color="#a0a0a0", default_lw=0.13)
|
auch Referenz-Sublayer kriegen, wirkt der Toggle automatisch."""
|
||||||
return "{}::{}".format(geschoss_name, sub)
|
parent_sub, child_sub = _ensure_referenz_child_in_doc(
|
||||||
|
doc, "20", ["wand", "wände", "waende"], "Wände")
|
||||||
|
return "{}::{}::{}".format(geschoss_name, parent_sub, child_sub)
|
||||||
|
|
||||||
|
|
||||||
def _layer_path_schnittlinie(doc, geschoss_name):
|
def _layer_path_schnittlinie(doc, geschoss_name):
|
||||||
@@ -10367,11 +10428,11 @@ def _migrate_referenz_layer_once(doc):
|
|||||||
Geschoss aufgerufen, was via _find_ebene_sublayer_name den Auto-Add
|
Geschoss aufgerufen, was via _find_ebene_sublayer_name den Auto-Add
|
||||||
+ broadcast_state ausloest."""
|
+ broadcast_state ausloest."""
|
||||||
if doc is None: return
|
if doc is None: return
|
||||||
# Sticky-Version bumped: vorherige Versionen liefen ohne proaktive
|
# v4: Referenzlinien wandern von top-level '19_Referenzlinien' auf
|
||||||
# Ebenen-Registrierung — wenn die alten Keys gesetzt sind, wuerde die
|
# Sub-Sub-Layer '<parent_code>r_Referenz' unter den jeweiligen
|
||||||
# neue Logik nie greifen. v3 = aktuelle Implementierung.
|
# Bauteil-Parent (aktuell nur WAENDE). Sticky-Bump zwingt Re-Run.
|
||||||
try: key = "_dossier_referenz_migration_v3_" + str(doc.RuntimeSerialNumber)
|
try: key = "_dossier_referenz_migration_v4_" + str(doc.RuntimeSerialNumber)
|
||||||
except Exception: key = "_dossier_referenz_migration_v3_default"
|
except Exception: key = "_dossier_referenz_migration_v4_default"
|
||||||
if sc.sticky.get(key): return
|
if sc.sticky.get(key): return
|
||||||
sc.sticky[key] = True
|
sc.sticky[key] = True
|
||||||
n_moved = 0
|
n_moved = 0
|
||||||
@@ -11553,6 +11614,22 @@ def _install_listeners(bridge):
|
|||||||
schnitte.install_double_click_handler()
|
schnitte.install_double_click_handler()
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[ELEMENTE] schnitt dblclick install:", ex)
|
print("[ELEMENTE] schnitt dblclick install:", ex)
|
||||||
|
# Wand-Endpoint-Grip-Overlay (Display-Conduit + Mouse-Handler) —
|
||||||
|
# dicke klickbare Marker an den Achs-Endpunkten, auch wenn die
|
||||||
|
# Referenzlinien-Layer ausgeblendet ist.
|
||||||
|
try:
|
||||||
|
import wand_grips
|
||||||
|
wand_grips.install_handlers()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] wand_grips install:", ex)
|
||||||
|
# Schnittsymbol-Endpoint-Grips — analoges Overlay an den P1/P2 der
|
||||||
|
# Schnittlinie. Ziehen updated linePts + regeneriert das Symbol +
|
||||||
|
# re-aktiviert den Schnitt wenn aktiv.
|
||||||
|
try:
|
||||||
|
import schnitt_grips
|
||||||
|
schnitt_grips.install_handlers()
|
||||||
|
except Exception as ex:
|
||||||
|
print("[ELEMENTE] schnitt_grips install:", ex)
|
||||||
|
|
||||||
|
|
||||||
def _bridge_factory():
|
def _bridge_factory():
|
||||||
|
|||||||
+165
-1
@@ -863,7 +863,15 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
self._send_state(force=True)
|
self._send_state(force=True)
|
||||||
elif t == "SET_LINEWEIGHTS":
|
elif t == "SET_LINEWEIGHTS":
|
||||||
doc, _ = massstab._active_vp()
|
doc, _ = massstab._active_vp()
|
||||||
massstab._set_lineweights_enabled(doc, bool(p.get("enabled")))
|
enabled = bool(p.get("enabled"))
|
||||||
|
massstab._set_lineweights_enabled(doc, enabled)
|
||||||
|
# Print-View AN → Referenzlinien automatisch ausblenden (im
|
||||||
|
# gedruckten Plan haben Hilfslinien nichts verloren). Beim
|
||||||
|
# Ausschalten den vorherigen Sichtbarkeits-Stand restoren.
|
||||||
|
try:
|
||||||
|
self._sync_referenz_for_print(doc, enabled)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] sync referenz for print:", ex)
|
||||||
self._send_state(force=True)
|
self._send_state(force=True)
|
||||||
elif t == "SET_DPI":
|
elif t == "SET_DPI":
|
||||||
doc, _ = massstab._active_vp()
|
doc, _ = massstab._active_vp()
|
||||||
@@ -1087,6 +1095,76 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[OBERLEISTE] open layer-combinations:", ex)
|
print("[OBERLEISTE] open layer-combinations:", ex)
|
||||||
|
|
||||||
|
# --- Referenzlinien-Sichtbarkeit togglen ------------------------
|
||||||
|
# Shortcut fuer die Layer-Sichtbarkeit der Referenzlinien-Ebene
|
||||||
|
# (Code 19). Bleibt eine echte Ebene → Ausschnitte speichern den
|
||||||
|
# State automatisch mit. Oberleiste ist nur ein schnellerer Weg
|
||||||
|
# dazu als der Ebenen-Manager.
|
||||||
|
elif t == "TOGGLE_REFERENZLINIEN":
|
||||||
|
try:
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
want_visible = bool(p.get("visible"))
|
||||||
|
print("[OBERLEISTE] TOGGLE_REFERENZLINIEN -> {}".format(want_visible))
|
||||||
|
|
||||||
|
# Keyword-driven: alle Ebenen mit Namen 'Referenz' im JSON
|
||||||
|
# finden (rekursiv), deren Codes sammeln, dann ALLE Rhino-
|
||||||
|
# Layer mit diesen Code-Praefixen toggeln. Funktioniert
|
||||||
|
# bauteil-uebergreifend: WAENDE::20r_Referenz heute,
|
||||||
|
# DECKEN::30r_Referenz / TRAGWERK::50r_Referenz morgen.
|
||||||
|
#
|
||||||
|
# Backwards-Kompat: erfasst auch das alte top-level
|
||||||
|
# '19_Referenzlinien' (Name beginnt mit "Referenz").
|
||||||
|
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
|
ebenen = _json.loads(raw) if raw else []
|
||||||
|
if not isinstance(ebenen, list): ebenen = []
|
||||||
|
|
||||||
|
codes = [] # alle Codes deren Layer wir toggeln
|
||||||
|
def _collect(lst):
|
||||||
|
for e in lst:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
nm = (e.get("name") or "").strip().lower()
|
||||||
|
cd = e.get("code")
|
||||||
|
if cd and (nm == "referenz" or nm.startswith("referenz")):
|
||||||
|
codes.append(cd)
|
||||||
|
# Visible-Flag im JSON gleich mit-setzen
|
||||||
|
if e.get("visible", True) != want_visible:
|
||||||
|
e["visible"] = want_visible
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list): _collect(kids)
|
||||||
|
_collect(ebenen)
|
||||||
|
|
||||||
|
if codes:
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString("dossier_ebenen",
|
||||||
|
_json.dumps(ebenen, ensure_ascii=False))
|
||||||
|
except Exception: pass
|
||||||
|
print("[OBERLEISTE] Referenz-Codes gefunden: {}".format(codes))
|
||||||
|
|
||||||
|
# Rhino-Layer fuer jeden Code toggeln (Praefix-Match)
|
||||||
|
n_toggled = 0
|
||||||
|
if codes:
|
||||||
|
prefixes = tuple(c + "_" for c in codes)
|
||||||
|
for i in range(doc.Layers.Count):
|
||||||
|
layer = doc.Layers[i]
|
||||||
|
if layer is None or layer.IsDeleted: continue
|
||||||
|
if not layer.Name.startswith(prefixes): continue
|
||||||
|
try:
|
||||||
|
if layer.IsVisible != want_visible:
|
||||||
|
layer.IsVisible = want_visible
|
||||||
|
doc.Layers.Modify(layer, i, True)
|
||||||
|
n_toggled += 1
|
||||||
|
except Exception: pass
|
||||||
|
print("[OBERLEISTE] {} Rhino-Layer getoggelt".format(n_toggled))
|
||||||
|
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
rhinopanel._broadcast_state(doc)
|
||||||
|
self._send_state(force=True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[OBERLEISTE] TOGGLE_REFERENZLINIEN:", ex)
|
||||||
|
|
||||||
# --- Anordnen (DisplayOrder Z-Stack) ----------------------------
|
# --- Anordnen (DisplayOrder Z-Stack) ----------------------------
|
||||||
# Nutzt Rhinos native _BringToFront / _BringForward / _SendBackward
|
# Nutzt Rhinos native _BringToFront / _BringForward / _SendBackward
|
||||||
# / _SendToBack. Diese setzen Attributes.DisplayOrder — keine
|
# / _SendToBack. Diese setzen Attributes.DisplayOrder — keine
|
||||||
@@ -1195,6 +1273,69 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
"autoApplyLayout": bool(cfg.get("autoApplyLayout", False)),
|
"autoApplyLayout": bool(cfg.get("autoApplyLayout", False)),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def _sync_referenz_for_print(self, doc, print_enabled):
|
||||||
|
"""Druck-View AN → alle Referenz-Sub-Layer ausblenden + Sichtbarkeits-
|
||||||
|
Snapshot in sticky speichern. AUS → vorherige States restoren.
|
||||||
|
|
||||||
|
Findet die Codes keyword-basiert via dossier_ebenen-Tree (Name
|
||||||
|
startswith 'referenz'). Aendert NICHT das visible-Flag im JSON —
|
||||||
|
das gehoert dem User, wir overriden nur fuer die Druck-Sitzung."""
|
||||||
|
if doc is None: return
|
||||||
|
try: key = "_dossier_referenz_print_snapshot_" + str(doc.RuntimeSerialNumber)
|
||||||
|
except Exception: key = "_dossier_referenz_print_snapshot_default"
|
||||||
|
# Codes sammeln
|
||||||
|
try:
|
||||||
|
raw = doc.Strings.GetValue("dossier_ebenen")
|
||||||
|
ebenen = _json.loads(raw) if raw else []
|
||||||
|
except Exception: ebenen = []
|
||||||
|
codes = []
|
||||||
|
def _collect(lst):
|
||||||
|
for e in lst:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
nm = (e.get("name") or "").strip().lower()
|
||||||
|
cd = e.get("code")
|
||||||
|
if cd and (nm == "referenz" or nm.startswith("referenz")):
|
||||||
|
codes.append(cd)
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list): _collect(kids)
|
||||||
|
_collect(ebenen)
|
||||||
|
if not codes: return
|
||||||
|
prefixes = tuple(c + "_" for c in codes)
|
||||||
|
if print_enabled:
|
||||||
|
# Snapshot + Hide
|
||||||
|
snap = {}
|
||||||
|
for i in range(doc.Layers.Count):
|
||||||
|
layer = doc.Layers[i]
|
||||||
|
if layer is None or layer.IsDeleted: continue
|
||||||
|
if not layer.Name.startswith(prefixes): continue
|
||||||
|
snap[str(layer.Id)] = bool(layer.IsVisible)
|
||||||
|
if layer.IsVisible:
|
||||||
|
layer.IsVisible = False
|
||||||
|
doc.Layers.Modify(layer, i, True)
|
||||||
|
sc.sticky[key] = snap
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
print("[OBERLEISTE] Print AN: {} Referenz-Layer ausgeblendet".format(len(snap)))
|
||||||
|
else:
|
||||||
|
# Restore
|
||||||
|
snap = sc.sticky.get(key) or {}
|
||||||
|
n = 0
|
||||||
|
for i in range(doc.Layers.Count):
|
||||||
|
layer = doc.Layers[i]
|
||||||
|
if layer is None or layer.IsDeleted: continue
|
||||||
|
if not layer.Name.startswith(prefixes): continue
|
||||||
|
was = snap.get(str(layer.Id))
|
||||||
|
if was is None: continue
|
||||||
|
if layer.IsVisible != was:
|
||||||
|
layer.IsVisible = was
|
||||||
|
doc.Layers.Modify(layer, i, True)
|
||||||
|
n += 1
|
||||||
|
try: del sc.sticky[key]
|
||||||
|
except Exception: pass
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
print("[OBERLEISTE] Print AUS: {} Referenz-Layer-Sichtbarkeit restored".format(n))
|
||||||
|
|
||||||
def _send_state(self, force=False):
|
def _send_state(self, force=False):
|
||||||
doc, vp = massstab._active_vp()
|
doc, vp = massstab._active_vp()
|
||||||
info = massstab._compute_scale(doc, vp)
|
info = massstab._compute_scale(doc, vp)
|
||||||
@@ -1278,6 +1419,28 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
info["northAngle"] = 0
|
info["northAngle"] = 0
|
||||||
# Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight)
|
# Letzte ueber Topbar gesetzte Ansicht (fuer Active-Highlight)
|
||||||
info["lastSetView"] = self._last_set_view
|
info["lastSetView"] = self._last_set_view
|
||||||
|
# Referenzlinien-Sichtbarkeit fuer den Oberleiste-Toggle: alle
|
||||||
|
# Ebenen mit Name 'Referenz...' (keyword-driven, bauteil-uebergreifend)
|
||||||
|
# finden. Wenn ALLE visible → Button-State 'an', wenn min. eine
|
||||||
|
# unsichtbar → 'aus'. Default True (= an) wenn nichts gefunden.
|
||||||
|
ref_visible = True
|
||||||
|
try:
|
||||||
|
e_raw = doc.Strings.GetValue("dossier_ebenen") if doc else None
|
||||||
|
if e_raw:
|
||||||
|
vis_states = []
|
||||||
|
def _collect(lst):
|
||||||
|
for e in lst:
|
||||||
|
if not isinstance(e, dict): continue
|
||||||
|
nm = (e.get("name") or "").strip().lower()
|
||||||
|
if nm == "referenz" or nm.startswith("referenz"):
|
||||||
|
vis_states.append(bool(e.get("visible", True)))
|
||||||
|
kids = e.get("children")
|
||||||
|
if isinstance(kids, list): _collect(kids)
|
||||||
|
_collect(_json.loads(e_raw))
|
||||||
|
if vis_states:
|
||||||
|
ref_visible = all(vis_states)
|
||||||
|
except Exception: pass
|
||||||
|
info["referenzlinienVisible"] = ref_visible
|
||||||
# Command-Line State
|
# Command-Line State
|
||||||
prompt = _get_command_prompt()
|
prompt = _get_command_prompt()
|
||||||
info["cmdPrompt"] = prompt
|
info["cmdPrompt"] = prompt
|
||||||
@@ -1315,6 +1478,7 @@ class OberleisteBridge(panel_base.BaseBridge):
|
|||||||
info.get("textStyleActiveId"),
|
info.get("textStyleActiveId"),
|
||||||
len(info.get("textStyles") or []),
|
len(info.get("textStyles") or []),
|
||||||
info.get("lastSetView"),
|
info.get("lastSetView"),
|
||||||
|
info.get("referenzlinienVisible"),
|
||||||
prompt,
|
prompt,
|
||||||
)
|
)
|
||||||
if not force and sig == self._last_state_sig:
|
if not force and sig == self._last_state_sig:
|
||||||
|
|||||||
@@ -0,0 +1,409 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
schnitt_grips.py
|
||||||
|
Endpoint-Grips fuer Schnitt/Ansicht-Symbole im Plan.
|
||||||
|
|
||||||
|
Selber Pattern wie wand_grips.py: DisplayConduit zeichnet dicke Marker
|
||||||
|
an P1/P2 der Schnittlinie wenn das 2D-Symbol selektiert ist, MouseCallback
|
||||||
|
erkennt Klick + triggert GetPoint mit dem anderen Endpunkt als Anker.
|
||||||
|
|
||||||
|
Nach Confirm:
|
||||||
|
1. linePts im dossier_zeichnungsebenen-JSON updaten
|
||||||
|
2. Altes 2D-Symbol loeschen + neu generieren (mit korrekt rotierten Pfeilen)
|
||||||
|
3. Wenn der Schnitt aktiv ist: Clipping-Planes + View neu aufbauen
|
||||||
|
4. Panel-State broadcasten
|
||||||
|
|
||||||
|
Das war der einzige bisher fehlende Edit-Pfad fuer Schnitte — vorher musste
|
||||||
|
man linePts im JSON-Dialog manuell aendern, was unmoeglich war. Jetzt:
|
||||||
|
Symbol klicken, Marker greifen, ziehen, fertig.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Display as rd
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import scriptcontext as sc
|
||||||
|
import System
|
||||||
|
import System.Drawing as SD
|
||||||
|
|
||||||
|
|
||||||
|
# Selbe Konstanten wie wand_grips fuer visuelle Konsistenz
|
||||||
|
_HIT_RADIUS_PX = 14
|
||||||
|
_MARKER_RADIUS_PX = 7
|
||||||
|
_MARKER_RADIUS_HOVER_PX = 10
|
||||||
|
_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150)
|
||||||
|
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
|
||||||
|
_MARKER_HOVER = SD.Color.FromArgb(255, 255, 140, 60)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helpers --------------------------------------------------------------
|
||||||
|
|
||||||
|
def _read_schnitt_id(obj):
|
||||||
|
"""Wenn obj eine Schnittsymbol-Curve ist: liefere schnitt_id, sonst None."""
|
||||||
|
if obj is None or obj.IsDeleted: return None
|
||||||
|
try:
|
||||||
|
if obj.Attributes.GetUserString("dossier_schnitt_symbol") != "1":
|
||||||
|
return None
|
||||||
|
sid = obj.Attributes.GetUserString("dossier_schnitt_id")
|
||||||
|
return sid if sid else None
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_schnitt_entry(doc, schnitt_id):
|
||||||
|
"""Holt den Schnitt-Eintrag aus dossier_zeichnungsebenen."""
|
||||||
|
if not schnitt_id: return None
|
||||||
|
try:
|
||||||
|
raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
if not raw: return None
|
||||||
|
for z in json.loads(raw):
|
||||||
|
if isinstance(z, dict) and z.get("id") == schnitt_id \
|
||||||
|
and z.get("type") == "schnitt":
|
||||||
|
return z
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _schnitt_endpoints(z_entry):
|
||||||
|
"""(P1, P2) aus linePts. Z=0 (alle Schnittsymbole liegen flach im Plan)."""
|
||||||
|
pts = z_entry.get("linePts") if z_entry else None
|
||||||
|
if not pts or len(pts) < 2: return None, None
|
||||||
|
try:
|
||||||
|
p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0.0)
|
||||||
|
p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0.0)
|
||||||
|
return p1, p2
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _update_linePts(doc, schnitt_id, new_p1, new_p2):
|
||||||
|
"""Setzt linePts = [new_p1, new_p2] (beide rg.Point3d), regeneriert
|
||||||
|
das 2D-Symbol + re-aktiviert den Schnitt wenn aktiv + broadcastet.
|
||||||
|
Generische Funktion fuer Endpoint-Drag UND Mid-Drag (Whole-Line-
|
||||||
|
Translate). Liefert True bei Erfolg."""
|
||||||
|
try:
|
||||||
|
raw = doc.Strings.GetValue("dossier_zeichnungsebenen")
|
||||||
|
if not raw: return False
|
||||||
|
z_list = json.loads(raw)
|
||||||
|
target = None
|
||||||
|
for z in z_list:
|
||||||
|
if isinstance(z, dict) and z.get("id") == schnitt_id \
|
||||||
|
and z.get("type") == "schnitt":
|
||||||
|
target = z; break
|
||||||
|
if target is None: return False
|
||||||
|
pts = [[float(new_p1.X), float(new_p1.Y)],
|
||||||
|
[float(new_p2.X), float(new_p2.Y)]]
|
||||||
|
target["linePts"] = pts
|
||||||
|
try:
|
||||||
|
doc.Strings.SetString("dossier_zeichnungsebenen",
|
||||||
|
json.dumps(z_list, ensure_ascii=False))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] persist linePts:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Symbol regenerieren — Layer aus altem Symbol uebernehmen
|
||||||
|
# (geschoss-spezifisch, soll nicht auf default-Layer wandern).
|
||||||
|
import schnitte
|
||||||
|
old_objs = schnitte.find_symbol_objects_for(doc, schnitt_id)
|
||||||
|
symbol_layer_idx = -1
|
||||||
|
if old_objs:
|
||||||
|
try: symbol_layer_idx = old_objs[0].Attributes.LayerIndex
|
||||||
|
except Exception: pass
|
||||||
|
for o in old_objs:
|
||||||
|
try: doc.Objects.Delete(o.Id, True)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# Neue Curves mit aktualisierten linePts erzeugen
|
||||||
|
p1 = rg.Point3d(float(pts[0][0]), float(pts[0][1]), 0)
|
||||||
|
p2 = rg.Point3d(float(pts[1][0]), float(pts[1][1]), 0)
|
||||||
|
dir_sign = int(target.get("dirSign", 1) or 1)
|
||||||
|
new_curves = schnitte.make_schnitt_symbol(p1, p2, dir_sign,
|
||||||
|
target.get("name", ""))
|
||||||
|
first_new_id = None
|
||||||
|
for i, crv in enumerate(new_curves):
|
||||||
|
try:
|
||||||
|
attrs = Rhino.DocObjects.ObjectAttributes()
|
||||||
|
if symbol_layer_idx >= 0:
|
||||||
|
attrs.LayerIndex = symbol_layer_idx
|
||||||
|
attrs.SetUserString("dossier_schnitt_symbol", "1")
|
||||||
|
attrs.SetUserString("dossier_schnitt_id", schnitt_id)
|
||||||
|
gid = doc.Objects.AddCurve(crv, attrs)
|
||||||
|
if i == 0 and gid and gid != System.Guid.Empty:
|
||||||
|
first_new_id = gid
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] add new symbol curve:", ex)
|
||||||
|
|
||||||
|
# Neue Hauptlinie selektieren — damit der Conduit die Marker
|
||||||
|
# gleich wieder zeigt (sonst muesste der User nochmal klicken).
|
||||||
|
if first_new_id:
|
||||||
|
try: doc.Objects.Select(first_new_id, True)
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
# Re-aktivieren falls dieser Schnitt aktiv ist — aber NUR die
|
||||||
|
# Clipping-Planes neu aufbauen, View komplett in Ruhe lassen
|
||||||
|
# (skip_view=True). User editiert im Plan, soll nicht ploetzlich
|
||||||
|
# in die Section-View geschleudert oder gezoomt werden.
|
||||||
|
try:
|
||||||
|
active_id = doc.Strings.GetValue("dossier_active_id") or ""
|
||||||
|
if active_id == schnitt_id:
|
||||||
|
schnitte.activate_schnitt(doc, target, skip_view=True)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] re-activate:", ex)
|
||||||
|
|
||||||
|
# Panel-Broadcast (linePts haben sich geaendert, Ebenen-Panel will
|
||||||
|
# ggf. mit-rendern)
|
||||||
|
try:
|
||||||
|
import rhinopanel
|
||||||
|
rhinopanel._broadcast_state(doc)
|
||||||
|
except Exception: pass
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
return True
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] update endpoint:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# --- Display-Conduit ------------------------------------------------------
|
||||||
|
|
||||||
|
class _SchnittEndpointConduit(rd.DisplayConduit):
|
||||||
|
def __init__(self):
|
||||||
|
rd.DisplayConduit.__init__(self)
|
||||||
|
self.hot_key = None # (schnitt_id, 'p1'|'p2')
|
||||||
|
self.drag_key = None # waehrend aktivem Drag
|
||||||
|
self.drag_preview = None # rg.Line — Live-Vorschau
|
||||||
|
|
||||||
|
def _collect(self, doc):
|
||||||
|
"""Liefert Liste von (schnitt_id, z_entry, kind, world_pt) fuer alle
|
||||||
|
Schnitte deren Symbol-Curves selektiert sind (dedupliziert nach Id).
|
||||||
|
|
||||||
|
Drei Marker pro Schnitt:
|
||||||
|
- kind='p1' / 'p2' : Endpunkte (Endpoint-Drag)
|
||||||
|
- kind='mid' : Mittelpunkt (ganze Linie translaten)"""
|
||||||
|
out = []
|
||||||
|
seen = set()
|
||||||
|
try:
|
||||||
|
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
except Exception: return out
|
||||||
|
for obj in sel:
|
||||||
|
sid = _read_schnitt_id(obj)
|
||||||
|
if not sid or sid in seen: continue
|
||||||
|
seen.add(sid)
|
||||||
|
z = _find_schnitt_entry(doc, sid)
|
||||||
|
if z is None: continue
|
||||||
|
p1, p2 = _schnitt_endpoints(z)
|
||||||
|
if p1 is not None: out.append((sid, z, "p1", p1))
|
||||||
|
if p2 is not None: out.append((sid, z, "p2", p2))
|
||||||
|
if p1 is not None and p2 is not None:
|
||||||
|
mid = rg.Point3d((p1.X + p2.X) * 0.5,
|
||||||
|
(p1.Y + p2.Y) * 0.5, 0)
|
||||||
|
out.append((sid, z, "mid", mid))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def DrawForeground(self, e):
|
||||||
|
try:
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
# Bei mid-Drag alle Marker desselben Schnitts ausblenden — die
|
||||||
|
# ganze Linie bewegt sich, da macht das Zeichnen alter Positionen
|
||||||
|
# nur Verwirrung. Bei Endpoint-Drag: nur den gezogenen Marker
|
||||||
|
# ausblenden, andere bleiben als visueller Anker.
|
||||||
|
is_mid_drag = (self.drag_key is not None
|
||||||
|
and self.drag_key[1] == "mid")
|
||||||
|
for sid, _z, kind, pt in self._collect(doc):
|
||||||
|
if self.drag_key:
|
||||||
|
if is_mid_drag and self.drag_key[0] == sid:
|
||||||
|
continue
|
||||||
|
if self.drag_key == (sid, kind):
|
||||||
|
continue
|
||||||
|
is_hot = self.hot_key and self.hot_key == (sid, kind)
|
||||||
|
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
|
||||||
|
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
|
||||||
|
# Mid-Marker visuell anders (Quadrat statt Kreis) damit
|
||||||
|
# User sofort sieht: das verschiebt die ganze Linie.
|
||||||
|
style = rd.PointStyle.Square if kind == "mid" \
|
||||||
|
else rd.PointStyle.RoundControlPoint
|
||||||
|
try:
|
||||||
|
e.Display.DrawPoint(pt, style, r, fill)
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER)
|
||||||
|
except Exception: pass
|
||||||
|
if self.drag_preview is not None:
|
||||||
|
try:
|
||||||
|
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] DrawForeground:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
# --- MouseCallback --------------------------------------------------------
|
||||||
|
|
||||||
|
class _SchnittMouseHandler(Rhino.UI.MouseCallback):
|
||||||
|
def __init__(self, conduit):
|
||||||
|
Rhino.UI.MouseCallback.__init__(self)
|
||||||
|
self.conduit = conduit
|
||||||
|
self._busy = False
|
||||||
|
|
||||||
|
def _hit_test(self, view, screen_pt):
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return None
|
||||||
|
try: vp = view.ActiveViewport
|
||||||
|
except Exception: return None
|
||||||
|
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
|
||||||
|
for sid, z, kind, world_pt in self.conduit._collect(doc):
|
||||||
|
try:
|
||||||
|
s = vp.WorldToClient(world_pt)
|
||||||
|
dx = s.X - screen_pt.X
|
||||||
|
dy = s.Y - screen_pt.Y
|
||||||
|
if (dx * dx + dy * dy) <= thresh2:
|
||||||
|
return sid, z, kind, world_pt
|
||||||
|
except Exception: continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def OnMouseMove(self, e):
|
||||||
|
if self._busy: return
|
||||||
|
try:
|
||||||
|
view = e.View
|
||||||
|
if view is None: return
|
||||||
|
hit = self._hit_test(view, e.ViewportPoint)
|
||||||
|
new_key = (hit[0], hit[2]) if hit else None
|
||||||
|
if new_key != self.conduit.hot_key:
|
||||||
|
self.conduit.hot_key = new_key
|
||||||
|
try: view.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
def OnMouseDown(self, e):
|
||||||
|
if self._busy: return
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
if "Left" not in str(e.MouseButton): return
|
||||||
|
except Exception: pass
|
||||||
|
view = e.View
|
||||||
|
if view is None: return
|
||||||
|
hit = self._hit_test(view, e.ViewportPoint)
|
||||||
|
if hit is None: return
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
sid, z, kind, anchor_pt = hit
|
||||||
|
self._start_drag(view.Document, sid, z, kind, anchor_pt)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] OnMouseDown:", ex)
|
||||||
|
|
||||||
|
def _start_drag(self, doc, schnitt_id, z, kind, anchor_pt):
|
||||||
|
if doc is None: return
|
||||||
|
p1, p2 = _schnitt_endpoints(z)
|
||||||
|
if p1 is None or p2 is None: return
|
||||||
|
|
||||||
|
# Drei Drag-Modi:
|
||||||
|
# - kind='p1'/'p2': Endpunkt verschieben (anderer bleibt fix)
|
||||||
|
# - kind='mid' : ganze Linie translaten (Delta auf beide)
|
||||||
|
is_mid = (kind == "mid")
|
||||||
|
if is_mid:
|
||||||
|
anchor = rg.Point3d((p1.X + p2.X) * 0.5,
|
||||||
|
(p1.Y + p2.Y) * 0.5, 0)
|
||||||
|
prompt = "Schnittlinie verschieben (Esc=Abbruch)"
|
||||||
|
preview_initial = rg.Line(p1, p2)
|
||||||
|
else:
|
||||||
|
anchor = p2 if kind == "p1" else p1 # fixer Punkt
|
||||||
|
prompt = "Schnittlinie-Endpunkt: neuer Punkt (Esc=Abbruch)"
|
||||||
|
preview_initial = rg.Line(anchor, anchor_pt)
|
||||||
|
|
||||||
|
self.conduit.drag_key = (schnitt_id, kind)
|
||||||
|
self.conduit.drag_preview = preview_initial
|
||||||
|
self._busy = True
|
||||||
|
|
||||||
|
# Aktive Schnitt-Clipping-Planes sind gelockte Objekte mit grosser
|
||||||
|
# Ausdehnung — wenn der Cursor waehrend GetPoint deren Edge kreuzt,
|
||||||
|
# zeigt Rhino das "Verbot"-Cursor-Symbol (kein Pick auf Locked).
|
||||||
|
# Workaround: vor dem GetPoint verstecken, nach Confirm/Cancel
|
||||||
|
# restoren. Bei Confirm reaktiviert _update_linePts ohnehin die
|
||||||
|
# Planes neu, das Restore ist dann no-op.
|
||||||
|
hidden_clip_ids = []
|
||||||
|
try:
|
||||||
|
for obj in doc.Objects:
|
||||||
|
if obj is None or obj.IsDeleted: continue
|
||||||
|
try:
|
||||||
|
if obj.Attributes.GetUserString("dossier_schnitt_clip") == "1" \
|
||||||
|
and obj.Visible:
|
||||||
|
if doc.Objects.Hide(obj.Id, True):
|
||||||
|
hidden_clip_ids.append(obj.Id)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
confirmed = False
|
||||||
|
try:
|
||||||
|
gp = Rhino.Input.Custom.GetPoint()
|
||||||
|
gp.SetCommandPrompt(prompt)
|
||||||
|
gp.SetBasePoint(anchor, True)
|
||||||
|
gp.DrawLineFromPoint(anchor, True)
|
||||||
|
def _on_mouse_move(sender, args):
|
||||||
|
try:
|
||||||
|
cur = args.Point
|
||||||
|
if is_mid:
|
||||||
|
dx = cur.X - anchor.X
|
||||||
|
dy = cur.Y - anchor.Y
|
||||||
|
np1 = rg.Point3d(p1.X + dx, p1.Y + dy, 0)
|
||||||
|
np2 = rg.Point3d(p2.X + dx, p2.Y + dy, 0)
|
||||||
|
self.conduit.drag_preview = rg.Line(np1, np2)
|
||||||
|
else:
|
||||||
|
self.conduit.drag_preview = rg.Line(anchor, cur)
|
||||||
|
except Exception: pass
|
||||||
|
try: gp.MouseMove += _on_mouse_move
|
||||||
|
except Exception: pass
|
||||||
|
res = gp.Get()
|
||||||
|
if res == Rhino.Input.GetResult.Point:
|
||||||
|
new_pt = gp.Point()
|
||||||
|
if is_mid:
|
||||||
|
dx = new_pt.X - anchor.X
|
||||||
|
dy = new_pt.Y - anchor.Y
|
||||||
|
new_p1 = rg.Point3d(p1.X + dx, p1.Y + dy, 0)
|
||||||
|
new_p2 = rg.Point3d(p2.X + dx, p2.Y + dy, 0)
|
||||||
|
else:
|
||||||
|
if kind == "p1":
|
||||||
|
new_p1, new_p2 = new_pt, p2
|
||||||
|
else:
|
||||||
|
new_p1, new_p2 = p1, new_pt
|
||||||
|
confirmed = bool(_update_linePts(
|
||||||
|
doc, schnitt_id, new_p1, new_p2))
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] _start_drag:", ex)
|
||||||
|
finally:
|
||||||
|
if not confirmed:
|
||||||
|
for pid in hidden_clip_ids:
|
||||||
|
try: doc.Objects.Show(pid, True)
|
||||||
|
except Exception: pass
|
||||||
|
self.conduit.drag_key = None
|
||||||
|
self.conduit.drag_preview = None
|
||||||
|
self._busy = False
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
# --- Install / Teardown ---------------------------------------------------
|
||||||
|
|
||||||
|
_STICKY_CONDUIT = "_dossier_schnitt_grips_conduit"
|
||||||
|
_STICKY_HANDLER = "_dossier_schnitt_grips_handler"
|
||||||
|
|
||||||
|
|
||||||
|
def install_handlers():
|
||||||
|
"""Idempotent. Re-Load via _reset_panels.py disabled alte Refs zuerst."""
|
||||||
|
try:
|
||||||
|
old_conduit = sc.sticky.get(_STICKY_CONDUIT)
|
||||||
|
if old_conduit is not None:
|
||||||
|
try: old_conduit.Enabled = False
|
||||||
|
except Exception: pass
|
||||||
|
old_handler = sc.sticky.get(_STICKY_HANDLER)
|
||||||
|
if old_handler is not None:
|
||||||
|
try: old_handler.Enabled = False
|
||||||
|
except Exception: pass
|
||||||
|
conduit = _SchnittEndpointConduit()
|
||||||
|
conduit.Enabled = True
|
||||||
|
handler = _SchnittMouseHandler(conduit)
|
||||||
|
handler.Enabled = True
|
||||||
|
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||||
|
sc.sticky[_STICKY_HANDLER] = handler
|
||||||
|
print("[SCHNITT_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[SCHNITT_GRIPS] install:", ex)
|
||||||
+21
-4
@@ -109,11 +109,20 @@ def find_schnitt_clip_objects(doc):
|
|||||||
|
|
||||||
def clear_schnitt_clipping(doc):
|
def clear_schnitt_clipping(doc):
|
||||||
"""Loescht alle Schnitt-Clipping-Planes. Wird beim Wechsel weg vom
|
"""Loescht alle Schnitt-Clipping-Planes. Wird beim Wechsel weg vom
|
||||||
Schnitt-Modus aufgerufen (auf Geschoss oder anderen Schnitt)."""
|
Schnitt-Modus aufgerufen (auf Geschoss oder anderen Schnitt).
|
||||||
|
|
||||||
|
Wichtig: die Planes sind Mode=Locked (User soll sie nicht greifen).
|
||||||
|
doc.Objects.Delete() respektiert das Lock-Flag und schlaegt still
|
||||||
|
fehl. Deshalb erst auf Normal-Mode wechseln, dann loeschen."""
|
||||||
n = 0
|
n = 0
|
||||||
for obj in find_schnitt_clip_objects(doc):
|
for obj in find_schnitt_clip_objects(doc):
|
||||||
try:
|
try:
|
||||||
doc.Objects.Delete(obj.Id, True)
|
try:
|
||||||
|
attrs = obj.Attributes.Duplicate()
|
||||||
|
attrs.Mode = Rhino.DocObjects.ObjectMode.Normal
|
||||||
|
doc.Objects.ModifyAttributes(obj, attrs, True)
|
||||||
|
except Exception: pass
|
||||||
|
if doc.Objects.Delete(obj.Id, True):
|
||||||
n += 1
|
n += 1
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
print("[SCHNITT] clear: {}".format(ex))
|
print("[SCHNITT] clear: {}".format(ex))
|
||||||
@@ -144,7 +153,7 @@ def _add_clipping_plane(doc, plane, du, dv, vp_ids, role):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def activate_schnitt(doc, z):
|
def activate_schnitt(doc, z, skip_view=False):
|
||||||
"""Hauptfunktion: setzt Clipping-Planes + View fuer einen Schnitt-
|
"""Hauptfunktion: setzt Clipping-Planes + View fuer einen Schnitt-
|
||||||
oder Ansicht-Eintrag.
|
oder Ansicht-Eintrag.
|
||||||
|
|
||||||
@@ -160,6 +169,12 @@ def activate_schnitt(doc, z):
|
|||||||
|
|
||||||
View-Logik: Parallel-Projektion, Kamera bei mid - view_dir * dist,
|
View-Logik: Parallel-Projektion, Kamera bei mid - view_dir * dist,
|
||||||
target bei mid. Zoom auf bbox.
|
target bei mid. Zoom auf bbox.
|
||||||
|
|
||||||
|
skip_view=True: nur Clipping-Planes neu aufbauen, View komplett in
|
||||||
|
Ruhe lassen. Nutzt der Grip-Drag-Pfad — User editiert die linePts
|
||||||
|
im Plan, will NICHT dass die View ploetzlich in die Section springt
|
||||||
|
und re-zoomt. Bei Doppelklick / Panel-Klick bleibt skip_view=False
|
||||||
|
fuer den vollen Aktivierungs-Effekt.
|
||||||
"""
|
"""
|
||||||
if z is None: return
|
if z is None: return
|
||||||
pts = z.get("linePts") or []
|
pts = z.get("linePts") or []
|
||||||
@@ -232,7 +247,9 @@ def activate_schnitt(doc, z):
|
|||||||
obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back")
|
obj = _add_clipping_plane(doc, back_plane, du, dv, vp_ids, "back")
|
||||||
if obj is not None: n_planes += 1
|
if obj is not None: n_planes += 1
|
||||||
|
|
||||||
# View setzen: Parallel-Projektion, Kamera senkrecht zur Linie
|
# View setzen: Parallel-Projektion, Kamera senkrecht zur Linie.
|
||||||
|
# Bei skip_view=True (Grip-Drag-Re-Activate) komplett ueberspringen.
|
||||||
|
if not skip_view:
|
||||||
try:
|
try:
|
||||||
view = doc.Views.ActiveView
|
view = doc.Views.ActiveView
|
||||||
if view is None:
|
if view is None:
|
||||||
|
|||||||
@@ -0,0 +1,351 @@
|
|||||||
|
#! python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
wand_grips.py
|
||||||
|
Custom Endpoint-Grips fuer Waende — Display-Conduit + MouseCallback Overlay.
|
||||||
|
|
||||||
|
Problem das geloest wird:
|
||||||
|
- wand_axis liegt auf dem Referenzlinien-Sublayer (Code 19). Wenn der
|
||||||
|
User in einem Visibility-Mode ist der diesen Layer ausblendet, sind
|
||||||
|
die Achsen + ihre nativen Rhino-Grips unsichtbar.
|
||||||
|
- Native Grips sind 5–6 Pixel klein, schwer zu treffen.
|
||||||
|
- Klick neben den Grip greift das Wand-Volumen → ganze Wand wird
|
||||||
|
statt nur des Endpunkts verschoben.
|
||||||
|
|
||||||
|
Loesung:
|
||||||
|
- Display-Conduit zeichnet bei jeder selektierten Wand zwei dicke,
|
||||||
|
farbige Kreise an den Achs-Endpunkten — unabhaengig von der Layer-
|
||||||
|
Visibility (Conduit-Overlay laeuft ueber dem normalen Rendering).
|
||||||
|
- MouseCallback erkennt Mouse-Down nahe eines Markers, triggert eine
|
||||||
|
Rhino-GetPoint-Interaktion (mit Snap-Engine, OrthoMode, Tracking-
|
||||||
|
Linie zum fixen Endpunkt) und ersetzt nach Confirm den wand_axis.
|
||||||
|
- Der existierende _on_object_replaced-Handler regiert das Volumen
|
||||||
|
automatisch neu — keine manuelle Regen-Logik noetig.
|
||||||
|
|
||||||
|
Funktioniert sowohl wenn das wand_axis-Objekt eine Line ist als auch
|
||||||
|
Polyline (Multi-Segment-Wand). Bei Polyline: nur erster + letzter
|
||||||
|
Vertex sind als Endpoint-Grips ausgewiesen.
|
||||||
|
|
||||||
|
Module-Singleton — registriert sich einmal pro Rhino-Session via
|
||||||
|
sticky-Flag, Re-Loads ueber _reset_panels raeumen den alten Handler
|
||||||
|
sauber weg.
|
||||||
|
"""
|
||||||
|
import Rhino
|
||||||
|
import Rhino.Display as rd
|
||||||
|
import Rhino.Geometry as rg
|
||||||
|
import scriptcontext as sc
|
||||||
|
import System
|
||||||
|
import System.Drawing as SD
|
||||||
|
|
||||||
|
|
||||||
|
# --- Konstanten ------------------------------------------------------------
|
||||||
|
|
||||||
|
# Hit-Radius in Pixeln fuer Marker-Klick-Detection. Bewusst grosszuegig
|
||||||
|
# (~ 14px) damit der User nicht zielen muss.
|
||||||
|
_HIT_RADIUS_PX = 14
|
||||||
|
|
||||||
|
# Marker-Radius in Pixeln fuer das Drawing. 8px ist gut sichtbar ohne zu
|
||||||
|
# stoeren. Bei Hover etwas groesser (10px).
|
||||||
|
_MARKER_RADIUS_PX = 7
|
||||||
|
_MARKER_RADIUS_HOVER_PX = 10
|
||||||
|
|
||||||
|
# Farben — accent-gruen analog zum Dossier-Theme.
|
||||||
|
_MARKER_FILL = SD.Color.FromArgb(220, 95, 168, 150)
|
||||||
|
_MARKER_BORDER = SD.Color.FromArgb(255, 47, 93, 84)
|
||||||
|
_MARKER_HOVER = SD.Color.FromArgb(255, 255, 140, 60)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Helpers --------------------------------------------------------------
|
||||||
|
|
||||||
|
def _read_axis_type(obj):
|
||||||
|
"""Schnelle Pruefung ob obj eine wand_axis ist. Importiert elemente
|
||||||
|
lazy um Circular-Import beim Modul-Load zu vermeiden."""
|
||||||
|
if obj is None or obj.IsDeleted: return False
|
||||||
|
try:
|
||||||
|
return obj.Attributes.GetUserString("dossier_element_type") == "wand_axis"
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _find_axis_for_obj(doc, obj):
|
||||||
|
"""Gibt die wand_axis zurueck zu der dieses Objekt gehoert.
|
||||||
|
- Wenn obj selber eine wand_axis ist: return obj
|
||||||
|
- Wenn obj ein wand_volume ist: suche Source via element_id
|
||||||
|
|
||||||
|
Liefert None bei Mismatch oder fehlenden Tags."""
|
||||||
|
if obj is None or obj.IsDeleted: return None
|
||||||
|
attrs = obj.Attributes
|
||||||
|
try:
|
||||||
|
t = attrs.GetUserString("dossier_element_type")
|
||||||
|
eid = attrs.GetUserString("dossier_element_id")
|
||||||
|
if not t or not eid: return None
|
||||||
|
if t == "wand_axis": return obj
|
||||||
|
if t != "wand_volume": return None
|
||||||
|
# Source suchen — iteriere doc, finde wand_axis mit gleicher id
|
||||||
|
for o in doc.Objects:
|
||||||
|
if o is None or o.IsDeleted: continue
|
||||||
|
a2 = o.Attributes
|
||||||
|
try:
|
||||||
|
if a2.GetUserString("dossier_element_id") == eid and \
|
||||||
|
a2.GetUserString("dossier_element_type") == "wand_axis":
|
||||||
|
return o
|
||||||
|
except Exception: continue
|
||||||
|
except Exception: pass
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _curve_endpoints(curve):
|
||||||
|
"""Liefert (start_pt, end_pt) fuer eine wand_axis. Funktioniert fuer
|
||||||
|
LineCurve, PolylineCurve, NurbsCurve etc — alle Curve-Typen haben
|
||||||
|
PointAtStart/PointAtEnd. Bei degenerierten Curves None."""
|
||||||
|
if curve is None: return None, None
|
||||||
|
try:
|
||||||
|
return curve.PointAtStart, curve.PointAtEnd
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_axis_endpoint(doc, axis_obj, kind, new_pt):
|
||||||
|
"""Tauscht den Start- (kind='start') oder Endpunkt (kind='end') der
|
||||||
|
wand_axis-Curve gegen new_pt. Geht intelligent um mit:
|
||||||
|
- LineCurve: erzeuge neue Line vom fixen Punkt zum neuen Punkt
|
||||||
|
- PolylineCurve: ersetze ersten/letzten Vertex, Rest bleibt
|
||||||
|
- andere Curve-Typen: aktuell nur Line-Fallback (Erst/Letzt-Vertex
|
||||||
|
rekonstruieren)
|
||||||
|
Setzt die neue Geometrie via Objects.Replace — das feuert
|
||||||
|
ReplaceRhinoObject-Event, was den existierenden Wand-Regen anwirft."""
|
||||||
|
if axis_obj is None or axis_obj.IsDeleted: return False
|
||||||
|
geom = axis_obj.Geometry
|
||||||
|
if geom is None: return False
|
||||||
|
try:
|
||||||
|
# PolylineCurve mit > 2 Vertices: ersten/letzten Vertex ersetzen
|
||||||
|
if isinstance(geom, rg.PolylineCurve):
|
||||||
|
poly = geom.ToPolyline()
|
||||||
|
if poly is None or poly.Count < 2: return False
|
||||||
|
pts = list(poly)
|
||||||
|
if kind == "start":
|
||||||
|
pts[0] = new_pt
|
||||||
|
else:
|
||||||
|
pts[-1] = new_pt
|
||||||
|
new_poly = rg.Polyline(pts)
|
||||||
|
new_curve = rg.PolylineCurve(new_poly)
|
||||||
|
else:
|
||||||
|
# LineCurve oder unbekannter Typ → reduziere auf Line zwischen
|
||||||
|
# neuem + altem fixen Punkt.
|
||||||
|
p_start, p_end = _curve_endpoints(geom)
|
||||||
|
if p_start is None or p_end is None: return False
|
||||||
|
if kind == "start":
|
||||||
|
new_curve = rg.LineCurve(new_pt, p_end)
|
||||||
|
else:
|
||||||
|
new_curve = rg.LineCurve(p_start, new_pt)
|
||||||
|
return doc.Objects.Replace(axis_obj.Id, new_curve)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WAND_GRIPS] replace endpoint:", ex)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# --- Display-Conduit -------------------------------------------------------
|
||||||
|
|
||||||
|
class _EndpointConduit(rd.DisplayConduit):
|
||||||
|
"""Zeichnet bei jeder selektierten Wand zwei dicke Marker an den
|
||||||
|
Achs-Endpunkten. hot_key (axis_guid_str, 'start'|'end') hebt einen
|
||||||
|
Marker als Hover-Highlight hervor."""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
rd.DisplayConduit.__init__(self)
|
||||||
|
self.hot_key = None # (axis_id_str, kind) — fuer Hover
|
||||||
|
self.drag_key = None # (axis_id_str, kind) — waehrend aktivem Drag
|
||||||
|
self.drag_preview = None # rg.Line — Live-Vorschau waehrend GetPoint
|
||||||
|
|
||||||
|
def _collect_endpoints(self, doc):
|
||||||
|
"""Liefert Liste von (axis_obj, kind, world_pt) fuer alle selektier-
|
||||||
|
ten Waende. Iteriert die Selektion + dedupliziert Achsen (jede
|
||||||
|
Wand erscheint nur einmal, auch wenn mehrere Volumen mit-selek-
|
||||||
|
tiert sind)."""
|
||||||
|
out = []
|
||||||
|
seen_axis = set()
|
||||||
|
try:
|
||||||
|
sel = list(doc.Objects.GetSelectedObjects(False, False))
|
||||||
|
except Exception: return out
|
||||||
|
for obj in sel:
|
||||||
|
axis = _find_axis_for_obj(doc, obj)
|
||||||
|
if axis is None: continue
|
||||||
|
aid = str(axis.Id)
|
||||||
|
if aid in seen_axis: continue
|
||||||
|
seen_axis.add(aid)
|
||||||
|
p_start, p_end = _curve_endpoints(axis.Geometry)
|
||||||
|
if p_start is not None:
|
||||||
|
out.append((axis, "start", p_start))
|
||||||
|
if p_end is not None:
|
||||||
|
out.append((axis, "end", p_end))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def DrawForeground(self, e):
|
||||||
|
try:
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return
|
||||||
|
for axis, kind, pt in self._collect_endpoints(doc):
|
||||||
|
aid = str(axis.Id)
|
||||||
|
# Skip den gerade gezogenen Marker — der wird via
|
||||||
|
# drag_preview separat dargestellt.
|
||||||
|
if self.drag_key and self.drag_key == (aid, kind):
|
||||||
|
continue
|
||||||
|
is_hot = self.hot_key and self.hot_key == (aid, kind)
|
||||||
|
r = _MARKER_RADIUS_HOVER_PX if is_hot else _MARKER_RADIUS_PX
|
||||||
|
fill = _MARKER_HOVER if is_hot else _MARKER_FILL
|
||||||
|
# DrawPoint mit RoundControlPoint = gefuellter Kreis +
|
||||||
|
# Border. Sieht aus wie ein dicker Grip-Punkt.
|
||||||
|
try:
|
||||||
|
e.Display.DrawPoint(
|
||||||
|
pt, rd.PointStyle.RoundControlPoint, r, fill)
|
||||||
|
except Exception:
|
||||||
|
# Fallback fuer aeltere Rhino-Versionen: einfacher
|
||||||
|
# DrawDot mit Label "●"
|
||||||
|
e.Display.DrawDot(pt, "●", fill, _MARKER_BORDER)
|
||||||
|
# Drag-Preview-Linie waehrend GetPoint aktiv ist
|
||||||
|
if self.drag_preview is not None:
|
||||||
|
try:
|
||||||
|
e.Display.DrawLine(self.drag_preview, _MARKER_HOVER, 2)
|
||||||
|
except Exception: pass
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WAND_GRIPS] DrawForeground:", ex)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Mouse-Handler --------------------------------------------------------
|
||||||
|
|
||||||
|
class _EndpointMouseHandler(Rhino.UI.MouseCallback):
|
||||||
|
"""Erkennt Mouse-Down nahe eines Endpoint-Markers + triggert Rhino-
|
||||||
|
GetPoint fuer den neuen Endpunkt. Hover-Update via OnMouseMove fuer
|
||||||
|
visuelles Highlight."""
|
||||||
|
|
||||||
|
def __init__(self, conduit):
|
||||||
|
Rhino.UI.MouseCallback.__init__(self)
|
||||||
|
self.conduit = conduit
|
||||||
|
self._busy = False # Re-Entry-Schutz waehrend Drag-Get-Point
|
||||||
|
|
||||||
|
def _hit_test(self, view, screen_pt):
|
||||||
|
"""Liefert (axis, kind, world_pt) wenn screen_pt nahe eines Endpoint-
|
||||||
|
Markers liegt, sonst None. Iteriert die aktuelle Conduit-Liste."""
|
||||||
|
doc = Rhino.RhinoDoc.ActiveDoc
|
||||||
|
if doc is None: return None
|
||||||
|
try:
|
||||||
|
vp = view.ActiveViewport
|
||||||
|
except Exception: return None
|
||||||
|
thresh2 = _HIT_RADIUS_PX * _HIT_RADIUS_PX
|
||||||
|
for axis, kind, world_pt in self.conduit._collect_endpoints(doc):
|
||||||
|
try:
|
||||||
|
s = vp.WorldToClient(world_pt)
|
||||||
|
dx = s.X - screen_pt.X
|
||||||
|
dy = s.Y - screen_pt.Y
|
||||||
|
if (dx * dx + dy * dy) <= thresh2:
|
||||||
|
return axis, kind, world_pt
|
||||||
|
except Exception: continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
def OnMouseMove(self, e):
|
||||||
|
if self._busy: return
|
||||||
|
try:
|
||||||
|
view = e.View
|
||||||
|
if view is None: return
|
||||||
|
hit = self._hit_test(view, e.ViewportPoint)
|
||||||
|
new_key = (str(hit[0].Id), hit[1]) if hit else None
|
||||||
|
if new_key != self.conduit.hot_key:
|
||||||
|
self.conduit.hot_key = new_key
|
||||||
|
try: view.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
def OnMouseDown(self, e):
|
||||||
|
if self._busy: return
|
||||||
|
try:
|
||||||
|
# Nur linke Maustaste
|
||||||
|
try:
|
||||||
|
btn = e.MouseButton
|
||||||
|
btn_str = str(btn)
|
||||||
|
if "Left" not in btn_str:
|
||||||
|
return
|
||||||
|
except Exception: pass
|
||||||
|
view = e.View
|
||||||
|
if view is None: return
|
||||||
|
hit = self._hit_test(view, e.ViewportPoint)
|
||||||
|
if hit is None: return
|
||||||
|
# Default-Klick (Selection) abwuergen — wir uebernehmen
|
||||||
|
try: e.Cancel = True
|
||||||
|
except Exception: pass
|
||||||
|
axis, kind, world_pt = hit
|
||||||
|
self._start_drag(view.Document, axis, kind, world_pt)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WAND_GRIPS] OnMouseDown:", ex)
|
||||||
|
|
||||||
|
def _start_drag(self, doc, axis, kind, anchor_pt):
|
||||||
|
"""Startet eine Rhino-GetPoint-Interaktion um den neuen Endpunkt
|
||||||
|
zu picken. Der ANDERE Endpunkt (Fix-Punkt) wird als BasePoint
|
||||||
|
gesetzt — damit kriegt der User Tracking-Linie, Ortho-Mode etc.
|
||||||
|
wie bei _Move."""
|
||||||
|
if doc is None: return
|
||||||
|
geom = axis.Geometry
|
||||||
|
if geom is None: return
|
||||||
|
p_start, p_end = _curve_endpoints(geom)
|
||||||
|
if p_start is None or p_end is None: return
|
||||||
|
fixed_pt = p_end if kind == "start" else p_start
|
||||||
|
# Conduit-State: drag-Marker hervorheben + Preview-Linie
|
||||||
|
self.conduit.drag_key = (str(axis.Id), kind)
|
||||||
|
self.conduit.drag_preview = rg.Line(fixed_pt, anchor_pt)
|
||||||
|
self._busy = True
|
||||||
|
try:
|
||||||
|
gp = Rhino.Input.Custom.GetPoint()
|
||||||
|
gp.SetCommandPrompt("Wand-Endpunkt: neuer Punkt (Esc=Abbruch)")
|
||||||
|
gp.SetBasePoint(fixed_pt, True)
|
||||||
|
gp.DrawLineFromPoint(fixed_pt, True)
|
||||||
|
# Live-Preview ueber Conduit (zusaetzlich zu Rhinos eigener
|
||||||
|
# Tracking-Linie) — sieht ueblich, hilft beim Verstehen welcher
|
||||||
|
# Endpunkt sich bewegt.
|
||||||
|
def _on_mouse_move(sender, args):
|
||||||
|
try:
|
||||||
|
self.conduit.drag_preview = rg.Line(fixed_pt, args.Point)
|
||||||
|
except Exception: pass
|
||||||
|
try: gp.MouseMove += _on_mouse_move
|
||||||
|
except Exception: pass
|
||||||
|
res = gp.Get()
|
||||||
|
if res == Rhino.Input.GetResult.Point:
|
||||||
|
new_pt = gp.Point()
|
||||||
|
_replace_axis_endpoint(doc, axis, kind, new_pt)
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WAND_GRIPS] _start_drag:", ex)
|
||||||
|
finally:
|
||||||
|
self.conduit.drag_key = None
|
||||||
|
self.conduit.drag_preview = None
|
||||||
|
self._busy = False
|
||||||
|
try: doc.Views.Redraw()
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
|
||||||
|
# --- Install / Teardown ---------------------------------------------------
|
||||||
|
|
||||||
|
_STICKY_CONDUIT = "_dossier_wand_grips_conduit"
|
||||||
|
_STICKY_HANDLER = "_dossier_wand_grips_handler"
|
||||||
|
|
||||||
|
|
||||||
|
def install_handlers():
|
||||||
|
"""Idempotente Registrierung. Bei Modul-Reload wird der alte Conduit
|
||||||
|
+ Mouse-Handler zuerst disabled, dann neu erstellt + enabled. Sticky
|
||||||
|
haelt die Referenzen am Leben (sonst Garbage-Collection)."""
|
||||||
|
try:
|
||||||
|
old_conduit = sc.sticky.get(_STICKY_CONDUIT)
|
||||||
|
if old_conduit is not None:
|
||||||
|
try: old_conduit.Enabled = False
|
||||||
|
except Exception: pass
|
||||||
|
old_handler = sc.sticky.get(_STICKY_HANDLER)
|
||||||
|
if old_handler is not None:
|
||||||
|
try: old_handler.Enabled = False
|
||||||
|
except Exception: pass
|
||||||
|
|
||||||
|
conduit = _EndpointConduit()
|
||||||
|
conduit.Enabled = True
|
||||||
|
handler = _EndpointMouseHandler(conduit)
|
||||||
|
handler.Enabled = True
|
||||||
|
sc.sticky[_STICKY_CONDUIT] = conduit
|
||||||
|
sc.sticky[_STICKY_HANDLER] = handler
|
||||||
|
print("[WAND_GRIPS] Endpoint-Conduit + Mouse-Handler aktiv")
|
||||||
|
except Exception as ex:
|
||||||
|
print("[WAND_GRIPS] install:", ex)
|
||||||
+27
-11
@@ -16,6 +16,7 @@ import {
|
|||||||
applyTextStyle, saveTextStyle, deleteTextStyle,
|
applyTextStyle, saveTextStyle, deleteTextStyle,
|
||||||
setDarstellung,
|
setDarstellung,
|
||||||
arrangeSelection,
|
arrangeSelection,
|
||||||
|
toggleReferenzlinien,
|
||||||
} from './lib/rhinoBridge'
|
} from './lib/rhinoBridge'
|
||||||
|
|
||||||
const PRESETS = [
|
const PRESETS = [
|
||||||
@@ -470,7 +471,7 @@ export default function OberleisteApp() {
|
|||||||
// Buttons-Pill: gleiche Logik wie View-Toggle (weiss default,
|
// Buttons-Pill: gleiche Logik wie View-Toggle (weiss default,
|
||||||
// grün on hover, accent-fill wenn active)
|
// grün on hover, accent-fill wenn active)
|
||||||
const PILL_W = 140 // Gleiche Breite fuer Dropdown + Buttons-Pill
|
const PILL_W = 140 // Gleiche Breite fuer Dropdown + Buttons-Pill
|
||||||
const N_BTN = 4
|
const N_BTN = 3 // ohne Lineweights — der sitzt jetzt oben neben Dropdown
|
||||||
const BTN_W = Math.floor(PILL_W / N_BTN) // jeder Button gleich breit
|
const BTN_W = Math.floor(PILL_W / N_BTN) // jeder Button gleich breit
|
||||||
const SegBtn = ({ icon, onClick, title, disabled, active, isFirst, isLast }) => (
|
const SegBtn = ({ icon, onClick, title, disabled, active, isFirst, isLast }) => (
|
||||||
<button onClick={onClick} disabled={disabled} title={title}
|
<button onClick={onClick} disabled={disabled} title={title}
|
||||||
@@ -560,6 +561,10 @@ export default function OberleisteApp() {
|
|||||||
</div>
|
</div>
|
||||||
{/* Reihe 1, Spalte 2: Gesetzter Massstab Dropdown — KEIN Icon, gleiche
|
{/* Reihe 1, Spalte 2: Gesetzter Massstab Dropdown — KEIN Icon, gleiche
|
||||||
Breite wie Buttons-Pill darunter, exakt uebereinander */}
|
Breite wie Buttons-Pill darunter, exakt uebereinander */}
|
||||||
|
{/* Dropdown + Druck-Ansicht-Toggle in einer Flex-Reihe — der
|
||||||
|
Toggle sitzt jetzt oben statt unten im Zoom-Pill, weil er
|
||||||
|
massstabs-nah ist (Print-View = scale-korrekte Strichstaerken). */}
|
||||||
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||||
{customMode ? (
|
{customMode ? (
|
||||||
<input
|
<input
|
||||||
ref={customInputRef}
|
ref={customInputRef}
|
||||||
@@ -602,12 +607,22 @@ export default function OberleisteApp() {
|
|||||||
<option value="__custom__">Eigener…</option>
|
<option value="__custom__">Eigener…</option>
|
||||||
</BarCombo>
|
</BarCombo>
|
||||||
)}
|
)}
|
||||||
{/* Reihe 2, Spalte 2: Buttons-Pill — gleiche Breite wie Dropdown */}
|
<BarButton
|
||||||
|
icon={state.showLineweights ? 'print' : 'edit'}
|
||||||
|
active={state.showLineweights}
|
||||||
|
onClick={() => setShowLineweights(!state.showLineweights)}
|
||||||
|
title={state.showLineweights
|
||||||
|
? 'Print-View aktiv — klick zum Ausschalten'
|
||||||
|
: 'Strichstaerken anzeigen (Print-View)'} />
|
||||||
|
</div>
|
||||||
|
{/* Reihe 2, Spalte 2: Zoom-Pill + Referenzlinien-Toggle.
|
||||||
|
Symmetrisch zur Reihe 1 (Dropdown + Lineweights-Button). */}
|
||||||
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'inline-flex', width: PILL_W,
|
display: 'inline-flex', width: PILL_W,
|
||||||
height: BAR_H + 2, boxSizing: 'border-box',
|
height: BAR_H + 2, boxSizing: 'border-box',
|
||||||
border: '1px solid var(--border)', borderRadius: 999,
|
border: '1px solid var(--border)', borderRadius: 999,
|
||||||
overflow: 'hidden', flexShrink: 0, justifySelf: 'start',
|
overflow: 'hidden', flexShrink: 0,
|
||||||
}}>
|
}}>
|
||||||
<SegBtn icon="percent" onClick={apply100} isFirst
|
<SegBtn icon="percent" onClick={apply100} isFirst
|
||||||
disabled={isPerspective || !appliedScale}
|
disabled={isPerspective || !appliedScale}
|
||||||
@@ -615,15 +630,16 @@ export default function OberleisteApp() {
|
|||||||
<SegBtn icon="fit_screen" onClick={zoomExtents}
|
<SegBtn icon="fit_screen" onClick={zoomExtents}
|
||||||
title="Auf gesamten Inhalt zoomen" />
|
title="Auf gesamten Inhalt zoomen" />
|
||||||
<SegBtn icon="center_focus_strong" onClick={zoomSelection}
|
<SegBtn icon="center_focus_strong" onClick={zoomSelection}
|
||||||
title="Auf Selektion zoomen" />
|
|
||||||
<SegBtn
|
|
||||||
icon={state.showLineweights ? 'print' : 'edit'}
|
|
||||||
active={state.showLineweights}
|
|
||||||
onClick={() => setShowLineweights(!state.showLineweights)}
|
|
||||||
isLast
|
isLast
|
||||||
title={state.showLineweights
|
title="Auf Selektion zoomen" />
|
||||||
? 'Print-View aktiv — klick zum Ausschalten'
|
</div>
|
||||||
: 'Strichstärken anzeigen (Print-View)'} />
|
<BarButton
|
||||||
|
icon={state.referenzlinienVisible === false ? 'visibility_off' : 'visibility'}
|
||||||
|
active={state.referenzlinienVisible !== false}
|
||||||
|
onClick={() => toggleReferenzlinien(state.referenzlinienVisible === false)}
|
||||||
|
title={state.referenzlinienVisible === false
|
||||||
|
? 'Referenzlinien einblenden (Wandachsen, Oeffnungs-Punkte)'
|
||||||
|
: 'Referenzlinien ausblenden (Wandachsen, Oeffnungs-Punkte)'} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -175,6 +175,12 @@ export function openElementeProperties() { send('OPEN_ELEMENTE_PROPERTIES', {})
|
|||||||
export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) }
|
export function setDarstellung(d) { send('SET_DARSTELLUNG', { darstellung: d || '' }) }
|
||||||
// Anordnen — 2D-Z-Stack via Rhino-DisplayOrder. dir: 'front'|'forward'|'backward'|'back'
|
// Anordnen — 2D-Z-Stack via Rhino-DisplayOrder. dir: 'front'|'forward'|'backward'|'back'
|
||||||
export function arrangeSelection(dir) { send('ARRANGE', { dir }) }
|
export function arrangeSelection(dir) { send('ARRANGE', { dir }) }
|
||||||
|
// Referenzlinien-Layer (Code 19) on/off — Shortcut zur Layer-Sichtbarkeit
|
||||||
|
// damit der User nicht durchs Ebenen-Panel muss. Layer bleibt erhalten,
|
||||||
|
// Ausschnitte speichern den State automatisch mit.
|
||||||
|
export function toggleReferenzlinien(visible) {
|
||||||
|
send('TOGGLE_REFERENZLINIEN', { visible: !!visible })
|
||||||
|
}
|
||||||
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
// Schnitt/Ansicht — interaktiver 2-Punkt-Pick im Rhino-Viewport. Erzeugt
|
||||||
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
// eine neue Zeichnungsebene type=schnitt + 2D-Plan-Symbol + aktiviert sie.
|
||||||
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
// opts: { cutAtLine: bool, depthBack: m, heightMin: m, heightMax: m, namePrefix }
|
||||||
|
|||||||
Reference in New Issue
Block a user