Subtract lines

I have two lines.

I would like to subtract them so I am only left with the black bit of the longer line.

What’s the best way to do this?

I actually have dozens of lines in different directions and I want to keep the bits of lines that have no overlap… So a simple, “get endpoint” and make a new line will not work.

Hi,
Have you try Geometry.Split ?

1 Like

Oh nice catch @A.C3D!

There can be some issues with that when datasets have no overlapping cures and partially repeating overlapping curves. This might help in such situations:

Results (the initial curve sets outlined by hand in snipping tool - sorry for the lack of accuracy/clarity in my sketch)

3 Likes

That doesn’t tell me where the overlap is.

I want to remove the overlapping bits.

So a slightly more complicated version is two squares (big one and small one)…

I want to be left with the red bit as the bottom left bits overlap

Your snipping sketch skills are on fleek. :smiley:

The method seems really complex for something that, on the face of it, seems really simple…

Surely there’s a simpler way?

If you’re ok with an imperative code method (Design Script, Python, C#) you can iteratively use the Split method above, or simplify the process. It’s less complex than it looks based on list management requirements.

You can also file a feature request via the submit idea button on the public roadmap here.

The Python for 1 line vs 1 line is easy… but I’m struggling with lists vs lists. Was hoping I was missing something really obvious.

If you post that python (and confirm my initial curve set looks somewhat like what you’re using and that the previous results are somewhat right) I’ll have a look shortly.

import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import Point, Line

# Two lines... but I want lists
line1 = IN[0]
line2 = IN[1]

### Just realised this doesn't work if the lines don't have the same startpoint. 
# Function to subtract overlapping segment
def subtract_overlap(line1, line2):
    start1, end1 = line1.StartPoint, line1.EndPoint
    start2, end2 = line2.StartPoint, line2.EndPoint

    # Find the overlapping segment
    if start1.DistanceTo(start2) == 0:
        if end1.DistanceTo(end2) > 0:
            if start1.DistanceTo(end2) < start1.DistanceTo(end1):
                new_start = end2
                new_end = end1
            else:
                new_start = start1
                new_end = end1
        else:
            new_start, new_end = start1, end1
    else:
        new_start, new_end = start1, end1

    # Create new line(s)
    new_line = Line.ByStartPointEndPoint(new_start, new_end)
    return new_line

# Subtract the overlap
result_line = subtract_overlap(line1, line2)

OUT = result_line  

I’ve just spent the last hour on ChatGPT (I love ChatGPT)…

import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import Point, Line

# Two lists of lines
lines_list1 = IN[0]
lines_list2 = IN[1]

# Function to check if a point lies on a line segment
def is_point_on_segment(point, line):
    start, end = line.StartPoint, line.EndPoint
    return start.DistanceTo(point) + end.DistanceTo(point) - start.DistanceTo(end) < 0.001

# Function to subtract overlapping segment and return unique segments
def subtract_overlap(line1, line2):
    start1, end1 = line1.StartPoint, line1.EndPoint
    start2, end2 = line2.StartPoint, line2.EndPoint

    # Ensure start points are consistently ordered
    if start1.DistanceTo(start2) > end1.DistanceTo(start2):
        start1, end1 = end1, start1
    if start2.DistanceTo(start1) > end2.DistanceTo(start1):
        start2, end2 = end2, start2

    # Find intersection points
    intersection_points = []
    for point in [start2, end2]:
        if is_point_on_segment(point, line1):
            intersection_points.append(point)
    for point in [start1, end1]:
        if is_point_on_segment(point, line2):
            intersection_points.append(point)

    # If no intersection, return the original line and the line from list2
    if not intersection_points:
        return [line1], [line2]

    # Sort intersection points by their distance from start1
    intersection_points.sort(key=lambda p: p.DistanceTo(start1))

    result_lines1 = []
    result_lines2 = []
    if start1.DistanceTo(intersection_points[0]) > 0.001:
        result_lines1.append(Line.ByStartPointEndPoint(start1, intersection_points[0]))
    if end1.DistanceTo(intersection_points[-1]) > 0.001:
        result_lines1.append(Line.ByStartPointEndPoint(intersection_points[-1], end1))
    if start2.DistanceTo(intersection_points[0]) > 0.001:
        result_lines2.append(Line.ByStartPointEndPoint(start2, intersection_points[0]))
    if end2.DistanceTo(intersection_points[-1]) > 0.001:
        result_lines2.append(Line.ByStartPointEndPoint(intersection_points[-1], end2))

    return result_lines1, result_lines2

# Process each line in list1 against all lines in list2
def process_lines(lines_list1, lines_list2):
    processed_lines1 = []
    remaining_lines2 = lines_list2.copy()
    lines_to_remove = []

    for line1 in lines_list1:
        current_segments = [line1]

        for i, line2 in enumerate(lines_list2):
            new_segments = []
            for segment in current_segments:
                result_segments1, result_segments2 = subtract_overlap(segment, line2)
                new_segments.extend(result_segments1)
                remaining_lines2[i] = result_segments2
                if not result_segments2:  # If result_segments2 is empty, it means line2 was fully overlapped
                    lines_to_remove.append(line2)
            current_segments = new_segments

        processed_lines1.extend(current_segments)

    non_overlapped_lines2 = []
    for line2 in lines_list2:
        if line2 not in lines_to_remove:
            non_overlapped_lines2.append(line2)

    return processed_lines1 + non_overlapped_lines2

# Subtract the overlap for each line in list1 against all lines in list2
result_lines = process_lines(lines_list1, lines_list2)

OUT = result_lines

Here was my result (call ran long and I had to jump on another one right after it ended):

import sys
import clr
clr.AddReference('ProtoGeometry')
from Autodesk.DesignScript.Geometry import *

initialCrvs = IN[0]

newCrvs = []

for i in range(len(initialCrvs)):
    crv = initialCrvs.pop(0) #take out a curve
    others = []
    [[others.append(j) for j in crv.Intersect(i) if j.__class__ == Line] for i in initialCrvs]
    if len(others)!=0:
        
        others = PolySurface.ByJoinedSurfaces([i.Extrude(Vector.ZAxis()) for i in others])
        splitCrv = crv.Split(others)
        [newCrvs.append(i) for i in splitCrv if not i.PointAtParameter(0.5).DoesIntersect(others)]
    elif len(others) == 0:
        newCrvs.append(crv)
    initialCrvs.append(crv)

OUT = newCrvs

I beat your GPT by quite a few lines, but not sure which is faster.

image

Red is GPT, White is yours…

Altho… not entirely sure how accurate tune up is because I ran it again with a slightly different smaller rectangle and got:

image

(both nodes worked)

Looking at this again, the two code bases are different.

Yours takes a list of curves to remove from the base list of curves.
Mine gets the unique segment from a flat list of curves.

Shifted to a definition incase that helps anyone further:

import sys, clr #
clr.AddReference('ProtoGeometry') #add Dynamo's geometry library to the common language runtime
from Autodesk.DesignScript.Geometry import * #import Dynamo's geometry library

def UniqueCurveSegments(initialCurves):
    uniqueSegments = [] #empty list to hold the resulting curves
    for i in range(len(initialCurves)): #for each index in the list of curves
        crv = initialCurves.pop(0) #take out the first curve
        others = [] #an empty list to hold the others
        [[others.append(j) for j in crv.Intersect(i) if j.__class__ == Line] for i in initialCurves] #append the intersection between the curve and other items to the others list only if they intersect as a line
        if len(others)!=0: #if there are items in the others list
            others = PolySurface.ByJoinedSurfaces([i.Extrude(Vector.ZAxis()) for i in others]) #build a polysurface
            splitCrv = crv.Split(others) #split the curve
            [uniqueSegments.append(i) for i in splitCrv if not i.PointAtParameter(0.5).DoesIntersect(others)] #remove any segments with a midpoint not on the polysurface and append to the new curves list
        elif len(others) == 0: #if the curve didn't have any other intersecting elements it needs to be included by default
            uniqueSegments.append(crv) #append the curve to the list
        initialCurves.append(crv) #put the curve back in the list
    return uniqueSegments #return the new curves

initialCrvs = IN[0] #the list of curves from the Dynamo environment

newCrvs = UniqueCurveSegments(initialCrvs) #get the unique curve segements

OUT = newCrvs #return the unique curve segments to the Dynamo environment

I saw… Hence the two extra nodes:

image

List.Join might be better in these cases… think there is value in having for nodes for both Curve.Difference and Curves.UniqueSegments in Dynamo core. FYI @achintya_bhat

2 Likes

yeah… suppose… but I was just testing it and that’s the node that I thought of :smiley:

1 Like

Not to side track the great discussion on the line intersection. But if we’re assuming the lines are always closed curves, this problem could be simplified by using surfaces rather than lines.

But also I’m following this discussion closely, because I recently came a across a similar scenario where I was trying to find the edges where floor slabs touched (for doweling and slab folds).

Depends on what the end goal is. Also, converting to a surface is time consuming if you’re doing lots and lots.

I’m planning on using this to loop through a few hundred of these things so computational time is more important than normal.

Assuming all too surfaces are co-planar:

  1. Take the top surfaces from each slab.
  2. Join into a PolySurface
  3. Topology.Edges to get the edges of the polysurface.
  4. Edge.AdjacentSurfaces to get the adjacent surfaces to each edge.
  5. List.Count with level set to @L2 to get the number of faces on each edge.
  6. == to test if the count is 2.
  7. List.FilterByBoolMask to filter the edges by the resulting boolean from the equals node.
  8. Anything in the ‘in’ output from the FilterByBoolMask is a curve where two slabs meet. Anything in the ‘out’ is a perimeter edge (either interior loop or exterior).

If they aren’t coplanar you might have some issues with this method, as things like ramps might not be caught.

The issue that we will run into here is that an edge can be intersecting without being adjacent, don’t forget PolySurface.ByJoinedSurfaces is a join not a necessarily union. Imagine a rectangular building with n smaller private balcony slabs along the edges. We’ll end up with 1+n faces in the polysurface but no shared edges since all edges are unique.

My solution was to normalize all line directions and then do comparisons by grouped direction vectors. But it was a ton Python, and I’m assuming not well optimized.