From 7c22eee4d9afe90149017d6ee1a02c355256c19d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 4 Feb 2022 18:24:31 +0100 Subject: [PATCH] moved houdini into openpype --- openpype/hosts/houdini/api/__init__.py | 208 +++-------- openpype/hosts/houdini/api/lib.py | 190 +++++++++- openpype/hosts/houdini/api/pipeline.py | 347 ++++++++++++++++++ openpype/hosts/houdini/api/plugin.py | 69 +++- openpype/hosts/houdini/api/workio.py | 58 +++ .../hosts/houdini/startup/MainMenuCommon.xml | 12 - openpype/hosts/houdini/startup/scripts/123.py | 5 +- .../houdini/startup/scripts/houdinicore.py | 5 +- 8 files changed, 707 insertions(+), 187 deletions(-) create mode 100644 openpype/hosts/houdini/api/pipeline.py create mode 100644 openpype/hosts/houdini/api/workio.py diff --git a/openpype/hosts/houdini/api/__init__.py b/openpype/hosts/houdini/api/__init__.py index 7328236b97..e1500aa5f5 100644 --- a/openpype/hosts/houdini/api/__init__.py +++ b/openpype/hosts/houdini/api/__init__.py @@ -1,174 +1,60 @@ -import os -import sys -import logging -import contextlib +from .pipeline import ( + install, + uninstall, -import hou - -from pyblish import api as pyblish -from avalon import api as avalon - -import openpype.hosts.houdini -from openpype.hosts.houdini.api import lib - -from openpype.lib import ( - any_outdated + ls, + containerise, ) -from .lib import get_asset_fps +from .plugin import ( + Creator, +) -log = logging.getLogger("openpype.hosts.houdini") +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) -HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.houdini.__file__)) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") -CREATE_PATH = os.path.join(PLUGINS_DIR, "create") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") +from .lib import ( + lsattr, + lsattrs, + read, + + maintained_selection, + unique_name +) -def install(): +__all__ = [ + "install", + "uninstall", - pyblish.register_plugin_path(PUBLISH_PATH) - avalon.register_plugin_path(avalon.Loader, LOAD_PATH) - avalon.register_plugin_path(avalon.Creator, CREATE_PATH) + "ls", + "containerise", - log.info("Installing callbacks ... ") - # avalon.on("init", on_init) - avalon.before("save", before_save) - avalon.on("save", on_save) - avalon.on("open", on_open) - avalon.on("new", on_new) + "Creator", - pyblish.register_callback("instanceToggled", on_pyblish_instance_toggled) + # Workfiles API + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root", - log.info("Setting default family states for loader..") - avalon.data["familiesStateToggled"] = [ - "imagesequence", - "review" - ] + # Utility functions + "lsattr", + "lsattrs", + "read", - # add houdini vendor packages - hou_pythonpath = os.path.join(os.path.dirname(HOST_DIR), "vendor") + "maintained_selection", + "unique_name" +] - sys.path.append(hou_pythonpath) - - # Set asset FPS for the empty scene directly after launch of Houdini - # so it initializes into the correct scene FPS - _set_asset_fps() - - -def before_save(*args): - return lib.validate_fps() - - -def on_save(*args): - - avalon.logger.info("Running callback on save..") - - nodes = lib.get_id_required_nodes() - for node, new_id in lib.generate_ids(nodes): - lib.set_id(node, new_id, overwrite=False) - - -def on_open(*args): - - if not hou.isUIAvailable(): - log.debug("Batch mode detected, ignoring `on_open` callbacks..") - return - - avalon.logger.info("Running callback on open..") - - # Validate FPS after update_task_from_path to - # ensure it is using correct FPS for the asset - lib.validate_fps() - - if any_outdated(): - from openpype.widgets import popup - - log.warning("Scene has outdated content.") - - # Get main window - parent = hou.ui.mainQtWindow() - if parent is None: - log.info("Skipping outdated content pop-up " - "because Houdini window can't be found.") - else: - - # Show outdated pop-up - def _on_show_inventory(): - import avalon.tools.sceneinventory as tool - tool.show(parent=parent) - - dialog = popup.Popup(parent=parent) - dialog.setWindowTitle("Houdini scene has outdated content") - dialog.setMessage("There are outdated containers in " - "your Houdini scene.") - dialog.on_clicked.connect(_on_show_inventory) - dialog.show() - - -def on_new(_): - """Set project resolution and fps when create a new file""" - avalon.logger.info("Running callback on new..") - _set_asset_fps() - - -def _set_asset_fps(): - """Set Houdini scene FPS to the default required for current asset""" - - # Set new scene fps - fps = get_asset_fps() - print("Setting scene FPS to %i" % fps) - lib.set_scene_fps(fps) - - -def on_pyblish_instance_toggled(instance, new_value, old_value): - """Toggle saver tool passthrough states on instance toggles.""" - @contextlib.contextmanager - def main_take(no_update=True): - """Enter root take during context""" - original_take = hou.takes.currentTake() - original_update_mode = hou.updateModeSetting() - root = hou.takes.rootTake() - has_changed = False - try: - if original_take != root: - has_changed = True - if no_update: - hou.setUpdateMode(hou.updateMode.Manual) - hou.takes.setCurrentTake(root) - yield - finally: - if has_changed: - if no_update: - hou.setUpdateMode(original_update_mode) - hou.takes.setCurrentTake(original_take) - - if not instance.data.get("_allowToggleBypass", True): - return - - nodes = instance[:] - if not nodes: - return - - # Assume instance node is first node - instance_node = nodes[0] - - if not hasattr(instance_node, "isBypassed"): - # Likely not a node that can actually be bypassed - log.debug("Can't bypass node: %s", instance_node.path()) - return - - if instance_node.isBypassed() != (not old_value): - print("%s old bypass state didn't match old instance state, " - "updating anyway.." % instance_node.path()) - - try: - # Go into the main take, because when in another take changing - # the bypass state of a note cannot be done due to it being locked - # by default. - with main_take(no_update=True): - instance_node.bypass(not new_value) - except hou.PermissionError as exc: - log.warning("%s - %s", instance_node.path(), exc) +# Backwards API compatibility +open = open_file +save = save_file diff --git a/openpype/hosts/houdini/api/lib.py b/openpype/hosts/houdini/api/lib.py index 53f0e59ea9..eaaba94ed5 100644 --- a/openpype/hosts/houdini/api/lib.py +++ b/openpype/hosts/houdini/api/lib.py @@ -2,9 +2,11 @@ import uuid import logging from contextlib import contextmanager -from openpype.api import get_asset +import six + from avalon import api, io -from avalon.houdini import lib as houdini +from openpype.api import get_asset + import hou @@ -15,11 +17,11 @@ def get_asset_fps(): """Return current asset fps.""" return get_asset()["data"].get("fps") -def set_id(node, unique_id, overwrite=False): +def set_id(node, unique_id, overwrite=False): exists = node.parm("id") if not exists: - houdini.imprint(node, {"id": unique_id}) + imprint(node, {"id": unique_id}) if not exists and overwrite: node.setParm("id", unique_id) @@ -342,3 +344,183 @@ def render_rop(ropnode): import traceback traceback.print_exc() raise RuntimeError("Render failed: {0}".format(exc)) + + +def children_as_string(node): + return [c.name() for c in node.children()] + + +def imprint(node, data): + """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 + + Args: + node(hou.Node): node object from Houdini + data(dict): collection of attributes and their value + + Returns: + None + + """ + + parm_group = node.parmTemplateGroup() + + parm_folder = hou.FolderParmTemplate("folder", "Extra") + for key, value in data.items(): + if value is None: + continue + + if isinstance(value, float): + parm = hou.FloatParmTemplate(name=key, + label=key, + num_components=1, + default_value=(value,)) + elif isinstance(value, bool): + parm = hou.ToggleParmTemplate(name=key, + label=key, + default_value=value) + elif isinstance(value, int): + parm = hou.IntParmTemplate(name=key, + label=key, + num_components=1, + default_value=(value,)) + elif isinstance(value, six.string_types): + parm = hou.StringParmTemplate(name=key, + label=key, + num_components=1, + default_value=(value,)) + else: + raise TypeError("Unsupported type: %r" % type(value)) + + parm_folder.addParmTemplate(parm) + + parm_group.append(parm_folder) + node.setParmTemplateGroup(parm_group) + + +def lsattr(attr, value=None): + if value is None: + nodes = list(hou.node("/obj").allNodes()) + return [n for n in nodes if n.parm(attr)] + return lsattrs({attr: value}) + + +def lsattrs(attrs): + """Return nodes matching `key` and `value` + + Arguments: + attrs (dict): collection of attribute: value + + Example: + >> lsattrs({"id": "myId"}) + ["myNode"] + >> lsattr("id") + ["myNode", "myOtherNode"] + + Returns: + list + """ + + matches = set() + nodes = list(hou.node("/obj").allNodes()) # returns generator object + 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) + + +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 + return {parameter.name(): parameter.eval() for + parameter in node.spareParms()} + + +def unique_name(name, format="%03d", namespace="", prefix="", suffix="", + separator="_"): + """Return unique `name` + + The function takes into consideration an optional `namespace` + and `suffix`. The suffix is included in evaluating whether a + name exists - such as `name` + "_GRP" - but isn't included + in the returned value. + + If a namespace is provided, only names within that namespace + are considered when evaluating whether the name is unique. + + Arguments: + format (str, optional): The `name` is given a number, this determines + how this number is formatted. Defaults to a padding of 2. + E.g. my_name01, my_name02. + namespace (str, optional): Only consider names within this namespace. + suffix (str, optional): Only consider names with this suffix. + + Example: + >>> name = hou.node("/obj").createNode("geo", name="MyName") + >>> assert hou.node("/obj/MyName") + True + >>> unique = unique_name(name) + >>> assert hou.node("/obj/{}".format(unique)) + False + + """ + + iteration = 1 + + parts = [prefix, name, format % iteration, suffix] + if namespace: + parts.insert(0, namespace) + + unique = separator.join(parts) + children = children_as_string(hou.node("/obj")) + while unique in children: + iteration += 1 + unique = separator.join(parts) + + if suffix: + return unique[:-len(suffix)] + + return unique + + +@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) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py new file mode 100644 index 0000000000..fdd09d8067 --- /dev/null +++ b/openpype/hosts/houdini/api/pipeline.py @@ -0,0 +1,347 @@ +import os +import sys +import logging +import contextlib + +import hou + +import pyblish.api +import avalon.api +from avalon.pipeline import AVALON_CONTAINER_ID +from avalon.lib import find_submodule + +import openpype.hosts.houdini +from openpype.hosts.houdini.api import lib + +from openpype.lib import ( + any_outdated +) + +from .lib import get_asset_fps + +log = logging.getLogger("openpype.hosts.houdini") + +AVALON_CONTAINERS = "/obj/AVALON_CONTAINERS" +IS_HEADLESS = not hasattr(hou, "ui") + +HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.houdini.__file__)) +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") +CREATE_PATH = os.path.join(PLUGINS_DIR, "create") +INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") + + +self = sys.modules[__name__] +self._has_been_setup = False +self._parent = None +self._events = dict() + + +def install(): + _register_callbacks() + + pyblish.api.register_host("houdini") + pyblish.api.register_host("hython") + pyblish.api.register_host("hpython") + + pyblish.api.register_plugin_path(PUBLISH_PATH) + avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) + avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + + log.info("Installing callbacks ... ") + # avalon.on("init", on_init) + avalon.api.before("save", before_save) + avalon.api.on("save", on_save) + avalon.api.on("open", on_open) + avalon.api.on("new", on_new) + + pyblish.api.register_callback("instanceToggled", on_pyblish_instance_toggled) + + log.info("Setting default family states for loader..") + avalon.api.data["familiesStateToggled"] = [ + "imagesequence", + "review" + ] + + self._has_been_setup = True + # add houdini vendor packages + hou_pythonpath = os.path.join(os.path.dirname(HOST_DIR), "vendor") + + sys.path.append(hou_pythonpath) + + # Set asset FPS for the empty scene directly after launch of Houdini + # so it initializes into the correct scene FPS + _set_asset_fps() + + +def uninstall(): + """Uninstall Houdini-specific functionality of avalon-core. + + This function is called automatically on calling `api.uninstall()`. + """ + + pyblish.api.deregister_host("hython") + pyblish.api.deregister_host("hpython") + pyblish.api.deregister_host("houdini") + + +def _register_callbacks(): + for handler, event in self._events.copy().items(): + if event is None: + continue + + try: + hou.hipFile.removeEventCallback(event) + except RuntimeError as e: + log.info(e) + + self._events[on_file_event_callback] = hou.hipFile.addEventCallback( + on_file_event_callback + ) + + +def on_file_event_callback(event): + if event == hou.hipFileEventType.AfterLoad: + avalon.api.emit("open", [event]) + elif event == hou.hipFileEventType.AfterSave: + avalon.api.emit("save", [event]) + elif event == hou.hipFileEventType.BeforeSave: + avalon.api.emit("before_save", [event]) + elif event == hou.hipFileEventType.AfterClear: + avalon.api.emit("new", [event]) + + +def get_main_window(): + """Acquire Houdini's main window""" + if self._parent is None: + self._parent = hou.ui.mainQtWindow() + return self._parent + + +def teardown(): + """Remove integration""" + if not self._has_been_setup: + return + + self._has_been_setup = False + print("pyblish: Integration torn down successfully") + + +def containerise(name, + namespace, + nodes, + context, + loader=None, + suffix=""): + """Bundle `nodes` into a subnet and imprint it with metadata + + Containerisation enables a tracking of version, author and origin + for loaded assets. + + Arguments: + name (str): Name of resulting assembly + namespace (str): Namespace under which to host container + nodes (list): Long names of nodes to containerise + context (dict): Asset information + loader (str, optional): Name of loader used to produce this container. + suffix (str, optional): Suffix of container, defaults to `_CON`. + + Returns: + container (str): Name of container assembly + + """ + + # Ensure AVALON_CONTAINERS subnet exists + subnet = hou.node(AVALON_CONTAINERS) + if subnet is None: + obj_network = hou.node("/obj") + subnet = obj_network.createNode("subnet", + node_name="AVALON_CONTAINERS") + + # Create proper container name + container_name = "{}_{}".format(name, suffix or "CON") + container = hou.node("/obj/{}".format(name)) + container.setName(container_name, unique_name=True) + + data = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace, + "loader": str(loader), + "representation": str(context["representation"]["_id"]), + } + + lib.imprint(container, data) + + # "Parent" the container under the container network + hou.moveNodesTo([container], subnet) + + subnet.node(container_name).moveToGoodPosition() + + return container + + +def parse_container(container): + """Return the container node's full container data. + + Args: + container (hou.Node): A container node name. + + Returns: + dict: The container schema data for this container node. + + """ + data = lib.read(container) + + # Backwards compatibility pre-schemas for containers + data["schema"] = data.get("schema", "openpype:container-1.0") + + # Append transient data + data["objectName"] = container.path() + data["node"] = container + + return data + + +def ls(): + containers = [] + for identifier in (AVALON_CONTAINER_ID, + "pyblish.mindbender.container"): + containers += lib.lsattr("id", identifier) + + has_metadata_collector = False + config_host = find_submodule(avalon.api.registered_config(), "houdini") + if hasattr(config_host, "collect_container_metadata"): + has_metadata_collector = True + + for container in sorted(containers, + # Hou 19+ Python 3 hou.ObjNode are not + # sortable due to not supporting greater + # than comparisons + key=lambda node: node.path()): + data = parse_container(container) + + # Collect custom data if attribute is present + if has_metadata_collector: + metadata = config_host.collect_container_metadata(container) + data.update(metadata) + + yield data + + +def before_save(*args): + return lib.validate_fps() + + +def on_save(*args): + + log.info("Running callback on save..") + + nodes = lib.get_id_required_nodes() + for node, new_id in lib.generate_ids(nodes): + lib.set_id(node, new_id, overwrite=False) + + +def on_open(*args): + + if not hou.isUIAvailable(): + log.debug("Batch mode detected, ignoring `on_open` callbacks..") + return + + log.info("Running callback on open..") + + # Validate FPS after update_task_from_path to + # ensure it is using correct FPS for the asset + lib.validate_fps() + + if any_outdated(): + from openpype.widgets import popup + + log.warning("Scene has outdated content.") + + # Get main window + parent = get_main_window() + if parent is None: + log.info("Skipping outdated content pop-up " + "because Houdini window can't be found.") + else: + + # Show outdated pop-up + def _on_show_inventory(): + from openpype.tools.utils import host_tools + host_tools.show_scene_inventory(parent=parent) + + dialog = popup.Popup(parent=parent) + dialog.setWindowTitle("Houdini scene has outdated content") + dialog.setMessage("There are outdated containers in " + "your Houdini scene.") + dialog.on_clicked.connect(_on_show_inventory) + dialog.show() + + +def on_new(_): + """Set project resolution and fps when create a new file""" + log.info("Running callback on new..") + _set_asset_fps() + + +def _set_asset_fps(): + """Set Houdini scene FPS to the default required for current asset""" + + # Set new scene fps + fps = get_asset_fps() + print("Setting scene FPS to %i" % fps) + lib.set_scene_fps(fps) + + +def on_pyblish_instance_toggled(instance, new_value, old_value): + """Toggle saver tool passthrough states on instance toggles.""" + @contextlib.contextmanager + def main_take(no_update=True): + """Enter root take during context""" + original_take = hou.takes.currentTake() + original_update_mode = hou.updateModeSetting() + root = hou.takes.rootTake() + has_changed = False + try: + if original_take != root: + has_changed = True + if no_update: + hou.setUpdateMode(hou.updateMode.Manual) + hou.takes.setCurrentTake(root) + yield + finally: + if has_changed: + if no_update: + hou.setUpdateMode(original_update_mode) + hou.takes.setCurrentTake(original_take) + + if not instance.data.get("_allowToggleBypass", True): + return + + nodes = instance[:] + if not nodes: + return + + # Assume instance node is first node + instance_node = nodes[0] + + if not hasattr(instance_node, "isBypassed"): + # Likely not a node that can actually be bypassed + log.debug("Can't bypass node: %s", instance_node.path()) + return + + if instance_node.isBypassed() != (not old_value): + print("%s old bypass state didn't match old instance state, " + "updating anyway.." % instance_node.path()) + + try: + # Go into the main take, because when in another take changing + # the bypass state of a note cannot be done due to it being locked + # by default. + with main_take(no_update=True): + instance_node.bypass(not new_value) + except hou.PermissionError as exc: + log.warning("%s - %s", instance_node.path(), exc) diff --git a/openpype/hosts/houdini/api/plugin.py b/openpype/hosts/houdini/api/plugin.py index 63d9bba470..e64b505d2c 100644 --- a/openpype/hosts/houdini/api/plugin.py +++ b/openpype/hosts/houdini/api/plugin.py @@ -1,25 +1,82 @@ # -*- coding: utf-8 -*- """Houdini specific Avalon/Pyblish plugin definitions.""" import sys -from avalon.api import CreatorError -from avalon import houdini import six +import avalon.api +from avalon.api import CreatorError import hou from openpype.api import PypeCreatorMixin +from .lib import imprint class OpenPypeCreatorError(CreatorError): pass -class Creator(PypeCreatorMixin, houdini.Creator): +class Creator(PypeCreatorMixin, avalon.api.Creator): + """Creator plugin to create instances in Houdini + + To support the wide range of node types for render output (Alembic, VDB, + Mantra) the Creator needs a node type to create the correct instance + + By default, if none is given, is `geometry`. An example of accepted node + types: geometry, alembic, ifd (mantra) + + Please check the Houdini documentation for more node types. + + Tip: to find the exact node type to create press the `i` left of the node + when hovering over a node. The information is visible under the name of + the node. + + """ + + def __init__(self, *args, **kwargs): + super(Creator, self).__init__(*args, **kwargs) + self.nodes = list() + def process(self): + """This is the base functionality to create instances in Houdini + + The selected nodes are stored in self to be used in an override method. + This is currently necessary in order to support the multiple output + types in Houdini which can only be rendered through their own node. + + Default node type if none is given is `geometry` + + It also makes it easier to apply custom settings per instance type + + Example of override method for Alembic: + + def process(self): + instance = super(CreateEpicNode, self, process() + # Set paramaters for Alembic node + instance.setParms( + {"sop_path": "$HIP/%s.abc" % self.nodes[0]} + ) + + Returns: + hou.Node + + """ try: - # re-raise as standard Python exception so - # Avalon can catch it - instance = super(Creator, self).process() + if (self.options or {}).get("useSelection"): + self.nodes = hou.selectedNodes() + + # Get the node type and remove it from the data, not needed + node_type = self.data.pop("node_type", None) + if node_type is None: + node_type = "geometry" + + # Get out node + out = hou.node("/out") + instance = out.createNode(node_type, node_name=self.name) + instance.moveToGoodPosition() + + imprint(instance, self.data) + self._process(instance) + except hou.Error as er: six.reraise( OpenPypeCreatorError, diff --git a/openpype/hosts/houdini/api/workio.py b/openpype/hosts/houdini/api/workio.py new file mode 100644 index 0000000000..e7310163ea --- /dev/null +++ b/openpype/hosts/houdini/api/workio.py @@ -0,0 +1,58 @@ +"""Host API required Work Files tool""" +import os + +import hou +from avalon import api + + +def file_extensions(): + return api.HOST_WORKFILE_EXTENSIONS["houdini"] + + +def has_unsaved_changes(): + return hou.hipFile.hasUnsavedChanges() + + +def save_file(filepath): + + # Force forwards slashes to avoid segfault + filepath = filepath.replace("\\", "/") + + hou.hipFile.save(file_name=filepath, + save_to_recent_files=True) + + return filepath + + +def open_file(filepath): + + # Force forwards slashes to avoid segfault + filepath = filepath.replace("\\", "/") + + hou.hipFile.load(filepath, + suppress_save_prompt=True, + ignore_load_warnings=False) + + return filepath + + +def current_file(): + + current_filepath = hou.hipFile.path() + if (os.path.basename(current_filepath) == "untitled.hip" and + not os.path.exists(current_filepath)): + # By default a new scene in houdini is saved in the current + # working directory as "untitled.hip" so we need to capture + # that and consider it 'not saved' when it's in that state. + return None + + return current_filepath + + +def work_root(session): + work_dir = session["AVALON_WORKDIR"] + scene_dir = session.get("AVALON_SCENEDIR") + if scene_dir: + return os.path.join(work_dir, scene_dir) + else: + return work_dir diff --git a/openpype/hosts/houdini/startup/MainMenuCommon.xml b/openpype/hosts/houdini/startup/MainMenuCommon.xml index c34310cf72..bc4a2e809a 100644 --- a/openpype/hosts/houdini/startup/MainMenuCommon.xml +++ b/openpype/hosts/houdini/startup/MainMenuCommon.xml @@ -56,18 +56,6 @@ host_tools.show_workfiles(parent) ]]> - - - - - - - - - - diff --git a/openpype/hosts/houdini/startup/scripts/123.py b/openpype/hosts/houdini/startup/scripts/123.py index 4233d68c15..eb33b49759 100644 --- a/openpype/hosts/houdini/startup/scripts/123.py +++ b/openpype/hosts/houdini/startup/scripts/123.py @@ -1,9 +1,10 @@ -from avalon import api, houdini +import avalon.api +from openpype.hosts.houdini import api def main(): print("Installing OpenPype ...") - api.install(houdini) + avalon.api.install(api) main() diff --git a/openpype/hosts/houdini/startup/scripts/houdinicore.py b/openpype/hosts/houdini/startup/scripts/houdinicore.py index 4233d68c15..eb33b49759 100644 --- a/openpype/hosts/houdini/startup/scripts/houdinicore.py +++ b/openpype/hosts/houdini/startup/scripts/houdinicore.py @@ -1,9 +1,10 @@ -from avalon import api, houdini +import avalon.api +from openpype.hosts.houdini import api def main(): print("Installing OpenPype ...") - api.install(houdini) + avalon.api.install(api) main()