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