Skip to content

lib

Library of functions useful for 3dsmax pipeline.

build_general_output_filename(output_dir, filename)

Build a general output filename with the given directory, filename, and image format.

Parameters:

Name Type Description Default
output_dir str

The directory where the output file will be saved.

required
filename str

The base filename.

required

Returns:

Name Type Description
str str

The full path to the output file with the general naming convention.

Source code in client/ayon_max/api/lib.py
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
def build_general_output_filename(
    output_dir: str,
    filename: str,
) -> str:
    """Build a general output filename with the given directory, filename, and image format.

    Args:
        output_dir (str): The directory where the output file will be saved.
        filename (str): The base filename.

    Returns:
        str: The full path to the output file with the general naming convention.
    """
    generic_pattern = (
        r"^(?P<name>.+)\._(?P<element>[^.]+)\.(?P<ext>[a-zA-Z0-9]+)$"
    )
    match = re.match(generic_pattern, filename)
    if match:
        name = match.group("name")
        element = match.group("element")
        ext = match.group("ext")
        filename = f"{name}_{element}..{ext}"
    return os.path.join(output_dir, filename)

convert_unit_scale()

Convert system unit scale in 3dsMax for fbx export

Returns:

Name Type Description
str

unit scale

Source code in client/ayon_max/api/lib.py
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
def convert_unit_scale():
    """Convert system unit scale in 3dsMax
    for fbx export

    Returns:
        str: unit scale
    """
    unit_scale_dict = {
        "millimeters": "mm",
        "centimeters": "cm",
        "meters": "m",
        "kilometers": "km"
    }
    current_unit_scale = rt.Execute("units.MetricType as string")
    return unit_scale_dict[current_unit_scale]

ensure_sme_editor_active()

Ensure that Slate Material Editor is active during context

Source code in client/ayon_max/api/lib.py
891
892
893
894
895
896
897
898
899
900
901
902
@contextlib.contextmanager
def ensure_sme_editor_active():
    """Ensure that Slate Material Editor is active during context
    """
    was_open = rt.sme.isOpen()
    try:
        if not was_open:
            rt.sme.open()
        yield
    finally:
        if not was_open:
            rt.sme.close()

find_plugins(search_string)

Find if a plugin is loaded in 3dsMax

Parameters:

Name Type Description Default
search_string str

string to search for

required

Returns:

Name Type Description
bool bool

True if found, False otherwise

Source code in client/ayon_max/api/lib.py
765
766
767
768
769
770
771
772
773
774
775
776
def find_plugins(search_string: str) -> bool:
    """Find if a plugin is loaded in 3dsMax

    Args:
        search_string (str): string to search for

    Returns:
        bool: True if found, False otherwise
    """
    if any(search_string in plugin for plugin in get_plugins()):
        return True
    return False

get_all_children(parent, node_type=None)

Handy function to get all the children of a given node

Parameters:

Name Type Description Default
parent 3dsmax Node1

Node to get all children of.

required
node_type None, runtime.class

give class to check for e.g. rt.FFDBox/rt.GeometryClass etc.

None

Returns:

Name Type Description
list

list of all children of the parent node

Source code in client/ayon_max/api/lib.py
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
def get_all_children(parent, node_type=None):
    """Handy function to get all the children of a given node

    Args:
        parent (3dsmax Node1): Node to get all children of.
        node_type (None, runtime.class): give class to check for
            e.g. rt.FFDBox/rt.GeometryClass etc.

    Returns:
        list: list of all children of the parent node
    """
    def list_children(node):
        children = []
        for c in node.Children:
            children.append(c)
            children = children + list_children(c)
        return children
    child_list = list_children(parent)

    return ([x for x in child_list if rt.SuperClassOf(x) == node_type]
            if node_type else child_list)

get_ayon_data(container_modifier)

Get the AYON custom attribute data from container modifier

Parameters:

Name Type Description Default
container_modifier

container modifier

required

Returns:

Name Type Description
Property

AYONData custom attribute data

Source code in client/ayon_max/api/lib.py
966
967
968
969
970
971
972
973
974
975
976
977
978
def get_ayon_data(container_modifier):
    """Get the AYON custom attribute data from container modifier

    Args:
        container_modifier: container modifier

    Returns:
        Property: AYONData custom attribute data
    """
    if rt.isProperty(container_modifier, "AYONData"):
        return container_modifier.AYONData
    else:
        return container_modifier.openPypeData

get_current_renderer()

Notes

Get current renderer for Max

Returns:

Type Description

"{Current Renderer}:{Current Renderer}"

e.g. "Redshift_Renderer:Redshift_Renderer"

Source code in client/ayon_max/api/lib.py
223
224
225
226
227
228
229
230
231
232
def get_current_renderer():
    """
    Notes:
        Get current renderer for Max

    Returns:
        "{Current Renderer}:{Current Renderer}"
        e.g. "Redshift_Renderer:Redshift_Renderer"
    """
    return rt.renderers.production

get_default_render_folder(data, project_setting=None)

