I was wondering if there is a way to create one continuous curve from several overlapping curves such as in a Make2D output.
SelDup does not really work as the curves are overlapping but not exactly the same length. Join works only to some point. It either gives me some longer overlappig curves or one continuous curve with shorter curves overlapping.
I am working with a very complex Make2D graphic and I really don’t want to spend days sorting out curves.
Hello - if the multiple curves form a closed loop, CurveBoolean should be able to handle it. If the curves do not form a closed loop, it might still be simplest/quickest to add some to close it and then tidy up after the CurveBooelan.
@lisa.brunner93 as I am having fun right now in creating Pythonscripts via AI and wanted to have your described functionality for some time as well, here is my first take on that matter - try it, in my use-cases it worked out to help a lot - but I guess anyone can improve it further, have fun.
import rhinoscriptsyntax as rs
import scriptcontext as sc
import Rhino
def MergeOverlappingCurves():
"""
Creates a union of overlapping curves - one continuous curve
covering the entire span from start to end.
Uses only the specified tolerance for precision.
"""
# Tolerance = Model tolerance
default_tolerance = sc.doc.ModelAbsoluteTolerance
# Ask user for tolerance (Enter = use model tolerance)
tolerance = rs.GetReal("Tolerance for merging (Enter for model tolerance)",
default_tolerance, 0.0001, 1.0)
if tolerance is None: return
# Select curves
curve_ids = rs.GetObjects("Select curves to merge",
rs.filter.curve,
preselect=True)
if not curve_ids or len(curve_ids) < 2:
print("Please select at least 2 curves")
return
print("Processing {} curves with tolerance {}...".format(len(curve_ids), tolerance))
# Get curve objects
curves = []
for id in curve_ids:
crv = rs.coercecurve(id)
if crv:
curves.append(crv)
# Step 1: Collect ALL intersection and endpoint locations
all_split_points = []
# Add all endpoints
for crv in curves:
all_split_points.append(crv.PointAtStart)
all_split_points.append(crv.PointAtEnd)
# Add all intersection points
for i in range(len(curves)):
for j in range(i + 1, len(curves)):
intersection = Rhino.Geometry.Intersect.Intersection.CurveCurve(
curves[i], curves[j], tolerance, tolerance)
if intersection:
for event in intersection:
all_split_points.append(event.PointA)
all_split_points.append(event.PointB)
# Remove duplicate points
unique_split_points = []
for pt in all_split_points:
is_duplicate = False
for existing_pt in unique_split_points:
if pt.DistanceTo(existing_pt) < tolerance:
is_duplicate = True
break
if not is_duplicate:
unique_split_points.append(pt)
print("Found {} unique split points".format(len(unique_split_points)))
# Step 2: Split ALL curves at ALL split points
all_segments = []
for i in range(len(curves)):
crv = curves[i]
split_params = []
# Find which split points lie on this curve
for pt in unique_split_points:
success, t = crv.ClosestPoint(pt)
if success:
closest = crv.PointAt(t)
if pt.DistanceTo(closest) < tolerance:
# Point lies on curve - add parameter
if t > crv.Domain.Min + tolerance and t < crv.Domain.Max - tolerance:
split_params.append(t)
if len(split_params) > 0:
# Remove duplicates and sort
split_params = list(set(split_params))
split_params.sort()
# Create segments
all_params = [crv.Domain.Min] + split_params + [crv.Domain.Max]
for k in range(len(all_params) - 1):
segment = crv.Trim(all_params[k], all_params[k + 1])
if segment and segment.GetLength() > tolerance:
all_segments.append(segment)
print("Curve {} split into {} segments".format(i + 1, len(all_params) - 1))
else:
all_segments.append(crv)
print("Curve {} kept whole".format(i + 1))
print("Total segments: {}".format(len(all_segments)))
# Step 3: Remove duplicate segments (segments that overlap completely)
unique_segments = []
for i in range(len(all_segments)):
seg = all_segments[i]
seg_length = seg.GetLength()
# Sample points along segment
num_samples = 20
sample_points = []
for s in range(num_samples + 1):
t = seg.Domain.Min + (seg.Domain.Max - seg.Domain.Min) * s / num_samples
sample_points.append(seg.PointAt(t))
is_duplicate = False
# Check against already added unique segments
for unique_seg in unique_segments:
unique_length = unique_seg.GetLength()
# Check if all sample points lie on unique_seg
all_on_unique = True
for pt in sample_points:
success, t = unique_seg.ClosestPoint(pt)
if success:
closest = unique_seg.PointAt(t)
if pt.DistanceTo(closest) > tolerance:
all_on_unique = False
break
else:
all_on_unique = False
break
if all_on_unique:
# seg lies on unique_seg
if abs(seg_length - unique_length) < tolerance:
# Same length - it's a duplicate
is_duplicate = True
break
elif seg_length < unique_length:
# seg is shorter - it's covered by unique_seg
is_duplicate = True
break
else:
# seg is longer - replace unique_seg with seg
unique_segments.remove(unique_seg)
break
if not is_duplicate:
unique_segments.append(seg)
print("Keeping {} unique segments after removing duplicates".format(len(unique_segments)))
# Step 4: Snap endpoints of segments to each other if they're close
# This ensures they will join properly
for i in range(len(unique_segments)):
seg1 = unique_segments[i]
seg1_start = seg1.PointAtStart
seg1_end = seg1.PointAtEnd
for j in range(len(unique_segments)):
if i == j: continue
seg2 = unique_segments[j]
seg2_start = seg2.PointAtStart
seg2_end = seg2.PointAtEnd
# Check if endpoints are close and snap them
if seg1_end.DistanceTo(seg2_start) < tolerance:
# Snap seg2 start to seg1 end
seg2.SetStartPoint(seg1_end)
print("Snapped segment {} start to segment {} end".format(j, i))
elif seg1_end.DistanceTo(seg2_end) < tolerance:
# Snap seg2 end to seg1 end
seg2.SetEndPoint(seg1_end)
print("Snapped segment {} end to segment {} end".format(j, i))
elif seg1_start.DistanceTo(seg2_start) < tolerance:
# Snap seg2 start to seg1 start
seg2.SetStartPoint(seg1_start)
print("Snapped segment {} start to segment {} start".format(j, i))
elif seg1_start.DistanceTo(seg2_end) < tolerance:
# Snap seg2 end to seg1 start
seg2.SetEndPoint(seg1_start)
print("Snapped segment {} end to segment {} start".format(j, i))
# Delete original curves
for id in curve_ids:
rs.DeleteObject(id)
# Step 5: Add all segments to document
segment_ids = []
for seg in unique_segments:
seg_id = sc.doc.Objects.AddCurve(seg)
if seg_id:
segment_ids.append(seg_id)
print("Added {} segments to document".format(len(segment_ids)))
# Step 6: Join with the specified tolerance only
if len(segment_ids) > 1:
joined = rs.JoinCurves(segment_ids, delete_input=True, tolerance=tolerance)
if joined:
rs.SelectObjects(joined)
if len(joined) == 1:
print("Success! Created 1 merged curve.")
else:
print("Created {} curves. Some segments may not connect within tolerance.".format(len(joined)))
print("You may need to manually adjust or use a different tolerance.")
else:
rs.SelectObjects(segment_ids)
print("Could not join segments within tolerance {:.6f}".format(tolerance))
elif len(segment_ids) == 1:
rs.SelectObject(segment_ids[0])
print("Created 1 curve.")
sc.doc.Views.Redraw()
# Run script
if __name__ == "__main__":
MergeOverlappingCurves()