Unify/join overlapping curves into one continuous curve

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.

Does anyone have a way to work around this?

CurveBoolean might help if the curves represent closed areas…

thanks!
I didn’t know about CurveBoolean . Unfortunately most of my curves are open.

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.

-Pascal

@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()