Get the default render folder path for current context based on project settings

Parameters:

Name Type Description Default
data Dict

template data

required
project_setting Dict

project settings. Defaults to None.

None

Returns:

Name Type Description
str str

The default render folder path.

Source code in client/ayon_max/api/lib.py
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def get_default_render_folder(
    data: Dict,
    project_setting: Dict = None,
) -> str:
    """Get the default render folder path for current context based on project settings

    Args:
        data (Dict): template data
        project_setting (Dict, optional): project settings. Defaults to None.

    Returns:
        str: The default render folder path.
    """
    render_data = _sanitize_template_data(dict(data))
    if project_setting is None:
        project_name = get_current_project_name()
        project_setting = get_project_settings(project_name)

    work_dir, render_data = get_work_default_directory_and_data(render_data)
    render_data["work"] = work_dir
    render_folder = project_setting["max"]["RenderSettings"]["default_render_image_folder"]
    formatted_render_folder = StringTemplate(render_folder).format(render_data)
    normalized_render_folder = Path(formatted_render_folder)
    if not normalized_render_folder.is_absolute():
        normalized_render_folder = Path(work_dir) / normalized_render_folder
    return str(normalized_render_folder)

get_fps_for_current_context()

Get fps that should be set for current context.

Todos
  • Skip project value.
  • Merge logic with 'get_frame_range' and 'reset_scene_resolution' -> all the values in the functions can be collected at one place as they have same requirements.

Returns:

Type Description

Union[int, float]: FPS value.

Source code in client/ayon_max/api/lib.py
471
472
473
474
475
476
477
478
479
480
481
482
483
484
def get_fps_for_current_context():
    """Get fps that should be set for current context.

    Todos:
        - Skip project value.
        - Merge logic with 'get_frame_range' and 'reset_scene_resolution' ->
            all the values in the functions can be collected at one place as
            they have same requirements.

    Returns:
        Union[int, float]: FPS value.
    """
    task_entity = get_current_task_entity(fields={"attrib"})
    return task_entity["attrib"]["fps"]

get_frame_range(task_entity=None)

Get the current task frame range and handles

Parameters:

Name Type Description Default
task_entity dict

Task Entity.

None

Returns:

Name Type Description
dict Union[Dict[str, Any], None]

with frame start, frame end, handle start, handle end.

Source code in client/ayon_max/api/lib.py
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
def get_frame_range(task_entity=None) -> Union[Dict[str, Any], None]:
    """Get the current task frame range and handles

    Args:
        task_entity (dict): Task Entity.

    Returns:
        dict: with frame start, frame end, handle start, handle end.
    """
    # Set frame start/end
    if task_entity is None:
        task_entity = get_current_task_entity(fields={"attrib"})
    task_attributes = task_entity["attrib"]
    frame_start = int(task_attributes["frameStart"])
    frame_end = int(task_attributes["frameEnd"])
    handle_start = int(task_attributes["handleStart"])
    handle_end = int(task_attributes["handleEnd"])
    frame_start_handle = frame_start - handle_start
    frame_end_handle = frame_end + handle_end

    return {
        "frameStart": frame_start,
        "frameEnd": frame_end,
        "handleStart": handle_start,
        "handleEnd": handle_end,
        "frameStartHandle": frame_start_handle,
        "frameEndHandle": frame_end_handle,
    }

get_main_window()

Acquire Max's main window

Source code in client/ayon_max/api/lib.py
71
72
73
74
75
76
77
78
79
80
81
82
def get_main_window():
    """Acquire Max's main window"""
    from qtpy import QtWidgets
    top_widgets = QtWidgets.QApplication.topLevelWidgets()
    name = "QmaxApplicationWindow"
    for widget in top_widgets:
        if (
            widget.inherits("QMainWindow")
            and widget.metaObject().className() == name
        ):
            return widget
    raise RuntimeError('Count not find 3dsMax main window.')

get_max_version()

Args: get max version date for deadline

Returns:

Type Description

(25000, 62, 0, 25, 0, 0, 997, 2023, "")

max_info[7] = max version date

Source code in client/ayon_max/api/lib.py
578
579
580
581
582
583
584
585
586
587
588
def get_max_version():
    """
    Args:
    get max version date for deadline

    Returns:
        #(25000, 62, 0, 25, 0, 0, 997, 2023, "")
        max_info[7] = max version date
    """
    max_info = rt.MaxVersion()
    return max_info[7]

get_multipass_setting(renderer, project_setting=None)

Get the multipass setting for the given renderer.

Parameters:

Name Type Description Default
renderer str

The name of the renderer.

required
project_setting dict

The project settings. Defaults to None.

None

Returns:

Name Type Description
bool

True if multipass is enabled, False otherwise.

