🤔 Unique Data For PolyCurve/PolyLine Segment - Where To Store?

Hello everyone,

I’m returning to something that has been plaguing me for over a year.

How can I use an existing Rhino.Geometry class or create a new class that acts like a polyline, edits(vertices/segments), adds/deletes like a polyline, and looks like a polyline but retains metadata/user text attributes per segment.

The reason this is important (from our perspective) is that having the ability to “drive” advanced workflows (such as Grasshopper scripts or scripting functions with the polyline as an input) often requires a more nuanced understanding and retention of the overall data at the “segment” level.

Of course there is Explode and Shatter or only ever utilizing single line segments with user data attached and then post rationalizing “which are connected in a continuous chain” but this makes editing of said polyline/chain a bit of a UX and dictionary keeping nightmare.

The code below, when ran, will allow you to pick points and create a pseudo PolyCurve object that prints out the following:
-uuid for each curve segment
-polycurve_id for the overall polycurve that the segment belongs to
-the user text attributes assigned on per segment instance

Here is a visual example of the desired behavior, where a single polyline controls the overall “form” but each segment has unique metadata driving it’s behavior (in this case a very basic extrusion in Z direction of metadata key=height, value=“unique value per segment here” scenario)

So it may seem pretty obvious that this is “already acheivable” but the issue is that the exploding works on an index system meaning, if a segment is added or deleted, it “shifts” the data and you may now end up with one segment inheriting incorrect data from the segment that was added/removed because it now has a different index value.

Maintaining a UUID for each segment would prevent this (I believe?) but my issue is that there is no where I can find to actually store this information within the rhino object itself.

Here’s as far as I’ve gotten (python):

import Rhino
import rhinoscriptsyntax as rs
import uuid
import scriptcontext as sc


class CustomPolyCurve:
    def __init__(self, points):
        self.control_points = points
        self.polycurve_id = str(uuid.uuid4())  # Unique ID for the entire polycurve
        self.segment_metadata = []  # List to store segment data (UUIDs and other info)
        self.polycurve = Rhino.Geometry.PolyCurve()  # Rhino PolyCurve to hold segments
        self._initialize_segments()

    def _initialize_segments(self):
        """Initialize segments within the PolyCurve and assign UUIDs and metadata to each."""
        for i in range(len(self.control_points) - 1):
            segment_uuid = str(uuid.uuid4())
            start_point = self.control_points[i]
            end_point = self.control_points[i + 1]
            # Create a line segment between points and add to the PolyCurve
            line = Rhino.Geometry.LineCurve(start_point, end_point)
            self.polycurve.Append(line)  # Add the line to the PolyCurve

            # Store metadata for each segment
            self.segment_metadata.append({
                "uuid": segment_uuid,
                "polycurve_id": self.polycurve_id,
                "user_text": {}
            })

        # Add the PolyCurve to the document as a single entity
        self.polycurve_id = sc.doc.Objects.AddCurve(self.polycurve)
        sc.doc.Views.Redraw()

    def add_point(self, point):
        """Add a point to extend the polycurve with a new segment and UUID."""
        if self.control_points:
            last_point = self.control_points[-1]
            line = Rhino.Geometry.LineCurve(last_point, point)
            self.polycurve.Append(line)

            # Update control points and metadata
            segment_uuid = str(uuid.uuid4())
            self.segment_metadata.append({
                "uuid": segment_uuid,
                "polycurve_id": self.polycurve_id,
                "user_text": {}
            })

        self.control_points.append(point)
        # Update the PolyCurve in the document to reflect changes
        sc.doc.Objects.Replace(self.polycurve_id, self.polycurve)
        sc.doc.Views.Redraw()

    def get_segment_metadata(self, index):
        """Retrieve metadata for a specific segment."""
        if 0 <= index < len(self.segment_metadata):
            return self.segment_metadata[index]
        return None

    def set_segment_metadata(self, index, key, value):
        """Set custom metadata for a specific segment."""
        if 0 <= index < len(self.segment_metadata):
            self.segment_metadata[index]["user_text"][key] = value

    def redraw(self):
        """Redraw the PolyCurve in the document if control points or segments change."""
        sc.doc.Objects.Replace(self.polycurve_id, self.polycurve)
        sc.doc.Views.Redraw()


# Example usage
points = rs.GetPoints(True)  # Prompt user to draw polycurve in Rhino viewport

if points:
    polycurve = CustomPolyCurve(points)
    polycurve.set_segment_metadata(0, "developer status", "seeking help")
    polycurve.set_segment_metadata(3, "animal", "unicorn")
    polycurve.set_segment_metadata(1, "best software company", "McNeel")
    polycurve.redraw()  # Redraw the polycurve if control points were modified

    # Print segment metadata for verification
    print(f">PolyCurve Metadata:\n>>>{polycurve.get_segment_metadata(0)}\n>>>{polycurve.get_segment_metadata(1)}\n>>>{polycurve.get_segment_metadata(3)}\n<End PolyCurve Metadata")

I feel like this is either very easy or complex and I’m somewhere lost in the middle haha.

Thank you all for your help!

Include the index of the segment, within the Usertext key on the PC/Polyline? It just requires an internal convention for key-strings. Any helpers imaginable can be based on that.

1 Like

Thanks for your response @James_Parrott
Am I understanding correctly that with what you suggest, the index would change each time the polyline segment count changes which would lead to a naming convention like “segment 1” potentially having a user text key “index” with a value of “13” or some other disjointed value right?

Or is that what you mean by having an internal convention such as naming it “segment-“uuid here”” and then “current index: 13” or something like that?

EDIT:

I guess I’m less worried about how to organize the data and more concerned about how do I actually even store data per segment on a Rhino.Polyline? It doesn’t seem possible to have a polyline within rhino with per segment user text?

