Skip to content

load_blendscene

BlendSceneLoader

Bases: BlenderLoader

Load assets from a .blend file.

Source code in client/ayon_blender/plugins/load/load_blendscene.py
 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
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
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
205
206
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
class BlendSceneLoader(plugin.BlenderLoader):
    """Load assets from a .blend file."""

    product_types = {"blendScene"}
    representations = {"blend"}

    label = "Append Blend"
    icon = "code-fork"
    color = "orange"

    @staticmethod
    def _get_asset_container(collections):
        for coll in collections:
            parents = [c for c in collections if c.user_of_id(coll)]
            if coll.get(AVALON_PROPERTY) and not parents:
                return coll

        return None

    def _process_data(self, libpath, group_name, product_type):
        # Append all the data from the .blend file
        with bpy.data.libraries.load(
            libpath, link=False, relative=False
        ) as (data_from, data_to):
            for attr in dir(data_to):
                setattr(data_to, attr, getattr(data_from, attr))

        members = []

        # Rename the object to add the asset name
        for attr in dir(data_to):
            for data in getattr(data_to, attr):
                data.name = f"{group_name}:{data.name}"
                members.append(data)

        container = self._get_asset_container(
            data_to.collections)
        assert container, "No asset group found"

        container.name = group_name

        # Link the group to the scene
        bpy.context.scene.collection.children.link(container)

        # Remove the library from the blend file
        filepath = bpy.path.basename(libpath)
        # Blender has a limit of 63 characters for any data name.
        # If the filepath is longer, it will be truncated.
        if len(filepath) > 63:
            filepath = filepath[:63]
        library = bpy.data.libraries.get(filepath)
        bpy.data.libraries.remove(library)

        return container, members

    def process_asset(
        self, context: dict, name: str, namespace: Optional[str] = None,
        options: Optional[Dict] = None
    ) -> Optional[List]:
        """
        Arguments:
            name: Use pre-defined name
            namespace: Use pre-defined namespace
            context: Full parenthood of representation to load
            options: Additional settings dictionary
        """
        libpath = self.filepath_from_context(context)
        folder_name = context["folder"]["name"]
        product_name = context["product"]["name"]

        try:
            product_type = context["product"]["productType"]
        except ValueError:
            product_type = "model"

        asset_name = plugin.prepare_scene_name(folder_name, product_name)
        unique_number = plugin.get_unique_number(folder_name, product_name)
        group_name = plugin.prepare_scene_name(
            folder_name, product_name, unique_number
        )
        namespace = namespace or f"{folder_name}_{unique_number}"

        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
        if not avalon_container:
            avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
            bpy.context.scene.collection.children.link(avalon_container)

        container, members = self._process_data(
            libpath, group_name, product_type
        )

        avalon_container.children.link(container)

        data = {
            "schema": "openpype:container-2.0",
            "id": AVALON_CONTAINER_ID,
            "name": name,
            "namespace": namespace or '',
            "loader": str(self.__class__.__name__),
            "representation": context["representation"]["id"],
            "libpath": libpath,
            "asset_name": asset_name,
            "parent": context["representation"]["versionId"],
            "productType": context["product"]["productType"],
            "objectName": group_name,
            "members": members,
            "project_name": context["project"]["name"],
        }

        container[AVALON_PROPERTY] = data

        objects = [
            obj for obj in bpy.data.objects
            if obj.name.startswith(f"{group_name}:")
        ]

        self[:] = objects
        return objects

    def exec_update(self, container: Dict, context: Dict):
        """
        Update the loaded asset.
        """
        repre_entity = context["representation"]
        group_name = container["objectName"]
        asset_group = bpy.data.collections.get(group_name)
        libpath = Path(get_representation_path(repre_entity)).as_posix()

        assert asset_group, (
            f"The asset is not loaded: {container['objectName']}"
        )

        # Get the parents of the members of the asset group, so we can
        # re-link them after the update.
        # Also gets the transform for each object to reapply after the update.
        collection_parents = {}
        member_transforms = {}
        members = asset_group.get(AVALON_PROPERTY).get("members", [])
        loaded_collections = {c for c in bpy.data.collections if c in members}
        loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))
        for member in members:
            if isinstance(member, bpy.types.Object):
                member_parents = set(member.users_collection)
                member_transforms[member.name] = member.matrix_basis.copy()
            elif isinstance(member, bpy.types.Collection):
                member_parents = {
                    c for c in bpy.data.collections if c.user_of_id(member)}
            else:
                continue

            member_parents = member_parents.difference(loaded_collections)
            if member_parents:
                collection_parents[member.name] = list(member_parents)

        old_data = dict(asset_group.get(AVALON_PROPERTY))

        self.exec_remove(container)

        product_type = container.get("productType")
        if product_type is None:
            product_type = container["family"]
        asset_group, members = self._process_data(
            libpath, group_name, product_type
        )

        for member in members:
            if member.name in collection_parents:
                for parent in collection_parents[member.name]:
                    if isinstance(member, bpy.types.Object):
                        parent.objects.link(member)
                    elif isinstance(member, bpy.types.Collection):
                        parent.children.link(member)
            if member.name in member_transforms and isinstance(
                member, bpy.types.Object
            ):
                member.matrix_basis = member_transforms[member.name]

        avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
        avalon_container.children.link(asset_group)

        # Restore the old data, but reset members, as they don't exist anymore
        # This avoids a crash, because the memory addresses of those members
        # are not valid anymore
        old_data["members"] = []
        asset_group[AVALON_PROPERTY] = old_data

        new_data = {
            "libpath": libpath,
            "representation": repre_entity["id"],
            "parent": repre_entity["versionId"],
            "members": members,
            "project_name": context["project"]["name"],
        }

        imprint(asset_group, new_data)

    def exec_remove(self, container: Dict) -> bool:
        """
        Remove an existing container from a Blender scene.
        """
        group_name = container["objectName"]
        asset_group = bpy.data.collections.get(group_name)

        members = set(asset_group.get(AVALON_PROPERTY).get("members", []))

        if members:
            for attr_name in dir(bpy.data):
                attr = getattr(bpy.data, attr_name)
                if not isinstance(attr, bpy.types.bpy_prop_collection):
                    continue

                # ensure to make a list copy because we
                # we remove members as we iterate
                for data in list(attr):
                    if data not in members or data == asset_group:
                        continue

                    attr.remove(data)

        bpy.data.collections.remove(asset_group)

