Skip to content

workfile_template_builder

MayaPlaceholderPlugin

Bases: PlaceholderPlugin

Base Placeholder Plugin for Maya 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_maya/api/workfile_template_builder.py
 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
238
239
240
241
242
243
244
245
246
class MayaPlaceholderPlugin(PlaceholderPlugin):
    """Base Placeholder Plugin for Maya 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 = cmds.ls("*.plugin_identifier", long=True, objectsOnly=True)

            nodes_by_identifier = {}
            for node in nodes:
                identifier = cmds.getAttr("{}.plugin_identifier".format(node))
                nodes_by_identifier.setdefault(identifier, []).append(node)

            # 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 = None
        if self.use_selection_as_parent:
            selection = cmds.ls(selection=True)
            if len(selection) > 1:
                raise ValueError(
                    "More than one node is selected. "
                    "Please select only one to define the parent."
                )
            parent = selection[0] if selection else None

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

        placeholder = cmds.spaceLocator(name=placeholder_name)[0]
        if parent:
            placeholder = cmds.parent(placeholder, selection[0])[0]

        self.imprint(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
        for key in changed_values.keys():
            placeholder_item.data[key] = value
            if cmds.attributeQuery(key, node=node_name, exists=True):
                attribute = "{}.{}".format(node_name, key)
                cmds.deleteAttr(attribute)

        self.imprint(node_name, changed_values)

    def collect_placeholders(self):
        placeholders = []
        nodes_by_identifier = self._collect_scene_placeholders()
        for node in nodes_by_identifier.get(self.identifier, []):
            # TODO do data validations and maybe upgrades if they are invalid
            placeholder_data = self.read(node)
            placeholders.append(
                self.item_class(scene_identifier=node,
                                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.
        if not cmds.objExists(PLACEHOLDER_SET):
            cmds.sets(name=PLACEHOLDER_SET, empty=True)

        cmds.sets(node, addElement=PLACEHOLDER_SET)
        cmds.hide(node)
        cmds.setAttr("{}.hiddenInOutliner".format(node), True)

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

        Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.
        """
        node = placeholder.scene_identifier

        # To avoid that deleting a placeholder node will have Maya delete
        # any objectSets the node was a member of we will first remove it
        # from any sets it was a member of. This way the `PLACEHOLDERS_SET`
        # will survive long enough
        sets = cmds.listSets(o=node) or []
        for object_set in sets:
            cmds.sets(node, remove=object_set)

        cmds.delete(node)

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

        # Complicated data that can't be represented as flat maya 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)

    def read(self, node):
        """Read call for placeholder node"""

        data = read(node)

        # Complicated data that can't be represented as flat maya attributes
        # we read from json strings, e.g. multiselection EnumDef
        for key, value in data.items():
            if isinstance(value, str) and value.startswith("JSON::"):
                value = value[len("JSON::"):]   # strip of JSON:: prefix
                data[key] = json.loads(value)

        return data

delete_placeholder(placeholder)

Remove placeholder if building was successful

Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.

Source code in client/ayon_maya/api/workfile_template_builder.py
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
def delete_placeholder(self, placeholder):
    """Remove placeholder if building was successful

    Used only by PlaceholderCreateMixin and PlaceholderLoadMixin.
    """
    node = placeholder.scene_identifier

    # To avoid that deleting a placeholder node will have Maya delete
    # any objectSets the node was a member of we will first remove it
    # from any sets it was a member of. This way the `PLACEHOLDERS_SET`
    # will survive long enough
    sets = cmds.listSets(o=node) or []
    for object_set in sets:
        cmds.sets(node, remove=object_set)

    cmds.delete(node)

imprint(node, data)

Imprint call for placeholder node

Source code in client/ayon_maya/api/workfile_template_builder.py
223
224
225
226
227
228
229
230
231
232
def imprint(self, node, data):
    """Imprint call for placeholder node"""

    # Complicated data that can't be represented as flat maya 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_maya/api/workfile_template_builder.py
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
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.
    if not cmds.objExists(PLACEHOLDER_SET):
        cmds.sets(name=PLACEHOLDER_SET, empty=True)

    cmds.sets(node, addElement=PLACEHOLDER_SET)
    cmds.hide(node)
    cmds.setAttr("{}.hiddenInOutliner".format(node), True)

