Guest User

Nuke Indie Script 10-node limit bypass

a guest
Sep 7th, 2025
151
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 12.71 KB | Source Code | 0 0
  1. # -*- coding: utf-8 -*-
  2. #
  3. # Bypass Indie Nuke's Python 10-node limitation by using TCL to access the nodes.
  4. # Mimics common nuke.Node and nuke.Knob methods/properties for Tcl nodes
  5. #
  6. # Hey the Foundry - why not just fix the limitation instead of forcing
  7. # everyone to write workarounds like this?
  8. # A healhy indie ecosystem is good for everyone.
  9. # Increased productivity = more users = more sales = more happy customers.
  10.  
  11. # This file is provided as-is, without warranty of any kind.
  12. # Use at your own risk. Test before using in production.
  13.  
  14. # EXAMPLE USAGE:
  15. '''
  16. import importlib, tclnode_proxy as P
  17.  
  18. group=P.selectedNode()
  19. nodes = P.nodesInGroup(group=group, class_filter="Write")
  20. nodes = nodes + P.nodesInGroup(group=group, class_filter="Read")
  21.  
  22. fr_node = P.toNode("FrameRange1", group=group)
  23.  
  24. start = int ( fr_node.knob('first_frame').value() )
  25. end = int ( fr_node.knob('last_frame').value() )
  26.  
  27. print('Set frame range from ', start, 'to', end)
  28.  
  29. group.knob('lifetimeStart').setValue( start )
  30. group.knob('lifetimeEnd').setValue( end )
  31. group.knob('useLifetime').setValue(1)
  32. group.knob('postage_stamp').setValue(0)
  33.  
  34. fr_node.knob('first_frame').setValue('parent.lifetimeStart')
  35. fr_node.knob('last_frame').setValue('parent.lifetimeEnd')
  36.  
  37. for n in nodes:
  38.    print(n)
  39.    n.knob('first').setValue('parent.lifetimeStart')
  40.    n.knob('last').setValue('parent.lifetimeEnd')
  41.    n.knob('postage_stamp').setValue(0)
  42. '''
  43.  
  44. import nuke
  45. from contextlib import contextmanager
  46.  
  47. __all__ = [
  48.     "Node", "Knob",
  49.     "toNode", "allNodes", "selectedNode", "selectedNodes",
  50.     "nodesInGroup", "nodesRecursive",
  51. ]
  52.  
  53. # ---------------- low-level Tcl utils ----------------
  54.  
  55. def _tcl(cmd):
  56.     return nuke.tcl(cmd)
  57.  
  58. def _brace(s):
  59.     if s is None:
  60.         return "{}"
  61.     s = str(s).replace("{", r"\{").replace("}", r"\}")
  62.     return "{" + s + "}"
  63.  
  64. def _split_words(s):
  65.     return [w for w in s.split() if w]
  66.  
  67. def _handle_dot_knob(handle, knob=None):
  68.     """Make a single braced token like {node123.file} or {node123}."""
  69.     if knob is None:
  70.         return _brace(handle)
  71.     return _brace("%s.%s" % (handle, knob))
  72.  
  73. @contextmanager
  74. def _tcl_scope(group_path):
  75.     """Temporarily push into a group for Tcl commands."""
  76.     if group_path:
  77.         _tcl("push " + _brace(group_path))
  78.         try:
  79.             yield
  80.         finally:
  81.             _tcl("pop")
  82.     else:
  83.         yield
  84.  
  85. def _nodes_in_group(group):
  86.     """Return list of internal Tcl handles in a group/root across builds."""
  87.     variants = []
  88.     if group:
  89.         variants.append("nodes " + _brace(group))
  90.         variants.append("push " + _brace(group) + "; nodes; pop")
  91.     else:
  92.         variants.append("nodes root")
  93.         variants.append("nodes .")
  94.         variants.append("nodes")
  95.     for cmd in variants:
  96.         try:
  97.             out = _tcl(cmd)
  98.             return _split_words(out)
  99.         except Exception:
  100.             continue
  101.     return []
  102.  
  103. def _knob_names_union(handle, group=None):
  104.     """
  105.    Try several Tcl 'knobs' variants and union the names.
  106.    This makes enumeration robust across Nuke versions/builds.
  107.    """
  108.     variants = [
  109.         "knobs " + _brace(handle),                 # default
  110.         "knobs -a " + _brace(handle),              # 'all' (some builds)
  111.         "knobs -A " + _brace(handle),              # 'All' (older/alt flag)
  112.         "knobs -all " + _brace(handle),            # explicit 'all'
  113.         "knobs -internal " + _brace(handle),       # include hidden/internal
  114.         "knobs -u " + _brace(handle),              # user knobs only
  115.     ]
  116.  
  117.     names = set()
  118.     with _tcl_scope(group):
  119.         for cmd in variants:
  120.             try:
  121.                 out = _tcl(cmd)
  122.                 if out:
  123.                     for w in out.split():
  124.                         names.add(w)
  125.             except Exception:
  126.                 # Ignore unknown flags on this build
  127.                 continue
  128.     return sorted(names)
  129.  
  130. # ---------------- Knob wrapper (Tcl-only) ----------------
  131.  
  132. class Knob(object):
  133.     """Tcl-only knob wrapper mimicking common nuke.Knob methods."""
  134.     __slots__ = ("_handle", "_name", "_group")
  135.  
  136.     def __init__(self, node_handle, knob_name, group=None):
  137.         self._handle = node_handle
  138.         self._name   = knob_name
  139.         self._group  = group
  140.  
  141.     def __repr__(self):
  142.         return "<Knob %s on %s>" % (self._name, self._handle)
  143.  
  144.     def name(self):
  145.         return self._name
  146.  
  147.     # --- values ---
  148.     def value(self):
  149.         with _tcl_scope(self._group):
  150.             return _tcl("value " + _handle_dot_knob(self._handle, self._name))
  151.  
  152.     def getValue(self):
  153.         return self.value()
  154.  
  155.     def setValue(self, v):
  156.         with _tcl_scope(self._group):
  157.             if isinstance(v, bool):
  158.                 v = "1" if v else "0"
  159.             _tcl("knob " + _handle_dot_knob(self._handle, self._name) + " " + _brace(v))
  160.  
  161.     # --- typed helpers (optional) ---
  162.     def valueAsInt(self):
  163.         try: return int(float(self.value()))
  164.         except Exception: return 0
  165.  
  166.     def valueAsFloat(self):
  167.         try: return float(self.value())
  168.         except Exception: return 0.0
  169.  
  170.     def valueAsBool(self):
  171.         return self.value().strip().lower() in ("1", "true")
  172.  
  173. # ---------------- Node proxy (Tcl-only) ----------------
  174.  
  175. class Node(object):
  176.     """
  177.    Tcl-only proxy that looks/feels like nuke.Node for common tasks.
  178.    Construct via module helpers (toNode/allNodes) or from a handle.
  179.    """
  180.     __slots__ = ("_handle", "_group")
  181.  
  182.     def __init__(self, handle, group=None):
  183.         self._handle = handle  # internal Tcl id (e.g. node1abc...)
  184.         self._group  = group
  185.  
  186.     # --- representation ---
  187.     def __repr__(self):
  188.         try:
  189.             return "<Node %s [%s] handle=%s>" % (self.name(), self.Class(), self._handle)
  190.         except Exception:
  191.             return "<Node handle=%s>" % (self._handle,)
  192.  
  193.     # --- identity / basics ---
  194.     def Class(self):
  195.         with _tcl_scope(self._group):
  196.             return _tcl("class " + _brace(self._handle)).strip()
  197.  
  198.     def name(self):
  199.         with _tcl_scope(self._group):
  200.             return _tcl("value " + _handle_dot_knob(self._handle, "name")).strip()
  201.  
  202.     def setName(self, new_name):
  203.         with _tcl_scope(self._group):
  204.             _tcl("knob " + _handle_dot_knob(self._handle, "name") + " " + _brace(new_name))
  205.  
  206.     def fullName(self):
  207.         """Best-effort path 'Group/Sub/DisplayName'."""
  208.         nm = self.name()
  209.         return (self._group + "/" + nm) if self._group else nm
  210.  
  211.     def exists(self):
  212.         try:
  213.             self.Class()
  214.             return True
  215.         except Exception:
  216.             return False
  217.  
  218.     # --- knobs API ---
  219.     def knobs(self):
  220.         """Return dict of {knob_name: Knob} like nuke.Node.knobs()."""
  221.         names = _knob_names_union(self._handle, group=self._group)
  222.         return {k: Knob(self._handle, k, self._group) for k in names}
  223.  
  224.     def knob(self, name):
  225.         """Return a Knob wrapper or None if missing."""
  226.         if not name:
  227.             return None
  228.         with _tcl_scope(self._group):
  229.             try:
  230.                 # Probe by reading (empty ok); if it errors, knob doesn't exist
  231.                 _ = _tcl("value " + _handle_dot_knob(self._handle, name))
  232.                 return Knob(self._handle, name, self._group)
  233.             except Exception:
  234.                 return None
  235.  
  236.     def __getitem__(self, name):
  237.         """node['file'] -> Knob wrapper (like real Nuke)."""
  238.         k = self.knob(name)
  239.         if k is None:
  240.             raise KeyError("Knob not found: %s" % name)
  241.         return k
  242.    
  243.     def __str__(self):
  244.         return self.name()
  245.  
  246.     # --- convenience (common Node methods) ---
  247.     def setXpos(self, x):
  248.         self["xpos"].setValue(int(x))
  249.  
  250.     def setYpos(self, y):
  251.         self["ypos"].setValue(int(y))
  252.  
  253.     def xpos(self): return self["xpos"].valueAsInt()
  254.     def ypos(self): return self["ypos"].valueAsInt()
  255.  
  256.     def hideInputs(self, on=True):
  257.         self["hide_input"].setValue(bool(on))
  258.  
  259.     def error(self):
  260.         """Mirror Node.error() (returns string; may be empty)."""
  261.         try:
  262.             return self["error"].value()
  263.         except Exception:
  264.             return ""
  265.  
  266.     # --- execute/render (Write or executable nodes) ---
  267.     def execute(self, first=None, last=None, incr=1, ignore_errors=False):
  268.         """Execute this node over a frame range (falls back to script range)."""
  269.         f, l = _resolve_range_for(self, first, last, self._group)
  270.         return execute_nodes(self, first=f, last=l, incr=incr, group=self._group, ignore_errors=ignore_errors)
  271.  
  272. # ---------------- discovery / module-level helpers ----------------
  273.  
  274. def nodesInGroup(group=None, class_filter=None, include_disabled=True):
  275.     """Return Node proxies (Tcl handles) in a group/root."""
  276.     handles = _nodes_in_group(group)
  277.     nodes = [Node(h, group) for h in handles]
  278.  
  279.     if class_filter:
  280.         kept = []
  281.         with _tcl_scope(group):
  282.             for n in nodes:
  283.                 try:
  284.                     if _tcl("class " + _brace(n._handle)).strip() == class_filter:
  285.                         kept.append(n)
  286.                 except Exception:
  287.                     pass
  288.         nodes = kept  
  289.  
  290.     if not include_disabled:
  291.         nodes = [n for n in nodes if not (n.knob("disable") and n.knob("disable").valueAsBool())]
  292.  
  293.     return nodes
  294.  
  295. def nodesRecursive(start_group=None, class_filter=None, include_disabled=True):
  296.     out = []
  297.     def walk(gpath):
  298.         current = nodesInGroup(group=gpath, class_filter=class_filter, include_disabled=include_disabled)
  299.         for n in current:
  300.             out.append(n)
  301.             if n.Class() in ("Group", "LiveGroup", "Gizmo"):
  302.                 walk(n.fullName())
  303.     walk(start_group)
  304.     return out
  305.  
  306. def allNodes(node_class=None, group=None):
  307.     """Like nuke.allNodes(className=None, group=None)."""
  308.     return nodesInGroup(group=group, class_filter=node_class)
  309.  
  310. def toNode(display_name, group=None):
  311.     """Like nuke.toNode(name): first match by DISPLAY name in group/root."""
  312.     for n in nodesInGroup(group=group):
  313.         if n.name() == display_name:
  314.             return n
  315.     return None
  316.  
  317. def selectedNodes(group=None, class_filter=None):
  318.     out = []
  319.     for n in nodesInGroup(group=group, class_filter=class_filter):
  320.         k = n.knob("selected")
  321.         if k and k.valueAsBool():
  322.             out.append(n)
  323.     return out
  324.  
  325. def selectedNode(group=None):
  326.     arr = selectedNodes(group=group)
  327.     return arr[0] if arr else None
  328.  
  329. # ---------------- execute helpers (Tcl 'execute') ----------------
  330.  
  331. def _int_or_none(s):
  332.     try:
  333.         return int(float(str(s).strip()))
  334.     except Exception:
  335.         return None
  336.  
  337. def _script_frame_range():
  338.     f = _int_or_none(_tcl("value root.first_frame"))
  339.     l = _int_or_none(_tcl("value root.last_frame"))
  340.     return (f or 0, l or 0)
  341.  
  342. def _node_limit_range(node, group=None):
  343.     # Works for Write: use_limit/first/last
  344.     token = node._handle if isinstance(node, Node) else str(node)
  345.     with _tcl_scope(group):
  346.         try:
  347.             use = _tcl("value " + _handle_dot_knob(token, "use_limit")).strip().lower()
  348.             if use in ("1", "true"):
  349.                 f = _int_or_none(_tcl("value " + _handle_dot_knob(token, "first")))
  350.                 l = _int_or_none(_tcl("value " + _handle_dot_knob(token, "last")))
  351.                 if f is not None and l is not None:
  352.                     return (f, l)
  353.         except Exception:
  354.             pass
  355.     return (None, None)
  356.  
  357. def _resolve_range_for(node, first, last, group):
  358.     f = _int_or_none(first); l = _int_or_none(last)
  359.     if f is not None and l is not None:
  360.         return (f, l)
  361.     f2, l2 = _node_limit_range(node, group=group)
  362.     if f2 is not None and l2 is not None:
  363.         return (f2, l2)
  364.     return _script_frame_range()
  365.  
  366. def _range_list(first, last, incr=1):
  367.     first = int(first); last = int(last); incr = int(incr)
  368.     return _brace("%d %d" % (first, last)) if incr == 1 else _brace("%d %d %d" % (first, last, incr))
  369.  
  370. def _as_token(obj):
  371.     if isinstance(obj, Node):
  372.         return _brace(obj._handle)
  373.     return _brace(str(obj))
  374.  
  375. def execute_nodes(nodes, first=None, last=None, incr=1, group=None, ignore_errors=False):
  376.     if not isinstance(nodes, (list, tuple)):
  377.         nodes = [nodes]
  378.     # Resolve range once (common case); advanced users can pass explicit first/last.
  379.     f, l = _resolve_range_for(nodes[0], first, last, group)
  380.     toks = " ".join(_as_token(n) for n in nodes)
  381.     rng  = _range_list(f, l, incr=incr)
  382.     cmd  = "execute " + toks + " " + rng
  383.     with _tcl_scope(group):
  384.         try:
  385.             _tcl(cmd)
  386.         except Exception as e:
  387.             if not ignore_errors:
  388.                 raise
  389.             return str(e)
  390.  
Tags: python Indie Nuke
Advertisement
Add Comment
Please, Sign In to add comment