exec_remove(container)

Remove an existing container from a Blender scene.

Source code in client/ayon_blender/plugins/load/load_blendscene.py
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
def exec_remove(self, container: Dict) -> bool:
    """
    Remove an existing container from a Blender scene.
    """
    group_name = container["objectName"]
    asset_group = bpy.data.collections.get(group_name)

    members = set(asset_group.get(AVALON_PROPERTY).get("members", []))

    if members:
        for attr_name in dir(bpy.data):
            attr = getattr(bpy.data, attr_name)
            if not isinstance(attr, bpy.types.bpy_prop_collection):
                continue

            # ensure to make a list copy because we
            # we remove members as we iterate
            for data in list(attr):
                if data not in members or data == asset_group:
                    continue

                attr.remove(data)

    bpy.data.collections.remove(asset_group)

exec_update(container, context)

Update the loaded asset.

Source code in client/ayon_blender/plugins/load/load_blendscene.py
137
138
139
140
141
142
143
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
205
206
207
208
209
210
211
212
def exec_update(self, container: Dict, context: Dict):
    """
    Update the loaded asset.
    """
    repre_entity = context["representation"]
    group_name = container["objectName"]
    asset_group = bpy.data.collections.get(group_name)
    libpath = Path(get_representation_path(repre_entity)).as_posix()

    assert asset_group, (
        f"The asset is not loaded: {container['objectName']}"
    )

    # Get the parents of the members of the asset group, so we can
    # re-link them after the update.
    # Also gets the transform for each object to reapply after the update.
    collection_parents = {}
    member_transforms = {}
    members = asset_group.get(AVALON_PROPERTY).get("members", [])
    loaded_collections = {c for c in bpy.data.collections if c in members}
    loaded_collections.add(bpy.data.collections.get(AVALON_CONTAINERS))
    for member in members:
        if isinstance(member, bpy.types.Object):
            member_parents = set(member.users_collection)
            member_transforms[member.name] = member.matrix_basis.copy()
        elif isinstance(member, bpy.types.Collection):
            member_parents = {
                c for c in bpy.data.collections if c.user_of_id(member)}
        else:
            continue

        member_parents = member_parents.difference(loaded_collections)
        if member_parents:
            collection_parents[member.name] = list(member_parents)

    old_data = dict(asset_group.get(AVALON_PROPERTY))

    self.exec_remove(container)

    product_type = container.get("productType")
    if product_type is None:
        product_type = container["family"]
    asset_group, members = self._process_data(
        libpath, group_name, product_type
    )

    for member in members:
        if member.name in collection_parents:
            for parent in collection_parents[member.name]:
                if isinstance(member, bpy.types.Object):
                    parent.objects.link(member)
                elif isinstance(member, bpy.types.Collection):
                    parent.children.link(member)
        if member.name in member_transforms and isinstance(
            member, bpy.types.Object
        ):
            member.matrix_basis = member_transforms[member.name]

    avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
    avalon_container.children.link(asset_group)

    # Restore the old data, but reset members, as they don't exist anymore
    # This avoids a crash, because the memory addresses of those members
    # are not valid anymore
    old_data["members"] = []
    asset_group[AVALON_PROPERTY] = old_data

    new_data = {
        "libpath": libpath,
        "representation": repre_entity["id"],
        "parent": repre_entity["versionId"],
        "members": members,
        "project_name": context["project"]["name"],
    }

    imprint(asset_group, new_data)

