Skip to content

lib

add_self_publish_button(node)

Adds a self publish button to the rop node.

Source code in client/ayon_houdini/api/lib.py
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
def add_self_publish_button(node):
    """Adds a self publish button to the rop node."""

    label = os.environ.get("AYON_MENU_LABEL") or "AYON"

    button_parm = hou.ButtonParmTemplate(
        "ayon_self_publish",
        "{} Publish".format(label),
        script_callback="from ayon_houdini.api.lib import "
                        "self_publish; self_publish()",
        script_callback_language=hou.scriptLanguage.Python,
        join_with_next=True
    )

    template = node.parmTemplateGroup()
    template.insertBefore((0,), button_parm)
    node.setParmTemplateGroup(template)

connect_file_parm_to_loader(file_parm)

Connect the given file parm to a generic loader. If the parm is already connected to a generic loader node, go to that node.

Source code in client/ayon_houdini/api/lib.py
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
def connect_file_parm_to_loader(file_parm: hou.Parm):
    """Connect the given file parm to a generic loader.
    If the parm is already connected to a generic loader node, go to that node.
    """

    from .pipeline import get_or_create_avalon_container

    referenced_parm = file_parm.getReferencedParm()

    # If the parm has reference
    if file_parm != referenced_parm:
        referenced_node = referenced_parm.getReferencedParm().node()
        if referenced_node.type().name() == "ayon::generic_loader::1.0":
            show_node_parmeditor(referenced_node)
            return

    # Create a generic loader node and reference its file parm
    main_container = get_or_create_avalon_container()

    node_name = f"{file_parm.node().name()}_{file_parm.name()}_loader"
    load_node = main_container.createNode("ayon::generic_loader",
                                          node_name=node_name)
    load_node.moveToGoodPosition()

    # Set relative reference via hscript. This avoids the issues of
    # `setExpression` e.g. having a keyframe.
    relative_path = file_parm.node().relativePathTo(load_node)
    expression = rf'chs\(\"{relative_path}/file\"\)'  # noqa
    hou.hscript(
        'opparm -r'
        f' {file_parm.node().path()} {file_parm.name()} \\`{expression}\\`'
    )
    show_node_parmeditor(load_node)

context_options(context_options)

Context manager to set Solaris Context Options.

The original context options are restored after the context exits.

Parameters:

Name Type Description Default
context_options dict[str, str | float]

The Solaris Context Options to set.

required

Yields:

Type Description

dict[str, str | float]: The original context options that were changed.

Source code in client/ayon_houdini/api/lib.py
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
@contextmanager
def context_options(context_options: "dict[str, str | float]"):
    """Context manager to set Solaris Context Options.

    The original context options are restored after the context exits.

    Arguments:
        context_options (dict[str, str | float]):
            The Solaris Context Options to set.

    Yields:
        dict[str, str | float]: The original context options that were changed.

    """
    # Get the original context options and their values
    original_context_options: "dict[str, str | float]" = {}
    for name in hou.contextOptionNames():
        original_context_options[name] = hou.contextOption(name)

    try:
        # Override the context options
        for name, value in context_options.items():
            hou.setContextOption(name, value)
        yield original_context_options
    finally:
        # Restore original context options that we changed
        for name in context_options:
            if name in original_context_options:
                hou.setContextOption(name, original_context_options[name])
            else:
                # Clear context option
                hou.setContextOption(name, None)

find_active_network(category, default)

Find the first active network editor in the UI.

If no active network editor pane is found at the given category then the default path will be used as fallback.

For example, to find an active LOPs network:

network = find_active_network( ... category=hou.lopNodeTypeCategory(), ... fallback="/stage" ... ) hou.Node("/stage/lopnet1")

Parameters:

Name Type Description Default
category NodeTypeCategory

The node network category type.

required
default str

The default path to fallback to if no active pane is found with the given category, e.g. "/obj"

required

Returns:

Type Description

hou.Node: The node network to return.

Source code in client/ayon_houdini/api/lib.py
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
def find_active_network(category, default):
    """Find the first active network editor in the UI.

    If no active network editor pane is found at the given category then the
    `default` path will be used as fallback.

    For example, to find an active LOPs network:
    >>> network = find_active_network(
    ...     category=hou.lopNodeTypeCategory(),
    ...     fallback="/stage"
    ... )
    hou.Node("/stage/lopnet1")

    Arguments:
        category (hou.NodeTypeCategory): The node network category type.
        default (str): The default path to fallback to if no active pane
            is found with the given category, e.g. "/obj"

    Returns:
        hou.Node: The node network to return.

    """
    # Find network editors that are current tab of given category
    index = 0
    while True:
        pane = hou.ui.paneTabOfType(hou.paneTabType.NetworkEditor, index)
        if pane is None:
            break

        index += 1
        if not pane.isCurrentTab():
            continue

        pwd = pane.pwd()
        if pwd.type().category() != category:
            continue

        if not pwd.isEditable():
            continue

        return pwd

    # Default to the fallback if no valid candidate was found
    return hou.node(default)

find_rop_input_dependencies(input_tuple)

Self publish from ROP nodes.

Returns:

Type Description

list of the RopNode.path() that can be found inside

the input tuple.

Source code in client/ayon_houdini/api/lib.py
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
def find_rop_input_dependencies(input_tuple):
    """Self publish from ROP nodes.

    Arguments:
        tuple (hou.RopNode.inputDependencies) which can be a nested tuples
        represents the input dependencies of the ROP node, consisting of ROPs,
        and the frames that need to be be rendered prior to rendering the ROP.

    Returns:
        list of the RopNode.path() that can be found inside
        the input tuple.
    """

    out_list = []
    if isinstance(input_tuple[0], hou.RopNode):
        return input_tuple[0].path()

    if isinstance(input_tuple[0], tuple):
        for item in input_tuple:
            out_list.append(find_rop_input_dependencies(item))

    return out_list

format_as_collections(files, pattern='{head}{padding}{tail} [{ranges}]')

Return list of files as formatted sequence collections.

Source code in client/ayon_houdini/api/lib.py
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
def format_as_collections(
    files: list[str],
    pattern: str = "{head}{padding}{tail} [{ranges}]"
) -> list[str]:
    """Return list of files as formatted sequence collections."""

    collections, remainder = clique.assemble(files)
    result = [collection.format(pattern) for collection in collections]
    result.extend(remainder)
    return result

get_background_images(node, raw=False)

"Return background images defined inside node.

Similar to nodegraphutils.saveBackgroundImages but this method also allows to retrieve the data as JSON encodable data instead of hou.NetworkImage instances when using raw=True

