# InstancePath and make\_unique

**URL:** <https://forums.sketchup.com/t/instancepath-and-make-unique/141626>\
**Category:** Ruby API\
**Created:** [November 8, 2020, 6:07am UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626 "2020-11-08T06:07:05Z")\
**Posts on this page:** 10\
**Page:** 1

<div class="post-metadata">

**Author:** ![armin.rigo](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/armin.rigo/32/52126_2.png) [@armin.rigo](https://forums.sketchup.com/u/armin.rigo)\
**Post date:** [November 8, 2020, 6:07am UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/1 "2020-11-08T06:07:05Z")

</div>

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 = Sketchup::InstancePath.new [grp1, f]
path1.valid? # true
grp1.make_unique
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?

---

<div class="post-metadata">

**Author:** ![DanRathbun](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/danrathbun/32/500_2.png) [@DanRathbun](https://forums.sketchup.com/u/DanRathbun)\
**Post date:** [November 8, 2020, 12:09pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/2 "2020-11-08T12:09:22Z")

</div>

> [@armin.rigo](#):
>
> What is the fix?

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

```ruby
new_grp = grp1.make_unique

```

… and then build a new valid InstancePath.

---

<div class="post-metadata">

**Author:** ![armin.rigo](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/armin.rigo/32/52126_2.png) [@armin.rigo](https://forums.sketchup.com/u/armin.rigo)\
**Post date:** [November 8, 2020, 12:30pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/3 "2020-11-08T12:30:33Z")

</div>

`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.)

---

<div class="post-metadata">

**Author:** ![DanRathbun](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/danrathbun/32/500_2.png) [@DanRathbun](https://forums.sketchup.com/u/DanRathbun)\
**Post date:** [November 8, 2020, 12:44pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/4 "2020-11-08T12:44:19Z")

</div>

> [@armin.rigo](#):
>
> `grp1.make_unique` just returns `grp1` .

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 ?

---

<div class="post-metadata">

**Author:** ![armin.rigo](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/armin.rigo/32/52126_2.png) [@armin.rigo](https://forums.sketchup.com/u/armin.rigo)\
**Post date:** [November 8, 2020, 2:01pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/5 "2020-11-08T14:01:08Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![DanRathbun](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/danrathbun/32/500_2.png) [@DanRathbun](https://forums.sketchup.com/u/DanRathbun)\
**Post date:** [November 8, 2020, 4:18pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/6 "2020-11-08T16:18:00Z")

</div>

> [@armin.rigo](#):
>
> As far as I understand it, this method is either doing nothing or making a new `definition`, …

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.

> [@armin.rigo](#):
>
> … and then `grp.definition` is made to point to that; i.e. the Group object itself is modified in-place.

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

---

<div class="post-metadata">

**Author:** ![slbaumgartner](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/slbaumgartner/32/7302_2.png) [@slbaumgartner](https://forums.sketchup.com/u/slbaumgartner)\
**Post date:** [November 8, 2020, 4:18pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/7 "2020-11-08T16:18:38Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![DanRathbun](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/danrathbun/32/500_2.png) [@DanRathbun](https://forums.sketchup.com/u/DanRathbun)\
**Post date:** [November 8, 2020, 4:19pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/8 "2020-11-08T16:19:37Z")

</div>

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.)

---

<div class="post-metadata">

**Author:** ![DanRathbun](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/danrathbun/32/500_2.png) [@DanRathbun](https://forums.sketchup.com/u/DanRathbun)\
**Post date:** [November 8, 2020, 4:31pm UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/9 "2020-11-08T16:31:37Z")

</div>

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

> <https://github.com/SketchUp/api-issue-tracker/issues/197>
>
> https://github.com/SketchUp/api-issue-tracker/issues/154#issuecomment-350517665
> …
> 
> \> ... also neither say exactly what happens when uniquing is \*\*not\*\* needed.
> \> Ie, there is only one instance in the model.
> \> 
> \> Is the call ignored, and the same old reference to the original receiver returned ?

---

<div class="post-metadata">

**Author:** ![armin.rigo](https://sea2.discourse-cdn.com/sketchup/user_avatar/forums.sketchup.com/armin.rigo/32/52126_2.png) [@armin.rigo](https://forums.sketchup.com/u/armin.rigo)\
**Post date:** [November 9, 2020, 8:51am UTC](https://forums.sketchup.com/t/instancepath-and-make-unique/141626/10 "2020-11-09T08:51:07Z")

</div>

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 = instance_paths.map { |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
        end
        path.drop(i)
    }

    uniquify_tags = Set[]
    begin
        # 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)
                end
            }
        }

        # proceed by calling #make_unique
        real_entities = {} # mapping {definition: {old_entity_id: new_entity}}
        opt_made_unique = Set[]
        result = instance_paths.map { |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
                    grp.make_unique
                    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
                            uniquify_tags.add(new_entity)
                            real_mapping[old_entity_id] = new_entity
                        }
                        real_entities[grp.definition] = real_mapping
                    end
                end
                real_mapping = real_entities[walk.definition]
                if real_mapping
                    entity = real_mapping[entity.entityID]
                    throw KeyError if !entity # this should not occur
                end
                walk = entity
            }
            walk
        }
    ensure
        # clean up
        uniquify_tags.each { |entity| entity.delete_attribute(UNIQUIFY_DICT_TAG_NAME) }
    end
    return result
end

```