read(node)

Read call for placeholder node

Source code in client/ayon_maya/api/workfile_template_builder.py
234
235
236
237
238
239
240
241
242
243
244
245
246
def read(self, node):
    """Read call for placeholder node"""

    data = read(node)

    # Complicated data that can't be represented as flat maya attributes
    # we read from json strings, e.g. multiselection EnumDef
    for key, value in data.items():
        if isinstance(value, str) and value.startswith("JSON::"):
            value = value[len("JSON::"):]   # strip of JSON:: prefix
            data[key] = json.loads(value)

    return data

MayaTemplateBuilder

Bases: AbstractTemplateBuilder

Concrete implementation of AbstractTemplateBuilder for maya

Source code in client/ayon_maya/api/workfile_template_builder.py
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
class MayaTemplateBuilder(AbstractTemplateBuilder):
    """Concrete implementation of AbstractTemplateBuilder for maya"""

    use_legacy_creators = True

    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 cmds.objExists(PLACEHOLDER_SET):
            raise TemplateAlreadyImported((
                "Build template already loaded\n"
                "Clean scene if needed (File > New Scene)"
            ))

        cmds.sets(name=PLACEHOLDER_SET, empty=True)
        new_nodes = cmds.file(
            path,
            i=True,
            returnNewNodes=True,
            preserveReferences=True,
            loadReferenceDepth="all",
        )

        # make default cameras non-renderable
        default_cameras = [cam for cam in cmds.ls(cameras=True)
                           if cmds.camera(cam, query=True, startupCamera=True)]
        for cam in default_cameras:
            if not cmds.attributeQuery("renderable", node=cam, exists=True):
                self.log.debug(
                    "Camera {} has no attribute 'renderable'".format(cam)
                )
                continue
            cmds.setAttr("{}.renderable".format(cam), 0)

        cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True)

        imported_sets = cmds.ls(new_nodes, set=True)
        if not imported_sets:
            return True

        # update imported sets information
        folder_path = get_current_folder_path()
        for node in imported_sets:
            if not cmds.attributeQuery("id", node=node, exists=True):
                continue
            if cmds.getAttr("{}.id".format(node)) not in {
                AYON_INSTANCE_ID, AVALON_INSTANCE_ID
            }:
                continue
            if not cmds.attributeQuery("folderPath", node=node, exists=True):
                continue

            cmds.setAttr(
                "{}.folderPath".format(node), folder_path, type="string")

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

    cmds.sets(name=PLACEHOLDER_SET, empty=True)
    new_nodes = cmds.file(
        path,
        i=True,
        returnNewNodes=True,
        preserveReferences=True,
        loadReferenceDepth="all",
    )

    # make default cameras non-renderable
    default_cameras = [cam for cam in cmds.ls(cameras=True)
                       if cmds.camera(cam, query=True, startupCamera=True)]
    for cam in default_cameras:
        if not cmds.attributeQuery("renderable", node=cam, exists=True):
            self.log.debug(
                "Camera {} has no attribute 'renderable'".format(cam)
            )
            continue
        cmds.setAttr("{}.renderable".format(cam), 0)

    cmds.setAttr(PLACEHOLDER_SET + ".hiddenInOutliner", True)

    imported_sets = cmds.ls(new_nodes, set=True)
    if not imported_sets:
        return True

    # update imported sets information
    folder_path = get_current_folder_path()
    for node in imported_sets:
        if not cmds.attributeQuery("id", node=node, exists=True):
            continue
        if cmds.getAttr("{}.id".format(node)) not in {
            AYON_INSTANCE_ID, AVALON_INSTANCE_ID
        }:
            continue
        if not cmds.attributeQuery("folderPath", node=node, exists=True):
            continue

        cmds.setAttr(
            "{}.folderPath".format(node), folder_path, type="string")

    return True