Merge pull request #3763 from pypeclub/feature/OP-3851_Update-nuke-template-build

Nuke: Build workfile by template
This commit is contained in:
Jakub Trllo 2022-09-02 15:41:38 +02:00 committed by GitHub
commit 1d5e691daf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1088 additions and 38 deletions

View file

@ -78,6 +78,23 @@ class Context:
_project_doc = None _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): class Knobby(object):
"""For creating knob which it's type isn't mapped in `create_knobs` """For creating knob which it's type isn't mapped in `create_knobs`
@ -2706,32 +2723,25 @@ class DirmapCache:
@contextlib.contextmanager @contextlib.contextmanager
def _duplicate_node_temp(): def node_tempfile():
"""Create a temp file where node is pasted during duplication. """Create a temp file where node is pasted during duplication.
This is to avoid using clipboard for node duplication. This is to avoid using clipboard for node duplication.
""" """
duplicate_node_temp_path = os.path.join( tmp_file = tempfile.NamedTemporaryFile(
tempfile.gettempdir(), mode="w", prefix="openpype_nuke_temp_", suffix=".nk", delete=False
"openpype_nuke_duplicate_temp_{}".format(os.getpid())
) )
tmp_file.close()
# This can happen only if 'duplicate_node' would be node_tempfile_path = tmp_file.name
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)
try: try:
# Yield the path where node can be copied # Yield the path where node can be copied
yield duplicate_node_temp_path yield node_tempfile_path
finally: finally:
# Remove the file at the end # Remove the file at the end
os.remove(duplicate_node_temp_path) os.remove(node_tempfile_path)
def duplicate_node(node): def duplicate_node(node):
@ -2740,7 +2750,7 @@ def duplicate_node(node):
# select required node for duplication # select required node for duplication
node.setSelected(True) node.setSelected(True)
with _duplicate_node_temp() as filepath: with node_tempfile() as filepath:
# copy selected to temp filepath # copy selected to temp filepath
nuke.nodeCopy(filepath) nuke.nodeCopy(filepath)
@ -2815,3 +2825,100 @@ def ls_img_sequence(path):
} }
return False 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
]

View file

@ -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

View file

