DOSSIER Multi-Phase: C#-Plugin + Yak + Wandstile + UX-Polish

- C#-Plugin "DOSSIER" mit 23 nativen Commands (dWall, dDoor, ..., dSection)
  - Native Command-Namen + Autocomplete + saubere History
  - Idle-Defer + RhinoCode-API → kein _-RunPythonScript-Echo
  - Yak-Paket via build.sh, Install in ~/Library/.../packages/8.0/
- Launcher (Tauri):
  - dossier_init Tauri-Command + Setup-Tab in Settings
  - Yak-Install + StartupCommands-XML + Window-Layout in einem Schritt
  - clean-rhino.sh fuer reproduzierbare Resets
  - check_dossier_initialized triggert Auto-Open-Setup beim ersten Start
- Wand-Architektur:
  - Chain-Logik DEAKTIVIERT → jede Wand baut eigenes Volume (individuell
    anwaehlbar, einzeln loeschbar)
  - Polyline-Wand: jedes Segment = eigene Wand
  - Smart-Split fuer wand_axis/decke/dach/raum/aussparung/traeger
  - Auto-Group axis+volume → kein ChooseOne-Dialog, Delete loescht beides
  - Stale-Mitre-Fix: Joint-Cache wird vor jedem Wand-Regen invalidiert
  - T-Junction-Tolerance auf 1mm (war 1cm, lieferte falsche T-Mitres)
- Wand-Stile:
  - Schema in dossier_project_settings.wand_styles (Material + Prio +
    Default-Dicke + Referenz, oder Layered mit Schichten)
  - dWall-Command Stil-Picker
  - ProjectSettingsDialog: Sidebar-Layout (Pill-Selection) +
    Wandstile-Tab mit Liste/Editor
  - _wand_chain_compat benutzt style_id
  - Prio-Dominanz: hoehere Prio gewinnt Eckverbindung, niedrigere wird
    T-mitered (siehe _resolve_corner_miter)
