Skip to content
This repository has been archived by the owner on Sep 20, 2024. It is now read-only.

Natron Integration #4269

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions openpype/hosts/natron/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .addon import (
NatronAddon,
NATRON_HOST_DIR,
)


__all__ = (
"NatronAddon",
"NATRON_HOST_DIR"
)
31 changes: 31 additions & 0 deletions openpype/hosts/natron/addon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import os
from openpype.modules import OpenPypeModule, IHostAddon


NATRON_HOST_DIR = os.path.dirname(os.path.abspath(__file__))


class NatronAddon(OpenPypeModule, IHostAddon):
name = "natron"
host_name = "natron"

def initialize(self, module_settings):
self.enabled = True

def add_implementation_envs(self, env, _app):
# Add requirements to natron startup
startup_path = os.path.join(NATRON_HOST_DIR, "startup")
if env.get("NATRON_PLUGIN_PATH"):
startup_path += os.pathsep + env["NATRON_PLUGIN_PATH"]

env["NATRON_PLUGIN_PATH"] = startup_path

def get_launch_hook_paths(self, app):
if app.host_name != self.host_name:
return []
return [
os.path.join(NATRON_HOST_DIR, "hooks")
]

def get_workfile_extensions(self):
return [".gfr"]
16 changes: 16 additions & 0 deletions openpype/hosts/natron/api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .pipeline import (
NatronHost,

imprint_container,

get_app

)

__all__ = [
"NatronHost",

"imprint_container",

"get_app"
]
138 changes: 138 additions & 0 deletions openpype/hosts/natron/api/pipeline.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
"""Pipeline tools for OpenPype Gaffer integration."""
import os
import logging

from openpype.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost

import pyblish.api

from openpype.pipeline import (
register_creator_plugin_path,
register_loader_plugin_path,
AVALON_CONTAINER_ID
)
from openpype.hosts.natron import NATRON_HOST_DIR

log = logging.getLogger("openpype.hosts.natron")

PLUGINS_DIR = os.path.join(NATRON_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")


def get_app():
"""Return first available Natron app instance?"""
# TODO: Implement
return None


class NatronHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost):
name = "natron"

def __init__(self):
super(NatronHost, self).__init__()
self._has_been_setup = False

def install(self):
pyblish.api.register_host("natron")

pyblish.api.register_plugin_path(PUBLISH_PATH)
register_loader_plugin_path(LOAD_PATH)
register_creator_plugin_path(CREATE_PATH)

log.info("Installing callbacks ... ")
# register_event_callback("init", on_init)
# self._register_callbacks()
# register_event_callback("before.save", before_save)
# register_event_callback("save", on_save)
# register_event_callback("open", on_open)
# register_event_callback("new", on_new)

# pyblish.api.register_callback(
# "instanceToggled", on_pyblish_instance_toggled
# )

self._has_been_setup = True

def has_unsaved_changes(self):
# TODO: Implement
return False

def get_workfile_extensions(self):
return [".ntp"]

def save_workfile(self, dst_path=None):
if not dst_path:
dst_path = self.get_current_workfile()

app = get_app()
return app.saveProjectAs(dst_path)

def open_workfile(self, filepath):

if not os.path.exists(filepath):
raise RuntimeError("File does not exist: {}".format(filepath))

app = get_app()
app.loadProject(filepath)
return filepath

def get_current_workfile(self):
app = get_app()
return app.getProjectParam("projectPath").getValue()

def get_containers(self):
return []

@staticmethod
def create_context_node():
pass

def update_context_data(self, data, changes):
pass

def get_context_data(self):
pass


def imprint_container(node,
name,
namespace,
context,
loader=None):
"""Imprint a Loader with metadata

Containerisation enables a tracking of version, author and origin
for loaded assets.

Arguments:
tool (object): The node in Fusion to imprint as container, usually a
Loader.
name (str): Name of resulting assembly
namespace (str): Namespace under which to host container
context (dict): Asset information
loader (str, optional): Name of loader used to produce this container.

Returns:
None

"""

