Python - User Manageable Selection Set For GH Reference?

Hello,

Using Rhinoscriptsyntax with rs.SelectObjects() and unselectall it’s easy enough to get current selections from the Rhino Document for GH to reference.

However, I’m struggling trying to create a persistent “selection set”.

The functionality I’m after is that, if objects are selected in Rhino document and the user clicks one of the input buttons such as add, remove, or clear; the script will modify a persistent data list that stores the selected objects.

So lets say the list has 3 objects in in, I click a new object in Rhino and then the add button, I would like the persistent list to be appended.

If I choose one of the objects and click the Remove button, if it exists in the persistent list it should then be removed.

Lastly the clear button would completely clear the selection set list.

I realize similar functionality could be achieved with a generic data collection in GH but I’m after utilizing the Remote Control Panel/Grasshopper Panel to expose these as User Input Buttons to users that won’t be going inside the Grasshopper environment.

I looked into Named Selections in Rhino but that didn’t appear to have the functionality to remove/add individual items, only make new or delete existing sets.

I’ve researched sticky dictionaries, global params, other things but I’m stuck on how to achieve the persistency of data in gh Python.

The end result would be a component in GH with an output of objects in the SO output and the inputs are buttons that will be “Published” to the Remote Control Panel/Grasshopper Panel.

Thanks for the help and leads!

Here’s my code and gh file thus far:

import rhinoscriptsyntax as rs

Selection_Set = []

def get_ids():
    if Selection_Set:
        IDs = [str(obj) for obj in Selection_Set]
        return Selection_Set, IDs
    else:
        return None, None

def add_selected_objects():
    add_objs = rs.SelectedObjects()
    if add_objs:
        Selection_Set.extend(add_objs)

def remove_selected_objects():
    rem_objs = rs.SelectedObjects()
    if rem_objs:
        for obj in rem_objs:
            if obj in Selection_Set:
                Selection_Set.remove(obj)

def clear_selection_set():
    rs.UnselectAllObjects()
    Selection_Set.clear()

ghenv.Component.Message = str(len(Selection_Set)) + " Objects Stored"

if E:
    SO, ID = get_ids()

if R:
    remove_selected_objects()

if C:
    clear_selection_set()

SO = Selection_Set

selection_set_component_01a.gh (6.3 KB)

The issues with persistency stem from downstream Grasshopper components getting recomputed every time a connected upstream component gets prompted to change, or from the entire canvas getting recomputed for whatever reason. Your GHPython components will thus re-run their internal code without reference to what already has been processed.

As you mentioned above, the most straightforward approaches to make data stick is the sticky dictionary or global variables. There are other ways, like storing data in external text files, databases, and so on.

Anyway, global variables will make variables stick within the global context of the component, whereas the sticky dictionary exists outside of component scope and can be accessed from within all GHPython components, which means that it lets you share data wirelessly between GHPython components.

Here’s an example using sticky:

import Rhino as rc
import scriptcontext as sc


def get_selected_ids(include_lights=False, include_grips=False):
    """Returns an iterator of IDs of currently selected objects in the active Rhino document."""
    for o in rc.RhinoDoc.ActiveDoc.Objects.GetSelectedObjects(include_lights, include_grips):
        yield o.Id


def add_selected():
    """Adds currently selected objects from the active Rhino document
        to a set of unique IDs in the sticky dictionary."""
    if "ObjectPool" not in sc.sticky.keys():
        sc.sticky["ObjectPool"] = set()
    for id in get_selected_ids():
        sc.sticky["ObjectPool"].add(id)


def remove_selected():
    """Removes currently selected objects from the active Rhino document
        from a set of unique IDs in the sticky dictionary."""
    if "ObjectPool" in sc.sticky.keys():
        for id in get_selected_ids():
            sc.sticky["ObjectPool"].discard(id)
    

def clear_object_pool():
    """Clears the set of unique IDs in the sticky dictionary."""
    sc.sticky["ObjectPool"].clear()


