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

Commit

Permalink
Merge pull request #3763 from pypeclub/feature/OP-3851_Update-nuke-te…
Browse files Browse the repository at this point in the history
…mplate-build

Nuke: Build workfile by template
  • Loading branch information
iLLiCiTiT authored Sep 2, 2022
2 parents aa46a19 + 5d6b672 commit 1d5e691
Show file tree
Hide file tree
Showing 21 changed files with 1,088 additions and 38 deletions.
137 changes: 122 additions & 15 deletions openpype/hosts/nuke/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,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 @@ -2706,32 +2723,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 @@ -2740,7 +2750,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 @@ -2815,3 +2825,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

0 comments on commit 1d5e691

Please sign in to comment.