Source code in client/ayon_houdini/api/lib.py
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
def get_background_images(node, raw=False):
    """"Return background images defined inside node.

    Similar to `nodegraphutils.saveBackgroundImages` but this method also
    allows to retrieve the data as JSON encodable data instead of
    `hou.NetworkImage` instances when using `raw=True`
    """

    def _parse(image_data):
        image = hou.NetworkImage(image_data["path"],
                                 hou.BoundingRect(*image_data["rect"]))
        if "relativetopath" in image_data:
            image.setRelativeToPath(image_data["relativetopath"])
        if "brightness" in image_data:
            image.setBrightness(image_data["brightness"])
        return image

    data = node.userData("backgroundimages")
    if not data:
        return []

    try:
        images = json.loads(data)
    except json.decoder.JSONDecodeError:
        images = []

    if not raw:
        images = [_parse(_data) for _data in images]
    return images

get_camera_from_container(container)

Get camera from container node.

Source code in client/ayon_houdini/api/lib.py
837
838
839
840
841
842
843
844
845
846
847
def get_camera_from_container(container):
    """Get camera from container node. """

    cameras = container.recursiveGlob(
        "*",
        filter=hou.nodeTypeFilter.ObjCamera,
        include_subnets=False
    )

    assert len(cameras) == 1, "Camera instance must have only one camera"
    return cameras[0]

get_color_management_preferences()

Get default OCIO preferences

Source code in client/ayon_houdini/api/lib.py
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
def get_color_management_preferences():
    """Get default OCIO preferences"""

    preferences = {
        "config": hou.Color.ocio_configPath(),
        "display": hou.Color.ocio_defaultDisplay(),
        "view": hou.Color.ocio_defaultView()
    }

    # Note: For whatever reason they are cases where `view` may be an empty
    #  string even though a valid default display is set where `PyOpenColorIO`
    #  does correctly return the values.
    # Workaround to get the correct default view
    if preferences["config"] and not preferences["view"]:
        log.debug(
            "Houdini `hou.Color.ocio_defaultView()` returned empty value."
            " Falling back to `PyOpenColorIO` to get the default view.")
        try:
            import PyOpenColorIO
        except ImportError:
            log.warning(
                "Unable to workaround empty return value of "
                "`hou.Color.ocio_defaultView()` because `PyOpenColorIO` is "
                "not available.")
            return preferences

        config_path = preferences["config"]
        config = PyOpenColorIO.Config.CreateFromFile(config_path)
        display = config.getDefaultDisplay()
        assert display == preferences["display"], \
            "Houdini default OCIO display must match config default display"
        view = config.getDefaultView(display)
        preferences["display"] = display
        preferences["view"] = view

    return preferences

get_context_var_changes()

get context var changes.

Source code in client/ayon_houdini/api/lib.py
948
949
950
951
952
953
954
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
994
995
def get_context_var_changes():
    """get context var changes."""

    houdini_vars_to_update = {}

    project_settings = get_current_project_settings()
    houdini_vars_settings = \
        project_settings["houdini"]["general"]["update_houdini_var_context"]

    if not houdini_vars_settings["enabled"]:
        return houdini_vars_to_update

    houdini_vars = houdini_vars_settings["houdini_vars"]

    # No vars specified - nothing to do
    if not houdini_vars:
        return houdini_vars_to_update

    # Get Template data
    template_data = get_current_context_template_data_with_entity_attrs()

    # Set Houdini Vars
    for item in houdini_vars:
        # For consistency reasons we always force all vars to be uppercase
        # Also remove any leading, and trailing whitespaces.
        var = item["var"].strip().upper()

        # get and resolve template in value
        item_value = StringTemplate.format_template(
            item["value"],
            template_data
        )

        if var == "JOB" and item_value == "":
            # sync $JOB to $HIP if $JOB is empty
            item_value = os.environ["HIP"]

        if item["is_directory"]:
            item_value = item_value.replace("\\", "/")

        current_value = hou.hscript("echo -n `${}`".format(var))[0]

        if current_value != item_value:
            houdini_vars_to_update[var] = (
                current_value, item_value, item["is_directory"]
            )

    return houdini_vars_to_update

get_current_context_template_data_with_entity_attrs()

Return template data including current context folder and task attribs.

Output contains
  • Regular template data from get_template_data
  • 'folderAttributes' key with folder attribute values.
  • 'taskAttributes' key with task attribute values.

Returns:

Type Description

dict[str, Any]: Template data to fill templates.

Source code in client/ayon_houdini/api/lib.py
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
def get_current_context_template_data_with_entity_attrs():
    """Return template data including current context folder and task attribs.

    Output contains:
      - Regular template data from `get_template_data`
      - 'folderAttributes' key with folder attribute values.
      - 'taskAttributes' key with task attribute values.

    Returns:
         dict[str, Any]: Template data to fill templates.

    """
    context = get_current_context()
    project_name = context["project_name"]
    folder_path = context["folder_path"]
    task_name = context["task_name"]
    host_name = get_current_host_name()

    project_entity = ayon_api.get_project(project_name)
    anatomy = Anatomy(project_name, project_entity=project_entity)
    folder_entity = ayon_api.get_folder_by_path(project_name, folder_path)
    task_entity = ayon_api.get_task_by_name(
        project_name, folder_entity["id"], task_name
    )

    # get context specific vars
    folder_attributes = folder_entity["attrib"]
    task_attributes = task_entity["attrib"]

    # compute `frameStartHandle` and `frameEndHandle`
    for attributes in [folder_attributes, task_attributes]:
        frame_start = attributes.get("frameStart")
        frame_end = attributes.get("frameEnd")
        handle_start = attributes.get("handleStart")
        handle_end = attributes.get("handleEnd")
        if frame_start is not None and handle_start is not None:
            attributes["frameStartHandle"] = frame_start - handle_start
        if frame_end is not None and handle_end is not None:
            attributes["frameEndHandle"] = frame_end + handle_end

    template_data = get_template_data(
        project_entity, folder_entity, task_entity, host_name
    )
    template_data["root"] = anatomy.roots
    template_data["folderAttributes"] = folder_attributes
    template_data["taskAttributes"] = task_attributes

    return template_data

get_entity_fps(entity=None)

Return current task fps or fps from an entity.

Source code in client/ayon_houdini/api/lib.py
40
41
42
43
44
45
def get_entity_fps(entity=None):
    """Return current task fps or fps from an entity."""

    if entity is None:
        entity = get_current_task_entity(fields=["attrib.fps"])
    return entity["attrib"]["fps"]

get_frame_data(node, log=None)

Get the frame data: frameStartHandle, frameEndHandle and byFrameStep.

This function uses Houdini node's trange, t1,t2andt3` parameters as the source of truth for the full inclusive frame range to render, as such these are considered as the frame range including the handles.

The non-inclusive frame start and frame end without handles can be computed by subtracting the handles from the inclusive frame range.

Parameters:

Name Type Description Default
node Node

ROP node to retrieve frame range from, the frame range is assumed to be the frame range including the start and end handles.

required

Returns:

Name Type Description
dict

frame data for frameStartHandle, frameEndHandle and byFrameStep.

