Skip to content

lib

Library of functions useful for 3dsmax pipeline.

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
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
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
790
791
792
793
794
795
796
797
798
799
800
801
@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
665
666
667
668
669
670
671
672
673
674
675
676
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
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
865
866
867
868
869
870
871
872
873
874
875
876
877
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
191
192
193
194
195
196
197
198
199
200
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
371
372
373
374
375
376
377
378
379
380
381
382
383
384
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
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
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
39
40
41
42
43
44
45
46
47
48
49
50
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
478
479
480
481
482
483
484
485
486
487
488
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
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
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
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
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
649
650
651
652
653
654
655
656
657
658
659
660
661
662
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
779
780
781
782
783
784
785
786
787
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
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
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
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
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)

Get V-Ray specific settings from the renderer.

Source code in client/ayon_max/api/lib.py
218
219
220
221
222
223
224
def get_vray_settings(renderer):
    """Get V-Ray specific settings from the renderer."""
    renderer_class = get_current_renderer()
    if "GPU" in renderer:
        return renderer_class.V_Ray_settings
    else:
        return renderer_class

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
491
492
493
494
495
496
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
67
68
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
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
@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
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
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

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
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
@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
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
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
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
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
298
299
300
301
302
303
304
305
306
307
308
309
310
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
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
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_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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
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
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
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
499
500
501
502
503
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
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
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
804
805
806
807
808
809
810
811
812
813
814
@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
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
@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
551
552
553
554
555
556
557
558
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
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
880
881
882
883
884
885
886
887
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
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
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
704
705
706
707
708
709
710
711
712
713
714
715
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
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
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()