process_asset(context, name, namespace=None, options=None)

Parameters:

Name Type Description Default
name str

Use pre-defined name

required
namespace Optional[str]

Use pre-defined namespace

None
context dict

Full parenthood of representation to load

required
options Optional[Dict]

Additional settings dictionary

None
Source code in client/ayon_blender/plugins/load/load_blendscene.py
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def process_asset(
    self, context: dict, name: str, namespace: Optional[str] = None,
    options: Optional[Dict] = None
) -> Optional[List]:
    """
    Arguments:
        name: Use pre-defined name
        namespace: Use pre-defined namespace
        context: Full parenthood of representation to load
        options: Additional settings dictionary
    """
    libpath = self.filepath_from_context(context)
    folder_name = context["folder"]["name"]
    product_name = context["product"]["name"]

    try:
        product_type = context["product"]["productType"]
    except ValueError:
        product_type = "model"

    asset_name = plugin.prepare_scene_name(folder_name, product_name)
    unique_number = plugin.get_unique_number(folder_name, product_name)
    group_name = plugin.prepare_scene_name(
        folder_name, product_name, unique_number
    )
    namespace = namespace or f"{folder_name}_{unique_number}"

    avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
    if not avalon_container:
        avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
        bpy.context.scene.collection.children.link(avalon_container)

    container, members = self._process_data(
        libpath, group_name, product_type
    )

    avalon_container.children.link(container)

    data = {
        "schema": "openpype:container-2.0",
        "id": AVALON_CONTAINER_ID,
        "name": name,
        "namespace": namespace or '',
        "loader": str(self.__class__.__name__),
        "representation": context["representation"]["id"],
        "libpath": libpath,
        "asset_name": asset_name,
        "parent": context["representation"]["versionId"],
        "productType": context["product"]["productType"],
        "objectName": group_name,
        "members": members,
        "project_name": context["project"]["name"],
    }

    container[AVALON_PROPERTY] = data

    objects = [
        obj for obj in bpy.data.objects
        if obj.name.startswith(f"{group_name}:")
    ]

    self[:] = objects
    return objects