Source code in client/ayon_houdini/api/lib.py
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
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
def get_frame_data(node, log=None):
    """Get the frame data: `frameStartHandle`, `frameEndHandle`
    and `byFrameStep`.

    This function uses Houdini node's `trange`, `t1, `t2` and `t3`
    parameters as the source of truth for the full inclusive frame
    range to render, as such these are considered as the frame
    range including the handles.

    The non-inclusive frame start and frame end without handles
    can be computed by subtracting the handles from the inclusive
    frame range.

    Args:
        node (hou.Node): ROP node to retrieve frame range from,
            the frame range is assumed to be the frame range
            *including* the start and end handles.

    Returns:
        dict: frame data for `frameStartHandle`, `frameEndHandle`
            and `byFrameStep`.

    """

    if log is None:
        log = self.log

    data = {}

    if node.parm("trange") is None:
        log.debug(
            "Node has no 'trange' parameter: {}".format(node.path())
        )
        return data

    if node.evalParm("trange") == 0:
        data["frameStartHandle"] = hou.intFrame()
        data["frameEndHandle"] = hou.intFrame()
        data["byFrameStep"] = 1.0

        log.info(
            "Node '{}' has 'Render current frame' set.\n"
            "Task handles are ignored.\n"
            "frameStart and frameEnd are set to the "
            "current frame.".format(node.path())
        )
    else:
        data["frameStartHandle"] = int(node.evalParm("f1"))
        data["frameEndHandle"] = int(node.evalParm("f2"))
        data["byFrameStep"] = node.evalParm("f3")

    return data

get_lops_rop_context_options(ropnode)

Return the Context Options that a LOP ROP node uses.

Source code in client/ayon_houdini/api/lib.py
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def get_lops_rop_context_options(
        ropnode: hou.RopNode) -> "dict[str, str | float]":
    """Return the Context Options that a LOP ROP node uses."""
    rop_context_options: "dict[str, str | float]" = {}

    # Always set @ropname and @roppath
    # See: https://www.sidefx.com/docs/houdini/hom/hou/isAutoContextOption.html
    rop_context_options["ropname"] = ropnode.name()
    rop_context_options["roppath"] = ropnode.path()

    # Set @ropcook, @ropstart, @ropend and @ropinc if setropcook is enabled
    setropcook_parm = ropnode.parm("setropcook")
    if setropcook_parm:
        setropcook = setropcook_parm.eval()
        if setropcook:
            # TODO: Support "Render Frame Range from Stage" correctly
            # TODO: Support passing in the start, end, and increment values
            #  for the cases where this may need to consider overridden
            #  frame ranges for `RopNode.render()` calls.
            trange = ropnode.evalParm("trange")
            if trange == 0:
                # Current frame
                start: float = hou.frame()
                end: float = start
                inc: float = 1.0
            elif trange in {1, 2}:
                # Frame range
                start: float = ropnode.evalParm("f1")
                end: float = ropnode.evalParm("f2")
                inc: float = ropnode.evalParm("f3")
            else:
                raise ValueError("Unsupported trange value: %s" % trange)
            rop_context_options["ropcook"] = 1.0
            rop_context_options["ropstart"] = start
            rop_context_options["ropend"] = end
            rop_context_options["ropinc"] = inc

    # Get explicit context options set on the ROP node.
    num = ropnode.evalParm("optioncount")
    for i in range(1, num + 1):
        # Ignore disabled options
        if not ropnode.evalParm(f"optionenable{i}"):
            continue

        name: str = ropnode.evalParm(f"optionname{i}")
        option_type: str = ropnode.evalParm(f"optiontype{i}")
        if option_type == "string":
            value: str = ropnode.evalParm(f"optionstrvalue{i}")
        elif option_type == "float":
            value: float = ropnode.evalParm(f"optionfloatvalue{i}")
        else:
            raise ValueError(f"Unsupported option type: {option_type}")
        rop_context_options[name] = value

    return rop_context_options

get_main_window()

Acquire Houdini's main window

Source code in client/ayon_houdini/api/lib.py
541
542
543
544
545
def get_main_window():
    """Acquire Houdini's main window"""
    if self._parent is None:
        self._parent = hou.ui.mainQtWindow()
    return self._parent

get_node_thumbnail(node, first_only=True)

Return node thumbnails.

Return network background images that are linked to the given node. By default, only returns the first one found, unless first_only is False.

Returns:

Type Description

Union[hou.NetworkImage, List[hou.NetworkImage]]: Connected network images

Source code in client/ayon_houdini/api/lib.py
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
def get_node_thumbnail(node, first_only=True):
    """Return node thumbnails.

    Return network background images that are linked to the given node.
    By default, only returns the first one found, unless `first_only` is False.

    Returns:
        Union[hou.NetworkImage, List[hou.NetworkImage]]:
            Connected network images

    """
    parent = node.parent()
    images = get_background_images(parent)
    node_path = node.path()

    def is_attached_to_node(image):
        return image.relativeToPath() == node_path

    attached_images = filter(is_attached_to_node, images)

    # Find first existing image attached to node
    if first_only:
        return next(attached_images, None)
    else:
        return attached_images

get_obj_node_output(obj_node)

Find output node.

If the node has any output node return the output node with the minimum outputidx. When no output is present return the node with the display flag set. If no output node is detected then None is returned.

Parameters:

Name Type Description Default
node Node

The node to retrieve a single the output node for.

required

Returns:

Type Description

Optional[hou.Node]: The child output node.

Source code in client/ayon_houdini/api/lib.py
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
def get_obj_node_output(obj_node):
    """Find output node.

    If the node has any output node return the
    output node with the minimum `outputidx`.
    When no output is present return the node
    with the display flag set. If no output node is
    detected then None is returned.

    Arguments:
        node (hou.Node): The node to retrieve a single
            the output node for.

    Returns:
        Optional[hou.Node]: The child output node.

    """

    outputs = obj_node.subnetOutputs()
    if not outputs:
        return

    elif len(outputs) == 1:
        return outputs[0]

    else:
        return min(outputs,
                   key=lambda node: node.evalParm('outputidx'))

get_output_children(output_node, include_sops=True)

Recursively return a list of all output nodes contained in this node including this node.

It works in a similar manner to output_node.allNodes().

Source code in client/ayon_houdini/api/lib.py
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
def get_output_children(output_node, include_sops=True):
    """Recursively return a list of all output nodes
    contained in this node including this node.

    It works in a similar manner to output_node.allNodes().
    """
    out_list = [output_node]

    if output_node.childTypeCategory() == hou.objNodeTypeCategory():
        for child in output_node.children():
            out_list += get_output_children(child, include_sops=include_sops)

    elif include_sops and \
            output_node.childTypeCategory() == hou.sopNodeTypeCategory():
        out = get_obj_node_output(output_node)
        if out:
            out_list += [out]

    return out_list

get_output_parameter(node)

Return the render output parameter of the given node

Example

root = hou.node("/obj") my_alembic_node = root.createNode("alembic") get_output_parameter(my_alembic_node)

"filename"

Notes