Hi @michaelvollrath,

A Polyline is nothing more than a collection of points.

If you need to store custom data, then you might consider using a PolylineCurve, rather than a Polyline.

– Dale

2 Likes

Thanks @dale , this class is certainly more robust for my needs in general.

However, it does not seem to allow per segment user data (user data stored at the subcurve level). Is that correct?

I’m specifically after trying to set user data per segment within a PolylineCurve

Thanks for your response!

Yes, this is correct.

Sure, you can do this.

– Dale

Awesome!

Pardon my ignorance but I don’t see any way to access the segments in the API references?
https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.polylinecurve

I see UserData at large but nothing about segment specific…

Do I have to assign user data to individual curves and THEN create a PolylineCurve from those curves?

Thanks for the help @dale

Sorry, I misread the above - I though we were talking about PolyCurves, not Polyline curves.

A polyline curve does not have segments. Thus, there is no way to setup per-segment data.

– Dale

All good! But PolyCurves do have per-segment data?

I see an “IsNested” property on PolyCurves:

https://developer.rhino3d.com/api/rhinocommon/rhino.geometry.polycurve

I also see “AppendSegment”, “SegementCurve[index]”

So I see that I can access segments by index but I don’t see anything about user data per segment?

Make your curve segments, add your data, make a polycurve…

You can also add your data to the polycurve as long as the data has a way of identifying what segment it belongs to.

– Dale

Okay, thank you @dale , that does make sense to me.

There is no PolyCurve object IN 3D Rhinospace though is there?

Meaning, if I have my PolyCurve with all its respective curve segments and their user data per segment I can only add that PolyCurve to the Rhino document by converting it to a Polyline.

Is that correct?

@michaelvollrath - Rhino does haves a PolyCurve objects. They are generally created by using the Join command. But they can be created in other ways too.

mport Rhino
import scriptcontext as sc

point0 = Rhino.Geometry.Point3d(0, 0, 0)
point1 = Rhino.Geometry.Point3d(5, 0, 0)
point2 = Rhino.Geometry.Point3d(5, 5, 0)
point3 = Rhino.Geometry.Point3d(0, 5, 0)

line_curve0 = Rhino.Geometry.LineCurve(point0, point1)
line_curve1 = Rhino.Geometry.LineCurve(point1, point2)
line_curve2 = Rhino.Geometry.LineCurve(point2, point3)

polycurve = Rhino.Geometry.PolyCurve()
polycurve.AppendSegment(line_curve0)
polycurve.AppendSegment(line_curve1)
polycurve.AppendSegment(line_curve2)

id = sc.doc.Objects.AddCurve(polycurve)
sc.doc.Objects.Select(id)
sc.doc.Views.Redraw()
Rhino.RhinoApp.RunScript("_What", True)

– Dale

2 Likes

Thank you @dale, this is very helpful.

AddCurve(polycurve) is one of those things that feels painfully obvious in seeing it but I actually wouldn’t have made that connect and would have assumed it wanted “single” curve objects for that method.

Thanks for taking the time to outline this with the code example

@michaelvollrath
should the user be able to work / edit the PolyCurve ?
many rhino commands will result in Polylines if the input was a PolyCurve with Lines only.

1 Like

Hi @Tom_P,

Yes that’s the main thing I’m after. Having a chain of segments or polygon shape that is editable with its control points and works with split and join operations but can have the user set user text uniquely for each segment while retaining the editability of the chain of segments as if it were all connected like how a Polyline vertex being moved moves the associated point for each adjacent segment because it’s a single control point.

A use case example is generating a 3D roof from the Rhino PolyCurve where the curve acts as the “control grips/driver” of a roof form solver function.

Each segment lets the user change a key/value property like “pitch” so if the user chooses “6:12” for one segment and “8:12” for another segment the 3D roof would have those “per instance pitch values” applied to the 3d face angle based on that segments user data.

If a user adds another segment or removes a segment I want the previous segments still existing to still have the same user data attached to them.

So if a user added a segment of the Polyline curve that had the “8:12” property set, then that roof face should not become “6:12” just because the list length and Indices changed of the segments. If segment index 0 gets deleted it shouldn’t pass the values to the next segment over it should keep the values persistently for each segment.

If no user data is specified for a segment it would use a default value.

From extensive testing it doesn’t seem possible to to do this with indexing alone because the index value will change when a segment is added or removed in the middle of the list of segments so relying on the index is not a president way to handle it.

explode, split, join as rhino commands will destroy the polycurve-nature and result in a polyine.
the user-data attached to the segments is lost.
if you experiment - use the geometrys user-data / text.
the rhino properties show the attributes user text.

I am interested because I searched into the same direction to make a simple g-code editor - with no success.

1 Like

Ahh yes I do recall coming across your unsolved post as well.

Yea very similar use case. I’ll share whatever I come up with in researching it. To me it’s a broadly applicable “thing to solve” and quite yet, but soon to be a bottleneck if I can’t solve.

Thanks for the ideas and response

Quite right. I didn’t realise new segments would be added - sorry.

If the uuid is used instead of an index, be careful to use the uuid from the Rhino context. Grasshopper context uuids can change on recomputation of the sheet.

Presumably EleFront doesn’t support this already?

No worries at all!

I did notice this as well so I’ve been generating UUIDs with Python and storing relevant information in dicts or sticky (depending) so that I don’t lose info on recompute.

The great thing about user text of course is it’s persistence being stored at the object level.

Elefront has been great but I no longer use it after R8 and I’m developing for a cross platform plugin so I do not not want to have any reliance on any other third party plugin even if it achieved the intended functionality.

1 Like

just to keep things linked:
here is the similar post - with more or less same needs:

1 Like