Source code in client/ayon_max/api/lib.py
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
def get_multipass_setting(renderer, project_setting=None):
    """Get the multipass setting for the given renderer.

    Args:
        renderer (str): The name of the renderer.
        project_setting (dict, optional): The project settings. Defaults to None.

    Returns:
        bool: True if multipass is enabled, False otherwise.
    """
    if project_setting is None:
        project_setting = get_project_settings(
            get_current_project_name()
        )
    render_settings = (
        project_setting["max"]["RenderSettings"]
    )
    if renderer.startswith("V_Ray_"):
        vray_render_setting = render_settings.get("vray_render_settings", {})
        return (
            vray_render_setting.get("separate_render_channels", False)
        )
    elif renderer == "Redshift_Renderer":
        redshift_render_setting = render_settings.get("redshift_render_settings", {})
        return (
            redshift_render_setting.get("separate_aov_files", False)
        )

    return False

get_namespace(container_name)

Get the namespace and name of the sub-container

Parameters:

Name Type Description Default
container_name str

the name of master container

required

Raises:

Type Description
RuntimeError

when there is no master container found

Returns:

Name Type Description
namespace str

namespace of the sub-container

name str

name of the sub-container

Source code in client/ayon_max/api/lib.py
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
def get_namespace(container_name):
    """Get the namespace and name of the sub-container

    Args:
        container_name (str): the name of master container

    Raises:
        RuntimeError: when there is no master container found

    Returns:
        namespace (str): namespace of the sub-container
        name (str): name of the sub-container
    """
    node = rt.getNodeByName(container_name)
    if not node:
        raise RuntimeError("Master Container Not Found..")
    name = rt.getUserProp(node, "name")
    namespace = rt.getUserProp(node, "namespace")
    return namespace, name

get_plugins()

Get all loaded plugins in 3dsMax

Returns:

Name Type Description
plugin_info_list list

a list of loaded plugins

Source code in client/ayon_max/api/lib.py
749
750
751
752
753
754
755
756
757
758
759
760
761
762
def get_plugins() -> list:
    """Get all loaded plugins in 3dsMax

    Returns:
        plugin_info_list: a list of loaded plugins
    """
    manager = rt.PluginManager
    count = manager.pluginDllCount
    plugin_info_list = []
    for p in range(1, count + 1):
        plugin_info = manager.pluginDllName(p)
        plugin_info_list.append(plugin_info)

    return plugin_info_list

get_target_sme_view(target_view)

Return a Slate Material Editor view by index.

Parameters:

Name Type Description Default
target_view int

Active SME view index.

required

Returns:

Name Type Description
IObject

The requested SME view object.

Source code in client/ayon_max/api/lib.py
879
880
881
882
883
884
885
886
887
888
def get_target_sme_view(target_view: int):
    """Return a Slate Material Editor view by index.

    Args:
        target_view (int): Active SME view index.

    Returns:
        IObject: The requested SME view object.
    """
    return rt.sme.GetView(target_view)

get_tyflow_export_operators()

Get Tyflow Export Particles Operators.

Returns:

Name Type Description
list

Particle operators

Source code in client/ayon_max/api/lib.py
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
def get_tyflow_export_operators():
    """Get Tyflow Export Particles Operators.

    Returns:
        list: Particle operators

    """
    operators = []
    members = [obj for obj in rt.Objects if rt.ClassOf(obj) == rt.tyFlow]
    for member in members:
        obj = member.baseobject
        anim_names = rt.GetSubAnimNames(obj)
        for anim_name in anim_names:
            sub_anim = rt.GetSubAnim(obj, anim_name)
            if not rt.isKindOf(sub_anim, rt.tyEvent):
                continue
            node_names = rt.GetSubAnimNames(sub_anim)
            for node_name in node_names:
                node_sub_anim = rt.GetSubAnim(sub_anim, node_name)
                if rt.hasProperty(node_sub_anim, "exportMode"):
                    operators.append(node_sub_anim)
    return operators

get_view_node_from_sme_view(sme_view, view_node_name)

Get view node from SME view

Parameters:

Name Type Description Default
sme_view IFP_NodeViewImp

Target SME View

required
view_node_name str

view node name

required

Returns: IObject: view node object

Source code in client/ayon_max/api/lib.py
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
def get_view_node_from_sme_view(sme_view, view_node_name):
    """Get view node from SME view

    Args:
        sme_view (rt.IFP_NodeViewImp): Target SME View
        view_node_name (str): view node name
    Returns:
        IObject: view node object
    """
    for i in range(sme_view.GetNumNodes() + 1):
        node = sme_view.GetNode(i)
        if node is None:
            continue
        if node.name == view_node_name:
            return node
    raise ValueError(f"View node {view_node_name} not found in SME view.")

get_vray_settings(renderer_name, renderer)

Get V-Ray specific settings from the renderer.

Parameters:

Name Type Description Default
renderer_name str

The name of the renderer.

required
renderer Any

The renderer object.

required

Returns:

Name Type Description
Any Any

The V-Ray settings object.

Source code in client/ayon_max/api/lib.py
312
313
314
315
316
317
318
319
320
321
322
323
324
def get_vray_settings(renderer_name: str, renderer: Any) -> Any:
    """Get V-Ray specific settings from the renderer.

    Args:
        renderer_name (str): The name of the renderer.
        renderer (Any): The renderer object.

    Returns:
        Any: The V-Ray settings object.
    """
    if "GPU" in renderer_name:
        return renderer.V_Ray_settings
    return renderer