I'm using node.type().name() to get on par with the creators, Because the return value of node.type().name() is the same string value used in creators e.g. instance_data.update({"node_type": "alembic"})

Rop nodes in different network categories have the same output parameter. So, I took that into consideration as a hint for future development.

Parameters:

Name Type Description Default
node(hou.Node)

node instance

required

Returns:

Type Description

hou.Parm

Source code in client/ayon_houdini/api/lib.py
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
def get_output_parameter(node):
    """Return the render output parameter of the given node

    Example:
        root = hou.node("/obj")
        my_alembic_node = root.createNode("alembic")
        get_output_parameter(my_alembic_node)
        >>> "filename"

    Notes:
        I'm using node.type().name() to get on par with the creators,
            Because the return value of `node.type().name()` is the
            same string value used in creators
            e.g. instance_data.update({"node_type": "alembic"})

        Rop nodes in different network categories have
            the same output parameter.
            So, I took that into consideration as a hint for
            future development.

    Args:
        node(hou.Node): node instance

    Returns:
        hou.Parm
    """

    node_type = node.type().name()

    # Figure out which type of node is being rendered
    if node_type in {"alembic", "rop_alembic"}:
        return node.parm("filename")
    elif node_type == "arnold":
        if node_type.evalParm("ar_ass_export_enable"):
            return node.parm("ar_ass_file")
        return node.parm("ar_picture")
    elif node_type in {
        "geometry",
        "rop_geometry",
        "filmboxfbx",
        "rop_fbx"
    }:
        return node.parm("sopoutput")
    elif node_type == "comp":
        return node.parm("copoutput")
    elif node_type in {"karma", "opengl", "flipbook"}:
        return node.parm("picture")
    elif node_type == "ifd":  # Mantra
        if node.evalParm("soho_outputmode"):
            return node.parm("soho_diskfile")
        return node.parm("vm_picture")
    elif node_type == "Redshift_Proxy_Output":
        return node.parm("RS_archive_file")
    elif node_type == "Redshift_ROP":
        return node.parm("RS_outputFileNamePrefix")
    elif node_type in {"usd", "usd_rop", "usdexport"}:
        return node.parm("lopoutput")
    elif node_type in {"usdrender", "usdrender_rop"}:
        return node.parm("outputimage")
    elif node_type == "vray_renderer":
        return node.parm("SettingsOutput_img_file_path")

    raise TypeError("Node type '%s' not supported" % node_type)

get_resolution_from_entity(entity)

Get resolution from the given entity.

Parameters:

Name Type Description Default
entity dict[str, Any]

Project, Folder or Task entity.

required

Returns:

Type Description

Union[Tuple[int, int], None]: Resolution width and height.

Source code in client/ayon_houdini/api/lib.py
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
def get_resolution_from_entity(entity):
    """Get resolution from the given entity.

    Args:
        entity (dict[str, Any]): Project, Folder or Task entity.

    Returns:
        Union[Tuple[int, int], None]: Resolution width and height.

    """
    if not entity or "attrib" not in entity:
        raise ValueError(f"Entity is not valid: \"{entity}\"")

    attributes = entity["attrib"]
    resolution_width = attributes.get("resolutionWidth")
    resolution_height = attributes.get("resolutionHeight")

    # Make sure both width and height are set
    if resolution_width is None or resolution_height is None:
        print(f"No resolution information found in entity: '{entity}'")
        return None

    return int(resolution_width), int(resolution_height)

get_scene_viewer(visible_only=True)

Return an instance of a visible viewport.

There may be many, some could be closed, any visible are current

Parameters:

Name Type Description Default
visible_only Optional[bool]

Only return viewers that currently are the active tab (and hence are visible).

True

Returns:

Type Description

Optional[hou.SceneViewer]: A scene viewer, if any.

Source code in client/ayon_houdini/api/lib.py
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
def get_scene_viewer(visible_only=True):
    """
    Return an instance of a visible viewport.

    There may be many, some could be closed, any visible are current

    Arguments:
        visible_only (Optional[bool]): Only return viewers that currently
            are the active tab (and hence are visible).

    Returns:
        Optional[hou.SceneViewer]: A scene viewer, if any.
    """
    panes = hou.ui.paneTabs()
    panes = [x for x in panes if x.type() == hou.paneTabType.SceneViewer]

    if visible_only:
        return next((pane for pane in panes if pane.isCurrentTab()), None)

    panes = sorted(panes, key=lambda x: x.isCurrentTab())
    if panes:
        return panes[-1]

    return None

imprint(node, data, update=False)

Store attributes with value on a node

Depending on the type of attribute it creates the correct parameter template. Houdini uses a template per type, see the docs for more information.

http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html

Because of some update glitch where you cannot overwrite existing ParmTemplates on node using: setParmTemplates() and parmTuplesInFolder() update is done in another pass.

Parameters:

Name Type Description Default
node(hou.Node)

node object from Houdini

required
data(dict)

collection of attributes and their value

required
update bool

flag if imprint should update already existing data or leave them untouched and only add new.

False

Returns:

Type Description

None

Source code in client/ayon_houdini/api/lib.py
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
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
341
342
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
369
370
371
372
373
374
375
376
377
378
379
380
381
def imprint(node, data, update=False):
    """Store attributes with value on a node

    Depending on the type of attribute it creates the correct parameter
    template. Houdini uses a template per type, see the docs for more
    information.

    http://www.sidefx.com/docs/houdini/hom/hou/ParmTemplate.html

    Because of some update glitch where you cannot overwrite existing
    ParmTemplates on node using:
        `setParmTemplates()` and `parmTuplesInFolder()`
    update is done in another pass.

    Args:
        node(hou.Node): node object from Houdini
        data(dict): collection of attributes and their value
        update (bool, optional): flag if imprint should update
            already existing data or leave them untouched and only
            add new.

    Returns:
        None

    """
    if not data:
        return
    if not node:
        self.log.error("Node is not set, calling imprint on invalid data.")
        return

    current_parms = {p.name(): p for p in node.spareParms()}
    update_parm_templates = []
    new_parm_templates = []

    for key, value in data.items():
        if value is None:
            continue

        parm_template = get_template_from_value(key, value)

        if key in current_parms:
            if node.evalParm(key) == value:
                continue
            if not update:
                log.debug(f"{key} already exists on {node}")
            else:
                log.debug(f"replacing {key}")
                update_parm_templates.append(parm_template)
            continue

        new_parm_templates.append(parm_template)

    if not new_parm_templates and not update_parm_templates:
        return

    parm_group = node.parmTemplateGroup()

    # Add new parm templates
    if new_parm_templates:
        parm_folder = parm_group.findFolder("Extra")

        # if folder doesn't exist yet, create one and append to it,
        # else append to existing one
        if not parm_folder:
            parm_folder = hou.FolderParmTemplate("folder", "Extra")
            parm_folder.setParmTemplates(new_parm_templates)
            parm_group.append(parm_folder)
        else:
            # Add to parm template folder instance then replace with updated
            # one in parm template group
            for template in new_parm_templates:
                parm_folder.addParmTemplate(template)
            parm_group.replace(parm_folder.name(), parm_folder)

    # Update existing parm templates
    for parm_template in update_parm_templates:
        parm_group.replace(parm_template.name(), parm_template)

        # When replacing a parm with a parm of the same name it preserves its
        # value if before the replacement the parm was not at the default,
        # because it has a value override set. Since we're trying to update the
        # parm by using the new value as `default` we enforce the parm is at
        # default state
        node.parm(parm_template.name()).revertToDefaults()

    node.setParmTemplateGroup(parm_group)

