Get "Island-Groups" of Polyline Offset Curves

I am using the Clipper Plugin to get internal offsets of a polyline.

The Polyline creates “islands”. You can see them in the following image highlighted by different colors. I want to get these Islands as individual list of curves (one list for Red, one for Blue, one for Black)

Is this possible within the clipper plugin?
Is there a good way of querying the output of the offset polyline component to get these islands as individual lists.

I am guessing that the code that the clipper plugin is based on knows the individual islands but they are not grouped in that way in the output (probably for good reason).

Any help or Ideas would be appreciated.

OffsetIslands01.gh (11.4 KB)

Clipper the plugin outputs the same thing as the library Clipper
For a polyline an offset outputs a list of contours and a list of holes.
There is no relation between the red and blue curves as they are not for the same value of offset.

But for each branch you can look if there is 2 or more offsets.I am quite sure you can’t rely on the order. The best will be to test if curve is inside or outside. Then you can draw a tree
Curve (n, 1) contains (curve(n+1, 1), curve(n+1, 2))

1 Like

Thank you for your insight.
I can indeed not rely on order of branches that hold multiple curves.
I was hoping I could avoid testing for inclusion. The curves will get quite complex and I might have to do a lot of comparisons.
But it seems this is the most reliable and straighforward way.

Do you have the original closed shapes you used before they were unified into a region, prior to the offset?

If you do, you could grab the centroid of each and do some proximity thing or point grouping by measuring distances to the offsets afterwards?

Just a quick idea, it might not be what you need or I might be minsunderstanding.

1 Like

@René_Corella
That is actually very smart, however I will not always have the original, individual closed curves.
The method is supposed to be so general that it also works with closed curves that were created by other means.

1 Like

I think you understood me but if not, you can rely on order of branches but not on the order in the list.
A Branch of datatree contain the result for a value of offset.

I add to work of this time of problem for my tools in Nautilus, I add to “bridge” layers to make a continuous curve

2 Likes

Makes sense - I guess you could still do proximity/point-grouping by grabbing the current centroids of the offsets:

super quick example, not sure it’s applicable:
OffsetIslands01.gh (18.7 KB)

1 Like

I think I understood. The branches correspond to the individual offsets,
But sometimes the first item in the list is the one that is included in the following curves, sometimes its the second one. (this is difficult to describe with words sorry)

If this was consistent I could traverse the tree and
put the branch item into a list
if the branch has multiple items put the first item into list 1 and the next item into a new list
if the branch item count reduces I can continue adding the new branches to the second list.

This works for some offsets but is not consistent.

1 Like

I was looking into this, however, sometimes the curves make a u-shape.
Their centroid is outside of their boundary and this messes up the inclusion detection or goruping.

I think I will have to do an inclusion detection with 1 (or more) points on the curve.
I just need to figure out a smart way to go about it.

If you want consistent result you can make your own library using clipper and doing an offset one after the other or use Anemone if you want to stay in Rhino
Curve (n) = Offset of Curve (n-1) with distance D

and not Curve (n) = offset of Curve (0) with distance 10*D

1 Like


I wonder how CAM tools do this. They seem to have no Issue segmenting the shape into multiple regions.

This might be an option as well.
I would create a funciton that recursively offsets all curves that are created when it is offset.
I would then have a tree I can follow to know what is included in which curve.

I did that some time ago

2 Likes

I implemented something very similar already.
However, as far as I understand this is doing something else. It retracts for each closed curve.
I only want to retract if I have to start a new “island” otherwise the path should just move outwards in the same plane.

1 Like

I think I found a good way of doing this.

I will go from the inside out.
If the next branch has just one item, add it to the running list
If there is more than one curve check which of them includes the previous ones and add another running list for the new island

Checking for inclusion should be fast because I only need to check one point of the curve.

If the number of items reduces pop the latest running listt from the list of running lists.

Will report if this works

I created two solutions to this:

1. Check for inclusion between all curves and bulid inclusion groups from there

Here is the code for that:


import Grasshopper as gh
from Grasshopper.Kernel.Data import GH_Path
from Grasshopper import DataTree
import Rhino.Geometry as rg

# Step 1: Find containment relationships
curve_containment = [
    (i, [
        j for j, curve_inner in enumerate(curves) 
        if i != j and curves[i].Contains(curve_inner.PointAt(0), rg.Plane.WorldXY, 0.01) == rg.PointContainment.Inside
    ]) 
    for i in range(len(curves))
]

# Step 2: Sort curves by the number of contained curves
sorted_curves = sorted(curve_containment, key=lambda x: len(x[1]))

# Step 3: Create groups
used_curves = set()
curve_groups = []

for index, contained_indices in sorted_curves:
    if index in used_curves:
        continue
    current_group = [index]
    used_curves.add(index)
    for next_index, next_contained_indices in sorted_curves:
        if next_index not in used_curves and all(i in next_contained_indices for i in current_group):
            current_group.append(next_index)
            used_curves.add(next_index)
    curve_groups.append(current_group)

# Step 4: Determine group containment using the outermost curve of each group
group_containment = [
    (i, [
        j for j, group_inner in enumerate(curve_groups) 
        if i != j and group_inner[0] in curve_containment[curve_groups[i][0]][1]
    ])
    for i in range(len(curve_groups))
]

# Step 5: Sort groups from innermost to outermost
sorted_groups = sorted(group_containment, key=lambda x: len(x[1]))
sorted_actual_groups = [curve_groups[group[0]] for group in sorted_groups]

# Convert indices to actual curve objects
sorted_actual_curve_groups = [[curves[idx] for idx in group] for group in sorted_actual_groups]

# Step 6: Add sorted groups to DataTree
CurveGroups = DataTree[object]()
for i, sublist in enumerate(sorted_actual_curve_groups):
    CurveGroups.AddRange(sublist, GH_Path(i))

2. Solution: Offset recursively with the Rhino Offset command and save lists of concentric offsets.
This has the added benefit of filling an offset curve fully without having to manually set an amount of offsets.

This is the code:

import Rhino
import ghpythonlib.treehelpers as th

def offset_curve_recursive(curve, offset_distance, plane):
    
    curve_groups = []
    finished_groups = []
    curve_groups.append([curve])
    offset_possible = True

    while offset_possible:
        for i, curve_group in enumerate(curve_groups):
            print(i)
            if i in finished_groups:
                continue
            
            curve = curve_group[-1]
            offset_curves = curve.Offset(plane, offset_distance, 0.01, Rhino.Geometry.CurveOffsetCornerStyle.Round)
            if offset_curves:
                if len(offset_curves) > 0:
                    for j, crv in enumerate(offset_curves):
                        if j == 0:
                            curve_groups[i].append(crv)
                        else:
                            curve_groups.append([crv])
                    continue
            else:
                finished_groups.append(i)
                if len(finished_groups) == len(curve_groups):
                    offset_possible = False
    
    print(curve_groups)
    return curve_groups

# Example Usage in Grasshopper

input_curves = curves  # Input as a list of curves from Grasshopper
offset_distance = distance  # Offset distance as a float from Grasshopper
plane = Rhino.Geometry.Plane.WorldXY  # Example: Use WorldXY plane for offsetting

# Call the function
result = offset_curve_recursive(input_curves, offset_distance, plane)

# Flatten and prepare the output for Grasshopper
output_curves = th.list_to_tree(result)
CurveGroups = output_curves

Grasshopper file:
OffsetIslands_Solved.gh (17.5 KB)

1 Like