Files
DOSSIER/rhino/aliases/cmd/smart_split.py
T
karim 18d6d98e07 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
2026-05-30 12:46:53 +02:00

268 lines
10 KiB
Python

#! 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()