is_version_up_workfile_menu_enabled() cached

Check if the 'Version Up Workfile' menu should be enabled.

It's cached because we don't care about updating the menu during the current Houdini session and this allows us to avoid re-querying the project settings each time.

Source code in client/ayon_houdini/api/lib.py
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
@lru_cache(1)
def is_version_up_workfile_menu_enabled() -> bool:
    """Check if the 'Version Up Workfile' menu should be enabled.

    It's cached because we don't care about updating the menu during the
    current Houdini session and this allows us to avoid re-querying the
    project settings each time.

    """
    project_settings = get_current_project_settings()
    if project_settings["core"]["tools"]["ayon_menu"].get(
        "version_up_current_workfile"
    ):
        return True
    return False

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

Return nodes that have attr When value is not None it will only return nodes matching that value for the given attribute. Args: attr (str): Name of the attribute (hou.Parm) value (object, Optional): The value to compare the attribute too. When the default None is provided the value check is skipped. root (str): The root path in Houdini to search in. Returns: list: Matching nodes that have attribute with value.

Source code in client/ayon_houdini/api/lib.py
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def lsattr(attr, value=None, root="/"):
    """Return nodes that have `attr`
     When `value` is not None it will only return nodes matching that value
     for the given attribute.
     Args:
         attr (str): Name of the attribute (hou.Parm)
         value (object, Optional): The value to compare the attribute too.
            When the default None is provided the value check is skipped.
        root (str): The root path in Houdini to search in.
    Returns:
        list: Matching nodes that have attribute with value.
    """
    if value is None:
        # Use allSubChildren() as allNodes() errors on nodes without
        # permission to enter without a means to continue of querying
        # the rest
        nodes = hou.node(root).allSubChildren()
        return [n for n in nodes if n.parm(attr)]
    return lsattrs({attr: value})

lsattrs(attrs, root='/')

Return nodes matching key and value Arguments: attrs (dict): collection of attribute: value root (str): The root path in Houdini to search in. Example: >> lsattrs({"id": "myId"}) ["myNode"] >> lsattr("id") ["myNode", "myOtherNode"] Returns: list: Matching nodes that have attribute with value.

Source code in client/ayon_houdini/api/lib.py
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def lsattrs(attrs, root="/"):
    """Return nodes matching `key` and `value`
    Arguments:
        attrs (dict): collection of attribute: value
        root (str): The root path in Houdini to search in.
    Example:
        >> lsattrs({"id": "myId"})
        ["myNode"]
        >> lsattr("id")
        ["myNode", "myOtherNode"]
    Returns:
        list: Matching nodes that have attribute with value.
    """

    matches = set()
    # Use allSubChildren() as allNodes() errors on nodes without
    # permission to enter without a means to continue of querying
    # the rest
    nodes = hou.node(root).allSubChildren()
    for node in nodes:
        for attr in attrs:
            if not node.parm(attr):
                continue
            elif node.evalParm(attr) != attrs[attr]:
                continue
            else:
                matches.add(node)

    return list(matches)

maintained_selection()

Maintain selection during context Example: >>> with maintained_selection(): ... # Modify selection ... node.setSelected(on=False, clear_all_selected=True) >>> # Selection restored

Source code in client/ayon_houdini/api/lib.py
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
@contextmanager
def maintained_selection():
    """Maintain selection during context
    Example:
        >>> with maintained_selection():
        ...     # Modify selection
        ...     node.setSelected(on=False, clear_all_selected=True)
        >>> # Selection restored
    """

    previous_selection = hou.selectedNodes()
    try:
        yield
    finally:
        # Clear the selection
        # todo: does hou.clearAllSelected() do the same?
        for node in hou.selectedNodes():
            node.setSelected(on=False)

        if previous_selection:
            for node in previous_selection:
                node.setSelected(on=True)

parm_values(overrides)

Override Parameter values during the context. Arguments: overrides (List[Tuple[hou.Parm, Any]]): The overrides per parm that should be applied during context.

Source code in client/ayon_houdini/api/lib.py
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
@contextmanager
def parm_values(overrides):
    """Override Parameter values during the context.
    Arguments:
        overrides (List[Tuple[hou.Parm, Any]]): The overrides per parm
            that should be applied during context.
    """

    originals = []
    try:
        for parm, value in overrides:
            originals.append((parm, parm.eval()))
            parm.set(value)
        yield
    finally:
        for parm, value in originals:
            # Parameter might not exist anymore so first
            # check whether it's still valid
            if hou.parm(parm.path()):
                parm.set(value)

prompt_reset_context()

Prompt the user what context settings to reset. This prompt is used on saving to a different task to allow the scene to get matched to the new context.

Source code in client/ayon_houdini/api/lib.py
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
def prompt_reset_context():
    """Prompt the user what context settings to reset.
    This prompt is used on saving to a different task to allow the scene to
    get matched to the new context.
    """
    # TODO: Cleanup this prototyped mess of imports and odd dialog
    from ayon_core.tools.attribute_defs.dialog import (
        AttributeDefinitionsDialog
    )
    from ayon_core.style import load_stylesheet
    from ayon_core.lib import BoolDef, UILabelDef

    definitions = [
        UILabelDef(
            label=(
                "You are saving your workfile into a different folder or task."
                "\n\n"
                "Would you like to update some settings to the new context?\n"
            )
        ),
        BoolDef(
            "fps",
            label="FPS",
            tooltip="Reset workfile FPS",
            default=True
        ),
        BoolDef(
            "frame_range",
            label="Frame Range",
            tooltip="Reset workfile start and end frame ranges",
            default=True
        ),
        BoolDef(
            "instances",
            label="Publish instances",
            tooltip="Update all publish instance's folder and task to match "
                    "the new folder and task",
            default=True
        ),
    ]

    dialog = AttributeDefinitionsDialog(definitions)
    dialog.setWindowTitle("Saving to different context.")
    dialog.setStyleSheet(load_stylesheet())
    if not dialog.exec_():
        return None

    options = dialog.get_values()
    if options["fps"] or options["frame_range"]:
        reset_framerange(
            fps=options["fps"],
            frame_range=options["frame_range"]
        )

    if options["instances"]:
        update_content_on_context_change()

    dialog.deleteLater()

publisher_show_and_publish(comment=None)

Open publisher window and trigger publishing action.

Parameters:

Name Type Description Default
comment Optional[str]

Comment to set in publisher window.

None
Source code in client/ayon_houdini/api/lib.py
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
def publisher_show_and_publish(comment=None):
    """Open publisher window and trigger publishing action.

    Args:
        comment (Optional[str]): Comment to set in publisher window.
    """

    main_window = get_main_window()
    publisher_window = get_tool_by_name(
        tool_name="publisher",
        parent=main_window,
    )
    publisher_window.show_and_publish(comment)

read(node)

Read the container data in to a dict

Parameters:

Name Type Description Default
node(hou.Node)

Houdini node

required

Returns:

Type Description

dict

Source code in client/ayon_houdini/api/lib.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
def read(node):
    """Read the container data in to a dict

    Args:
        node(hou.Node): Houdini node

    Returns:
        dict

    """
    # `spareParms` returns a tuple of hou.Parm objects
    data = {}
    if not node:
        return data
    for parameter in node.spareParms():
        value = parameter.eval()
        # test if value is json encoded dict
        if isinstance(value, str) and \
                value.startswith(JSON_PREFIX):
            try:
                value = json.loads(value[len(JSON_PREFIX):])
            except json.JSONDecodeError:
                # not a json
                pass
        data[parameter.name()] = value

    return data

remove_all_thumbnails(node)

Remove all node thumbnails.

Removes all network background images that are linked to the given node.

Source code in client/ayon_houdini/api/lib.py
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
def remove_all_thumbnails(node):
    """Remove all node thumbnails.

    Removes all network background images that are linked to the given node.
    """
    parent = node.parent()
    images = get_background_images(parent)
    node_path = node.path()
    images = [
        image for image in images if image.relativeToPath() != node_path
    ]
    set_background_images(parent, images)

render_rop(ropnode, frame_range=None)

Render ROP node utility for Publishing.

This renders a ROP node with the settings we want during Publishing.

Parameters:

Name Type Description Default
ropnode RopNode

Node to render

required
frame_range tuple

Copied from Houdini's help.. Sequence of 2 or 3 values, overrides the frame range and frame increment to render. The first two values specify the start and end frames, and the third value (if given) specifies the frame increment. If no frame increment is given and the ROP node doesn't specify a frame increment, then a value of 1 will be used. If no frame range is given, and the ROP node doesn't specify a frame range, then the current frame will be rendered.

None
Source code in client/ayon_houdini/api/lib.py
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
283
284
285
286
287
288
289
290
291
292
def render_rop(ropnode, frame_range=None):
    """Render ROP node utility for Publishing.

    This renders a ROP node with the settings we want during Publishing.

    Args:
        ropnode (hou.RopNode): Node to render
        frame_range (tuple): Copied from Houdini's help..
            Sequence of 2 or 3 values, overrides the frame range and frame
            increment to render. The first two values specify the start and
            end frames, and the third value (if given) specifies the frame
            increment. If no frame increment is given and the ROP node
            doesn't specify a frame increment, then a value of 1 will be
            used. If no frame range is given, and the ROP node doesn't
            specify a frame range, then the current frame will be rendered.
    """

    if frame_range is None:
        frame_range = ()

    # Print verbose when in batch mode without UI
    verbose = not hou.isUIAvailable()

    # Render
    try:
        ropnode.render(verbose=verbose,
                       # Allow Deadline to capture completion percentage
                       output_progress=verbose,
                       # Render only this node
                       # (do not render any of its dependencies)
                       ignore_inputs=True,
                       frame_range=frame_range)
    except hou.Error as exc:
        # The hou.Error is not inherited from a Python Exception class,
        # so we explicitly capture the houdini error, otherwise pyblish
        # will remain hanging.
        import traceback
        traceback.print_exc()
        raise RuntimeError("Render failed: {0}".format(exc))

reset_framerange(fps=True, frame_range=True)

Set frame range and FPS to current folder.

Source code in client/ayon_houdini/api/lib.py
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
def reset_framerange(fps=True, frame_range=True):
    """Set frame range and FPS to current folder."""

    task_entity = get_current_task_entity(fields={"attrib"})

    # Set FPS
    if fps:
        fps = get_entity_fps(task_entity)
        print("Setting scene FPS to {}".format(int(fps)))
        set_scene_fps(fps)

    if frame_range:

        # Set Start and End Frames
        task_attrib = task_entity["attrib"]
        frame_start = task_attrib.get("frameStart", 0)
        frame_end = task_attrib.get("frameEnd", 0)

        handle_start = task_attrib.get("handleStart", 0)
        handle_end = task_attrib.get("handleEnd", 0)

        frame_start -= int(handle_start)
        frame_end += int(handle_end)

        # Set frame range and FPS
        hou.playbar.setFrameRange(frame_start, frame_end)
        hou.playbar.setPlaybackRange(frame_start, frame_end)
        hou.setFrame(frame_start)

sceneview_snapshot(sceneview, filepath='$HIP/thumbnails/$HIPNAME.$F4.jpg', frame_start=None, frame_end=None)

Take a snapshot of your scene view.

It takes snapshot of your scene view for the given frame range. So, it's capable of generating snapshots image sequence. It works in different Houdini context e.g. Objects, Solaris

Example:: >>> from ayon_houdini.api import lib >>> sceneview = hou.ui.paneTabOfType(hou.paneTabType.SceneViewer) >>> lib.sceneview_snapshot(sceneview)

Notes

.png output will render poorly, so use .jpg.

How it works: Get the current sceneviewer (may be more than one or hidden) and screengrab the perspective viewport to a file in the publish location to be picked up with the publish.

Credits: https://www.sidefx.com/forum/topic/42808/?page=1#post-354796

Parameters:

Name Type Description Default
sceneview SceneViewer

The scene view pane from which you want to take a snapshot.

required
filepath str

thumbnail filepath. it expects $F4 token when frame_end is bigger than frame_star other wise each frame will override its predecessor.

'$HIP/thumbnails/$HIPNAME.$F4.jpg'
frame_start int

the frame at which snapshot starts

None
frame_end int

the frame at which snapshot ends