get_work_default_directory_and_data(data)

Helping function for formatting of anatomy paths

Parameters:

Name Type Description Default
data Dict

dictionary with attributes used for formatting

required

Returns:

Type Description
tuple[str, Dict]

tuple[str, Dict]: - Path to the default work directory for current context. - Sanitized template data used for formatting.

Source code in client/ayon_max/api/lib.py
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
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
278
279
280
281
282
def get_work_default_directory_and_data(data: Dict) -> tuple[str, Dict]:
    """Helping function for formatting of anatomy paths

    Arguments:
        data (Dict): dictionary with attributes used for formatting

    Returns:
        tuple[str, Dict]:
            - Path to the default work directory for current context.
            - Sanitized template data used for formatting.
    """

    project_name = get_current_project_name()
    anatomy = Anatomy(project_name)

    data = _sanitize_template_data(dict(data))

    version = data.get("version")
    if version is None:
        project_filename = rt.maxFileName
        data["version"] = get_version_from_path(project_filename)

    folder_path = data["folderPath"]
    task_name = data["task"]
    host_name = get_current_host_name()

    context_data = get_template_data_with_names(
        project_name, folder_path, task_name, host_name
    )
    data.update(context_data)
    product_type = data["productType"]
    product_base_type = data.get("productBaseType")
    if not product_base_type:
        product_base_type = product_type

    data.update({
        "root": anatomy.roots,
        "subset": data["productName"],
        "family": product_base_type,
        "product": {
            "name": data["productName"],
            "type": product_type,
            "basetype": product_base_type,
        },
    })
    work_default_dir_template = anatomy.get_template_item("work", "default", "directory")
    normalized_dir = work_default_dir_template.format_strict(data).normalized()
    return str(normalized_dir), data

is_general_default_output_regex_matched(filename)

Check if the filename matches the general default render output pattern.

The general default output pattern expects a double-dot separator before the file extension: <name>..<extension> (e.g., John_Doe..exr). This is the standard naming convention for render outputs when configured through the create render instance settings.

Parameters:

Name Type Description Default
filename str

The filename to check.

required

Returns:

Name Type Description
bool bool

True if the filename matches the pattern (name..extension), False otherwise.

Source code in client/ayon_max/api/lib.py
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
def is_general_default_output_regex_matched(filename) -> bool:
    """Check if the filename matches the general default render output pattern.

    The general default output pattern expects a double-dot separator before
    the file extension: `<name>..<extension>` (e.g., `John_Doe..exr`).
    This is the standard naming convention for render outputs when configured
    through the create render instance settings.

    Args:
        filename (str): The filename to check.

    Returns:
        bool: True if the filename matches the pattern (name..extension),
            False otherwise.
    """
    pattern = r".*\.{2}[a-zA-Z0-9]+$"
    return re.match(pattern, filename) is not None

is_headless()

Check if 3dsMax runs in batch mode. If it returns True, it runs in 3dsbatch.exe If it returns False, it runs in 3dsmax.exe

Source code in client/ayon_max/api/lib.py
591
592
593
594
595
596
def is_headless():
    """Check if 3dsMax runs in batch mode.
    If it returns True, it runs in 3dsbatch.exe
    If it returns False, it runs in 3dsmax.exe
    """
    return rt.maxops.isInNonInteractiveMode()

lsattr(attr, value=None, root=None)

List nodes having attribute with specified value.

Parameters:

Name Type Description Default
attr str

Attribute name to match.

required
value (str, Optional)

Value to match, of omitted, all nodes with specified attribute are returned no matter of value.

None
root (str, Optional)

Root node name. If omitted, scene root is used.

None

Returns:

Type Description
list

list of nodes.

Source code in client/ayon_max/api/lib.py
 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
def lsattr(
        attr: str,
        value: Union[str, None] = None,
        root: Union[str, None] = None) -> list:
    """List nodes having attribute with specified value.

    Args:
        attr (str): Attribute name to match.
        value (str, Optional): Value to match, of omitted, all nodes
            with specified attribute are returned no matter of value.
        root (str, Optional): Root node name. If omitted, scene root is used.

    Returns:
        list of nodes.
    """
    root = rt.RootNode if root is None else rt.GetNodeByName(root)

    def output_node(node, nodes):
        nodes.append(node)
        for child in node.Children:
            output_node(child, nodes)

    nodes = []
    output_node(root, nodes)
    return [
        n for n in nodes
        if rt.GetUserProp(n, attr) == value
    ] if value else [
        n for n in nodes
        if rt.GetUserProp(n, attr)
    ]

maintained_sme_view_nodes_selection(current_sme_view, texture_node)

Maintain selection of nodes in SME view during context

Parameters:

Name Type Description Default
view_node_name IFP_NodeViewImp

SNE View Node Object

required
texture_node Node

Texture Node Object

