It would be extremely helpful if the Grasshopper Join Curves component had an “i” (Index) output alongside its “C” (Curves) output. This new output would provide the input component indices for each joined curve, structured in a data tree synchronized with the joined curves on output “C”.
Is there any chance of seeing this feature in an upcoming Grasshopper release? It would be invaluable for data-driven definitions where attributes from the original input curves need to be inherited by the resulting joined curves.
Currently, using proximity-based methods like Closest Point becomes extremely slow when dealing with large datasets (e.g., 100,000 curves).
In the meantime, could anyone share a fast workaround or script (C#/Python) that outperforms the Closest Point approach?
The situation is that my Grasshopper definition processes a directory containing up to several hundred 2D DXF files (in this specific case, 97 files), which need to be converted according to our internal rules—for example, joining line segments for converting to circular holes, joining contour segments into continuous outlines, etc.
To attach the origin attributes (such as the source file/drawing name and layer name) to the newly joined curves, I rely on the data provided by the Rhino 8 import component at the beginning of the definition. Since these attributes are originally assigned to the individual input curve segments, I need to know precisely which source segments were used to create each joined curve so that their drawing name and layer name can be inherited.
Unfortunately, I cannot share the directory of drawing files due to confidentiality, these are on the protected company server, but I have attached the specific section of the definition that handles assigning the drawing number and source layer to the joined curves for each file.
I once faced the same problem and after some reasoning with Claude I found the RTree approach to increase speed. here is the code for GHpython in Rhino 7
# ============================================================
# RTree Closest Point - Grasshopper GhPython Script Component
# Rhino 7 / IronPython 2.7
#
# INPUT:
# P | Point3d | List Access -> punti da interrogare (N)
# C | Point3d | List Access -> nuvola di ricerca (M)
#
# OUTPUT (rinomina i parametri del componente):
# cp -> punto piu vicino
# ci -> indice nel Cloud
# cd -> distanza
#
# NOTA: IronPython e' lento nei loop stretti. Su dataset molto grandi
# la versione C# e' tipicamente 5-20x piu' rapida a parita' di algoritmo.
# ============================================================
import Rhino
from Rhino.Geometry import RTree, Sphere, BoundingBox
cp = []
ci = []
cd = []
# --- Guardie sugli input
if P and C:
# --- 1. Costruzione dell'indice spaziale (una volta sola)
tree = RTree.CreateFromPointArray(C)
# --- 2. Raggio iniziale stimato dalla densita' del cloud
bb = BoundingBox(C)
diag = bb.Diagonal.Length
if diag > 0:
r0 = diag / (float(len(C)) ** (1.0 / 3.0))
else:
r0 = 1.0
if r0 <= 0:
r0 = 1.0
rMax = diag * 1.01 if diag > 0 else 1e12
# --- 3. Stato condiviso col callback.
# In Python 2 non esiste 'nonlocal': uso una lista mutabile
# [indice_migliore, distanza_migliore] che la closure puo' modificare.
state = [-1, float("inf")]
needle = None
def callback(sender, e):
# e.Id e' l'indice del punto dentro la lista C
d = C[e.Id].DistanceTo(needle)
if d < state[1]:
state[1] = d
state[0] = e.Id
# --- 4. Loop sulle query
for pt in P:
needle = pt
state[0] = -1
state[1] = float("inf")
# Ricerca a raggio crescente: il primo raggio che restituisce
# almeno un hit contiene garantitamente il closest point globale.
r = r0
while state[0] < 0 and r <= rMax:
tree.Search(Sphere(needle, r), callback)
r *= 2.0
# Fallback brute force difensivo (cloud degenere)
if state[0] < 0:
for j, q in enumerate(C):
d = q.DistanceTo(needle)
if d < state[1]:
state[1] = d
state[0] = j
cp.append(C[state[0]])
ci.append(state[0])
cd.append(state[1])
I imagine it can be improved (with python 3 in rhino 8+ or with C#), but that’s a starting point. Try it and maybe explore further with some AI assistance (or without it if you are not a monkey like me)
RTree in 2000 characters
The problem. You’re in Times Square and want the nearest of 5 million coffee shops worldwide. Measuring all 5 million is absurd.
The structure. Shops are pre-packed into nested boxes: continents → countries → cities→ blocks → shops. That’s the RTree.
The record. The distance to the best shop found so far. Starts at infinity, only ever moves down.
The rule. Asking “how far is this box’s edge?” is one cheap calculation. Asking “what’s inside?” means opening it and measuring everything. So you ask the cheap one:
Box edge farther than my record → discard the whole box unopened.
One comparison kills thousands of points. The smaller the record, the more boxes die.
The walkthrough. You land in Westchester, find a shop → record 40 km. That instantly kills Asia (10,000 km), Europe (5,500), California (4,100), Boston (300), Philadelphia (130) — the planet, gone in five comparisons. But Brooklyn (8 km) and New Jersey (8 km) survive; 40 km is too blunt for local work.
Open New Jersey, find a shop at 12 km → record 12 km. Long Island (20 km) and the Bronx (15 km) die — killed without measuring anything inside them, purely because the record improved elsewhere.
Open Brooklyn, shop at 9 km → record 9 km. Queens (10 km) dies.
Open Midtown → shop at 200 m. Everything remaining dies. Done: 4 boxes opened, a few dozen shops measured.
The two lessons. The knife sharpens as it cuts — each shop found is both a candidate and the tool that makes the rest cheaper. And correctness is never at risk: the record only descends, so a bad start costs time, not accuracy.
In your code. The search sphere is the starting record, handed over up front instead of found by luck. diag / M^(1/3) is the average point spacing — a guess at “how far is my nearest neighbour?”, i.e. a pre-sharpened knife. Too small costs one empty search and a doubling; too large dumps half the cloud into your callback. Hence: start tight, grow.
I don’t know if you can do this purely by proximity. Does the definition hold when multiple curves meet at the same point? When the joining is preserve false/true? But really, Join Curves should produce a map so that we are not looking for workarounds.
Since I was at it, I thought about it a bit more (this time with some AI assistance for the code part) and ended up with a rough solution, so I’ll have something ready next time this comes up. It’s approximate: curves that are different but happen to share the same midpoint could cause issues, though that’s an edge case.
The definition is meant to work with any input data tree structure (I noticed some confusion around flatten/simplify in a few of the proposed solutions). I moved to C# since it’s notoriously faster; you can see the computation cost compared to Join Curves in the screenshot.
Grab the definition inside the main group and that’s your new Join Curves component, with the indices of the input curves in the output as well. You could wrap it into a cluster or do whatever you prefer. Alternatively, the whole thing could be rethought inside a single C# node (If it does speed up even more), but for now this is good enough for me: the bottleneck was the slow Closest Point, and using an RTree solves it.
Unfortunately, the definition you posted doesn’t work as expected. Although the data structure of the curve indices matches that of the joined curves, the issue is that when I select the input curves carrying the user texts using these indices, they aren’t the actual curves that make up the given joined element.
I have uploaded an internalized example earlier, please test it with that as well!
(In the example I attached, the nodes are Autograph nodes because they don’t wipe the user texts attached to the objects.)
That’s what I did.
Maybe if you upload something that heavy, you could think of us people who are poor ( on ram) and disable it before. Or put just a reasonable amount of curves:)