None
Source code in client/ayon_houdini/api/lib.py
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
def sceneview_snapshot(
        sceneview,
        filepath="$HIP/thumbnails/$HIPNAME.$F4.jpg",
        frame_start=None,
        frame_end=None):
    """Take a snapshot of your scene view.

    It takes snapshot of your scene view for the given frame range.
    So, it's capable of generating snapshots image sequence.
    It works in different Houdini context e.g. Objects, Solaris

    Example::
        >>> from ayon_houdini.api import lib
        >>> sceneview = hou.ui.paneTabOfType(hou.paneTabType.SceneViewer)
        >>> lib.sceneview_snapshot(sceneview)

    Notes:
        .png output will render poorly, so use .jpg.

        How it works:
            Get the current sceneviewer (may be more than one or hidden)
            and screengrab the perspective viewport to a file in the
            publish location to be picked up with the publish.

        Credits:
            https://www.sidefx.com/forum/topic/42808/?page=1#post-354796

    Args:
        sceneview (hou.SceneViewer): The scene view pane from which you want
                                     to take a snapshot.
        filepath (str): thumbnail filepath. it expects `$F4` token
                        when frame_end is bigger than frame_star other wise
                        each frame will override its predecessor.
        frame_start (int): the frame at which snapshot starts
        frame_end (int): the frame at which snapshot ends
    """

    if frame_start is None:
        frame_start = hou.frame()
    if frame_end is None:
        frame_end = frame_start

    if not isinstance(sceneview, hou.SceneViewer):
        log.debug("Wrong Input. {} is not of type hou.SceneViewer."
                  .format(sceneview))
        return
    viewport = sceneview.curViewport()

    flip_settings = sceneview.flipbookSettings().stash()
    flip_settings.frameRange((frame_start, frame_end))
    flip_settings.output(filepath)
    flip_settings.outputToMPlay(False)
    sceneview.flipbook(viewport, flip_settings)
    log.debug("A snapshot of sceneview has been saved to: {}".format(filepath))

self_publish()

Self publish from ROP nodes.

Firstly, it gets the node and its dependencies. Then, it deactivates all other ROPs And finally, it triggers the publishing action.

Source code in client/ayon_houdini/api/lib.py
1083
1084
1085
1086
1087
1088
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
def self_publish():
    """Self publish from ROP nodes.

    Firstly, it gets the node and its dependencies.
    Then, it deactivates all other ROPs
    And finally, it triggers the publishing action.
    """

    result, comment = hou.ui.readInput(
        "Add Publish Comment",
        buttons=("Publish", "Cancel"),
        title="Publish comment",
        close_choice=1
    )

    if result:
        return

    current_node = hou.node(".")
    inputs_paths = find_rop_input_dependencies(
        current_node.inputDependencies()
    )
    inputs_paths.append(current_node.path())

    host = registered_host()
    context = CreateContext(host, reset=True)

    for instance in context.instances:
        node_path = instance.data.get("instance_node")
        instance["active"] = node_path and node_path in inputs_paths

    context.save_changes()

    publisher_show_and_publish(comment)

set_background_images(node, images)

Set hou.NetworkImage background images under given hou.Node

Similar to: nodegraphutils.loadBackgroundImages

Source code in client/ayon_houdini/api/lib.py
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
def set_background_images(node, images):
    """Set hou.NetworkImage background images under given hou.Node

    Similar to: `nodegraphutils.loadBackgroundImages`

    """

    def _serialize(image):
        """Return hou.NetworkImage as serialized dict"""
        if isinstance(image, dict):
            # Assume already serialized, only do some minor validations
            if "path" not in image:
                raise ValueError("Missing `path` key in image dictionary.")
            if "rect" not in image:
                raise ValueError("Missing `rect` key in image dictionary.")
            if len(image["rect"]) != 4:
                raise ValueError("`rect` value must be list of four floats.")
            return image

        rect = image.rect()
        rect_min = rect.min()
        rect_max = rect.max()
        data = {
            "path": image.path(),
            "rect": [rect_min.x(), rect_min.y(), rect_max.x(), rect_max.y()],
        }
        if image.brightness() != 1.0:
            data["brightness"] = image.brightness()
        if image.relativeToPath():
            data["relativetopath"] = image.relativeToPath()
        return data

    with hou.undos.group('Edit Background Images'):
        if images:
            assert all(isinstance(image, (dict, hou.NetworkImage))
                       for image in images)
            data = json.dumps([_serialize(image) for image in images])
            node.setUserData("backgroundimages", data)
        else:
            node.destroyUserData("backgroundimages", must_exist=False)

set_camera_resolution(camera, entity=None)

Apply resolution to camera from task or folder entity.

Parameters:

Name Type Description Default
camera OpNode

Camera node.

required
entity Optional[Dict[str, Any]]

Folder or task entity. If not provided falls back to current task entity.

None
Source code in client/ayon_houdini/api/lib.py
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
def set_camera_resolution(camera, entity=None):
    """Apply resolution to camera from task or folder entity.

    Arguments:
        camera (hou.OpNode): Camera node.
        entity (Optional[Dict[str, Any]]): Folder or task entity.
            If not provided falls back to current task entity.
    """

    if not entity:
        entity = get_current_task_entity()

    resolution = get_resolution_from_entity(entity)

    if resolution:
        print("Setting camera resolution: {} -> {}x{}".format(
            camera.name(), resolution[0], resolution[1]
        ))
        camera.parm("resx").set(resolution[0])
        camera.parm("resy").set(resolution[1])

set_node_thumbnail(node, image_path, rect=None)

Set hou.NetworkImage attached to node.

If an existing connected image is found it assumes that is the existing thumbnail and will update that particular instance instead.

When image_path is None an existing attached hou.NetworkImage will be removed.

Parameters:

Name Type Description Default
node Node

Node to set thumbnail for.

required
image_path Union[str, None]

Path to image to set. If None is set then the thumbnail will be removed if it exists.

required
rect BoundingRect

Bounding rect for the relative placement to the node.

None

Returns:

Type Description

hou.NetworkImage or None: The network image that was set or None if instead it not set or removed.

Source code in client/ayon_houdini/api/lib.py
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
def set_node_thumbnail(node, image_path, rect=None):
    """Set hou.NetworkImage attached to node.

    If an existing connected image is found it assumes that is the existing
    thumbnail and will update that particular instance instead.

    When `image_path` is None an existing attached `hou.NetworkImage` will be
    removed.

    Arguments:
        node (hou.Node): Node to set thumbnail for.
        image_path (Union[str, None]): Path to image to set.
            If None is set then the thumbnail will be removed if it exists.
        rect (hou.BoundingRect): Bounding rect for the relative placement
            to the node.

    Returns:
        hou.NetworkImage or None: The network image that was set or None if
            instead it not set or removed.

    """

    parent = node.parent()
    images = get_background_images(parent)

    node_path = node.path()
    # Find first existing image attached to node
    index, image = next(
        (
            (index, image) for index, image in enumerate(images) if
            image.relativeToPath() == node_path
        ),
        (None, None)
    )
    if image_path is None:
        # Remove image if it exists
        if image:
            images.remove(image)
            set_background_images(parent, images)
        return

    if rect is None:
        rect = hou.BoundingRect(-1, -1, 1, 1)

    if isinstance(image_path, hou.NetworkImage):
        image = image_path
        if index is not None:
            images[index] = image
        else:
            images.append(image)
    elif image is None:
        # Create the image
        image = hou.NetworkImage(image_path, rect)
        image.setRelativeToPath(node.path())
        images.append(image)
    else:
        # Update first existing image
        image.setRect(rect)
        image.setPath(image_path)

    set_background_images(parent, images)

    return image

set_review_color_space(node, review_color_space='', log=None)

Set ociocolorspace parameter for the given OpenGL node.

