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

Nuke: Build workfile by template #3763

Merged
merged 15 commits into from
Sep 2, 2022
Merged
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
137 changes: 122 additions & 15 deletions openpype/hosts/nuke/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,23 @@ class Context:
_project_doc = None


def get_main_window():
"""Acquire Nuke's main window"""
if Context.main_window is None:
from Qt import QtWidgets

top_widgets = QtWidgets.QApplication.topLevelWidgets()
name = "Foundry::UI::DockMainWindow"
for widget in top_widgets:
if (
widget.inherits("QMainWindow")
and widget.metaObject().className() == name
):
Context.main_window = widget
break
return Context.main_window


class Knobby(object):
"""For creating knob which it's type isn't mapped in `create_knobs`

Expand Down Expand Up @@ -2702,32 +2719,25 @@ def sync_module(cls):


@contextlib.contextmanager
def _duplicate_node_temp():
def node_tempfile():
"""Create a temp file where node is pasted during duplication.

This is to avoid using clipboard for node duplication.
"""

duplicate_node_temp_path = os.path.join(
tempfile.gettempdir(),
"openpype_nuke_duplicate_temp_{}".format(os.getpid())
tmp_file = tempfile.NamedTemporaryFile(
mode="w", prefix="openpype_nuke_temp_", suffix=".nk", delete=False
)

# This can happen only if 'duplicate_node' would be
if os.path.exists(duplicate_node_temp_path):
log.warning((
"Temp file for node duplication already exists."
" Trying to remove {}"
).format(duplicate_node_temp_path))
os.remove(duplicate_node_temp_path)
tmp_file.close()
node_tempfile_path = tmp_file.name

try:
# Yield the path where node can be copied
yield duplicate_node_temp_path
yield node_tempfile_path

finally:
# Remove the file at the end
os.remove(duplicate_node_temp_path)
os.remove(node_tempfile_path)


def duplicate_node(node):
Expand All @@ -2736,7 +2746,7 @@ def duplicate_node(node):
# select required node for duplication
node.setSelected(True)

with _duplicate_node_temp() as filepath:
with node_tempfile() as filepath:
# copy selected to temp filepath
nuke.nodeCopy(filepath)

Expand Down Expand Up @@ -2807,3 +2817,100 @@ def ls_img_sequence(path):
}

return False


def get_group_io_nodes(nodes):
"""Get the input and the output of a group of nodes."""

if not nodes:
raise ValueError("there is no nodes in the list")

input_node = None
output_node = None

if len(nodes) == 1:
input_node = output_node = nodes[0]

else:
for node in nodes:
if "Input" in node.name():
input_node = node

if "Output" in node.name():
output_node = node

if input_node is not None and output_node is not None:
break

if input_node is None:
raise ValueError("No Input found")

if output_node is None:
raise ValueError("No Output found")
return input_node, output_node


def get_extreme_positions(nodes):
"""Get the 4 numbers that represent the box of a group of nodes."""

if not nodes:
raise ValueError("there is no nodes in the list")

nodes_xpos = [n.xpos() for n in nodes] + \
[n.xpos() + n.screenWidth() for n in nodes]

nodes_ypos = [n.ypos() for n in nodes] + \
[n.ypos() + n.screenHeight() for n in nodes]

min_x, min_y = (min(nodes_xpos), min(nodes_ypos))
max_x, max_y = (max(nodes_xpos), max(nodes_ypos))
return min_x, min_y, max_x, max_y


def refresh_node(node):
"""Correct a bug caused by the multi-threading of nuke.

Refresh the node to make sure that it takes the desired attributes.
"""

x = node.xpos()
y = node.ypos()
nuke.autoplaceSnap(node)
node.setXYpos(x, y)


def refresh_nodes(nodes):
for node in nodes:
refresh_node(node)


def get_names_from_nodes(nodes):
"""Get list of nodes names.

Args:
nodes(List[nuke.Node]): List of nodes to convert into names.

Returns:
List[str]: Name of passed nodes.
"""

return [
node.name()
for node in nodes
]


def get_nodes_by_names(names):
"""Get list of nuke nodes based on their names.

Args:
names (List[str]): List of node names to be found.

