From 919f1cafecca658883b4eb6032c24f84c896e6c3 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 27 Dec 2019 12:32:09 +0800 Subject: [PATCH 01/33] Fix TypeError: 'Node' object does not support item assignment --- avalon/nuke/lib.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/avalon/nuke/lib.py b/avalon/nuke/lib.py index 2a8a5b170..1359161ba 100644 --- a/avalon/nuke/lib.py +++ b/avalon/nuke/lib.py @@ -36,7 +36,7 @@ def reset_selection(): """Deselect all selected nodes """ for node in nuke.selectedNodes(): - node["selected"] = False + node["selected"].setValue(False) def select_nodes(nodes): From 56f55fc1f296d07d05c97796967f597340983bb5 Mon Sep 17 00:00:00 2001 From: David Lai Date: Fri, 27 Dec 2019 12:36:01 +0800 Subject: [PATCH 02/33] Push root logging message level to INFO level --- setup/nuke/nuke_path/menu.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup/nuke/nuke_path/menu.py b/setup/nuke/nuke_path/menu.py index 3ce80d64b..8e1d032ed 100644 --- a/setup/nuke/nuke_path/menu.py +++ b/setup/nuke/nuke_path/menu.py @@ -1,3 +1,6 @@ +import logging import avalon.api import avalon.nuke + +logging.getLogger("").setLevel(20) # INFO level avalon.api.install(avalon.nuke) From 1ff01fa9b4be22c3592561563bee5ade86aba350 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 16:38:54 +0800 Subject: [PATCH 03/33] Implement `lsattr` --- avalon/nuke/lib.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/avalon/nuke/lib.py b/avalon/nuke/lib.py index 1359161ba..a85024eda 100644 --- a/avalon/nuke/lib.py +++ b/avalon/nuke/lib.py @@ -286,6 +286,59 @@ def compat_prefixed(knob_name): return data +def lsattr(attr, value=None, type=None, group=None, recursive=False): + """Return nodes matching `key` and `value` + + Arguments: + attr (str): Name of Node knob + value (object, optional): Value of attribute. If none + is provided, return all nodes with this attribute. + type (str, optional): Node class name + group (nuke.Node, optional): Listing nodes from `group`, default `root` + recursive (bool, optional): Whether to look into child groups. + + Return: + list: matching nodes. + + """ + group = group or nuke.toNode("root") + + if value is None: + args = (type, ) if type else () + nodes = nuke.allNodes(*args, group=group, recurseGroups=recursive) + return [n for n in nodes if n.knob(attr)] + return lsattrs({attr: value}, type=type, group=group, recursive=recursive) + + +def lsattrs(attrs, type=None, group=None, recursive=False): + """Return nodes with the given attribute(s). + + Arguments: + attrs (dict): Name and value pairs of expected matches + type (str, optional): Node class name + group (nuke.Node, optional): Listing nodes from `group`, default `root` + recursive (bool, optional): Whether to look into child groups. + + Return: + list: matching nodes. + + """ + matches = set() + args = (type, ) if type else () + group = group or nuke.toNode("root") + nodes = nuke.allNodes(*args, group=group, recurseGroups=recursive) + for node in nodes: + for attr in attrs: + knob = node.knob(attr) + if not knob: + continue + elif knob.getValue() != attrs[attr]: + continue + else: + matches.add(node) + return list(matches) + + def add_publish_knob(node): """Add Publish knob to node From 9df1197a0b185b4821dccbb6e7e3d83ab80610c4 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 18:17:37 +0800 Subject: [PATCH 04/33] Implement node avalonId set/get and copies finder This change is prerequisite for implementing node knob update/override system in Nuke. --- avalon/nuke/lib.py | 53 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/avalon/nuke/lib.py b/avalon/nuke/lib.py index a85024eda..8f50c04d4 100644 --- a/avalon/nuke/lib.py +++ b/avalon/nuke/lib.py @@ -5,6 +5,7 @@ import logging from collections import OrderedDict +from .. import io from ..vendor import six, clique log = logging.getLogger(__name__) @@ -339,6 +340,58 @@ def lsattrs(attrs, type=None, group=None, recursive=False): return list(matches) +def set_id(node, container_id=None): + """Set 'avalonId' to `node` + + Args: + node (nuke.Node): Node that id to apply to + container_id (str, optional): `node` container's id + + """ + data = OrderedDict() + data["avalonId"] = str(io.ObjectId()) + if container_id: + data["containerId"] = container_id + + set_avalon_knob_data(node, data) + + return data["avalonId"] + + +def get_id(node, container_id=False): + """Get 'avalonId' of `node` + + Args: + node (nuke.Node): Node that id to apply to + container_id (bool, optional): Whether change to get `node` + container's id instead of `node` avalonId. Default False. + + """ + knob = "containerId" if container_id else "avalonId" + id_knob = node.knobs().get("avalon:" + knob) + return id_knob.value() if id_knob else None + + +def find_copies(source, group=None, recursive=True): + """Return nodes that has same 'avalonId' as `source` + + Args: + source (nuke.Node): The node to find copies from + group (nuke.Node, optional): Find copies from `group`, default `root` + recursive (bool, optional): Whether to look into child groups, + default True. + + """ + copies = list() + source_id = get_id(source) + if source_id: + copies = lsattr("avalon:avalonId", + type=source.Class(), + group=group, + recursive=recursive) + return copies + + def add_publish_knob(node): """Add Publish knob to node From 75e82c8938dfb0a30df464c02759a245789451c1 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 17:57:59 +0800 Subject: [PATCH 05/33] Refactored `containerise` and `ls` --- avalon/nuke/pipeline.py | 106 ++++++++++++++++++++++++++++++++++------ 1 file changed, 91 insertions(+), 15 deletions(-) diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py index 31ac2295e..2db5f9633 100644 --- a/avalon/nuke/pipeline.py +++ b/avalon/nuke/pipeline.py @@ -18,6 +18,7 @@ self = sys.modules[__name__] self._parent = None # Main Window cache +AVALON_CONTAINERS = "AVALON_CONTAINERS" AVALON_CONFIG = os.environ["AVALON_CONFIG"] @@ -55,28 +56,41 @@ def reload_pipeline(): _register_events() -def containerise(node, - name, +def containerise(name, namespace, + nodes, context, loader=None, - data=None): + data=None, + suffix="CON"): """Bundle `node` into an assembly and imprint it with metadata Containerisation enables a tracking of version, author and origin for loaded assets. Arguments: - node (nuke.Node): Nuke's node object to imprint as container name (str): Name of resulting assembly namespace (str): Namespace under which to host container + nodes (list): A list of `nuke.Node` object to containerise context (dict): Asset information loader (str, optional): Name of node used to produce this container. + data (dict, optional): Additional data to imprint. + suffix (str, optional): Suffix of container, defaults to `_CON`. Returns: node (nuke.Node): containerised nuke's node object """ + from nukescripts import autoBackdrop + + if isinstance(name, nuke.Node): + # For compatibling with old style args + # containerise(node, name, namespace, context, ...) + _ = nodes + nodes = [name] + name = namespace + namespace = _ + data = OrderedDict( [ ("schema", "avalon-core:container-2.0"), @@ -84,15 +98,53 @@ def containerise(node, ("name", name), ("namespace", namespace), ("loader", str(loader)), - ("representation", context["representation"]["_id"]), + ("representation", str(context["representation"]["_id"])), ], **data or dict() ) + container_color = data.pop("color", int("0x7A7A7AFF", 16)) + container_name = "%s_%s_%s" % (namespace, name, suffix) - lib.set_avalon_knob_data(node, data) + lib.reset_selection() + lib.select_nodes(nodes) - return node + container = autoBackdrop() + container.setName(container_name) + container["label"].setValue(container_name) + container["tile_color"].setValue(container_color) + container["selected"].setValue(True) + # (NOTE) Backdrop may not fully cover if there's only one node, so we + # expand backdrop a bit ot ensure that. + container["bdwidth"].setValue(container["bdwidth"].value() + 100) + container["bdheight"].setValue(container["bdheight"].value() + 100) + container["xpos"].setValue(container["xpos"].value() - 50) + + lib.set_avalon_knob_data(container, data) + + container_id = lib.set_id(container) + for node in nodes: + lib.set_id(node, container_id=container_id) + + # Containerising + + nuke.nodeCopy("_containerizing_") + + main_container = nuke.toNode(AVALON_CONTAINERS) + if main_container is None: + main_container = nuke.createNode("Group") + main_container.setName(AVALON_CONTAINERS) + main_container["postage_stamp"].setValue(True) + main_container["note_font_size"].setValue(40) + main_container["tile_color"].setValue(int("0x283648FF", 16)) + main_container["xpos"].setValue(-500) + main_container["ypos"].setValue(-500) + + main_container.begin() + nuke.nodePaste("_containerizing_") + main_container.end() + + return container def parse_container(node): @@ -189,6 +241,24 @@ def process(self): return instance +def _ls1(): + """Yields all nodes for listing Avalon containers""" + for node in nuke.allNodes(recurseGroups=False): + yield node + + +def _ls2(): + """Yields nodes that has 'avalon:id' knob from AVALON_CONTAINERS""" + for node in nuke.allNodes("BackdropNode", + group=nuke.toNode(AVALON_CONTAINERS)): + knob = node.fullName() + ".avalon:id" + if nuke.exists(knob) and nuke.knob(knob) == AVALON_CONTAINER_ID: + yield node + + +_ls = _ls1 if os.getenv("AVALON_NUKE_CONTAINERS_AT_LARGE") else _ls2 + + def ls(): """List available containers. @@ -199,16 +269,22 @@ def ls(): See the `container.json` schema for details on how it should look, and the Maya equivalent, which is in `avalon.maya.pipeline` """ - all_nodes = nuke.allNodes(recurseGroups=False) + config = find_host_config(api.registered_config()) + has_metadata_collector = hasattr(config, "collect_container_metadata") + + container_nodes = _ls() + + for container in container_nodes: + data = parse_container(container) + if data is None: + continue - # TODO: add readgeo, readcamera, readimage - nodes = [n for n in all_nodes] + # Collect custom data if attribute is present + if has_metadata_collector: + metadata = config.collect_container_metadata(container) + data.update(metadata) - for n in nodes: - log.debug("name: `{}`".format(n.name())) - container = parse_container(n) - if container: - yield container + yield data def install(config): From 3c685d019576b7a4165b3387bb607b95f1657557 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 17:58:30 +0800 Subject: [PATCH 06/33] Remove container data validation --- avalon/nuke/pipeline.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py index 2db5f9633..26d08ec24 100644 --- a/avalon/nuke/pipeline.py +++ b/avalon/nuke/pipeline.py @@ -161,14 +161,6 @@ def parse_container(node): """ data = lib.read(node) - # (TODO) Remove key validation when `ls` has re-implemented. - # - # If not all required data return the empty container - required = ["schema", "id", "name", - "namespace", "loader", "representation"] - if not all(key in data for key in required): - return - # Store the node's name data["objectName"] = node["name"].value() # Store reference to the node object From c84473b81eed654d1b031fd6feab020ffb8061c7 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 20:15:23 +0800 Subject: [PATCH 07/33] Implement node sync The key of node update/override system. --- avalon/nuke/lib.py | 77 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/avalon/nuke/lib.py b/avalon/nuke/lib.py index 8f50c04d4..3b647a34b 100644 --- a/avalon/nuke/lib.py +++ b/avalon/nuke/lib.py @@ -392,6 +392,83 @@ def find_copies(source, group=None, recursive=True): return copies +@contextlib.contextmanager +def sync_copies(nodes, force=False): + """Context manager for Syncing nodes' knobs + + When updating subset by `Loader.update`, use this context to auto sync all + copies of the subset. + + By default, only knobs that haven't been modified, compares to the original + one inside the "AVALON_CONTAINERS". But if `force` set to True, all knobs + will be updated. + + Example: + ``` + class Loader(avalon.api.Loader): + + def update(self, container, representation): + + with lib.sync_copies(nodes): + # Update subset + ... + # All copies of `nodes` updated + + with lib.sync_copies([container_node], force=True): + # Update container data + ... + # All copies of `container_node` updated + + ``` + + Args: + nodes (list): Nodes to sync + force (bool, optional): Whether to force updating all knobs + + """ + def is_knob_eq(knob_a, knob_b): + return knob_a.toScript() == knob_b.toScript() + + def sync_knob(knob_a, knob_b): + script = knob_a.toScript() + knob_b.fromScript(script) + + staged = dict() + origin = dict() + + # Collect knobs for updating + for node in nodes: + targets = list() + + sources = node.knobs() + origin[node] = sources + + for copy in find_copies(node): + for name, knob in copy.knobs().items(): + if name not in sources: + continue + # Only update knob that hasn't been modified + if force or is_knob_eq(sources[name], knob): + targets.append(knob.fullyQualifiedName()) + + if targets: + staged[node] = targets + + try: + yield # Update `nodes` + + finally: + # Sync update result to all copies + for node, targets in staged.items(): + updates = origin[node] + + for knob in targets: + copied, knob = knob.rsplit(".", 1) + copied = nuke.toNode(copied) + + sync_knob(updates[knob], copied[knob]) + + def add_publish_knob(node): """Add Publish knob to node From 98d35620a28d5a5d8806a61cbc6487c273788c00 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 20:22:11 +0800 Subject: [PATCH 08/33] Fix Qt4 compat --- avalon/tools/models.py | 5 +++-- avalon/tools/sceneinventory/app.py | 12 ++++++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/avalon/tools/models.py b/avalon/tools/models.py index 513b3ffc3..c73516150 100644 --- a/avalon/tools/models.py +++ b/avalon/tools/models.py @@ -2,7 +2,7 @@ import logging import collections -from ..vendor.Qt import QtCore, QtGui +from ..vendor.Qt import Qt, QtCore, QtGui from ..vendor import qtawesome from .. import io from .. import style @@ -62,7 +62,8 @@ def setData(self, index, value, role=QtCore.Qt.EditRole): item[key] = value # passing `list()` for PyQt5 (see PYSIDE-462) - self.dataChanged.emit(index, index, list()) + args = () if Qt.IsPySide or Qt.IsPyQt4 else ([], ) + self.dataChanged.emit(index, index, *args) # must return true if successful return True diff --git a/avalon/tools/sceneinventory/app.py b/avalon/tools/sceneinventory/app.py index 155868991..ad466dfa3 100644 --- a/avalon/tools/sceneinventory/app.py +++ b/avalon/tools/sceneinventory/app.py @@ -315,22 +315,22 @@ def get_children(i): child = model.index(row, 0, parent=i) yield child - subitems = set() + subitems = dict() for i in indices: valid_parent = i.parent().isValid() - if valid_parent and i not in subitems: - subitems.add(i) + if valid_parent and str(i) not in subitems: + subitems[str(i)] = i if self._hierarchy_view: # Assume this is a group node for child in get_children(i): - subitems.add(child) + subitems[str(child)] = child else: # is top level node for child in get_children(i): - subitems.add(child) + subitems[str(child)] = child - return list(subitems) + return list(subitems.values()) def show_version_dialog(self, items): """Create a dialog with the available versions for the selected file From 134b4cd2398919d5cd6a32390c08b9aacc6235a1 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 20:53:36 +0800 Subject: [PATCH 09/33] Collect containerized nodes while parsing container --- avalon/nuke/pipeline.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py index 26d08ec24..2889866df 100644 --- a/avalon/nuke/pipeline.py +++ b/avalon/nuke/pipeline.py @@ -165,6 +165,11 @@ def parse_container(node): data["objectName"] = node["name"].value() # Store reference to the node object data["_node"] = node + # Get containerized nodes + if node.fullName() == "%s.%s" % (AVALON_CONTAINERS, node.name()): + data["_members"] = lib.lsattr("avalon:containerId", + value=data["avalonId"], + group=nuke.toNode(AVALON_CONTAINERS)) return data @@ -189,6 +194,10 @@ def update_container(node, keys=None): if not container: raise TypeError("Not a valid container node.") + # Remove unprintable entries + container.pop("_node", None) + container.pop("_members", None) + container.update(keys) node = lib.set_avalon_knob_data(node, container) From bd88c1ad04b0d621d8bd6b41ccc5361292b33bf9 Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 20:56:00 +0800 Subject: [PATCH 10/33] Provide a way to keep old style `containerise` --- avalon/nuke/pipeline.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/avalon/nuke/pipeline.py b/avalon/nuke/pipeline.py index 2889866df..1cb8e5445 100644 --- a/avalon/nuke/pipeline.py +++ b/avalon/nuke/pipeline.py @@ -21,6 +21,8 @@ AVALON_CONTAINERS = "AVALON_CONTAINERS" AVALON_CONFIG = os.environ["AVALON_CONFIG"] +USE_OLD_CONTAINER_STYLE = os.getenv("AVALON_NUKE_CONTAINERS_AT_LARGE") + def reload_pipeline(): """Attempt to reload pipeline at run-time. @@ -103,6 +105,14 @@ def containerise(name, **data or dict() ) + + if USE_OLD_CONTAINER_STYLE: + node = nodes[0] + lib.set_avalon_knob_data(node, data) + return node + + # New style + container_color = data.pop("color", int("0x7A7A7AFF", 16)) container_name = "%s_%s_%s" % (namespace, name, suffix) @@ -257,7 +267,7 @@ def _ls2(): yield node -_ls = _ls1 if os.getenv("AVALON_NUKE_CONTAINERS_AT_LARGE") else _ls2 +_ls = _ls1 if USE_OLD_CONTAINER_STYLE else _ls2 def ls(): From 05b5086341b5fce88f5668770ff1012aeeb789dd Mon Sep 17 00:00:00 2001 From: David Lai Date: Mon, 6 Jan 2020 21:34:42 +0800 Subject: [PATCH 11/33] Vendorize `knobby` --- avalon/nuke/vendor/__init__.py | 0 avalon/nuke/vendor/knobby/__init__.py | 16 ++ avalon/nuke/vendor/knobby/parser.py | 143 ++++++++++++ avalon/nuke/vendor/knobby/util.py | 319 ++++++++++++++++++++++++++ 4 files changed, 478 insertions(+) create mode 100644 avalon/nuke/vendor/__init__.py create mode 100644 avalon/nuke/vendor/knobby/__init__.py create mode 100644 avalon/nuke/vendor/knobby/parser.py create mode 100644 avalon/nuke/vendor/knobby/util.py diff --git a/avalon/nuke/vendor/__init__.py b/avalon/nuke/vendor/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/avalon/nuke/vendor/knobby/__init__.py b/avalon/nuke/vendor/knobby/__init__.py new file mode 100644 index 000000000..cad32a59c --- /dev/null +++ b/avalon/nuke/vendor/knobby/__init__.py @@ -0,0 +1,16 @@ + +__version__ = "0.1.1" + +from . import parser + +try: + from . import util +except ImportError: + # No nuke module + util = None + + +__all__ = [ + "parser", + "util", +] diff --git a/avalon/nuke/vendor/knobby/parser.py b/avalon/nuke/vendor/knobby/parser.py new file mode 100644 index 000000000..80fad194d --- /dev/null +++ b/avalon/nuke/vendor/knobby/parser.py @@ -0,0 +1,143 @@ + +import re +from collections import deque + + +def parse(script): + """Parse Nuke node's TCL script string into nested list structure + + Args: + script (str): Node knobs TCL script string + + Returns: + Tablet: A list containing knob scripts or tab knobs that has parsed + into list + + """ + queue = deque(script.split("\n")) + tab = Tablet() + tab.consume(queue) + return tab + + +TYPE_NODE = 0 +TYPE_KNOBS = 1 +TYPE_GROUP = -2 +TYPE_KNOBS_CLOSE = -1 +TYPE_GROUP_CLOSE = -3 + +TAB_PATTERN = re.compile( + "addUserKnob {20 " + "(?P\\S+)" + "(| l (?P