data = [
("schema", "openpype:container-2.0"),
("id", AVALON_CONTAINER_ID),
("name", str(name)),
("namespace", str(namespace)),
("loader", str(loader)),
("representation", str(context["representation"]["_id"])),
]

imprint(node, data)


def imprint(node, data, section="OpenPype"):
# TODO: Implement
pass
80 changes: 80 additions & 0 deletions openpype/hosts/natron/plugins/load/actions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""A module containing generic loader actions that will display in the Loader.

"""

from openpype.pipeline import load


class NatronSetFrameRangeLoader(load.LoaderPlugin):
"""Set frame range excluding pre- and post-handles"""

families = ["animation",
"camera",
"imagesequence",
"yeticache",
"pointcache",
"render"]
representations = ["*"]

label = "Set frame range"
order = 11
icon = "clock-o"
color = "white"

def load(self, context, name, namespace, data):

from openpype.hosts.natron.api import get_app

version = context['version']
version_data = version.get("data", {})

start = version_data.get("frameStart", None)
end = version_data.get("frameEnd", None)

if start is None or end is None:
print("Skipping setting frame range because start or "
"end frame data is missing..")
return

app = get_app()
app.getProjectParam('frameRange').set(int(start), int(end))


class NatronSetFrameRangeWithHandlesLoader(load.LoaderPlugin):
"""Set frame range including pre- and post-handles"""

families = ["animation",
"camera",
"imagesequence",
"yeticache",
"pointcache",
"render"]
representations = ["*"]

label = "Set frame range (with handles)"
order = 12
icon = "clock-o"
color = "white"

def load(self, context, name, namespace, data):

from openpype.hosts.natron.api import get_app

version = context['version']
version_data = version.get("data", {})

start = version_data.get("frameStart", None)
end = version_data.get("frameEnd", None)

if start is None or end is None:
print("Skipping setting frame range because start or "
"end frame data is missing..")
return

# Include handles
handles = version_data.get("handles", 0)
start -= handles
end += handles

app = get_app()
app.getProjectParam('frameRange').set(int(start), int(end))
64 changes: 64 additions & 0 deletions openpype/hosts/natron/plugins/load/load_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import os

from openpype.pipeline import (
load,
get_representation_path,
)
from openpype.hosts.natron.api import get_app, imprint_container


class NatronLoadImage(load.LoaderPlugin):
"""Load Image or Image sequence"""

families = ["imagesequence", "review", "render", "plate"]
representations = ["*"]

label = "Load sequence"
order = -10
icon = "code-fork"
color = "orange"

def load(self, context, name, namespace, data):
# Create the Loader with the filename path set
app = get_app()
node = app.createNode("fr.inria.openfx.ReadOIIO")

path = self._convert_path(self.fname)
node.getParam("filename").setValue(path)

imprint_container(node,
name=name,
namespace=namespace,
context=context,
loader=self.__class__.__name__)

def switch(self, container, representation):
self.update(container, representation)

def update(self, container, representation):

path = get_representation_path(representation)
path = self._convert_path(path)

node = container["_node"]
node.getParam("filename").setValue(path)

# Update the imprinted representation
node.getParam("openpype.representation").setValue(
str(representation["_id"])
)

def remove(self, container):
node = container["_node"]

parent = node.parent()
parent.removeChild(node)

def _convert_path(self, path):
root = os.path.dirname(path)
fname = os.path.basename(path)

# TODO: Actually detect whether it's a sequence. And support _ too.
prefix, padding, suffix = fname.rsplit(".", 2)
fname = ".".join([prefix, "#" * len(padding), suffix])
return os.path.join(root, fname).replace("\\", "/")
12 changes: 12 additions & 0 deletions openpype/hosts/natron/startup/init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# -*- coding: utf-8 -*-
"""OpenPype startup script for Natron."""
from openpype.pipeline import install_host
from openpype.hosts.natron.api import NatronHost


def _install_openpype():
print("Installing OpenPype ...")
install_host(NatronHost())


_install_openpype()
Loading
Loading