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..4054d5991f
--- /dev/null
+++ b/openpype/hosts/houdini/api/pipeline.py
@@ -0,0 +1,349 @@
+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 event in self._events.copy().values():
+ 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/plugins/create/create_hda.py b/openpype/hosts/houdini/plugins/create/create_hda.py
index 459da8bfdf..0a9c1bad1e 100644
--- a/openpype/hosts/houdini/plugins/create/create_hda.py
+++ b/openpype/hosts/houdini/plugins/create/create_hda.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
-from openpype.hosts.houdini.api import plugin
-from avalon.houdini import lib
-from avalon import io
import hou
+from avalon import io
+from openpype.hosts.houdini.api import lib
+from openpype.hosts.houdini.api import plugin
class CreateHDA(plugin.Creator):
diff --git a/openpype/hosts/houdini/plugins/load/load_alembic.py b/openpype/hosts/houdini/plugins/load/load_alembic.py
index df66d56008..eaab81f396 100644
--- a/openpype/hosts/houdini/plugins/load/load_alembic.py
+++ b/openpype/hosts/houdini/plugins/load/load_alembic.py
@@ -1,6 +1,7 @@
+import os
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
class AbcLoader(api.Loader):
@@ -14,8 +15,6 @@ class AbcLoader(api.Loader):
color = "orange"
def load(self, context, name=None, namespace=None, data=None):
-
- import os
import hou
# Format file name, Houdini only wants forward slashes
diff --git a/openpype/hosts/houdini/plugins/load/load_camera.py b/openpype/hosts/houdini/plugins/load/load_camera.py
index 8b98b7c05e..8916d3b9b7 100644
--- a/openpype/hosts/houdini/plugins/load/load_camera.py
+++ b/openpype/hosts/houdini/plugins/load/load_camera.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
ARCHIVE_EXPRESSION = ('__import__("_alembic_hom_extensions")'
diff --git a/openpype/hosts/houdini/plugins/load/load_hda.py b/openpype/hosts/houdini/plugins/load/load_hda.py
index 6610d5e513..f5f2fb7481 100644
--- a/openpype/hosts/houdini/plugins/load/load_hda.py
+++ b/openpype/hosts/houdini/plugins/load/load_hda.py
@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
class HdaLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/load/load_image.py b/openpype/hosts/houdini/plugins/load/load_image.py
index 4ff2777d77..39f583677b 100644
--- a/openpype/hosts/houdini/plugins/load/load_image.py
+++ b/openpype/hosts/houdini/plugins/load/load_image.py
@@ -1,7 +1,7 @@
import os
from avalon import api
-from avalon.houdini import pipeline, lib
+from openpype.hosts.houdini.api import lib, pipeline
import hou
diff --git a/openpype/hosts/houdini/plugins/load/load_usd_layer.py b/openpype/hosts/houdini/plugins/load/load_usd_layer.py
index 7483101409..232ce59479 100644
--- a/openpype/hosts/houdini/plugins/load/load_usd_layer.py
+++ b/openpype/hosts/houdini/plugins/load/load_usd_layer.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline, lib
+from openpype.hosts.houdini.api import lib, pipeline
class USDSublayerLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/load/load_usd_reference.py b/openpype/hosts/houdini/plugins/load/load_usd_reference.py
index cab3cb5269..224bfc2d61 100644
--- a/openpype/hosts/houdini/plugins/load/load_usd_reference.py
+++ b/openpype/hosts/houdini/plugins/load/load_usd_reference.py
@@ -1,5 +1,5 @@
from avalon import api
-from avalon.houdini import pipeline, lib
+from openpype.hosts.houdini.api import lib, pipeline
class USDReferenceLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/load/load_vdb.py b/openpype/hosts/houdini/plugins/load/load_vdb.py
index 5f7e400b39..40aa7a1d18 100644
--- a/openpype/hosts/houdini/plugins/load/load_vdb.py
+++ b/openpype/hosts/houdini/plugins/load/load_vdb.py
@@ -2,7 +2,7 @@ import os
import re
from avalon import api
-from avalon.houdini import pipeline
+from openpype.hosts.houdini.api import pipeline
class VdbLoader(api.Loader):
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances.py b/openpype/hosts/houdini/plugins/publish/collect_instances.py
index 12d118f0cc..d38927984a 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances.py
@@ -2,7 +2,7 @@ import hou
import pyblish.api
-from avalon.houdini import lib
+from openpype.hosts.houdini.api import lib
class CollectInstances(pyblish.api.ContextPlugin):
diff --git a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
index 7df5e8b6f2..0600730d00 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_instances_usd_layered.py
@@ -1,6 +1,6 @@
import hou
import pyblish.api
-from avalon.houdini import lib
+from openpype.hosts.houdini.api import lib
import openpype.hosts.houdini.api.usd as hou_usdlib
import openpype.lib.usdlib as usdlib
diff --git a/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py b/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py
index 3ae16efe56..c635a53074 100644
--- a/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py
+++ b/openpype/hosts/houdini/plugins/publish/collect_remote_publish.py
@@ -2,7 +2,7 @@ import pyblish.api
import openpype.api
import hou
-from avalon.houdini import lib
+from openpype.hosts.houdini.api import lib
class CollectRemotePublishSettings(pyblish.api.ContextPlugin):
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()