required
Source code in client/ayon_max/api/lib.py
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
@contextlib.contextmanager
def maintained_sme_view_nodes_selection(current_sme_view, texture_node):
    """Maintain selection of nodes in SME view during context

    Args:
        view_node_name (IFP_NodeViewImp): SNE View Node Object
        texture_node (Node): Texture Node Object
    """
    previous_selection = [
        node.reference for node in current_sme_view.GetSelectedNodes()
        if node.reference != texture_node
    ]
    try:
        current_sme_view.SelectNone()
        current_sme_view.setSelectedNodes([texture_node])
        yield

    finally:
        if previous_selection:
            current_sme_view.setSelectedNodes(previous_selection)

object_transform_set(container_children)

A function which allows to store the transform of previous loaded object(s) Args: container_children(list): A list of nodes

Returns:

Name Type Description
transform_set dict

A dict with all transform data of

the previous loaded object(s)

Source code in client/ayon_max/api/lib.py
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
def object_transform_set(container_children):
    """A function which allows to store the transform of
    previous loaded object(s)
    Args:
        container_children(list): A list of nodes

    Returns:
        transform_set (dict): A dict with all transform data of
        the previous loaded object(s)
    """
    transform_set = {}

    for node in container_children:
        name = f"{node.name}.rotation"
        transform_set[name] = node.rotation
        name = f"{node.name}.scale"
        transform_set[name] = node.scale
        name = f"{node.name}.translate"
        transform_set[name] = node.pos
    return transform_set

reformat_filename(filename)

Reformat render output filename to standardized pattern.

Converts filenames from two non-standard render patterns(One from Redshift render elements, another Vray render elements with the edge case of not using vray-style output.) to a unified format: <name>.<frame>.<extension>. This handles both Redshift render element naming and general render output naming conventions.

Conversion patterns
  • Redshift: Main._Cryptomatte.1001.exr → Main.Cryptomatte.1001.exr
  • Vray (edge case): Main_tmp..Cryptomatte.1001.exr → Main_tmp.Cryptomatte.1001.exr

Parameters:

Name Type Description Default
filename str

The filename to reformat.

required

Returns:

Name Type Description
str str

The reformatted filename in name.frame.extension format.

Source code in client/ayon_max/api/lib.py
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
def reformat_filename(filename: str) -> str:
    """Reformat render output filename to standardized pattern.

    Converts filenames from two non-standard render patterns(One from Redshift render elements,
    another Vray render elements with the edge case of not using vray-style output.) to a unified
    format: `<name>.<frame>.<extension>`. This handles both Redshift
    render element naming and general render output naming conventions.

    Conversion patterns:
        - Redshift: `Main._Cryptomatte.1001.exr` → `Main.Cryptomatte.1001.exr`
        - Vray (edge case): `Main_tmp..Cryptomatte.1001.exr` → `Main_tmp.Cryptomatte.1001.exr`

    Args:
        filename (str): The filename to reformat.

    Returns:
        str: The reformatted filename in name.frame.extension format.
    """
    # Match: base name, underscore part, extension
    redshift_pattern = (
        r"^(?P<name>.+)\._(?P<element>[^.]+)\.(?P<frame>[^.]+)\.(?P<ext>[a-zA-Z0-9]+)$"
    )
    match = re.match(redshift_pattern, filename)
    vray_pattern = r"^(?P<name>.+)\.\.(?P<frame>[^.]+)\.(?P<ext>[a-zA-Z0-9]+)$"
    match_vray = re.match(vray_pattern, filename)
    if match:
        name = match.group("name")
        element = match.group("element")
        frame = match.group("frame")
        ext = match.group("ext")
        return f"{name}.{element}.{frame}.{ext}"
    elif match_vray:
        name = match_vray.group("name")
        frame = match_vray.group("frame")
        ext = match_vray.group("ext")
        return f"{name}.{frame}.{ext}"
    else:
        # fallback if pattern doesn't match
        return filename

render_resolution(width, height)

Set render resolution option during context

Parameters:

Name Type Description Default
width int

render width

required
height int

render height

required
Source code in client/ayon_max/api/lib.py
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
@contextlib.contextmanager
def render_resolution(width, height):
    """Set render resolution option during context

    Args:
        width (int): render width
        height (int): render height
    """
    current_renderWidth = rt.renderWidth
    current_renderHeight = rt.renderHeight
    try:
        rt.renderWidth = width
        rt.renderHeight = height
        yield
    finally:
        rt.renderWidth = current_renderWidth
        rt.renderHeight = current_renderHeight

reset_colorspace()

OCIO Configuration Supports in 3dsMax 2024+

Source code in client/ayon_max/api/lib.py
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
def reset_colorspace():
    """OCIO Configuration
    Supports in 3dsMax 2024+

    """
    if int(get_max_version()) < 2024:
        return
    colorspace_mgr = rt.ColorPipelineMgr
    ocio_config_path = os.getenv("OCIO")
    colorspace_mgr.Mode = rt.Name("OCIO_EnvVar")
    if not ocio_config_path:
        max_config_data = colorspace.get_current_context_imageio_config_preset()
        if max_config_data:
            ocio_config_path = max_config_data["path"]
            colorspace_mgr.Mode = rt.Name("OCIO_Custom")
            colorspace_mgr.OCIOConfigPath = ocio_config_path