if __name__ == "__main__":
    if x == 0:
        add_selected()
    elif x == 1:
        remove_selected()
    elif x == 2:
        clear_object_pool()

    a = list(sc.sticky["ObjectPool"])

set_from_rhino.gh (2.7 KB)

2 Likes

Thank you so much @diff-arch ! Works perfectly! I modified it ever so slightly to take a separate “Add, Remove, and Clear” boolean inputs because I’m exposing these in a Rhino UI panel outside of GH.

And then Rhino 8 Grasshopper Model Object Node didn’t quite like the output formatting so I converted the GUIDs to strings and ran them through the following sequence:

Script Output → GUID Node → Content Information → Model Object

Graph Space:

Model Space:

I could not seem to figure out how have the output directly to model objects. It technically outputs to model objects but for some reason the meta data that the Model Object node carries with it did not come through. I need to do some digging into the API or forums for what kind of access methods are available for Model Objects as I don’t think this has been updated in the API documentation to date (that I am aware of)

Modified Code:

import Rhino as rc
import scriptcontext as sc
import System

def get_selected_ids(include_lights=False, include_grips=False):
    """Returns an iterator of IDs of currently selected objects in the active Rhino document."""
    for o in rc.RhinoDoc.ActiveDoc.Objects.GetSelectedObjects(include_lights, include_grips):
        yield o.Id

def add_selected():
    """Adds currently selected objects from the active Rhino document
        to a set of unique IDs in the sticky dictionary."""
    if "ObjectPool" not in sc.sticky.keys():
        sc.sticky["ObjectPool"] = set()
    for id in get_selected_ids():
        sc.sticky["ObjectPool"].add(id)

def remove_selected():
    """Removes currently selected objects from the active Rhino document
        from a set of unique IDs in the sticky dictionary."""
    if "ObjectPool" in sc.sticky.keys():
        for id in get_selected_ids():
            sc.sticky["ObjectPool"].discard(id)

def clear_object_pool():
    """Clears the set of unique IDs in the sticky dictionary."""
    sc.sticky["ObjectPool"].clear()

# Check if any action button was pressed
if A:
    add_selected()
elif R:
    remove_selected()
elif C:
    clear_object_pool()

# Check if manual refresh button U is True
if U:
    # Rerun the actions
    if A:
        add_selected()
    elif R:
        remove_selected()
    elif C:
        clear_object_pool()

SO = list(sc.sticky["ObjectPool"])

ID = []

for id in SO:
    ID.append(System.Guid.ToString(id))

count = str(len(SO))

if len(SO) == 1:
    ghenv.Component.Message = count + " Object Stored"
else:
    ghenv.Component.Message = count + " Objects Stored"

Really appreciate your help!

1 Like

You’re welcome.

I’m not a hundred percent certain, but it could be that the Rhino geometry is cast to a Grasshopper geometry type, while being funnelled through the component output and any other information thus lost?

The unique ID (i.e. GUID) points to the original Rhino geometry, so you should be able to get any associated information with that.

What specific metadata are you interested in?

Well just the “overall container” that is the Model Object node. It contains the Object Name (if applicable), any layer information, user text data, geometry, linetype information, etc. An “all-in-one” bucket so to speak.

Most importantly many downstream operations that do filter/query the data inside the model objects are expecting model objects and not generic geometry.

I think I saw a post on the forum about accessing the ModelObject data type I just need to dig and find it and I’ll post back here.

You can see here the SO output is the original a input of your script you shared:

And here’s the same node after passing the GUID to the Model information node (essentially retrieving the model object from the string GUID):

Oh, I see.

Simply change this function to return the objects instead:

def get_selected_objects(include_lights=False, include_grips=False):
    """Returns an iterator of currently selected objects in the active Rhino document."""
    for o in rc.RhinoDoc.ActiveDoc.Objects.GetSelectedObjects(include_lights, include_grips):
        yield o  # <--- This is the object

Here’s the relevant documentation of what information you can extract, besides its ID.

1 Like

Thanks! I’ll study the link