InstancePath and make_unique

Hi all,

Say I have an InstancePath pointing to a specific face in a specific subgroup. I would like to change, say, the material on that face. The face might be in a group whose definition is currently shared with other groups. This is an internal optimization; for the user, what should occur is that only one face is changed, the one in the specific group mentioned in the InstancePath. The answer appears to be “call the #make_unique method on the Group object”. Right, but that method makes the InstancePath invalid! Try:

f = grp1.entities[3]   # my face inside 'grp1', which is not unique
path1 = [grp1, f]
path1.valid?   # true
path1.valid?   # false!

The reason is clear: the face f is a member of the original entities collection, but #make_unique created a new grp1.definition with a new copy of the whole entities collection. So the InstancePath [grp1, f] is no longer valid now. What is the fix? I can think about horrible hacks like finding the index of f inside f.parent.entities, and locating the face at the same index inside the new grp1.definition.entities; does that even work reliably?

Get a reference to the new group when you make it unqiue …

new_grp = grp1.make_unique

… and then build a new valid InstancePath.

grp1.make_unique just returns grp1. The problem is not the grp1 element in the InstancePath, but all further elements, which need to be replaced by their new version in the new copy of the group. I’m looking for a reasonable way to do that, which would not be index-based and dependent on the old and new group to have exactly the same order for their entities. (Maybe there isn’t such a reasonable way.)

If it does, then it is an API bug. Please report it.
It is (according to the docs) supposed to return the new group instance.

You might temporarily attach an attribute dictionary to the face.
Do the uniquing.
Then search the new group’s entities for the face that has the dictionary.
Assign the material.
Delete the dictionaries in both group definition’s entities.

The alternative might be to temporarily create a new material with a unique name.
Save the face’s current material to a reference.
Assign the uniquely named material.
Do the #make_unique.
Then change the new group’s face’s material.
Restore the original’s material.
Remove the temporary “tag” material.

There are similar ways also to “tag” the face before doing the uniquing.
Use a temporary layer-tag ?

Ah yes, good idea to attach some tag on the face and then search for it after #make_unique. Thanks! The idea also extends to the case where I start with several InstancePaths and want to have the same change applied to all of them.

I double-checked, and grp.make_unique seems to always return grp for me. The documentation doesn’t clearly say it’s wrong, I believe. As far as I understand it, this method is either doing nothing or making a new definition, and then grp.definition is made to point to that; i.e. the Group object itself is modified in-place. See the example for that method: make_unique is called but its result is ignored.

It most definitely creates a new definition, IF there are more than 1 instance of the old group definition. If the group is already unique, (ie has only 1 instance,) then there is no need to clone the definition.

I’d need to test to be sure what’s going on.

I think that make_unique always returns its receiver, the Group in was invoked on. The change, if any, takes place beneath the covers by creating a new ComponentDefinition for the (now unique) Group. If there is only one copy of the Group, make_unique is a no-op.

That’s essentially the same as what @DanRathbun just wrote.

But the docs state that a new group is returned.

But as Armin is saying, he thinks that the API is reusing the reference, rather than created a new one (as the old one would become invalid anyway.)

This tweaked my memory and it seems we’ve discussed the need for API doc clarification in 2 issue threads already …

Here is what I came up with. I thought I’d post it here because it’s not completely trivial. See comments for usage.

UNIQUIFY_DICT_TAG_NAME = "yourcompany_yourextension_TAG"

def uniquify_groups_and_pick_entities(instance_paths)
    # Return the actual entities at the end of the given InstancePaths
    # ("leaf" entities).
    # If there are groups along the paths, they are made unique.  This
    # is meant to be used when you want to change something about each
    # of these entities.  After calling this function, the InstancePaths
    # are not valid any more if groups were made unique!
    # In more details, 'instance_paths' should be a list of valid
    # InstancePath objects, or plain lists.  If they are plain lists,
    # they can start at the currently-opened group or component; they
    # don't need to be a complete path from the top level.  This
    # function makes change to the model by calling Group#make_unique,
    # so it should be used as part of a transaction.
    instance_paths = { |path|
        path = path.to_a if path.is_a? Sketchup::InstancePath
        # shorten 'path' to the longest tail so that all items, apart from the leaf,
        # are Group objects (not ComponentInstance).
        i = path.length - 1
        while ((i > 0) && (path[i - 1].is_a? Sketchup::Group))
            i -= 1

    uniquify_tags = Set[]
        # puts an attribute dictionary UNIQUIFY_DICT_TAG_NAME on all groups and leaf
        # entities concerned, containing the original entityID of that entity, before
        # we do any #make_unique.  The set 'uniquify_tags' identifies which entities
        # have had this attribute dictionary added.
        groups_to_make_unique = Set[]
        instance_paths.each { |path|
            path.drop(1).each { |entity|
                if uniquify_tags.add? entity
                    entity.set_attribute(UNIQUIFY_DICT_TAG_NAME, "o", entity.entityID)

        # proceed by calling #make_unique
        real_entities = {}    # mapping {definition: {old_entity_id: new_entity}}
        opt_made_unique = Set[]
        result = { |path|
            walk = path[0]
            path.drop(1).each { |entity|
                # 'walk' is a Sketchup::Group
                if opt_made_unique.add? walk
                    grp = walk
                    old_def = grp.definition
                    if grp.definition != old_def    # if make_unique was not a no-op
                        # build 'real_entities', and record in 'uniquify_tags' the new copies
                        real_mapping = {}
                        grp.definition.entities.each { |new_entity|
                            old_entity_id = new_entity.get_attribute(UNIQUIFY_DICT_TAG_NAME, "o")
                            next if old_entity_id == nil
                            real_mapping[old_entity_id] = new_entity
                        real_entities[grp.definition] = real_mapping
                real_mapping = real_entities[walk.definition]
                if real_mapping
                    entity = real_mapping[entity.entityID]
                    throw KeyError if !entity    # this should not occur
                walk = entity
        # clean up
        uniquify_tags.each { |entity| entity.delete_attribute(UNIQUIFY_DICT_TAG_NAME) }
    return result
1 Like