reset_frame_range(fps=True)

Set frame range to current folder. This is part of 3dsmax documentation:

A System Global variable which lets you get and

set an Interval value that defines the start and end frames of the Active Time Segment.

frameRate: A System Global variable which lets you get and set an Integer value that defines the current scene frame rate in frames-per-second.

Source code in client/ayon_max/api/lib.py
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
def reset_frame_range(fps: bool = True):
    """Set frame range to current folder.
    This is part of 3dsmax documentation:

    animationRange: A System Global variable which lets you get and
        set an Interval value that defines the start and end frames
        of the Active Time Segment.
    frameRate: A System Global variable which lets you get
            and set an Integer value that defines the current
            scene frame rate in frames-per-second.
    """
    if fps:
        rt.frameRate = float(get_fps_for_current_context())

    frame_range = get_frame_range()

    set_timeline(
        frame_range["frameStartHandle"], frame_range["frameEndHandle"])
    set_render_frame_range(
        frame_range["frameStartHandle"], frame_range["frameEndHandle"])

    project_name = get_current_project_name()
    settings = get_project_settings(project_name).get("max")
    auto_key_default_key_time = settings.get(
        "auto_key_default", {}).get("defualt_key_time")
    rt.maxOps.autoKeyDefaultKeyTime = auto_key_default_key_time

reset_scene_resolution(task_entity=None)

Apply the scene resolution from the project definition

scene resolution can be overwritten by a folder if the folder.attrib contains any information regarding scene resolution.

Source code in client/ayon_max/api/lib.py
398
399
400
401
402
403
404
405
406
407
408
409
410
def reset_scene_resolution(task_entity=None):
    """Apply the scene resolution from the project definition

    scene resolution can be overwritten by a folder if the folder.attrib
    contains any information regarding scene resolution.
    """
    if task_entity is None:
        task_entity = get_current_task_entity(fields={"attrib"})
    task_attributes = task_entity["attrib"]
    width = int(task_attributes["resolutionWidth"])
    height = int(task_attributes["resolutionHeight"])

    set_scene_resolution(width, height)

set_context_settings(resolution=True, frame_range=True, scene_units=False, colorspace=True)

Apply the project settings from the project definition

Settings can be overwritten by an folder if the folder.attrib contains any information regarding those settings.

Examples of settings

frame range resolution

Returns:

Type Description

None

Source code in client/ayon_max/api/lib.py
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
def set_context_settings(resolution=True,
                         frame_range=True,
                         scene_units=False,
                         colorspace=True):
    """Apply the project settings from the project definition

    Settings can be overwritten by an folder if the folder.attrib contains
    any information regarding those settings.

    Examples of settings:
        frame range
        resolution

    Returns:
        None
    """
    if resolution:
        reset_scene_resolution()
    if frame_range:
        reset_frame_range()
    if scene_units:
        set_unit_scale(scene_units=scene_units)
    if colorspace:
        reset_colorspace()

set_correct_workfile_name_for_render_output(instance, filepath)

Replace the original workfile token with the current scene name.

Parameters:

Name Type Description Default
instance Instance

The Pyblish instance.

required
filepath str

The file path to update.

required

Returns:

Name Type Description
str str

The updated file path with the current workfile name.

Source code in client/ayon_max/api/lib.py
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
def set_correct_workfile_name_for_render_output(
    instance: pyblish.api.Instance,
    filepath: str,
) -> str:
    """Replace the original workfile token with the current scene name.

    Args:
        instance (pyblish.api.Instance): The Pyblish instance.
        filepath (str): The file path to update.

    Returns:
        str: The updated file path with the current workfile name.

    """
    project_settings = instance.context.data["project_settings"]
    render_root = os.path.normpath(
        get_default_render_folder(instance.data, project_settings)
    )
    normalized_path = os.path.normpath(filepath)

    current_file = os.path.basename(instance.context.data["currentFile"])
    current_workfile_filename = os.path.splitext(current_file)[0].strip(".")

    pattern = (
        rf"^{re.escape(render_root)}"
        rf"[\\/](?P<workfile>[^\\/]+)"
        rf"(?P<rest>(?:[\\/].*)?)$"
    )
    match = re.match(pattern, normalized_path)
    if not match:
        return ""

    render_workfile_name = match.group("workfile")
    if render_workfile_name == current_workfile_filename:
        return filepath

    rest = match.group("rest") or ""
    return os.path.join(
        render_root,
        current_workfile_filename,
        rest.lstrip("\\/"),
    )

set_render_frame_range(start_frame, end_frame)

Note

Frame range can be specified in different types. Possible values are: * 1 - Single frame. * 2 - Active time segment ( animationRange ). * 3 - User specified Range. * 4 - User specified Frame pickup string (for example 1,3,5-12).

