Skip to content

workfile_template_builder

3dsmax workfile template builder implementation

MaxPlaceholderPlugin

Bases: PlaceholderPlugin

Base Placeholder Plugin for 3ds Max 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_max/api/workfile_template_builder.py
 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
class MaxPlaceholderPlugin(PlaceholderPlugin):
    """Base Placeholder Plugin for 3ds Max 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 rt.Objects
                if rt.doesUserPropExist(node, "plugin_identifier")
            ]

            nodes_by_identifier = {}
            for node in nodes:
                identifier = rt.getUserProp(node, "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 = rt.getCurrentSelection()
            if len(selection) > 1:
                raise ValueError(
                    "More than one node 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 = rt.Container(name=placeholder_name)
        if parent_object:
            parent_object.children = placeholder
            imprinted_placeholder = parent_object.name
        else:
            imprinted_placeholder = placeholder.name

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

        # Delete attributes to ensure we imprint new data with correct type
        target_node = rt.getNodeByName(node_name)
        for key in changed_values.keys():
            placeholder_item.data[key] = value
            if rt.getUserProp(target_node, key) is not None:
                rt.deleteUserProp(target_node, key)

        self.imprint(node_name, 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 = rt.getNodeByName(node_name)
            placeholder_data = read(node)
            # Ensure order is converted to int for proper sorting
            if "order" in placeholder_data:
                placeholder_data["order"] = int(placeholder_data["order"])
            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 = rt.getNodebyName(PLACEHOLDER_SET)
        if placeholder_set:
            placeholder_set = rt.Container(name=PLACEHOLDER_SET)
        placeholder_set.children = node
        node.isHidden = True

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

        Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.
        """
        node = placeholder.scene_identifier
        node_to_removed = rt.getNodeByName(node)
        if node_to_removed:
            rt.Delete(node_to_removed)

    def imprint(self, node, data):
        """Imprint call for placeholder node"""

        # Complicated data that can't be represented as flat 3dsmax attributes
        # we write to json strings, e.g. multiselection EnumDef
        for key, value in data.items():
            if isinstance(value, (list, tuple, dict)):
                data[key] = "JSON::{}".format(json.dumps(value))

        imprint(node, data)

delete_placeholder(placeholder)

Remove placeholder if building was successful

Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.

Source code in client/ayon_max/api/workfile_template_builder.py
197
198
199
200
201
202
203
204
205
def delete_placeholder(self, placeholder):
    """Remove placeholder if building was successful

    Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.
    """
    node = placeholder.scene_identifier
    node_to_removed = rt.getNodeByName(node)
    if node_to_removed:
        rt.Delete(node_to_removed)

imprint(node, data)

Imprint call for placeholder node

Source code in client/ayon_max/api/workfile_template_builder.py
207
208
209
210
211
212
213
214
215
216
def imprint(self, node, data):
    """Imprint call for placeholder node"""

    # Complicated data that can't be represented as flat 3dsmax attributes
    # we write to json strings, e.g. multiselection EnumDef
    for key, value in data.items():
        if isinstance(value, (list, tuple, dict)):
            data[key] = "JSON::{}".format(json.dumps(value))

    imprint(node, data)

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_max/api/workfile_template_builder.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
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 = rt.getNodebyName(PLACEHOLDER_SET)
    if placeholder_set:
        placeholder_set = rt.Container(name=PLACEHOLDER_SET)
    placeholder_set.children = node
    node.isHidden = True

MaxTemplateBuilder

Bases: AbstractTemplateBuilder

Concrete implementation of AbstractTemplateBuilder for 3dsmax

