Skip to content

workfile_template_builder

Blender workfile template builder implementation

BlenderPlaceholderPlugin

Bases: PlaceholderPlugin

Base Placeholder Plugin for Blender with one unified cache.

Creates a locator as placeholder node, which during populate provide all of its attributes defined on the locator's transform in placeholder.data and where placeholder.scene_identifier is the full path to the node.

Inherited classes must still implement populate_placeholder

Source code in client/ayon_blender/api/workfile_template_builder.py
 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
class BlenderPlaceholderPlugin(PlaceholderPlugin):
    """Base Placeholder Plugin for Blender with one unified cache.

    Creates a locator as placeholder node, which during populate provide
    all of its attributes defined on the locator's transform in
    `placeholder.data` and where `placeholder.scene_identifier` is the
    full path to the node.

    Inherited classes must still implement `populate_placeholder`

    """

    use_selection_as_parent = True
    item_class = PlaceholderItem

    def _create_placeholder_name(self, placeholder_data):
        return self.identifier.replace(".", "_")

    def _collect_scene_placeholders(self):
        nodes_by_identifier = self.builder.get_shared_populate_data(
            "placeholder_nodes"
        )
        if nodes_by_identifier is None:
            # Cache placeholder data to shared data
            nodes = [
                node for node in bpy.data.collections
                if get_ayon_property(node)
            ]

            nodes_by_identifier = {}
            for node in nodes:
                ayon_prop = get_ayon_property(node)
                identifier = ayon_prop.get("plugin_identifier")
                nodes_by_identifier.setdefault(identifier, []).append(node.name)

            # Set the cache
            self.builder.set_shared_populate_data(
                "placeholder_nodes", nodes_by_identifier
            )
        return nodes_by_identifier

    def create_placeholder(self, placeholder_data):

        parent_object = None
        if self.use_selection_as_parent:
            selection = get_selected_collections()
            if len(selection) > 1:
                raise ValueError(
                    "More than one collection is selected. "
                    "Please select only one to define the parent."
                )
            parent_object = selection[0] if selection else None

        placeholder_data["plugin_identifier"] = self.identifier
        placeholder_name = self._create_placeholder_name(placeholder_data)

        placeholder = bpy.data.collections.new(placeholder_name)
        if parent_object:
            parent_object.children.link(placeholder)
            imprinted_placeholder = parent_object
        else:
            bpy.context.scene.collection.children.link(placeholder)
            imprinted_placeholder = placeholder

        imprint(imprinted_placeholder, placeholder_data)

    def update_placeholder(self, placeholder_item, placeholder_data):
        node_name = placeholder_item.scene_identifier

        changed_values = {}
        for key, value in placeholder_data.items():
            if value != placeholder_item.data.get(key):
                changed_values[key] = value

        target_collection = bpy.data.collections.get(node_name)
        target_property = get_ayon_property(target_collection)
        # Delete attributes to ensure we imprint new data with correct type
        for key in changed_values.keys():
            if key in target_property:
                target_property.pop(key, None)

        imprint(target_collection, changed_values)

    def collect_placeholders(self):
        placeholders = []
        nodes_by_identifier = self._collect_scene_placeholders()
        for node_name in nodes_by_identifier.get(self.identifier, []):
            # TODO do data validations and maybe upgrades if they are invalid
            node = bpy.data.collections.get(node_name)
            placeholder_data = get_ayon_property(node)
            # Convert IDPropertyGroup to dict to avoid pickle errors
            if placeholder_data:
                placeholder_data = dict(placeholder_data)
            placeholders.append(
                self.item_class(scene_identifier=node_name,
                                data=placeholder_data,
                                plugin=self)
            )

        return placeholders

    def delete_placeholder(self, placeholder):
        """Remove placeholder if building was successful

        Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.
        """
        node = placeholder.scene_identifier
        node_to_removed = bpy.data.collections.get(node)
        if node_to_removed:
            bpy.data.collections.remove(node_to_removed)

delete_placeholder(placeholder)

Remove placeholder if building was successful

Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.

Source code in client/ayon_blender/api/workfile_template_builder.py
146
147
148
149
150
151
152
153
154
def delete_placeholder(self, placeholder):
    """Remove placeholder if building was successful

    Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.
    """
    node = placeholder.scene_identifier
    node_to_removed = bpy.data.collections.get(node)
    if node_to_removed:
        bpy.data.collections.remove(node_to_removed)

BlenderTemplateBuilder

Bases: AbstractTemplateBuilder

Concrete implementation of AbstractTemplateBuilder for Blender