Todo

Current type is hard-coded, there should be a custom setting for this.

Source code in client/ayon_max/api/lib.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
def set_render_frame_range(start_frame, end_frame):
    """
    Note:
        Frame range can be specified in different types. Possible values are:
        * `1` - Single frame.
        * `2` - Active time segment ( animationRange ).
        * `3` - User specified Range.
        * `4` - User specified Frame pickup string (for example `1,3,5-12`).

    Todo:
        Current type is hard-coded, there should be a custom setting for this.
    """
    rt.rendTimeType = 3
    if start_frame is not None and end_frame is not None:
        rt.rendStart = int(start_frame)
        rt.rendEnd = int(end_frame)

set_scene_resolution(width, height)

Set the render resolution

Parameters:

Name Type Description Default
width int

value of the width

required
height int

value of the height

required

Returns:

Type Description

None

Source code in client/ayon_max/api/lib.py
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
def set_scene_resolution(width: int, height: int):
    """Set the render resolution

    Args:
        width(int): value of the width
        height(int): value of the height

    Returns:
        None

    """
    # make sure the render dialog is closed
    # for the update of resolution
    # Changing the Render Setup dialog settings should be done
    # with the actual Render Setup dialog in a closed state.
    if rt.renderSceneDialog.isOpen():
        rt.renderSceneDialog.close()

    rt.renderWidth = width
    rt.renderHeight = height

set_timeline(frameStart, frameEnd)

Set frame range for timeline editor in Max

Source code in client/ayon_max/api/lib.py
599
600
601
602
603
def set_timeline(frameStart, frameEnd):
    """Set frame range for timeline editor in Max
    """
    rt.animationRange = rt.interval(int(frameStart), int(frameEnd))
    return rt.animationRange

set_unit_scale(project_settings=None, scene_units=False)

Function to set unit scale in Metric Args: project_settings (dict, optional): project settings. scene_units (bool, optional): whether to set scene units.

Source code in client/ayon_max/api/lib.py
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
def set_unit_scale(project_settings=None, scene_units=False):
    """Function to set unit scale in Metric
    Args:
        project_settings (dict, optional): project settings.
        scene_units (bool, optional): whether to set scene units.
    """
    if project_settings is None:
        project_name = get_current_project_name()
        project_settings = get_project_settings(project_name).get("max")
    scale_settings = project_settings["unit_scale_settings"]
    scene_scale_enabled = scale_settings.get("enabled", False)
    if scene_scale_enabled or scene_units:
        scene_scale = scale_settings["scene_unit_scale"]
        rt.units.DisplayType = rt.Name("Metric")
        rt.units.MetricType = rt.Name(scene_scale)

set_viewport_type(viewport_type=None)

Set viewport type during context

Source code in client/ayon_max/api/lib.py
905
906
907
908
909
910
911
912
913
914
915
@contextlib.contextmanager
def set_viewport_type(viewport_type=None):
    """Set viewport type during context"""
    if viewport_type is None:
        viewport_type = rt.Name("view_camera")
    previous_viewport_type = rt.viewport.getType()
    rt.viewport.setType(viewport_type)
    try:
        yield
    finally:
        rt.viewport.setType(previous_viewport_type)

suspended_refresh()

Suspended refresh for scene and modify panel redraw.

Source code in client/ayon_max/api/lib.py
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
@contextlib.contextmanager
def suspended_refresh():
    """Suspended refresh for scene and modify panel redraw.
    """
    if is_headless():
        yield
        return
    rt.disableSceneRedraw()
    rt.suspendEditing()
    try:
        yield

    finally:
        rt.enableSceneRedraw()
        rt.resumeEditing()

unique_namespace(namespace, format='%02d', prefix='', suffix='', con_suffix='CON')

Return unique namespace

Parameters:

Name Type Description Default
namespace str

Name of namespace to consider

required
format str

Formatting of the given iteration number

'%02d'
suffix str

Only consider namespaces with this suffix.

''
con_suffix

max only, for finding the name of the master container

'CON'

unique_namespace("bar")

bar01

unique_namespace(":hello")

:hello01

unique_namespace("bar:", suffix="_NS")

bar01_NS:

Source code in client/ayon_max/api/lib.py
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
def unique_namespace(namespace, format="%02d",
                     prefix="", suffix="", con_suffix="CON"):
    """Return unique namespace

    Arguments:
        namespace (str): Name of namespace to consider
        format (str, optional): Formatting of the given iteration number
        suffix (str, optional): Only consider namespaces with this suffix.
        con_suffix: max only, for finding the name of the master container

    >>> unique_namespace("bar")
    # bar01
    >>> unique_namespace(":hello")
    # :hello01
    >>> unique_namespace("bar:", suffix="_NS")
    # bar01_NS:

    """

    def current_namespace():
        current = namespace
        # When inside a namespace Max adds no trailing :
        if not current.endswith(":"):
            current += ":"
        return current

    # Always check against the absolute namespace root
    # There's no clash with :x if we're defining namespace :a:x
    ROOT = ":" if namespace.startswith(":") else current_namespace()

    # Strip trailing `:` tokens since we might want to add a suffix
    start = ":" if namespace.startswith(":") else ""
    end = ":" if namespace.endswith(":") else ""
    namespace = namespace.strip(":")
    if ":" in namespace:
        # Split off any nesting that we don't uniqify anyway.
        parents, namespace = namespace.rsplit(":", 1)
        start += parents + ":"
        ROOT += start

    iteration = 1
    increment_version = True
    while increment_version:
        nr_namespace = namespace + format % iteration
        unique = prefix + nr_namespace + suffix
        container_name = f"{unique}:{namespace}{con_suffix}"
        if not rt.getNodeByName(container_name):
            name_space = start + unique + end
            increment_version = False
            return name_space
        else:
            increment_version = True
        iteration += 1

update_content_on_context_change()

This will update scene content to match new folder on context change

Source code in client/ayon_max/api/lib.py
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
def update_content_on_context_change():
    """
    This will update scene content to match new folder on context change
    """

    host = registered_host()
    create_context = CreateContext(host, discover_publish_plugins=False)
    task_entity = create_context.get_current_task_entity()

    instance_values = {
        "folderPath": create_context.get_current_folder_path(),
        "task": task_entity["name"],
    }
    creator_attribute_values = {
        "frameStart": float(task_entity["attrib"]["frameStart"]),
        "frameEnd": float(task_entity["attrib"]["frameEnd"]),
        "handleStart": float(task_entity["attrib"]["handleStart"]),
        "handleEnd": float(task_entity["attrib"]["handleEnd"]),
    }

    has_changes = False
    for instance in create_context.instances:
        for key, value in instance_values.items():
            if key not in instance or instance[key] == value:
                continue

            # Update instance value
            print(f"Updating {instance.product_name} {key} to: {value}")
            instance[key] = value
            has_changes = True

        creator_attributes = instance.creator_attributes
        for key, value in creator_attribute_values.items():
            if (
                    key not in creator_attributes
                    or creator_attributes[key] == value
            ):
                continue

            # Update instance creator attribute value
            print(f"Updating {instance.product_name} {key} to: {value}")
            creator_attributes[key] = value
            has_changes = True

    if has_changes:
        create_context.save_changes()

update_modifier_node_names(event, node)

Update the name of the nodes after renaming

Parameters:

Name Type Description Default
event MXSWrapperBase

Event Name ( Mandatory argument for rt.NodeEventCallback)

required
node list

Event Number ( Mandatory argument for rt.NodeEventCallback)

required
Source code in client/ayon_max/api/lib.py
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
def update_modifier_node_names(event, node):
    """Update the name of the nodes after renaming

    Args:
        event (pymxs.MXSWrapperBase): Event Name (
            Mandatory argument for rt.NodeEventCallback)
        node (list): Event Number (
            Mandatory argument for rt.NodeEventCallback)

    """
    containers = []
    for obj in rt.Objects:
        if rt.ClassOf(obj) != rt.Container:
            continue

        if rt.getUserProp(obj, "id") in {
            AVALON_INSTANCE_ID,
            AYON_INSTANCE_ID
        }:
            continue

        product_base_type = rt.getUserProp(obj, "productBaseType")
        if not product_base_type:
            product_base_type = rt.getUserProp(obj, "productType")

        if product_base_type in {"workfile", "tyflow"}:
            containers.append(obj)

    if not containers:
        return
    for container in containers:
        modifier = container.modifiers[0]
        ayon_data = get_ayon_data(modifier)
        updated_node_names = [
            str(node.node) for node in ayon_data.all_handles
        ]
        rt.setProperty(ayon_data, "sel_list", updated_node_names)

validate_unit_scale(project_settings=None)

Apply the unit scale setting to 3dsMax

Source code in client/ayon_max/api/lib.py
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
def validate_unit_scale(project_settings=None):
    """Apply the unit scale setting to 3dsMax
    """

    if is_headless():
        return
    if project_settings is None:
        project_name = get_current_project_name()
        project_settings = get_project_settings(project_name).get("max")
    scene_scale_enabled = project_settings["unit_scale_settings"]["enabled"]
    if not scene_scale_enabled:
        log.info("Using default scale display type.")
        rt.units.DisplayType = rt.Name("Generic")
        return
    scene_scale = project_settings["unit_scale_settings"]["scene_unit_scale"]
    if rt.units.DisplayType == rt.Name("Metric") and (
        rt.units.MetricType == rt.Name(scene_scale)
    ):
        return

    parent = get_main_window()
    dialog = SimplePopup(parent=parent)
    dialog.setWindowTitle("Wrong Unit Scale")
    dialog.set_message("Scene units do not match studio/project preferences.")
    dialog.set_button_text("Fix")
    dialog.setStyleSheet(load_stylesheet())

    dialog.on_clicked.connect(partial(set_unit_scale, project_settings))
    dialog.show()