@ -22,10 +22,16 @@ from openpype.pipeline import (
AVALON_CONTAINER_ID, AVALON_CONTAINER_ID,
) )
from openpype.pipeline.workfile import BuildWorkfile from openpype.pipeline.workfile import BuildWorkfile
from openpype.pipeline.workfile.build_template import (
build_workfile_template,
update_workfile_template
)
from openpype.tools.utils import host_tools from openpype.tools.utils import host_tools
from .command import viewer_update_and_undo_stop from .command import viewer_update_and_undo_stop
from .lib import ( from .lib import (
Context,
get_main_window,
add_publish_knob, add_publish_knob,
WorkfileSettings, WorkfileSettings,
process_workfile_builder, process_workfile_builder,
@ -33,7 +39,9 @@ from .lib import (
check_inventory_versions, check_inventory_versions,
set_avalon_knob_data, set_avalon_knob_data,
read_avalon_data, read_avalon_data,
Context )
from .lib_template_builder import (
create_placeholder, update_placeholder
) )
log = Logger.get_logger(__name__) log = Logger.get_logger(__name__)
@ -53,23 +61,6 @@ if os.getenv("PYBLISH_GUI", None):
pyblish.api.register_gui(os.getenv("PYBLISH_GUI", None)) pyblish.api.register_gui(os.getenv("PYBLISH_GUI", 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
def reload_config(): def reload_config():
"""Attempt to reload pipeline at run-time. """Attempt to reload pipeline at run-time.
@ -219,6 +210,24 @@ def _install_menu():
lambda: BuildWorkfile().process() lambda: BuildWorkfile().process()
) )
menu_template = menu.addMenu("Template Builder") # creating template menu
menu_template.addCommand(
"Build Workfile from template",
lambda: build_workfile_template()
)
menu_template.addCommand(
"Update Workfile",
lambda: update_workfile_template()
)
menu_template.addSeparator()
menu_template.addCommand(
"Create Place Holder",
lambda: create_placeholder()
)
menu_template.addCommand(
"Update Place Holder",
lambda: update_placeholder()
)
menu.addSeparator() menu.addSeparator()
menu.addCommand( menu.addCommand(
"Experimental tools...", "Experimental tools...",

View file

@ -0,0 +1,639 @@
import re
import collections
import nuke
from openpype.client import get_representations
from openpype.pipeline import legacy_io
from openpype.pipeline.workfile.abstract_template_loader import (
AbstractPlaceholder,
AbstractTemplateLoader,
)
from .lib import (
find_free_space_to_paste_nodes,
get_extreme_positions,
get_group_io_nodes,
imprint,
refresh_node,
refresh_nodes,
reset_selection,
get_names_from_nodes,
get_nodes_by_names,
select_nodes,
duplicate_node,
node_tempfile,
)
from .lib_template_builder import (
delete_placeholder_attributes,
get_placeholder_attributes,
hide_placeholder_attributes
)
PLACEHOLDER_SET = "PLACEHOLDERS_SET"
class NukeTemplateLoader(AbstractTemplateLoader):
"""Concrete implementation of AbstractTemplateLoader for Nuke
"""
def import_template(self, path):
"""Import template into current scene.
Block if a template is already loaded.
Args:
path (str): A path to current template (usually given by
get_template_path implementation)
Returns:
bool: Wether the template was succesfully imported or not
"""
# TODO check if the template is already imported
nuke.nodePaste(path)
reset_selection()
return True
def preload(self, placeholder, loaders_by_name, last_representation):
placeholder.data["nodes_init"] = nuke.allNodes()
placeholder.data["last_repre_id"] = str(last_representation["_id"])
def populate_template(self, ignored_ids=None):
processed_key = "_node_processed"
processed_nodes = []
nodes = self.get_template_nodes()
while nodes:
# Mark nodes as processed so they're not re-executed
# - that can happen if processing of placeholder node fails
for node in nodes:
imprint(node, {processed_key: True})
processed_nodes.append(node)
super(NukeTemplateLoader, self).populate_template(ignored_ids)
# Recollect nodes to repopulate
nodes = []
for node in self.get_template_nodes():
# Skip already processed nodes
if (
processed_key in node.knobs()
and node.knob(processed_key).value()
):
continue
nodes.append(node)
for node in processed_nodes:
knob = node.knob(processed_key)
if knob is not None:
node.removeKnob(knob)
@staticmethod
def get_template_nodes():
placeholders = []
all_groups = collections.deque()
all_groups.append(nuke.thisGroup())
while all_groups:
group = all_groups.popleft()
for node in group.nodes():
if isinstance(node, nuke.Group):
all_groups.append(node)
node_knobs = node.knobs()
if (
"builder_type" not in node_knobs
or "is_placeholder" not in node_knobs
or not node.knob("is_placeholder").value()
):
continue
if "empty" in node_knobs and node.knob("empty").value():
continue
placeholders.append(node)
return placeholders
def update_missing_containers(self):
nodes_by_id = collections.defaultdict(list)
for node in nuke.allNodes():
node_knobs = node.knobs().keys()
if "repre_id" in node_knobs:
repre_id = node.knob("repre_id").getValue()
nodes_by_id[repre_id].append(node.name())
if "empty" in node_knobs:
node.removeKnob(node.knob("empty"))
imprint(node, {"empty": False})
for node_names in nodes_by_id.values():
node = None
for node_name in node_names:
node_by_name = nuke.toNode(node_name)
if "builder_type" in node_by_name.knobs().keys():
node = node_by_name
break
if node is None:
continue
placeholder = nuke.nodes.NoOp()
placeholder.setName("PLACEHOLDER")
placeholder.knob("tile_color").setValue(4278190335)
attributes = get_placeholder_attributes(node, enumerate=True)
imprint(placeholder, attributes)
pos_x = int(node.knob("x").getValue())
pos_y = int(node.knob("y").getValue())
placeholder.setXYpos(pos_x, pos_y)
imprint(placeholder, {"nb_children": 1})
refresh_node(placeholder)
self.populate_template(self.get_loaded_containers_by_id())
def get_loaded_containers_by_id(self):
repre_ids = set()
for node in nuke.allNodes():
if "repre_id" in node.knobs():
repre_ids.add(node.knob("repre_id").getValue())
# Removes duplicates in the list
return list(repre_ids)
def delete_placeholder(self, placeholder):
placeholder_node = placeholder.data["node"]
last_loaded = placeholder.data["last_loaded"]
if not placeholder.data["delete"]:
if "empty" in placeholder_node.knobs().keys():
placeholder_node.removeKnob(placeholder_node.knob("empty"))
imprint(placeholder_node, {"empty": True})
return
if not last_loaded:
nuke.delete(placeholder_node)
return
if "last_loaded" in placeholder_node.knobs().keys():
for node_name in placeholder_node.knob("last_loaded").values():
node = nuke.toNode(node_name)
try:
delete_placeholder_attributes(node)
except Exception:
pass
last_loaded_names = [
loaded_node.name()
for loaded_node in last_loaded
]
imprint(placeholder_node, {"last_loaded": last_loaded_names})
for node in last_loaded:
refresh_node(node)
refresh_node(placeholder_node)
if "builder_type" not in node.knobs().keys():
attributes = get_placeholder_attributes(placeholder_node, True)
imprint(node, attributes)
imprint(node, {"is_placeholder": False})
hide_placeholder_attributes(node)
node.knob("is_placeholder").setVisible(False)
imprint(
node,
{
"x": placeholder_node.xpos(),
"y": placeholder_node.ypos()
}
)
node.knob("x").setVisible(False)
node.knob("y").setVisible(False)
nuke.delete(placeholder_node)
class NukePlaceholder(AbstractPlaceholder):
"""Concrete implementation of AbstractPlaceholder for Nuke"""
optional_keys = {"asset", "subset", "hierarchy"}
def get_data(self, node):
user_data = dict()
node_knobs = node.knobs()
for attr in self.required_keys.union(self.optional_keys):
if attr in node_knobs:
user_data[attr] = node_knobs[attr].getValue()
user_data["node"] = node
nb_children = 0
if "nb_children" in node_knobs:
nb_children = int(node_knobs["nb_children"].getValue())
user_data["nb_children"] = nb_children
siblings = []
if "siblings" in node_knobs:
siblings = node_knobs["siblings"].values()
user_data["siblings"] = siblings
node_full_name = node.fullName()
user_data["group_name"] = node_full_name.rpartition(".")[0]
user_data["last_loaded"] = []
user_data["delete"] = False
self.data = user_data
def parent_in_hierarchy(self, containers):
return
def create_sib_copies(self):
""" creating copies of the palce_holder siblings (the ones who were
loaded with it) for the new nodes added
Returns :
copies (dict) : with copied nodes names and their copies
"""
copies = {}
siblings = get_nodes_by_names(self.data["siblings"])
for node in siblings:
new_node = duplicate_node(node)
x_init = int(new_node.knob("x_init").getValue())
y_init = int(new_node.knob("y_init").getValue())
new_node.setXYpos(x_init, y_init)
if isinstance(new_node, nuke.BackdropNode):
w_init = new_node.knob("w_init").getValue()
h_init = new_node.knob("h_init").getValue()
new_node.knob("bdwidth").setValue(w_init)
new_node.knob("bdheight").setValue(h_init)
refresh_node(node)
if "repre_id" in node.knobs().keys():
node.removeKnob(node.knob("repre_id"))
copies[node.name()] = new_node
return copies
def fix_z_order(self):
"""Fix the problem of z_order when a backdrop is loaded."""
nodes_loaded = self.data["last_loaded"]
loaded_backdrops = []
bd_orders = set()
for node in nodes_loaded:
if isinstance(node, nuke.BackdropNode):
loaded_backdrops.append(node)
bd_orders.add(node.knob("z_order").getValue())
if not bd_orders:
return
sib_orders = set()
for node_name in self.data["siblings"]:
node = nuke.toNode(node_name)
if isinstance(node, nuke.BackdropNode):
sib_orders.add(node.knob("z_order").getValue())
if not sib_orders:
return
min_order = min(bd_orders)
max_order = max(sib_orders)
for backdrop_node in loaded_backdrops:
z_order = backdrop_node.knob("z_order").getValue()
backdrop_node.knob("z_order").setValue(
z_order + max_order - min_order + 1)
def update_nodes(self, nodes, considered_nodes, offset_y=None):
"""Adjust backdrop nodes dimensions and positions.
Considering some nodes sizes.
Args:
nodes (list): list of nodes to update
considered_nodes (list): list of nodes to consider while updating
positions and dimensions
offset (int): distance between copies
"""
placeholder_node = self.data["node"]
min_x, min_y, max_x, max_y = get_extreme_positions(considered_nodes)
diff_x = diff_y = 0
contained_nodes = [] # for backdrops
if offset_y is None:
width_ph = placeholder_node.screenWidth()
height_ph = placeholder_node.screenHeight()
diff_y = max_y - min_y - height_ph
diff_x = max_x - min_x - width_ph
contained_nodes = [placeholder_node]
min_x = placeholder_node.xpos()
min_y = placeholder_node.ypos()
else:
siblings = get_nodes_by_names(self.data["siblings"])
minX, _, maxX, _ = get_extreme_positions(siblings)
diff_y = max_y - min_y + 20
diff_x = abs(max_x - min_x - maxX + minX)
contained_nodes = considered_nodes
if diff_y <= 0 and diff_x <= 0:
return
for node in nodes:
refresh_node(node)
if (
node == placeholder_node
or node in considered_nodes
):
continue
if (
not isinstance(node, nuke.BackdropNode)
or (
isinstance(node, nuke.BackdropNode)
and not set(contained_nodes) <= set(node.getNodes())
)
):
if offset_y is None and node.xpos() >= min_x:
node.setXpos(node.xpos() + diff_x)
if node.ypos() >= min_y:
node.setYpos(node.ypos() + diff_y)
else:
width = node.screenWidth()
height = node.screenHeight()
node.knob("bdwidth").setValue(width + diff_x)
node.knob("bdheight").setValue(height + diff_y)
refresh_node(node)
def imprint_inits(self):
"""Add initial positions and dimensions to the attributes"""
for node in nuke.allNodes():
refresh_node(node)
imprint(node, {"x_init": node.xpos(), "y_init": node.ypos()})
node.knob("x_init").setVisible(False)
node.knob("y_init").setVisible(False)
width = node.screenWidth()
height = node.screenHeight()
if "bdwidth" in node.knobs():
imprint(node, {"w_init": width, "h_init": height})
node.knob("w_init").setVisible(False)
node.knob("h_init").setVisible(False)
refresh_node(node)
def imprint_siblings(self):
"""
- add siblings names to placeholder attributes (nodes loaded with it)
- add Id to the attributes of all the other nodes
"""
loaded_nodes = self.data["last_loaded"]
loaded_nodes_set = set(loaded_nodes)
data = {"repre_id": str(self.data["last_repre_id"])}
for node in loaded_nodes:
node_knobs = node.knobs()
if "builder_type" not in node_knobs:
# save the id of representation for all imported nodes
imprint(node, data)
node.knob("repre_id").setVisible(False)
refresh_node(node)
continue
if (
"is_placeholder" not in node_knobs
or (
"is_placeholder" in node_knobs
and node.knob("is_placeholder").value()
)
):
siblings = list(loaded_nodes_set - {node})
siblings_name = get_names_from_nodes(siblings)
siblings = {"siblings": siblings_name}
imprint(node, siblings)
def set_loaded_connections(self):
"""
set inputs and outputs of loaded nodes"""
placeholder_node = self.data["node"]
input_node, output_node = get_group_io_nodes(self.data["last_loaded"])
for node in placeholder_node.dependent():
for idx in range(node.inputs()):
if node.input(idx) == placeholder_node:
node.setInput(idx, output_node)
for node in placeholder_node.dependencies():
for idx in range(placeholder_node.inputs()):
if placeholder_node.input(idx) == node:
input_node.setInput(0, node)
def set_copies_connections(self, copies):
"""Set inputs and outputs of the copies.
Args:
copies (dict): Copied nodes by their names.
"""
last_input, last_output = get_group_io_nodes(self.data["last_loaded"])
siblings = get_nodes_by_names(self.data["siblings"])
siblings_input, siblings_output = get_group_io_nodes(siblings)
copy_input = copies[siblings_input.name()]
copy_output = copies[siblings_output.name()]
for node_init in siblings:
if node_init == siblings_output:
continue
node_copy = copies[node_init.name()]
for node in node_init.dependent():
for idx in range(node.inputs()):
if node.input(idx) != node_init:
continue
if node in siblings:
copies[node.name()].setInput(idx, node_copy)
else:
last_input.setInput(0, node_copy)
for node in node_init.dependencies():
for idx in range(node_init.inputs()):
if node_init.input(idx) != node:
continue
if node_init == siblings_input:
copy_input.setInput(idx, node)
elif node in siblings:
node_copy.setInput(idx, copies[node.name()])
else:
node_copy.setInput(idx, last_output)
siblings_input.setInput(0, copy_output)
def move_to_placeholder_group(self, nodes_loaded):
"""
opening the placeholder's group and copying loaded nodes in it.
Returns :
nodes_loaded (list): the new list of pasted nodes
"""
groups_name = self.data["group_name"]
reset_selection()
select_nodes(nodes_loaded)
if groups_name:
with node_tempfile() as filepath:
nuke.nodeCopy(filepath)
for node in nuke.selectedNodes():
nuke.delete(node)
group = nuke.toNode(groups_name)
group.begin()
nuke.nodePaste(filepath)
nodes_loaded = nuke.selectedNodes()
return nodes_loaded
def clean(self):
# deselect all selected nodes
placeholder_node = self.data["node"]
# getting the latest nodes added
nodes_init = self.data["nodes_init"]
nodes_loaded = list(set(nuke.allNodes()) - set(nodes_init))
self.log.debug("Loaded nodes: {}".format(nodes_loaded))
if not nodes_loaded:
return
self.data["delete"] = True
nodes_loaded = self.move_to_placeholder_group(nodes_loaded)
self.data["last_loaded"] = nodes_loaded
refresh_nodes(nodes_loaded)
# positioning of the loaded nodes
min_x, min_y, _, _ = get_extreme_positions(nodes_loaded)
for node in nodes_loaded:
xpos = (node.xpos() - min_x) + placeholder_node.xpos()
ypos = (node.ypos() - min_y) + placeholder_node.ypos()
node.setXYpos(xpos, ypos)
refresh_nodes(nodes_loaded)
self.fix_z_order() # fix the problem of z_order for backdrops
self.imprint_siblings()
if self.data["nb_children"] == 0:
# save initial nodes postions and dimensions, update them
# and set inputs and outputs of loaded nodes
self.imprint_inits()
self.update_nodes(nuke.allNodes(), nodes_loaded)
self.set_loaded_connections()
elif self.data["siblings"]:
# create copies of placeholder siblings for the new loaded nodes,
# set their inputs and outpus and update all nodes positions and
# dimensions and siblings names
siblings = get_nodes_by_names(self.data["siblings"])
refresh_nodes(siblings)
copies = self.create_sib_copies()
new_nodes = list(copies.values()) # copies nodes
self.update_nodes(new_nodes, nodes_loaded)
placeholder_node.removeKnob(placeholder_node.knob("siblings"))
new_nodes_name = get_names_from_nodes(new_nodes)
imprint(placeholder_node, {"siblings": new_nodes_name})
self.set_copies_connections(copies)
self.update_nodes(
nuke.allNodes(),
new_nodes + nodes_loaded,
20
)
new_siblings = get_names_from_nodes(new_nodes)
self.data["siblings"] = new_siblings
else:
# if the placeholder doesn't have siblings, the loaded
# nodes will be placed in a free space
xpointer, ypointer = find_free_space_to_paste_nodes(
nodes_loaded, direction="bottom", offset=200
)
node = nuke.createNode("NoOp")
reset_selection()
nuke.delete(node)
for node in nodes_loaded:
xpos = (node.xpos() - min_x) + xpointer
ypos = (node.ypos() - min_y) + ypointer
node.setXYpos(xpos, ypos)
self.data["nb_children"] += 1
reset_selection()
# go back to root group
nuke.root().begin()
def get_representations(self, current_asset_doc, linked_asset_docs):
project_name = legacy_io.active_project()
builder_type = self.data["builder_type"]
if builder_type == "context_asset":
context_filters = {
"asset": [re.compile(self.data["asset"])],
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representations": [self.data["representation"]],
"family": [self.data["family"]]
}
elif builder_type != "linked_asset":
context_filters = {
"asset": [
current_asset_doc["name"],
re.compile(self.data["asset"])
],
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representation": [self.data["representation"]],
"family": [self.data["family"]]
}
else:
asset_regex = re.compile(self.data["asset"])
linked_asset_names = []
for asset_doc in linked_asset_docs:
asset_name = asset_doc["name"]
if asset_regex.match(asset_name):
linked_asset_names.append(asset_name)
if not linked_asset_names:
return []
context_filters = {
"asset": linked_asset_names,
"subset": [re.compile(self.data["subset"])],
"hierarchy": [re.compile(self.data["hierarchy"])],
"representation": [self.data["representation"]],
"family": [self.data["family"]],
}
return list(get_representations(
project_name,
context_filters=context_filters
))
def err_message(self):
return (
"Error while trying to load a representation.\n"
"Either the subset wasn't published or the template is malformed."
"\n\n"
"Builder was looking for:\n{attributes}".format(
attributes="\n".join([
"{}: {}".format(key.title(), value)
for key, value in self.data.items()]
)
)
)

View file

@ -177,7 +177,7 @@ class AbstractTemplateLoader:
build_info["profiles"], build_info["profiles"],
{ {
"task_types": task_type, "task_types": task_type,
"tasks": task_name "task_names": task_name
} }
) )

View file

@ -1,3 +1,4 @@
import os
from importlib import import_module from importlib import import_module
from openpype.lib import classes_from_module from openpype.lib import classes_from_module
from openpype.host import HostBase from openpype.host import HostBase
@ -30,7 +31,7 @@ def build_workfile_template(*args):
template_loader.populate_template() template_loader.populate_template()
def update_workfile_template(args): def update_workfile_template(*args):
template_loader = build_template_loader() template_loader = build_template_loader()
template_loader.update_missing_containers() template_loader.update_missing_containers()
@ -42,7 +43,10 @@ def build_template_loader():
if isinstance(host, HostBase): if isinstance(host, HostBase):
host_name = host.name host_name = host.name
else: else:
host_name = host.__name__.partition('.')[2] host_name = os.environ.get("AVALON_APP")
if not host_name:
host_name = host.__name__.split(".")[-2]
module_path = _module_path_format.format(host=host_name) module_path = _module_path_format.format(host=host_name)
module = import_module(module_path) module = import_module(module_path)
if not module: if not module:

View file

@ -980,4 +980,4 @@
"ValidateNoAnimation": false "ValidateNoAnimation": false
} }
} }
} }

