Skip to content

plugin_load

add_override(loaded_collection)

Add overrides for the loaded armatures.

Source code in client/ayon_blender/api/plugin_load.py
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
def add_override(
    loaded_collection: bpy.types.Collection,
) -> bpy.types.Collection:
    """Add overrides for the loaded armatures."""
    overridden_collections = list(
        get_overridden_collections_from_reference_collection(loaded_collection)
    )
    context = bpy.context
    scene = context.scene
    loaded_objects = loaded_collection.all_objects
    # This slightly convoluted way of running the operator seems necessary to
    # have it work reliably for more than 1 rig on both Linux and Windows.
    # Giving it a 'random' object from the collection seems to override
    # everything contained in the loaded collection.
    context.view_layer.objects.active = loaded_objects[0]

    from .plugin import create_blender_context  # todo: move import
    operator_context = create_blender_context(
        active=loaded_objects[0],
        selected=loaded_objects
    )

    # https://blender.stackexchange.com/questions/289245/how-to-make-a-blender-library-override-in-python  # noqa
    # https://docs.blender.org/api/current/bpy.types.ID.html#bpy.types.ID.override_hierarchy_create  # noqa
    if bpy.app.version[0] >= 4:
        with bpy.context.temp_override(**operator_context):
            loaded_collection.override_hierarchy_create(
                scene, context.view_layer, do_fully_editable=True
            )

    scene.collection.children.unlink(loaded_collection)

    if overridden_collections:
        local_collection = get_local_collection(
            overridden_collections,
            loaded_collection,
        )
    else:
        local_collection = loaded_collection

    return local_collection

find_collection_by_name(target_name)

Find a collection by name, handling name collision suffixes (e.g. "MyColl.001").

Parameters:

Name Type Description Default
target_name str

The target collection name to search for.

required

Returns:

Type Description

bpy.types.Collection or None: The found collection or None.

Source code in client/ayon_blender/api/plugin_load.py
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def find_collection_by_name(target_name):
    """Find a collection by name, handling name collision suffixes (e.g. "MyColl.001").

    Args:
        target_name (str): The target collection name to search for.

    Returns:
        bpy.types.Collection or None: The found collection or None.
    """
    candidates = [
        col for col in bpy.data.collections
        if col.name == target_name
        or col.name.startswith(target_name + ".")
    ]
    candidates.sort(key=lambda col: col.name)
    return candidates[-1] if candidates else None

find_objects_by_name(target_name)

Find an object by name, handling name collision suffixes (e.g. "MyColl.001").

Parameters:

Name Type Description Default
target_name str

The target object name to search for.

required

Returns:

Type Description

bpy.types.Object or None: The found object or None.

Source code in client/ayon_blender/api/plugin_load.py
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
def find_objects_by_name(target_name):
    """Find an object by name, handling name collision suffixes (e.g. "MyColl.001").

    Args:
        target_name (str): The target object name to search for.

    Returns:
        bpy.types.Object or None: The found object or None.
    """
    candidates = [
        obj for obj in bpy.data.objects
        if obj.name == target_name
        or obj.name.startswith(target_name + ".")
    ]
    candidates.sort(key=lambda obj: obj.name)
    return candidates[-1] if candidates else None

get_local_collection(overridden_collections, loaded_collection)

Get the local (overridden) collection.

To get it we check all collections with a library override and check if they have the loaded collection as their reference. If a collection is not in the provided (known) override collections, we assume it's the newly created one.

Source code in client/ayon_blender/api/plugin_load.py
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def get_local_collection(
    overridden_collections: list[bpy.types.Collection],
    loaded_collection: bpy.types.Collection,
) -> bpy.types.Collection:
    """Get the local (overridden) collection.

    To get it we check all collections with a library override and check if
    they have the loaded collection as their reference. If a collection is
    not in the provided (known) override collections, we assume it's the newly
    created one.
    """
    local_collections: set[bpy.types.Collection] = set()
    for collection in get_overridden_collections_from_reference_collection(
        loaded_collection
    ):
        if collection not in overridden_collections:
            local_collections.add(collection)
    if len(local_collections) != 1:
        raise RuntimeError("Could not find the overridden collection.")

    return local_collections.pop()