- Cmd+G fuer Group (Geschoss-Up auf Alias 'gu')
- Welcome + Cheatsheet borderless mit X/Back-Buttons
- BeginCommand-Hook fuer Gestaltung-Panel-Auto-Open
- panel_base: Python.NET-Enum-Fix fuer Material-Render
This commit is contained in:
2026-05-30 12:46:53 +02:00
parent 7930705d01
commit 18d6d98e07
54 changed files with 5575 additions and 398 deletions
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'aussparung'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("aussparung")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'dach'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("dach")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'decke'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("decke")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Alias 'dkeys': oeffnet DOSSIER Shortcuts-Cheatsheet
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import welcome
welcome.show_cheatsheet()
+8
View File
@@ -0,0 +1,8 @@
#! python3
# -*- coding: utf-8 -*-
# Alias 'dwelcome': zeigt DOSSIER Welcome-Screen manuell (force-mode,
# ignoriert version-marker + optout)
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import welcome
welcome._show_welcome_now()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'fenster'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("fenster")
+436
View File
@@ -0,0 +1,436 @@
#! python3
# -*- coding: utf-8 -*-
# Pipette / Einstellungen-übernehmen: User klickt ein Source-Objekt, dessen
# Attribute werden zur aktuellen Default-Einstellung gemacht — der naechste
# gezeichnete Curve/Rectangle/etc. erbt sie automatisch.
#
# Was uebernommen wird:
# 1. Layer → wird zum Current Layer
# 2. Color (wenn per-Object Override) → wird Current Object-Color
# 3. Linetype (per-Object) → Current
# 4. PlotWeight (per-Object) → Current
# 5. Fuer DOSSIER-Elemente (wand_axis, treppe_axis, etc.) → spezifische
# UserStrings (Dicke, Modus, Breite, Stufen etc.) werden in sticky
# gespeichert als _last_* → nachste Create-Wand/Treppe etc. nimmt sie.
# 6. Bei Hatch-Quelle → wechselt auf den Curve dahinter (Hatch hat selten
# direkt Sinn als Pipette-Quelle, eher der gefuellte Rahmen).
import scriptcontext as sc
import Rhino
import Rhino.Input.Custom as ric
import Rhino.DocObjects as rdoc
from Rhino.Input import GetResult
# Welche UserStrings pro DOSSIER-Type als sticky _last_* gespeichert werden,
# damit das naechste Create-Cmd sie als Default uebernimmt.
_DOSSIER_INHERIT = {
"wand_axis": [
("dossier_wand_dicke", "wand_dicke"),
("dossier_wand_referenz", "wand_referenz"),
("dossier_wand_modus", "wand_modus"),
],
"treppe_axis": [
("dossier_treppe_breite", "treppe_breite"),
("dossier_treppe_n", "treppe_n"),
("dossier_treppe_referenz", "treppe_referenz"),
("dossier_treppe_modus", "treppe_modus"),
("dossier_treppe_lauf_d", "treppe_lauf_d"),
("dossier_treppe_art", "treppe_art"),
],
"decke_outline": [
("dossier_decke_dicke", "decke_dicke"),
("dossier_decke_modus", "decke_modus"),
],
"dach_outline": [
("dossier_dach_dicke", "dach_dicke"),
("dossier_dach_neigung", "dach_neigung"),
],
"stuetze_point": [
("dossier_trag_profil", "stuetze_profil"),
("dossier_trag_b", "stuetze_b"),
("dossier_trag_h", "stuetze_h"),
],
"traeger_axis": [
("dossier_trag_profil", "traeger_profil"),
("dossier_trag_b", "traeger_b"),
("dossier_trag_h", "traeger_h"),
],
"oeffnung_point": [
("dossier_oeff_breite", "oeff_breite"),
("dossier_oeff_hoehe", "oeff_hoehe"),
],
}
def _save_sticky(key, value):
sc.sticky["elemente_last_" + key] = value
def _find_curve_behind_hatch(doc, hatch_obj):
"""Hatches haben in DOSSIER oft eine zugeordnete Source-Curve (gestaltung
speichert die Curve-ID auf der Hatch via 'ebenen_fill_owner')."""
try:
owner = hatch_obj.Attributes.GetUserString("ebenen_fill_owner") or ""
if owner:
import System
cid = System.Guid(owner)
cobj = doc.Objects.FindId(cid)
if cobj is not None and not cobj.IsDeleted: return cobj
except Exception: pass
return None
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
go = ric.GetObject()
go.SetCommandPrompt("Pipette: Quell-Objekt picken (Attribute uebernehmen)")
go.GeometryFilter = (rdoc.ObjectType.Curve
| rdoc.ObjectType.Brep
| rdoc.ObjectType.Hatch
| rdoc.ObjectType.PointSet
| rdoc.ObjectType.Point
| rdoc.ObjectType.Annotation
| rdoc.ObjectType.TextDot)
go.SubObjectSelect = False
if go.Get() != GetResult.Object:
print("[PIPETTE] abgebrochen"); return
src = go.Object(0).Object()
if src is None: return
# Wenn Hatch gepickt, switch zur Source-Curve (gefuelltes Rechteck als
# Pipette-Quelle ist intuitiver als die Hatch selbst)
src_geom_type = type(src.Geometry).__name__
if src_geom_type == "Hatch":
cobj = _find_curve_behind_hatch(doc, src)
if cobj is not None:
src = cobj
print("[PIPETTE] Hatch → zugeordnete Curve verwendet")
sa = src.Attributes
msgs = []
# 1. Layer als Current setzen
try:
if doc.Layers.CurrentLayerIndex != sa.LayerIndex:
doc.Layers.SetCurrentLayerIndex(sa.LayerIndex, True)
try: lname = doc.Layers[sa.LayerIndex].FullPath
except Exception: lname = "idx=" + str(sa.LayerIndex)
msgs.append("Layer={}".format(lname))
except Exception as ex:
print("[PIPETTE] Layer-Set:", ex)
# 2. Color
try:
cs = Rhino.ApplicationSettings.AppearanceSettings
if sa.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromObject
cs.DefaultObjectColor = sa.ObjectColor
msgs.append("Color=obj")
else:
cs.DefaultObjectColorSource = rdoc.ObjectColorSource.ColorFromLayer
msgs.append("Color=byLayer")
except Exception as ex:
print("[PIPETTE] Color-Set:", ex)
# 3. Linetype + 4. PlotWeight — komplexer, weil Rhino keine direkten
# AppearanceSettings dafuer hat. Wir ueberspringen bewusst, weil der
# Layer-Wechsel die meisten Faelle abdeckt (Linetype + PlotWeight
# kommen typisch ByLayer).
# 5. DOSSIER-spezifische Attrs in sticky uebernehmen
try:
dtype = sa.GetUserString("dossier_element_type") or ""
if dtype in _DOSSIER_INHERIT:
inherited = []
for us_key, sticky_key in _DOSSIER_INHERIT[dtype]:
v = sa.GetUserString(us_key)
if v is None or v == "": continue
# Numerische Werte ggf. konvertieren
if any(k in sticky_key for k in ("dicke", "breite", "hoehe",
"neigung", "lauf_d", "_b", "_h")):
try: v = float(v)
except Exception: pass
elif "n" == sticky_key or sticky_key.endswith("_n"):
try: v = int(float(v))
except Exception: pass
_save_sticky(sticky_key, v)
inherited.append("{}={}".format(sticky_key, v))
if inherited:
msgs.append("DOSSIER " + dtype + ": " + ", ".join(inherited))
except Exception as ex:
print("[PIPETTE] DOSSIER-Inherit:", ex)
if msgs:
print("[PIPETTE] Uebernommen: " + " | ".join(msgs))
else:
print("[PIPETTE] Keine Aenderung (Source identisch zu Defaults)")
# 7. Per-Object Custom-Hatch / Custom-Attrs: speichern als "pending"
# + one-shot Listener auf AddRhinoObject — wenn naechster Curve
# gezeichnet ist, alle Custom-Attrs auf den uebertragen.
_setup_pending_apply(doc, src)
# 6. Auto-Chain: passendes Draw-Command starten basierend auf
# Source-Typ. So hat der User direkt "die richtige Tool in der Hand".
_auto_chain(doc, src)
def _capture_source_hatch_props(doc, src_obj):
"""Wenn Source einen per-Object Custom-Hatch hat, sample dessen
Properties (Pattern/Scale/Rotation/Color)."""
try:
sa = src_obj.Attributes
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
if not fill_hid: return None
import System
hid = System.Guid(fill_hid)
hobj = doc.Objects.FindId(hid)
if hobj is None or hobj.IsDeleted: return None
hg = hobj.Geometry
ha = hobj.Attributes
if not hasattr(hg, "PatternIndex"): return None
return {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"layer_idx": int(ha.LayerIndex),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"plot_color_source": int(ha.PlotColorSource),
"plot_color_argb": int(ha.PlotColor.ToArgb()),
"linetype_source": int(ha.LinetypeSource),
"linetype_idx": int(ha.LinetypeIndex),
}
except Exception as ex:
print("[PIPETTE] capture-hatch:", ex)
return None
def _setup_pending_apply(doc, src_obj):
"""Speichert Source-Custom-Attrs in sticky + registriert one-shot
AddRhinoObject-Listener der die Attrs (inkl. Hatch) auf den naechsten
neuen Curve uebertraegt. Nach Apply wird Listener wieder entfernt."""
sa = src_obj.Attributes
# Custom-User-Strings sammeln (DOSSIER-Element-Typen + andere). Skip
# die Fill-Tracking-Keys weil wir den Hatch neu erstellen mit neuer ID.
skip_keys = {
"ebenen_fill_hatch_id", # zeigt auf alte Source-Hatch-ID
"ebenen_fill_owner",
}
user_strings = {}
try:
for k in sa.GetUserStringKeys():
if k in skip_keys: continue
v = sa.GetUserString(k)
if v is not None: user_strings[k] = v
except Exception as ex:
print("[PIPETTE] user-strings:", ex)
# Source-Geometrie Closed-State erfassen — wenn Source closed war,
# erzwingen wir nach dem Add auch auf der Kopie ein Close (Polyline
# bleibt sonst standardmaessig offen, hatten User-Feedback dazu).
src_closed = False
try:
import Rhino.Geometry as _rg
sg = src_obj.Geometry
if isinstance(sg, _rg.Curve) and sg.IsClosed:
src_closed = True
except Exception: pass
pending = {
"linetype_source": int(sa.LinetypeSource),
"linetype_idx": int(sa.LinetypeIndex),
"plot_weight_source": int(sa.PlotWeightSource),
"plot_weight": float(sa.PlotWeight),
"user_strings": user_strings,
"hatch_props": _capture_source_hatch_props(doc, src_obj),
"src_closed": src_closed,
}
sc.sticky["dossier_pipette_pending"] = pending
# One-shot handler — applied beim naechsten AddRhinoObject + entfernt sich
def _on_add(sender, e):
try:
obj = e.TheObject
if obj is None or obj.IsDeleted: return
import Rhino.Geometry as rg2
if not isinstance(obj.Geometry, rg2.Curve): return
_apply_pending(doc, obj, pending)
except Exception as ex:
print("[PIPETTE] one-shot apply:", ex)
finally:
try: Rhino.RhinoDoc.AddRhinoObject -= _on_add
except Exception: pass
sc.sticky.pop("dossier_pipette_pending", None)
try:
Rhino.RhinoDoc.AddRhinoObject += _on_add
except Exception as ex:
print("[PIPETTE] listener-install:", ex)
def _force_close_curve(crv):
"""Schliesst eine offene Polyline durch Anhaengen des Startpunkts.
Generische Curves: MakeClosed (nur wenn Endpunkte nahe) oder Join mit
Lueckensegment. Returns geschlossene Curve oder None bei Fehler."""
import Rhino.Geometry as rg2
if crv is None or crv.IsClosed: return None
try:
if isinstance(crv, rg2.PolylineCurve):
ok, pl = crv.TryGetPolyline()
if ok and pl is not None and pl.Count >= 2:
if pl[0].DistanceTo(pl[pl.Count - 1]) > 1e-9:
pl.Add(pl[0])
return rg2.PolylineCurve(pl)
return None
# Generic: erst MakeClosed (closed wenn Endpunkte innerhalb tol)
try:
if crv.MakeClosed(1e-6): return crv
except Exception: pass
# Fallback: Lueckensegment einfuegen + joinen
line = rg2.LineCurve(crv.PointAtEnd, crv.PointAtStart)
joined = rg2.Curve.JoinCurves([crv, line], 1e-6)
if joined and len(joined) > 0 and joined[0].IsClosed:
return joined[0]
except Exception as ex:
print("[PIPETTE] force-close:", ex)
return None
def _apply_pending(doc, new_obj, pending):
"""Wendet pending state auf das neu erzeugte Objekt an."""
import Rhino.Geometry as rg2
import System
# Close-Erzwingen wenn Source geschlossen war — Polyline-Command erzeugt
# standardmaessig offene Curves; Pipette soll den Closed-State erhalten.
if pending.get("src_closed"):
try:
crv = new_obj.Geometry
if isinstance(crv, rg2.Curve) and not crv.IsClosed:
closed = _force_close_curve(crv)
if closed is not None:
if doc.Objects.Replace(new_obj.Id, closed):
ref = doc.Objects.FindId(new_obj.Id)
if ref is not None: new_obj = ref
print("[PIPETTE] Polyline auto-geschlossen (Source war closed)")
except Exception as ex:
print("[PIPETTE] close-replace:", ex)
# Linetype + PlotWeight overrides
try:
na = new_obj.Attributes.Duplicate()
if pending["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
na.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
na.LinetypeIndex = pending["linetype_idx"]
if pending["plot_weight_source"] == int(rdoc.ObjectPlotWeightSource.PlotWeightFromObject):
na.PlotWeightSource = rdoc.ObjectPlotWeightSource.PlotWeightFromObject
na.PlotWeight = pending["plot_weight"]
# UserStrings 1:1 kopieren
for k, v in pending["user_strings"].items():
try: na.SetUserString(k, v)
except Exception: pass
doc.Objects.ModifyAttributes(new_obj, na, True)
except Exception as ex:
print("[PIPETTE] apply-attrs:", ex)
# Per-Object Custom-Hatch: nachbauen wenn Source einen hatte UND
# der neue Curve closed ist
hp = pending.get("hatch_props")
if hp is None: return
try:
crv = new_obj.Geometry
if not isinstance(crv, rg2.Curve) or not crv.IsClosed: return
tol = doc.ModelAbsoluteTolerance
hatches = rg2.Hatch.Create(crv, hp["pattern_idx"],
hp["rotation"], hp["scale"], tol)
if not hatches or len(hatches) == 0: return
ha = rdoc.ObjectAttributes()
ha.LayerIndex = hp["layer_idx"]
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
try:
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
except Exception: pass
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
ha.LinetypeIndex = hp["linetype_idx"]
ha.SetUserString("ebenen_fill_source", "object")
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
new_hid = doc.Objects.AddHatch(hatches[0], ha)
if new_hid and new_hid != System.Guid.Empty:
# Cross-Link: Curve speichert Hatch-ID
ca = new_obj.Attributes.Duplicate()
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
ca.SetUserString("ebenen_fill_source", "object")
doc.Objects.ModifyAttributes(new_obj, ca, True)
print("[PIPETTE] Per-Object Hatch uebernommen (Pattern={}, Scale={})"
.format(hp["pattern_idx"], hp["scale"]))
except Exception as ex:
print("[PIPETTE] hatch-replicate:", ex)
def _auto_chain(doc, src):
"""Startet das passende Draw-Command basierend auf Source-Typ."""
sa = src.Attributes
dtype = sa.GetUserString("dossier_element_type") or ""
geom = src.Geometry
geom_type = type(geom).__name__
# DOSSIER-BIM: triggere den Dispatcher
_DOSSIER_DRAW = {
"wand_axis": "wand",
"treppe_axis": "treppe",
"decke_outline": "decke",
"dach_outline": "dach",
"stuetze_point": "stuetze",
"traeger_axis": "traeger",
"oeffnung_point": None, # braucht parent-Wand-Kontext → skip auto-chain
"raum_outline": "raum",
}
if dtype in _DOSSIER_DRAW:
action = _DOSSIER_DRAW[dtype]
if action:
import os
_here = os.path.dirname(os.path.abspath(__file__))
wrapper = os.path.join(_here, action + ".py")
if os.path.exists(wrapper):
Rhino.RhinoApp.RunScript(
'_-RunPythonScript "{}"'.format(wrapper), False)
print("[PIPETTE] → starte DOSSIER {}".format(action))
return
# Standard-Rhino-Curves: detect Typ → entsprechendes Draw-Cmd
cmd = None
if geom_type == "LineCurve":
cmd = "_Line"
elif geom_type == "ArcCurve":
# ArcCurve mit voller Sweep = Kreis
try:
if geom.IsClosed: cmd = "_Circle"
else: cmd = "_Arc"
except Exception:
cmd = "_Arc"
elif geom_type == "PolylineCurve":
try:
ok, pl = geom.TryGetPolyline()
if ok and pl is not None and pl.IsClosed and pl.Count == 5:
# Geschlossen + 4 Segmente → vermutlich Rectangle
cmd = "_Rectangle"
else:
cmd = "_Polyline"
except Exception:
cmd = "_Polyline"
elif geom_type == "NurbsCurve":
cmd = "_Curve"
elif geom_type == "TextEntity":
cmd = "_Text"
if cmd:
Rhino.RhinoApp.RunScript(cmd, False)
print("[PIPETTE] → starte {}".format(cmd))
_run()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'raum'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("raum")
+57
View File
@@ -0,0 +1,57 @@
#! python3
# -*- coding: utf-8 -*-
# Wrapper fuer dSection: interaktiver Schnitt-Pick (2 Punkte + Blickrichtung).
# Defaults kommen aus Project-Settings.defaults; nach erfolgreicher
# Erstellung wird der neue Schnitt als aktive Zeichnungs-Ebene gesetzt.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
import Rhino
import scriptcontext as sc
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None:
print("[SECTION] kein aktives Dokument")
else:
try:
import schnitte
# Defaults aus Project-Settings; Fallback auf hartkodierte Werte.
defaults = {
"depthBack": 8.0, "heightMin": -1.0, "heightMax": 12.0,
"cutAtLine": True, "namePrefix": "S",
}
try:
import rhinopanel
ps = rhinopanel.load_project_settings(doc)
d = (ps or {}).get("defaults", {})
defaults["depthBack"] = float(d.get("schnittDepthBack", 8.0))
defaults["heightMin"] = float(d.get("schnittHeightMin", -1.0))
defaults["heightMax"] = float(d.get("schnittHeightMax", 12.0))
except Exception as ex:
print("[SECTION] defaults from project-settings:", ex)
sid = schnitte.pick_schnitt_interactive(doc, defaults=defaults)
if not sid:
print("[SECTION] abgebrochen")
else:
# Broadcast neue Zeichnungs-Ebene an Panels + auto-aktivieren
try:
eb = sc.sticky.get("ebenen_bridge")
if eb is not None:
eb._send_state()
except Exception as ex:
print("[SECTION] broadcast:", ex)
try:
import json
zraw = doc.Strings.GetValue("dossier_zeichnungsebenen") or "[]"
z_list = json.loads(zraw)
new_z = next((x for x in z_list
if isinstance(x, dict) and x.get("id") == sid), None)
if new_z is not None:
eb = sc.sticky.get("ebenen_bridge")
if eb is not None:
eb._set_active_zeichnungsebene(new_z)
print("[SECTION] erstellt: {}".format(sid))
except Exception as ex:
print("[SECTION] auto-activate:", ex)
except Exception as ex:
print("[SECTION] error:", ex)
+157
View File
@@ -0,0 +1,157 @@
#! python3
# -*- coding: utf-8 -*-
# Smart-Join: bei geschlossenen Curves → BooleanUnion (innere Linien weg),
# bei offenen Curves → normales _Join (Endpunkt-Verbindung).
# Sicherheits-Filter:
# A) Group by Layer + Object-Overrides (Color/Linetype/PlotWeight) + Fill —
# nur Curves mit IDENTISCHEN visuellen Attributen werden gemerged.
# C) Pre-Check Overlap — BooleanUnion liefert genauso viele Outputs wie
# Inputs wenn nichts overlapt → dann KEINE Aktion, Curves bleiben.
# Kombinierter Effekt: nur visuell zusammengehoerige UND tatsaechlich
# ueberlappende Curves werden zu einer Outline vereint.
import scriptcontext as sc
import Rhino
import Rhino.Geometry as rg
import Rhino.DocObjects as rdoc
def _attr_key(obj):
"""Tuple das definiert ob 2 Curves visuell identisch sind. Layer +
Per-Object-Overrides (alles was ByObject nicht ByLayer ist) + Fill-
State (Hatch-ID + No-Fill-Flag)."""
a = obj.Attributes
layer_idx = a.LayerIndex
# Color: nur Object-Override unterscheidend, ByLayer ist gleich.
col_key = ("layer",)
try:
if a.ColorSource == rdoc.ObjectColorSource.ColorFromObject:
col_key = ("obj", a.ObjectColor.ToArgb())
except Exception: pass
# Linetype
lt_key = ("layer",)
try:
if a.LinetypeSource == rdoc.ObjectLinetypeSource.LinetypeFromObject:
lt_key = ("obj", a.LinetypeIndex)
except Exception: pass
# PlotWeight
pw_key = ("layer",)
try:
if a.PlotWeightSource == rdoc.ObjectPlotWeightSource.PlotWeightFromObject:
pw_key = ("obj", float(a.PlotWeight))
except Exception: pass
# Fill / Hatch via gestaltung-UserStrings
fill_hatch = ""
fill_source = ""
no_fill = ""
try:
fill_hatch = a.GetUserString("ebenen_fill_hatch_id") or ""
fill_source = a.GetUserString("ebenen_fill_source") or ""
no_fill = a.GetUserString("ebenen_no_fill") or ""
except Exception: pass
# Fuer Gruppierung zaehlt: "hatte Fill ja/nein" + Quelle + No-Fill-Flag.
fill_key = (bool(fill_hatch), fill_source, no_fill)
return (layer_idx, col_key, lt_key, pw_key, fill_key)
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
sel = list(doc.Objects.GetSelectedObjects(False, False))
if not sel:
Rhino.RhinoApp.RunScript("_Join", False); return
# Curves nach Closed/Open trennen
closed_objs = []
has_non_closed = False
for obj in sel:
g = obj.Geometry
if isinstance(g, rg.Curve) and g.IsClosed:
closed_objs.append(obj)
else:
has_non_closed = True
# Wenn nicht ALLE closed sind → einfach Standard-Join
if has_non_closed or len(closed_objs) < 2:
Rhino.RhinoApp.RunScript("_Join", False); return
# Gruppieren nach (Layer + Attrs + Fill)
groups = {} # key → [obj, obj, ...]
for obj in closed_objs:
try:
k = _attr_key(obj)
except Exception:
k = ("ungroup", id(obj))
groups.setdefault(k, []).append(obj)
# gestaltung fuer Fill-Re-Apply
_g = None
try:
import gestaltung as _gmod; _g = _gmod
except Exception as iex:
print("[SMART-JOIN] gestaltung import:", iex)
tol = doc.ModelAbsoluteTolerance
ur = doc.BeginUndoRecord("DOSSIER Smart-Join (gruppiert)")
n_merged_total = 0
n_groups_ops = 0
try:
for key, objs in groups.items():
if len(objs) < 2: continue # einzelne Curve → nichts zu mergen
try:
curves = [o.Geometry for o in objs]
result = rg.Curve.CreateBooleanUnion(curves, tol)
except Exception as ex:
print("[SMART-JOIN] BooleanUnion in Gruppe fehlgeschlagen:", ex)
continue
if not result: continue
# C) Pre-Check Overlap: wenn result-Anzahl gleich input-Anzahl
# ist, gab's keinen tatsaechlichen Overlap → Gruppe nicht
# anfassen.
if len(result) >= len(objs):
continue
# Tatsaechlich gemerged → replace
attrs_template = objs[0].Attributes.Duplicate()
# Fill-Key clearen damit _apply_ebene_fill nicht "schon gefuellt"
# zurueckgibt
try:
attrs_template.SetUserString("ebenen_fill_hatch_id", "")
except Exception: pass
any_had_fill = bool(key[4][0]) # fill_key[0] = had-fill bool
new_ids = []
for crv in result:
nid = doc.Objects.AddCurve(crv, attrs_template)
if nid: new_ids.append(nid)
for o in objs:
try: doc.Objects.Delete(o.Id, True)
except Exception: pass
# Fill nachziehen wenn Inputs welche hatten
if any_had_fill and _g is not None:
for nid in new_ids:
try:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_g._apply_ebene_fill(doc, nobj)
except Exception as fex:
print("[SMART-JOIN] fill-apply:", fex)
n_merged_total += (len(objs) - len(result))
n_groups_ops += 1
finally:
doc.EndUndoRecord(ur)
if n_groups_ops == 0:
print("[SMART-JOIN] Nichts zu mergen — keine Curves overlappen "
"(oder verschiedene Attribute/Layer)")
else:
doc.Views.Redraw()
print("[SMART-JOIN] {} Gruppe(n) bearbeitet, {} Curve(s) zu Union vereint"
.format(n_groups_ops, n_merged_total))
_run()
+267
View File
@@ -0,0 +1,267 @@
#! python3
# -*- coding: utf-8 -*-
# Smart-Split: User zeichnet eine Splitlinie/Polylinie waehrend des Befehls
# (mehrere Klicks, Enter beendet die Eingabe). Alle Curves die die Linie
# schneidet werden gesplittet.
# - Offene Curves: bei den Schnittpunkten in offene Segmente.
# - GESCHLOSSENE Curves: in mehrere CLOSED Sub-Regionen via
# Curve.CreateBooleanRegions (funktioniert auch bei multi-segment
# Polylinien-Cuttern). Per-Object-Hatch wird auf alle Regionen repliziert.
# DOSSIER-Source-Typen (Wand-Achse etc.) bleiben geschuetzt.
import scriptcontext as sc
import Rhino
import Rhino.Input.Custom as ric
import Rhino.Geometry as rg
import Rhino.DocObjects as rdoc
from Rhino.Input import GetResult
# Was Smart-Split NIE anfasst:
# - oeffnung_point / stuetze_point: Punkte, nicht teilbar
# - schnitt_axis: Schnitt-Linien sollen bleiben, sonst kaputte Schnitte
# - treppe_axis: Treppen-State (Lauflinie, Schrittmass-Lock, Wendel-Sweep)
# waere bei einem Split inkonsistent
# Alles andere (wand/traeger/decke/dach/raum/aussparung) DARF gesplittet werden:
# der Add-Listener in elemente.py erkennt die Duplikat-IDs der neuen Stuecke
# und vergibt jedem Stueck ein frisches Element-ID + Regen → BIM-Volumen
# baut sich pro neuem Stueck neu auf.
_PROTECTED_TYPES = {
"treppe_axis",
"oeffnung_point", "stuetze_point", "schnitt_axis",
}
def _capture_hatch_props(doc, src_obj):
try:
sa = src_obj.Attributes
fill_hid = sa.GetUserString("ebenen_fill_hatch_id") or ""
if not fill_hid: return None
import System
hid = System.Guid(fill_hid)
hobj = doc.Objects.FindId(hid)
if hobj is None or hobj.IsDeleted: return None
hg = hobj.Geometry
ha = hobj.Attributes
if not hasattr(hg, "PatternIndex"): return None
return {
"pattern_idx": int(hg.PatternIndex),
"scale": float(hg.PatternScale),
"rotation": float(hg.PatternRotation),
"layer_idx": int(ha.LayerIndex),
"color_source": int(ha.ColorSource),
"color_argb": int(ha.ObjectColor.ToArgb()),
"plot_color_source": int(ha.PlotColorSource),
"plot_color_argb": int(ha.PlotColor.ToArgb()),
"linetype_source": int(ha.LinetypeSource),
"linetype_idx": int(ha.LinetypeIndex),
"fill_source": sa.GetUserString("ebenen_fill_source") or "object",
}
except Exception as ex:
print("[SMART-SPLIT] capture-hatch:", ex)
return None
def _replicate_hatch(doc, new_obj, hp):
if hp is None: return
import System
try:
crv = new_obj.Geometry
if not isinstance(crv, rg.Curve) or not crv.IsClosed: return
tol = doc.ModelAbsoluteTolerance
hatches = rg.Hatch.Create(crv, hp["pattern_idx"], hp["rotation"],
hp["scale"], tol)
if not hatches or len(hatches) == 0: return
ha = rdoc.ObjectAttributes()
ha.LayerIndex = hp["layer_idx"]
ha.ColorSource = rdoc.ObjectColorSource(hp["color_source"])
ha.ObjectColor = System.Drawing.Color.FromArgb(hp["color_argb"])
try:
ha.PlotColorSource = rdoc.ObjectPlotColorSource(hp["plot_color_source"])
ha.PlotColor = System.Drawing.Color.FromArgb(hp["plot_color_argb"])
except Exception: pass
if hp["linetype_source"] == int(rdoc.ObjectLinetypeSource.LinetypeFromObject):
ha.LinetypeSource = rdoc.ObjectLinetypeSource.LinetypeFromObject
ha.LinetypeIndex = hp["linetype_idx"]
ha.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
ha.SetUserString("ebenen_fill_owner", str(new_obj.Id))
new_hid = doc.Objects.AddHatch(hatches[0], ha)
if new_hid and new_hid != System.Guid.Empty:
ca = new_obj.Attributes.Duplicate()
ca.SetUserString("ebenen_fill_hatch_id", str(new_hid))
ca.SetUserString("ebenen_fill_source", hp.get("fill_source", "object"))
doc.Objects.ModifyAttributes(new_obj, ca, True)
except Exception as ex:
print("[SMART-SPLIT] hatch-replicate:", ex)
def _collect_polyline_cutter(prompt_first, prompt_more):
"""Sammelt n Punkte fuer den Cutter. Enter beendet (min. 2 Punkte).
ESC bricht ab. Returnt Polyline oder None."""
pts = []
while True:
gp = ric.GetPoint()
if not pts:
gp.SetCommandPrompt(prompt_first)
else:
gp.SetCommandPrompt(prompt_more + " (Enter zum Splitten, ESC = abbrechen)")
gp.SetBasePoint(pts[-1], True)
gp.DrawLineFromPoint(pts[-1], True)
gp.AcceptNothing(True)
res = gp.Get()
if res == GetResult.Nothing:
# Enter gedrueckt
if len(pts) >= 2: return rg.Polyline(pts)
print("[SMART-SPLIT] Mindestens 2 Punkte noetig"); return None
if res != GetResult.Point: return None
pts.append(gp.Point())
def _split_closed_with_cutter(closed_crv, cutter_crv, doc):
"""Splittet closed curve mit beliebigem cutter (Linie oder Polylinie) in
closed Sub-Regionen via Curve.CreateBooleanRegions."""
tol = doc.ModelAbsoluteTolerance
try:
# WorldXY-Plane als Default (DOSSIER ist 2D Plan-Workflow)
plane = rg.Plane.WorldXY
regions = rg.Curve.CreateBooleanRegions(
[closed_crv, cutter_crv], plane, False, tol)
if regions is None or regions.RegionCount == 0:
return None
out = []
for i in range(regions.RegionCount):
rcurves = list(regions.RegionCurves(i))
if not rcurves: continue
if len(rcurves) == 1:
if rcurves[0].IsClosed:
out.append(rcurves[0])
else:
# einzelne offene curve — sollte nicht passieren bei
# Boolean-Regions, aber defensiv
joined = rg.Curve.JoinCurves([rcurves[0]], tol)
if joined and len(joined) > 0 and joined[0].IsClosed:
out.append(joined[0])
else:
joined = rg.Curve.JoinCurves(rcurves, tol)
if joined:
for j in joined:
if j.IsClosed: out.append(j)
return out if out else None
except Exception as ex:
print("[SMART-SPLIT] closed-split:", ex)
return None
def _run():
doc = Rhino.RhinoDoc.ActiveDoc
if doc is None: return
# Polylinie als Cutter sammeln
poly = _collect_polyline_cutter(
"Splitlinie Startpunkt",
"Naechster Punkt")
if poly is None or poly.Count < 2:
return
cutter = rg.PolylineCurve(poly)
tol = doc.ModelAbsoluteTolerance
pre_sel = [o for o in doc.Objects.GetSelectedObjects(False, False)
if o is not None and not o.IsDeleted]
if pre_sel:
source = pre_sel
mode_label = "selektierte ({})".format(len(pre_sel))
else:
s = rdoc.ObjectEnumeratorSettings()
s.HiddenObjects = False; s.LockedObjects = False
source = list(doc.Objects.GetObjectList(s))
mode_label = "alle sichtbaren"
candidates_open = []
candidates_closed = []
for obj in source:
if obj is None or obj.IsDeleted: continue
try:
t = obj.Attributes.GetUserString("dossier_element_type") or ""
if t in _PROTECTED_TYPES: continue
except Exception: pass
g = obj.Geometry
if not isinstance(g, rg.Curve): continue
try:
ints = rg.Intersect.Intersection.CurveCurve(cutter, g, tol, tol)
except Exception:
continue
if not ints or ints.Count == 0: continue
if g.IsClosed:
candidates_closed.append((obj, g))
else:
params = []
for i in range(ints.Count):
ev = ints[i]
if ev.IsPoint:
params.append(ev.ParameterB)
else:
params.append(ev.ParameterB); params.append(ev.ParameterB2)
if params:
params = sorted(set(round(p, 6) for p in params))
candidates_open.append((obj, g, params))
if not candidates_open and not candidates_closed:
print("[SMART-SPLIT] Cutter schneidet nichts ({})".format(mode_label))
return
ur = doc.BeginUndoRecord("DOSSIER Smart-Split")
n_open = 0; n_closed = 0
try:
# Closed: Boolean-Regions → CLOSED Sub-Regionen + Fill replicate
for obj, crv in candidates_closed:
try:
regions = _split_closed_with_cutter(crv, cutter, doc)
if not regions or len(regions) <= 1: continue
hatch_props = _capture_hatch_props(doc, obj)
attrs = obj.Attributes.Duplicate()
try: attrs.SetUserString("ebenen_fill_hatch_id", "")
except Exception: pass
new_ids = []
for r in regions:
nid = doc.Objects.AddCurve(r, attrs)
if nid: new_ids.append(nid)
doc.Objects.Delete(obj.Id, True)
if hatch_props is not None:
for nid in new_ids:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_replicate_hatch(doc, nobj, hatch_props)
else:
try:
import gestaltung as _gmod
for nid in new_ids:
nobj = doc.Objects.FindId(nid)
if nobj is not None:
_gmod._apply_ebene_fill(doc, nobj)
except Exception: pass
n_closed += 1
except Exception as ex:
print("[SMART-SPLIT] closed-fail:", ex)
# Open: split bei Params
for obj, crv, params in candidates_open:
try:
pieces = crv.Split(params)
if not pieces or len(pieces) <= 1: continue
attrs = obj.Attributes.Duplicate()
for p in pieces:
doc.Objects.AddCurve(p, attrs)
doc.Objects.Delete(obj.Id, True)
n_open += 1
except Exception as ex:
print("[SMART-SPLIT] open-fail:", ex)
finally:
doc.EndUndoRecord(ur)
doc.Views.Redraw()
print("[SMART-SPLIT] {} closed-Regionen + {} offene Curves gesplittet "
"({} Cutter-Punkte, {})"
.format(n_closed, n_open, poly.Count, mode_label))
_run()
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'stempel'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("stempel")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'stuetze'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("stuetze")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'symbol'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("symbol")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'traeger'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("traeger")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'treppe'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("treppe")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'tuer'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("tuer")
+7
View File
@@ -0,0 +1,7 @@
#! python3
# -*- coding: utf-8 -*-
# Auto-Wrapper fuer Alias 'wand'. Importiert dossier_dispatch + ruft Action.
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import dossier_dispatch
dossier_dispatch.dispatch("wand")