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
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
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
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
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
798
799
800
801
802
803
804
805
806
807
808
809
@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
673
674
675
676
677
678
679
680
681
682
683
684
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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
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
873
874
875
876
877
878
879
880
881
882
883
884
885
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
193
194
195
196
197
198
199
200
201
202
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_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
379
380
381
382
383
384
385
386
387
388
389
390
391
392
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
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
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
41
42
43
44
45
46
47
48
49
50
51
52
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
486
487
488
489
490
491
492
493
494
495
496
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
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
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
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
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
657
658
659
660
661
662
663
664
665
666
667
668
669
670
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)

summary

Parameters:

Name Type Description Default
target_view int

active SME view

required

Returns: IObject: SME View object

Source code in client/ayon_max/api/lib.py
787
788
789
790
791
792
793
794
795
def get_target_sme_view(target_view: int):
    """_summary_

    Args:
        target_view (int): active SME view
    Returns:
        IObject: 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
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
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
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
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
220
221
222
223
224
225
226
227
228
229
230
231
232
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

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
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
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
499
500
501
502
503
504
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
@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
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
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
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
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
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
@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
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
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
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
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
306
307
308
309
310
311
312
313
314
315
316
317
318
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
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
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
 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
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
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(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 filepath

    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
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
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
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
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
507
508
509
510
511
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
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
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
812
813
814
815
816
817
818
819
820
821
822
@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
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
@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
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
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
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
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
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
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
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
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()