get_overridden_collections_from_reference_collection(reference_collection)

Get collections that are overridden versions of the reference collection.

Yields:

Type Description
Collection

All collections that have an override library and have the

Collection

reference_collection collection as reference.

Source code in client/ayon_blender/api/plugin_load.py
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def get_overridden_collections_from_reference_collection(
    reference_collection: bpy.types.Collection,
) -> Generator[bpy.types.Collection, None, None]:
    """Get collections that are overridden versions of the reference collection.

    Yields:
        All collections that have an override library and have the
        `reference_collection` collection as reference.
    """
    for collection in bpy.data.collections:
        if not collection.override_library:
            continue
        if collection.override_library.reference == reference_collection:
            yield collection

Load a collection to the scene using bpy.ops.wm.link (UI 1:1 behavior).

Parameters:

Name Type Description Default
filepath str

Path to the .blend file.

required
link bool

Whether to link or append the data.

True
group_name str

Name of the collection to load.

None
instance_collections bool

Whether to instance collections.

False
instance_object_data bool

Whether to instance object data.

False

Returns: bpy.types.Collection: The loaded collection datablock.

Source code in client/ayon_blender/api/plugin_load.py
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def link_collection(
    filepath,
    link=True,
    group_name=None,
    instance_collections=False,
    instance_object_data=False
) -> bpy.types.Collection:
    """Load a collection to the scene using bpy.ops.wm.link (UI 1:1 behavior).

    Args:
        filepath (str): Path to the .blend file.
        link (bool): Whether to link or append the data.
        group_name (str): Name of the collection to load.
        instance_collections (bool): Whether to instance collections.
        instance_object_data (bool): Whether to instance object data.
    Returns:
        bpy.types.Collection: The loaded collection datablock.
    """
    asset_container = get_collection(group_name)
    # Rule: remove the second token if it is exactly two digits (e.g. "_01_" or "_01:").
    target_name = re.sub(r"_\d{2}[:_]", "_", group_name)

    directory = os.path.join(filepath, "Collection") + os.sep
    op_filepath = directory + target_name

    # Ensure Blender links into our AYON container collection to avoid creating "LinkedData"
    try:
        view_layer = bpy.context.view_layer

        def _find_layer_collection(layer_coll, collection):
            if layer_coll.collection == collection:
                return layer_coll
            for ch in layer_coll.children:
                found = _find_layer_collection(ch, collection)
                if found:
                    return found
            return None

        lc = _find_layer_collection(view_layer.layer_collection, asset_container)
        if lc:
            view_layer.active_layer_collection = lc
    except Exception:
        pass

    bpy.ops.wm.link(
        filepath=op_filepath,
        directory=directory,
        filename=target_name,
        # link=True means keep data linked (AYON link workflow)
        link=link,
        relative_path=False,
        # Make behavior match UI toggles 1:1
        instance_collections=instance_collections,
        instance_object_data=instance_object_data,

        # Keep selection side effects minimal
        autoselect=False,
        active_collection=True,
    )

    return asset_container

load_collection(filepath, link=True, group_name=None)

Load a collection by bpy.data.libraries.load to the scene.

Source code in client/ayon_blender/api/plugin_load.py
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
def load_collection(
    filepath,
    link=True,
    group_name = None
) -> bpy.types.Collection:
    """Load a collection by bpy.data.libraries.load to the scene."""
    loaded_containers = []
    asset_container = get_collection(group_name)
    with bpy.data.libraries.load(filepath, link=link, relative=False) as (
        data_from,
        data_to,
    ):
        for attr in dir(data_to):
            setattr(data_to, attr, getattr(data_from, attr))

    for coll in data_to.collections:
        if coll is not None and coll.name not in asset_container.children:
            asset_container.children.link(coll)

    for obj in data_to.objects:
        if obj is not None and obj.name not in asset_container.objects:
            asset_container.objects.link(obj)

    loaded_containers = [asset_container]

    if len(loaded_containers) != 1:
        for loaded_container in loaded_containers:
            bpy.data.collections.remove(loaded_container)
        raise LoadError(
            "More then 1 'container' is loaded. That means the publish was "
            "not correct."
        )
    container_collection = loaded_containers[0]

    return container_collection