Returns:
List[nuke.Node]: List of nodes found by name.
"""

return [
nuke.toNode(name)
for name in names
]
220 changes: 220 additions & 0 deletions openpype/hosts/nuke/api/lib_template_builder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
from collections import OrderedDict

import qargparse

import nuke

from openpype.tools.utils.widgets import OptionDialog

from .lib import imprint, get_main_window


# To change as enum
build_types = ["context_asset", "linked_asset", "all_assets"]


def get_placeholder_attributes(node, enumerate=False):
list_atts = {
"builder_type",
"family",
"representation",
"loader",
"loader_args",
"order",
"asset",
"subset",
"hierarchy",
"siblings",
"last_loaded"
}
attributes = {}
for attr in node.knobs().keys():
if attr in list_atts:
if enumerate:
try:
attributes[attr] = node.knob(attr).values()
except AttributeError:
attributes[attr] = node.knob(attr).getValue()
else:
attributes[attr] = node.knob(attr).getValue()

return attributes


def delete_placeholder_attributes(node):
"""Delete all extra placeholder attributes."""

extra_attributes = get_placeholder_attributes(node)
for attribute in extra_attributes.keys():
try:
node.removeKnob(node.knob(attribute))
except ValueError:
continue


def hide_placeholder_attributes(node):
"""Hide all extra placeholder attributes."""

extra_attributes = get_placeholder_attributes(node)
for attribute in extra_attributes.keys():
try:
node.knob(attribute).setVisible(False)
except ValueError:
continue


def create_placeholder():
args = placeholder_window()
if not args:
# operation canceled, no locator created
return

placeholder = nuke.nodes.NoOp()
placeholder.setName("PLACEHOLDER")
placeholder.knob("tile_color").setValue(4278190335)

# custom arg parse to force empty data query
# and still imprint them on placeholder
# and getting items when arg is of type Enumerator
options = OrderedDict()
for arg in args:
if not type(arg) == qargparse.Separator:
options[str(arg)] = arg._data.get("items") or arg.read()
imprint(placeholder, options)
imprint(placeholder, {"is_placeholder": True})
placeholder.knob("is_placeholder").setVisible(False)


def update_placeholder():
placeholder = nuke.selectedNodes()
if not placeholder:
raise ValueError("No node selected")
if len(placeholder) > 1:
raise ValueError("Too many selected nodes")
placeholder = placeholder[0]

args = placeholder_window(get_placeholder_attributes(placeholder))
if not args:
return # operation canceled
# delete placeholder attributes
delete_placeholder_attributes(placeholder)

options = OrderedDict()
for arg in args:
if not type(arg) == qargparse.Separator:
options[str(arg)] = arg._data.get("items") or arg.read()
imprint(placeholder, options)


def imprint_enum(placeholder, args):
"""
Imprint method doesn't act properly with enums.
Replacing the functionnality with this for now
"""

enum_values = {
str(arg): arg.read()
for arg in args
if arg._data.get("items")
}
string_to_value_enum_table = {
build: idx
for idx, build in enumerate(build_types)
}
attrs = {}
for key, value in enum_values.items():
attrs[key] = string_to_value_enum_table[value]


def placeholder_window(options=None):
options = options or dict()
dialog = OptionDialog(parent=get_main_window())
dialog.setWindowTitle("Create Placeholder")

args = [
qargparse.Separator("Main attributes"),
qargparse.Enum(
"builder_type",
label="Asset Builder Type",
default=options.get("builder_type", 0),
items=build_types,
help="""Asset Builder Type
Builder type describe what template loader will look for.

context_asset : Template loader will look for subsets of
current context asset (Asset bob will find asset)

linked_asset : Template loader will look for assets linked
to current context asset.
Linked asset are looked in OpenPype database under field "inputLinks"
"""
),
qargparse.String(
"family",
default=options.get("family", ""),
label="OpenPype Family",
placeholder="ex: image, plate ..."),
qargparse.String(
"representation",
default=options.get("representation", ""),
label="OpenPype Representation",
placeholder="ex: mov, png ..."),
qargparse.String(
"loader",
default=options.get("loader", ""),
label="Loader",
placeholder="ex: LoadClip, LoadImage ...",
help="""Loader

Defines what openpype loader will be used to load assets.
Useable loader depends on current host's loader list.
Field is case sensitive.
"""),
qargparse.String(
"loader_args",
default=options.get("loader_args", ""),
label="Loader Arguments",
placeholder='ex: {"camera":"persp", "lights":True}',
help="""Loader

Defines a dictionnary of arguments used to load assets.
Useable arguments depend on current placeholder Loader.
Field should be a valid python dict. Anything else will be ignored.
"""),
qargparse.Integer(
"order",
default=options.get("order", 0),
min=0,
max=999,
label="Order",
placeholder="ex: 0, 100 ... (smallest order loaded first)",
help="""Order

Order defines asset loading priority (0 to 999)
Priority rule is : "lowest is first to load"."""),
qargparse.Separator(
"Optional attributes "),
qargparse.String(
"asset",
default=options.get("asset", ""),
label="Asset filter",
placeholder="regex filtering by asset name",
help="Filtering assets by matching field regex to asset's name"),
qargparse.String(
"subset",
default=options.get("subset", ""),
label="Subset filter",
placeholder="regex filtering by subset name",
help="Filtering assets by matching field regex to subset's name"),
qargparse.String(
"hierarchy",
default=options.get("hierarchy", ""),
label="Hierarchy filter",
placeholder="regex filtering by asset's hierarchy",
help="Filtering assets by matching field asset's hierarchy")
]
dialog.create(args)
if not dialog.exec_():
return None

return args
Loading