Set ociocolorspace parameter of the given node to to the given review_color_space value. If review_color_space is empty, a default colorspace corresponding to the display & view of the current Houdini session will be used.

Note

This function expects nodes of type opengl or flipbook.

Parameters:

Name Type Description Default
node Node

ROP node to set its ociocolorspace parm.

required
review_color_space str

Colorspace value for ociocolorspace parm.

''
log Logger

Logger to log to.

None
Source code in client/ayon_houdini/api/lib.py
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
934
935
936
937
938
939
940
941
942
943
944
945
def set_review_color_space(node, review_color_space="", log=None):
    """Set ociocolorspace parameter for the given OpenGL node.

    Set `ociocolorspace` parameter of the given node
    to to the given review_color_space value.
    If review_color_space is empty, a default colorspace corresponding to
    the display & view of the current Houdini session will be used.

    Note:
        This function expects nodes of type `opengl` or `flipbook`.

    Args:
        node (hou.Node): ROP node to set its ociocolorspace parm.
        review_color_space (str): Colorspace value for ociocolorspace parm.
        log (logging.Logger): Logger to log to.
    """

    if log is None:
        log = self.log

    if node.type().name() not in {"opengl", "flipbook"}:
        log.warning(
            "Type of given node {} not allowed."
            " only types `opengl` and `flipbook` are allowed."
            .format(node.type().name())
        )

    # Set Color Correction parameter to OpenColorIO
    colorcorrect_parm = node.parm("colorcorrect")
    if colorcorrect_parm.evalAsString() != "ocio":
        idx = colorcorrect_parm.menuItems().index("ocio")
        colorcorrect_parm.set(idx)
        log.debug(
            "'Color Correction' parm on '{}' has been set to '{}'"
            .format(node.path(), colorcorrect_parm.menuLabels()[idx])
        )

    node.setParms(
        {"ociocolorspace": review_color_space}
    )

    log.debug(
        "'OCIO Colorspace' parm on '{}' has been set to "
        "the view color space '{}'"
        .format(node.path(), review_color_space)
    )

show_node_parmeditor(node)

Show Parameter Editor for the Node.

Parameters:

Name Type Description Default
node Node

node instance

required
Source code in client/ayon_houdini/api/lib.py
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
def show_node_parmeditor(node):
    """Show Parameter Editor for the Node.

    Args:
        node (hou.Node): node instance
    """

    # Check if there's a floating parameter editor pane with its node
    #   set to the specified node.
    for tab in hou.ui.paneTabs():
        if (
            tab.type() == hou.paneTabType.Parm
            and tab.isFloating()
            and tab.currentNode() == node
        ):
            tab.setIsCurrentTab()
            return

    # We are using the hscript to create and set the network path of the pane
    # because hscript can set the node path without selecting the node.
    # Create a floating pane and set its name to the node path.
    hou.hscript(
        f"pane -F -m parmeditor -n {node.path()}"
    )
    # Hide network controls, turn linking off and set operator node path.
    hou.hscript(
        f"pane -a 1 -l 0 -H {node.path()} {node.path()}"
    )

splitext(name, allowed_multidot_extensions)

Split file name to name and extension.

Parameters:

Name Type Description Default
name str

File name to split.

required
allowed_multidot_extensions list of str

List of allowed multidot extensions.

required

Returns:

Name Type Description
tuple

Name and extension.

Source code in client/ayon_houdini/api/lib.py
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
def splitext(name, allowed_multidot_extensions):
    # type: (str, list) -> tuple
    """Split file name to name and extension.

    Args:
        name (str): File name to split.
        allowed_multidot_extensions (list of str): List of allowed multidot
            extensions.

    Returns:
        tuple: Name and extension.
    """

    for ext in allowed_multidot_extensions:
        if name.endswith(ext):
            return name[:-len(ext)], ext

    return os.path.splitext(name)

update_content_on_context_change()

Update all Creator instances to current asset

Source code in client/ayon_houdini/api/lib.py
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
def update_content_on_context_change():
    """Update all Creator instances to current asset"""
    host = registered_host()
    context = host.get_current_context()

    folder_path = context["folder_path"]
    task = context["task_name"]

    create_context = CreateContext(host, reset=True)

    for instance in create_context.instances:
        instance_folder_path = instance.get("folderPath")
        if instance_folder_path and instance_folder_path != folder_path:
            instance["folderPath"] = folder_path
        instance_task = instance.get("task")
        if instance_task and instance_task != task:
            instance["task"] = task

    create_context.save_changes()

update_houdini_vars_context()

Update task context variables

Source code in client/ayon_houdini/api/lib.py
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
def update_houdini_vars_context():
    """Update task context variables"""

    for var, (_old, new, is_directory) in get_context_var_changes().items():
        if is_directory:
            try:
                os.makedirs(new)
            except OSError as e:
                if e.errno != errno.EEXIST:
                    print(
                        "Failed to create ${} dir. Maybe due to "
                        "insufficient permissions.".format(var)
                    )

        hou.hscript("set {}={}".format(var, new))
        os.environ[var] = new
        print("Updated ${} to {}".format(var, new))

update_houdini_vars_context_dialog()

Show pop-up to update task context variables

Source code in client/ayon_houdini/api/lib.py
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
def update_houdini_vars_context_dialog():
    """Show pop-up to update task context variables"""
    update_vars = get_context_var_changes()
    if not update_vars:
        # Nothing to change
        print("Nothing to change, Houdini vars are already up to date.")
        return

    message = "\n".join(
        "${}: {} -> {}".format(var, old or "None", new or "None")
        for var, (old, new, _is_directory) in update_vars.items()
    )

    # TODO: Use better UI!
    parent = hou.ui.mainQtWindow()
    dialog = SimplePopup(parent=parent)
    dialog.setModal(True)
    dialog.setWindowTitle("Houdini scene has outdated task variables")
    dialog.set_message(message)
    dialog.set_button_text("Fix")

    # on_show is the Fix button clicked callback
    dialog.on_clicked.connect(update_houdini_vars_context)

    dialog.show()

validate_fps()

Validate current scene FPS and show pop-up when it is incorrect

Returns:

Type Description

bool

Source code in client/ayon_houdini/api/lib.py
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
def validate_fps():
    """Validate current scene FPS and show pop-up when it is incorrect

    Returns:
        bool

    """

    fps = get_entity_fps()
    current_fps = hou.fps()  # returns float

    if current_fps != fps:

        # Find main window
        parent = hou.ui.mainQtWindow()
        if parent is None:
            pass
        else:
            dialog = PopupUpdateKeys(parent=parent)
            dialog.setModal(True)
            dialog.setWindowTitle("Houdini scene does not match project FPS")
            dialog.set_message("Scene %i FPS does not match project %i FPS" %
                              (current_fps, fps))
            dialog.set_button_text("Fix")

            # on_show is the Fix button clicked callback
            dialog.on_clicked_state.connect(lambda: set_scene_fps(fps))

            dialog.show()

            return False

    return True