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
 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
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 post_placeholder_process(self, placeholder, failed):
        """Cleanup placeholder after load of its corresponding representations.

        Hide placeholder, add them to placeholder set.
        Used only by PlaceholderCreateMixin and PlaceholderLoadMixin

        Args:
            placeholder (PlaceholderItem): Item which was just used to load
                representation.
            failed (bool): Loading of representation failed.
        """
        # Hide placeholder and add them to placeholder set
        node = placeholder.scene_identifier

        # If we just populate the placeholders from current scene, the
        # placeholder set will not be created so account for that.
        placeholder_set = bpy.data.collections.get(PLACEHOLDER_SET)
        if placeholder_set:
            placeholder_set = bpy.data.collections.new(name=PLACEHOLDER_SET)
        placeholder_set.children = node

    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
192
193
194
195
196
197
198
199
200
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)

post_placeholder_process(placeholder, failed)

Cleanup placeholder after load of its corresponding representations.

Hide placeholder, add them to placeholder set. Used only by PlaceholderCreateMixin and PlaceholderLoadMixin

Parameters:

Name Type Description Default
placeholder PlaceholderItem

Item which was just used to load representation.

required
failed bool

Loading of representation failed.

required
Source code in client/ayon_blender/api/workfile_template_builder.py
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def post_placeholder_process(self, placeholder, failed):
    """Cleanup placeholder after load of its corresponding representations.

    Hide placeholder, add them to placeholder set.
    Used only by PlaceholderCreateMixin and PlaceholderLoadMixin

    Args:
        placeholder (PlaceholderItem): Item which was just used to load
            representation.
        failed (bool): Loading of representation failed.
    """
    # Hide placeholder and add them to placeholder set
    node = placeholder.scene_identifier

    # If we just populate the placeholders from current scene, the
    # placeholder set will not be created so account for that.
    placeholder_set = bpy.data.collections.get(PLACEHOLDER_SET)
    if placeholder_set:
        placeholder_set = bpy.data.collections.new(name=PLACEHOLDER_SET)
    placeholder_set.children = node

BlenderTemplateBuilder

Bases: AbstractTemplateBuilder

Concrete implementation of AbstractTemplateBuilder for Blender

Source code in client/ayon_blender/api/workfile_template_builder.py
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
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)

        Returns:
            bool: Whether the template was successfully imported or not
        """
        if bpy.data.collections.get(PLACEHOLDER_SET):
            raise TemplateAlreadyImported((
                "Build template already loaded\n"
                "Clean scene if needed (File > New Scene)"
            ))

        placeholder_collection = bpy.data.collections.new(PLACEHOLDER_SET)
        bpy.context.scene.collection.children.link(placeholder_collection)
        filepath = Path(path)
        if not filepath.exists():
            return False

        with bpy.data.libraries.load(filepath.as_posix()) as (data_src, data_dst):
            data_dst.collections = data_src.collections
            data_dst.objects = data_src.objects

        for target_object in data_dst.objects:
            bpy.context.scene.collection.objects.link(target_object)
        for target_collection in data_dst.collections:
            bpy.context.scene.collection.children.link(target_collection)

        # update imported sets information
        update_content_on_context_change()
        return True

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

Returns:

Name Type Description
bool

Whether the template was successfully imported or not

Source code in client/ayon_blender/api/workfile_template_builder.py
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
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)

    Returns:
        bool: Whether the template was successfully imported or not
    """
    if bpy.data.collections.get(PLACEHOLDER_SET):
        raise TemplateAlreadyImported((
            "Build template already loaded\n"
            "Clean scene if needed (File > New Scene)"
        ))

    placeholder_collection = bpy.data.collections.new(PLACEHOLDER_SET)
    bpy.context.scene.collection.children.link(placeholder_collection)
    filepath = Path(path)
    if not filepath.exists():
        return False

    with bpy.data.libraries.load(filepath.as_posix()) as (data_src, data_dst):
        data_dst.collections = data_src.collections
        data_dst.objects = data_src.objects

    for target_object in data_dst.objects:
        bpy.context.scene.collection.objects.link(target_object)
    for target_collection in data_dst.collections:
        bpy.context.scene.collection.children.link(target_collection)

    # update imported sets information
    update_content_on_context_change()
    return True

build_workfile_template(*args)

Build the workfile template.

Source code in client/ayon_blender/api/workfile_template_builder.py
239
240
241
242
def build_workfile_template(*args) -> None:
    """Build the workfile template."""
    builder = BlenderTemplateBuilder(registered_host())
    builder.build_template()

create_first_workfile_from_template(*args)

Create the first workfile from template for Blender.

Source code in client/ayon_blender/api/workfile_template_builder.py
233
234
235
236
def create_first_workfile_from_template(*args) -> None:
    """Create the first workfile from template for Blender."""
    builder = BlenderTemplateBuilder(registered_host())
    builder.build_template(workfile_creation_enabled=True)

create_placeholder(*args, **kwargs)

Create Workfile Placeholder for Blender.

Source code in client/ayon_blender/api/workfile_template_builder.py
259
260
261
262
263
264
265
266
267
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
251
252
253
254
255
256
def open_template(*args, **kwargs) -> None:
    """Open workfile template for Blender."""
    builder = BlenderTemplateBuilder(registered_host())
    main_window = kwargs.get("main_window")
    open_template_ui(builder, main_window=main_window)
    return 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
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
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
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
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