Source code in client/ayon_max/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
69
70
71
72
class MaxTemplateBuilder(AbstractTemplateBuilder):
    """Concrete implementation of AbstractTemplateBuilder for 3dsmax"""
    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 rt.getNodeByName(PLACEHOLDER_SET):
            raise TemplateAlreadyImported((
                "Build template already loaded\n"
                "Clean scene if needed (File > New Scene)"
            ))

        placeholder_container = rt.Container(name=PLACEHOLDER_SET)
        placeholder_container.isHidden = True

        filepath = Path(path)
        if not filepath.exists():
            return False
        rt.MergeMaxFile(
            filepath.as_posix(),
            rt.Name("deleteOldDups"),
            rt.Name("useMergedMtlDups"),
            quiet=True,
            includeFullGroup=True
        )
        max_objects = rt.getLastMergedNodes()
        if not max_objects:
            return True

        # 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_max/api/workfile_template_builder.py
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
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 rt.getNodeByName(PLACEHOLDER_SET):
        raise TemplateAlreadyImported((
            "Build template already loaded\n"
            "Clean scene if needed (File > New Scene)"
        ))

    placeholder_container = rt.Container(name=PLACEHOLDER_SET)
    placeholder_container.isHidden = True

    filepath = Path(path)
    if not filepath.exists():
        return False
    rt.MergeMaxFile(
        filepath.as_posix(),
        rt.Name("deleteOldDups"),
        rt.Name("useMergedMtlDups"),
        quiet=True,
        includeFullGroup=True
    )
    max_objects = rt.getLastMergedNodes()
    if not max_objects:
        return True

    # update imported sets information
    update_content_on_context_change()
    return True

build_workfile_template(*args)

Build the workfile template for 3ds Max.

Source code in client/ayon_max/api/workfile_template_builder.py
225
226
227
228
def build_workfile_template(*args) -> None:
    """Build the workfile template for 3ds Max."""
    builder = MaxTemplateBuilder(registered_host())
    builder.build_template()

create_first_workfile_from_template(*args)

Create the first workfile from template for 3ds Max.

Source code in client/ayon_max/api/workfile_template_builder.py
219
220
221
222
def create_first_workfile_from_template(*args) -> None:
    """Create the first workfile from template for 3ds Max."""
    builder = MaxTemplateBuilder(registered_host())
    builder.build_template(workfile_creation_enabled=True)

create_placeholder(*args)

Create Workfile Placeholder for 3ds Max.

Source code in client/ayon_max/api/workfile_template_builder.py
244
245
246
247
248
249
250
def create_placeholder(*args) -> None:
    """Create Workfile Placeholder for 3ds Max."""
    host = registered_host()
    builder = MaxTemplateBuilder(host)
    window = WorkfileBuildPlaceholderDialog(host, builder,
                                            parent=get_main_window())
    window.show()

open_template(*args)

Open the workfile template UI for 3ds Max.

Source code in client/ayon_max/api/workfile_template_builder.py
237
238
239
240
241
def open_template(*args) -> None:
    """Open the workfile template UI for 3ds Max."""
    host = registered_host()
    builder = MaxTemplateBuilder(host)
    open_template_ui(builder, main_window=get_main_window())

update_placeholder(*args)

Update Workfile Placeholder for 3ds Max.

Source code in client/ayon_max/api/workfile_template_builder.py
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
def update_placeholder(*args) -> None:
    """Update Workfile Placeholder for 3ds Max."""
    host = registered_host()
    builder = MaxTemplateBuilder(host)
    placeholder_items_by_id = {
        placeholder_item.scene_identifier: placeholder_item
        for placeholder_item in builder.get_placeholders()
    }
    placeholder_items = []
    for node in rt.getCurrentSelection():
        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]
    window = WorkfileBuildPlaceholderDialog(host, builder,
                                            parent=get_main_window())
    window.set_update_mode(placeholder_item)
    window.exec_()

update_workfile_template(*args)

Update the workfile template for 3ds Max.

Source code in client/ayon_max/api/workfile_template_builder.py
231
232
233
234
def update_workfile_template(*args) -> None:
    """Update the workfile template for 3ds Max."""
    builder = MaxTemplateBuilder(registered_host())
    builder.rebuild_template()