Source code in client/ayon_blender/api/workfile_template_builder.py
30
31
32
33
34
35
36
37
38
39
40
41
42
class BlenderTemplateBuilder(AbstractTemplateBuilder):
    """Concrete implementation of AbstractTemplateBuilder for Blender"""
    def import_template(self, path):
        """Import template into current scene.
        Block if a template is already loaded.

        Args:
            path (str): A path to current template (usually given by
            get_template_preset implementation)

        """
        bpy.ops.wm.read_homefile(filepath=path)
        update_content_on_context_change()

import_template(path)

Import template into current scene. Block if a template is already loaded.

Parameters:

Name Type Description Default
path str

A path to current template (usually given by

required
Source code in client/ayon_blender/api/workfile_template_builder.py
32
33
34
35
36
37
38
39
40
41
42
def import_template(self, path):
    """Import template into current scene.
    Block if a template is already loaded.

    Args:
        path (str): A path to current template (usually given by
        get_template_preset implementation)

    """
    bpy.ops.wm.read_homefile(filepath=path)
    update_content_on_context_change()

build_workfile_template(*args)

Build the workfile template.

Source code in client/ayon_blender/api/workfile_template_builder.py
199
200
201
202
def build_workfile_template(*args) -> None:
    """Build the workfile template."""
    builder = BlenderTemplateBuilder(registered_host())
    builder.build_template()

create_first_workfile_from_template()

Create the first workfile from template for Blender.

Source code in client/ayon_blender/api/workfile_template_builder.py
187
188
189
190
191
192
193
194
195
196
def create_first_workfile_from_template() -> None:
    """Create the first workfile from template for Blender."""
    builder = BlenderTemplateBuilder(registered_host())
    try:
        builder.build_template(workfile_creation_enabled=True)

    except TemplateProfileNotFound:
        log.warning(
            "Template profile not found. Skipping..."
        )

create_placeholder(*args, **kwargs)

Create Workfile Placeholder for Blender.

Source code in client/ayon_blender/api/workfile_template_builder.py
218
219
220
221
222
223
224
225
226
def create_placeholder(*args, **kwargs):
    """Create Workfile Placeholder for Blender."""
    host = registered_host()
    builder = BlenderTemplateBuilder(host)
    parent = kwargs.get("parent")
    window = WorkfileBuildPlaceholderDialog(host, builder,
                                            parent=parent)
    window.show()
    return window

open_template(*args, **kwargs)

Open workfile template for Blender.

Source code in client/ayon_blender/api/workfile_template_builder.py
211
212
213
214
215
def open_template(*args, **kwargs):
    """Open workfile template for Blender."""
    builder = BlenderTemplateBuilder(registered_host())
    main_window = kwargs.get("main_window")
    open_template_ui(builder, main_window=main_window)

set_folder_path_for_ayon_instances(folder_path)

Set the folder path for AYON instances in the Blender scene.

Parameters:

Name Type Description Default
folder_path str

The folder path to set for AYON instances.

required
Source code in client/ayon_blender/api/workfile_template_builder.py
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
def set_folder_path_for_ayon_instances(folder_path: str) -> None:
    """Set the folder path for AYON instances in the Blender scene.

    Args:
        folder_path (str): The folder path to set for AYON instances.
    """
    ayon_instances = bpy.data.collections.get(AYON_INSTANCES)
    ayon_instance_objs = (
        ayon_instances.objects if ayon_instances else []
    )

    # Consider any node tree objects as well
    node_tree_objects = []
    node_tree = get_scene_node_tree()
    if node_tree:
        node_tree_objects = node_tree.nodes

    for obj_or_col in itertools.chain(
            ayon_instance_objs,
            bpy.data.collections,
            node_tree_objects
    ):
        ayon_prop = get_ayon_property(obj_or_col)
        if not ayon_prop:
            continue
        if not ayon_prop.get("folderPath"):
            continue

        imprint(obj_or_col, {"folderPath": folder_path})

update_placeholder(*args, **kwargs)

Update Workfile Placeholder for Blender.

Source code in client/ayon_blender/api/workfile_template_builder.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
def update_placeholder(*args, **kwargs):
    """Update Workfile Placeholder for Blender."""
    host = registered_host()
    builder = BlenderTemplateBuilder(host)
    placeholder_items_by_id = {
        placeholder_item.scene_identifier: placeholder_item
        for placeholder_item in builder.get_placeholders()
    }
    placeholder_items = []
    for node in get_selected_collections():
        if node.name in placeholder_items_by_id:
            placeholder_items.append(placeholder_items_by_id[node.name])

    # TODO show UI at least
    if len(placeholder_items) == 0:
        raise ValueError("No node selected")

    if len(placeholder_items) > 1:
        raise ValueError("Too many selected nodes")

    placeholder_item = placeholder_items[0]
    parent = kwargs.get("parent")
    window = WorkfileBuildPlaceholderDialog(host, builder,
                                            parent=parent)
    window.set_update_mode(placeholder_item)
    window.show()
    return window