3609236da9
Bisher konnte dJoin nur L-Verbindungen herstellen (zwei Endpunkte zum Schnittpunkt der verlaengerten Tangenten ziehen). Neu auch T-Verbindungen: _t_join_attempt: pro Endpunkt-Kombination wird der naechste Punkt auf der ANDEREN Curve gesucht. Wenn distance < 20cm UND nicht nahe deren Endpunkt (= waere L-Sache) → snap diesen Endpunkt exakt auf die Curve. Die andere Curve bleibt unveraendert (= Through-Wand stays). _run: T-Join wird ZUERST probiert (spezifischer), L-Join als Fallback. UX: User selektiert 2 Waende die fast aber nicht ganz verbinden → Cmd+J (dJoin) → System erkennt T- oder L-Konfig und snappt entsprechend. Predictable + intentional, kein auto-snap-Magic mehr.
351 lines
13 KiB
Python
351 lines
13 KiB
Python
#! 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 _replace_curve_endpoint(curve, which_end, new_pt):
|
|
"""Ersetze Start- (which_end=0) oder End-Punkt (which_end=1). Liefert
|
|
eine neue Curve oder None bei nicht-unterstuetztem Typ."""
|
|
if isinstance(curve, rg.LineCurve):
|
|
if which_end == 0:
|
|
return rg.LineCurve(new_pt, curve.PointAtEnd)
|
|
return rg.LineCurve(curve.PointAtStart, new_pt)
|
|
if isinstance(curve, rg.PolylineCurve):
|
|
n = curve.PointCount
|
|
pts = [curve.Point(i) for i in range(n)]
|
|
if which_end == 0: pts[0] = new_pt
|
|
else: pts[-1] = new_pt
|
|
return rg.PolylineCurve(pts)
|
|
# Fallback: generische Curve via Extend
|
|
cu = curve.DuplicateCurve()
|
|
if cu is None: return None
|
|
end_enum = rg.CurveEnd.Start if which_end == 0 else rg.CurveEnd.End
|
|
try:
|
|
return cu.Extend(end_enum,
|
|
rg.CurveExtensionStyle.Line,
|
|
[rg.Point3d(new_pt)])
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def _walls_and_curves_from_sel(doc, sel):
|
|
"""Liefert (axes, generic_curves). Axes = dedup Wand-Achsen (per wall_id),
|
|
generic_curves = offene Kurven die KEINE Wand sind. wand_volumes werden
|
|
auf ihre Achse via wall_id resolved (auto-group bringt axis+volume
|
|
automatisch beide in sel)."""
|
|
seen_walls = set()
|
|
axes = []
|
|
generic = []
|
|
# Pre-Index wand_axis by wall_id fuer schnelles Lookup
|
|
axis_by_id = {}
|
|
for o in doc.Objects:
|
|
if o.Attributes.GetUserString("dossier_type") == "wand_axis":
|
|
wid = o.Attributes.GetUserString("dossier_element_id") or ""
|
|
if wid: axis_by_id[wid] = o
|
|
for obj in sel:
|
|
t = obj.Attributes.GetUserString("dossier_type") or ""
|
|
wid = obj.Attributes.GetUserString("dossier_element_id") or ""
|
|
if t == "wand_axis" and wid and wid not in seen_walls:
|
|
axes.append(obj); seen_walls.add(wid)
|
|
elif t == "wand_volume" and wid:
|
|
wall_ids = {wid}
|
|
members_raw = obj.Attributes.GetUserString(
|
|
"dossier_wand_chain_members") or ""
|
|
if members_raw:
|
|
try:
|
|
import json as _j
|
|
for c in _j.loads(members_raw):
|
|
if c: wall_ids.add(c)
|
|
except Exception: pass
|
|
for w in wall_ids:
|
|
if w in seen_walls: continue
|
|
ax = axis_by_id.get(w)
|
|
if ax is not None:
|
|
axes.append(ax); seen_walls.add(w)
|
|
elif t == "":
|
|
g = obj.Geometry
|
|
if isinstance(g, rg.Curve) and not g.IsClosed:
|
|
generic.append(obj)
|
|
return axes, generic
|
|
|
|
|
|
def _t_join_attempt(doc, sel):
|
|
"""T-Join: 2 OFFENE Kurven wobei der EINE Endpunkt der einen Kurve
|
|
nahe (< 20cm) auf der ANDEREN Kurve mitten landet (zwischen deren
|
|
Endpunkten). Schiebt diesen Endpunkt exakt auf die andere Kurve.
|
|
Die andere Kurve bleibt unveraendert.
|
|
|
|
Liefert True wenn ausgefuehrt."""
|
|
axes, generic = _walls_and_curves_from_sel(doc, sel)
|
|
if len(axes) == 2 and len(generic) == 0:
|
|
o1, o2 = axes[0], axes[1]
|
|
elif len(axes) == 0 and len(generic) == 2:
|
|
o1, o2 = generic[0], generic[1]
|
|
else:
|
|
return False
|
|
c1 = o1.Geometry; c2 = o2.Geometry
|
|
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
|
|
return False
|
|
if c1.IsClosed or c2.IsClosed: return False
|
|
tol_snap = 0.20 # 20 cm Snap-Radius fuer T-Verbindung
|
|
end_tol = 0.05 # 5cm: wenn closest-point nahe Endpunkt → eigentlich L
|
|
candidates = []
|
|
# Pro Endpunkt der einen Kurve: ClosestPoint auf der ANDEREN Kurve
|
|
for (a_obj, ac, b_obj, bc) in ((o1, c1, o2, c2), (o2, c2, o1, c1)):
|
|
for end in (0, 1):
|
|
ep = ac.PointAtStart if end == 0 else ac.PointAtEnd
|
|
try:
|
|
rc, t = bc.ClosestPoint(ep)
|
|
if not rc: continue
|
|
cp = bc.PointAt(t)
|
|
d = cp.DistanceTo(ep)
|
|
# Skip wenn schon snapped oder zu weit
|
|
if d < 1e-6 or d > tol_snap: continue
|
|
# Skip wenn cp nahe einem Endpunkt von bc — das ist L-Join Territory
|
|
ps = bc.PointAtStart; pe = bc.PointAtEnd
|
|
if cp.DistanceTo(ps) < end_tol or cp.DistanceTo(pe) < end_tol:
|
|
continue
|
|
candidates.append((d, a_obj, ac, end, cp))
|
|
except Exception: continue
|
|
if not candidates: return False
|
|
# Naechster Endpunkt → der wird gesnappt
|
|
candidates.sort(key=lambda x: x[0])
|
|
_d, a_obj, ac, end, cp = candidates[0]
|
|
new_c = _replace_curve_endpoint(ac, end, cp)
|
|
if new_c is None: return False
|
|
ur = doc.BeginUndoRecord("DOSSIER T-Join")
|
|
try:
|
|
ok = doc.Objects.Replace(a_obj.Id, new_c)
|
|
return bool(ok)
|
|
finally:
|
|
doc.EndUndoRecord(ur)
|
|
|
|
|
|
def _l_join_attempt(doc, sel):
|
|
"""Wenn genau 2 OFFENE Kurven (Wand-Achsen oder generische Lines)
|
|
selektiert sind, deren End-Tangenten sich in einem Punkt schneiden →
|
|
beide Kurven extend/shorten zu diesem Punkt (= L-Form). True wenn
|
|
ausgefuehrt."""
|
|
axes, generic = _walls_and_curves_from_sel(doc, sel)
|
|
# Erlaubte Konfigs: 2 Wand-Achsen ODER 2 generische Kurven (keine mix)
|
|
if len(axes) == 2 and len(generic) == 0:
|
|
o1, o2 = axes[0], axes[1]
|
|
elif len(axes) == 0 and len(generic) == 2:
|
|
o1, o2 = generic[0], generic[1]
|
|
else:
|
|
return False
|
|
c1 = o1.Geometry; c2 = o2.Geometry
|
|
if not (isinstance(c1, rg.Curve) and isinstance(c2, rg.Curve)):
|
|
return False
|
|
if c1.IsClosed or c2.IsClosed: return False
|
|
tol = max(doc.ModelAbsoluteTolerance, 1e-6)
|
|
# Closest endpoint pair (a_end, b_end ∈ {0=start, 1=end})
|
|
pairs = [
|
|
(c1.PointAtStart, c2.PointAtStart, 0, 0),
|
|
(c1.PointAtStart, c2.PointAtEnd, 0, 1),
|
|
(c1.PointAtEnd, c2.PointAtStart, 1, 0),
|
|
(c1.PointAtEnd, c2.PointAtEnd, 1, 1),
|
|
]
|
|
pairs.sort(key=lambda p: p[0].DistanceTo(p[1]))
|
|
p1, p2, e1, e2 = pairs[0]
|
|
if p1.DistanceTo(p2) < tol:
|
|
return False # bereits verbunden
|
|
def _out_dir(c, end):
|
|
return -c.TangentAtStart if end == 0 else c.TangentAtEnd
|
|
d1 = _out_dir(c1, e1)
|
|
d2 = _out_dir(c2, e2)
|
|
# Parallel-Check (Cross-Produkt-Laenge in XY)
|
|
cross_z = d1.X * d2.Y - d1.Y * d2.X
|
|
if abs(cross_z) < 1e-9: return False # parallel
|
|
# Unendliche Linien-Intersection
|
|
line1 = rg.Line(p1, p1 + d1)
|
|
line2 = rg.Line(p2, p2 + d2)
|
|
rc, t_a, t_b = rg.Intersect.Intersection.LineLine(line1, line2, tol, False)
|
|
if not rc: return False
|
|
ipt = line1.PointAt(t_a)
|
|
if line2.PointAt(t_b).DistanceTo(ipt) > 0.01:
|
|
return False # Schiefe Linien in 3D
|
|
nc1 = _replace_curve_endpoint(c1, e1, ipt)
|
|
nc2 = _replace_curve_endpoint(c2, e2, ipt)
|
|
if nc1 is None or nc2 is None: return False
|
|
ur = doc.BeginUndoRecord("DOSSIER L-Join")
|
|
try:
|
|
ok1 = doc.Objects.Replace(o1.Id, nc1)
|
|
ok2 = doc.Objects.Replace(o2.Id, nc2)
|
|
return bool(ok1 and ok2)
|
|
finally:
|
|
doc.EndUndoRecord(ur)
|
|
|
|
|
|
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
|
|
|
|
# T-Join: Endpunkt der einen Curve trifft mitten auf die andere → snap.
|
|
# L-Join: beide Endpunkte werden zum Schnittpunkt der verlaengerten Linien
|
|
# gezogen. T zuerst probieren (= spezifischer), dann L als Fallback.
|
|
if len(sel) >= 2:
|
|
try:
|
|
if _t_join_attempt(doc, sel):
|
|
doc.Views.Redraw()
|
|
print("[SMART-JOIN] T-Join: Endpunkt auf Achse gesnappt")
|
|
return
|
|
except Exception as ex:
|
|
print("[SMART-JOIN] T-Join error:", ex)
|
|
try:
|
|
if _l_join_attempt(doc, sel):
|
|
doc.Views.Redraw()
|
|
print("[SMART-JOIN] L-Join: 2 Curves zu L verbunden")
|
|
return
|
|
except Exception as ex:
|
|
print("[SMART-JOIN] L-Join error:", ex)
|
|
|
|
# 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()
|