View file

@ -325,5 +325,8 @@
} }
] ]
}, },
"templated_workfile_build": {
"profiles": []
},
"filters": {} "filters": {}
} }

View file

@ -308,6 +308,10 @@
"type": "schema_template", "type": "schema_template",
"name": "template_workfile_options" "name": "template_workfile_options"
}, },
{
"type": "schema",
"name": "schema_templated_workfile_build"
},
{ {
"type": "schema", "type": "schema",
"name": "schema_publish_gui_filter" "name": "schema_publish_gui_filter"

View file

@ -17,7 +17,7 @@
"type": "task-types-enum" "type": "task-types-enum"
}, },
{ {
"key": "tasks", "key": "task_names",
"label": "Task names", "label": "Task names",
"type": "list", "type": "list",
"object_type": "text" "object_type": "text"

View file

@ -202,3 +202,67 @@ This video shows a way to publish shot look as effect from Hiero to Nuke.
### Assembling edit from published shot versions ### Assembling edit from published shot versions
<iframe width="512px" height="288px" src="https://www.youtube.com/embed/5Wd6X-71vbg" frameborder="0" modestbranding="1" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe> <iframe width="512px" height="288px" src="https://www.youtube.com/embed/5Wd6X-71vbg" frameborder="0" modestbranding="1" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen="1"></iframe>
# Nuke Build Workfile
This is a tool of Node Graph initialisation using a pre-created template.
### Add a profile
The path to the template that will be used in the initialisation must be added as a profile on Project Settings.
![Create menu](assets/nuke_addProfile.png)
### Create Place Holder
![Create menu](assets/nuke_createPlaceHolder.png)
This tool creates a Place Holder, which is a node that will be replaced by published instances.
![Create menu](assets/nuke_placeHolderNode.png)
#### Result
- Create a red node called `PLACEHOLDER` which can be manipulated as wanted by using it in Node Graph.
![Create menu](assets/nuke_placeholder.png)
:::note
All published instances that will replace the place holder must contain unique input and output nodes in case they will not be imported as a single node.
:::
![Create menu](assets/nuke_publishedinstance.png)
The informations about these objects are given by the user by filling the extra attributes of the Place Holder
![Create menu](assets/nuke_fillingExtraAttributes.png)
### Update Place Holder
This tool alows the user to change the information provided in the extra attributes of the selected Place Holder.
![Create menu](assets/nuke_updatePlaceHolder.png)
### Build Workfile from template
This tool imports the template used and replaces the existed PlaceHolders with the corresponding published objects (which can contain Place Holders too). In case there is no published items with the description given, the place holder will remain in the node graph.
![Create menu](assets/nuke_buildWorfileFromTemplate.png)
#### Result
- Replace `PLACEHOLDER` node in the template with the published instance corresponding to the informations provided in extra attributes of the Place Holder
![Create menu](assets/nuke_buildworkfile.png)
:::note
In case the instance that will replace the Place holder **A** contains another Place Holder **B** that points to many published elements, all the nodes that were imported with **A** except **B** will be duplicated for each element that will replace **B**
:::
### Update Workfile
This tool can be used to check if some instances were published after the last build, so they will be imported.
![Create menu](assets/nuke_updateWorkfile.png)
:::note
Imported instances must not be deleted because they contain extra attributes that will be used to update the workfile since the place holder is been deleted.
:::

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB