Using Python To Scale Nodes In Nuke
In this guide we’ll look at a simplified version of the python script used in NodeGraphBuddy to scale selected nodes. You can download the full script, which also adjusts backdrops, and has many other features now as part of the BuddySystem!
Important Nuke Concepts
Let’s cover a few important things before we get into the code
The Knobs
xpos = a knob that stores the position of the left side of a node
ypos = a knob that stores the position of the top side of a node
screenWidth = a knob that stores the width of a node as displayed in the node graph
screenHeight = a knob that stores the height of a node as displayed in the node graph
Scaling As Movement
When we talk about scaling nodes what we're really doing is moving them in relation to a collective center point, via their x and y positions, so that it looks like the distance between them all is changing. The one exception is with a backdrop node, which we do want to actually change the overall size of
The Plan
I love a plan, let’s break down the things our scaling script needs to do:
Find the Position of Each Node
Find the Collective Center of All Nodes
Calculating the New Positions
Applying the New Positions (Scale)
We’ll start by only scaling horizontally (x position) to keep it really simple, and then expand at the end to include vertical scaling as well
1) Finding the Position of Each Node
It’s important to find the position of each node selected, so we can later use them in step 2 to find the collective center of them all
# Get selected nodes selected = nuke.selectedNodes() # An empty list to store x center data all_x_centers = [] # Loop through selected nodes and get their centers, then add to all_x_centers list for n in selected: center_x = n.xpos() all_x_centers.append(center_x)
2) Find the Collective Center of All Nodes
How do we find the center of a bunch of nodes?
It's easier than you think! Using a bit of simple math we can calculate the average (mean) position of all the selected nodes. You can do this by adding (sum in python) all the values in our all_x_centers list together, and then dividing by the amount of nodes selected (len in python.) The result is the center of all nodes selected!
Knowing that we can add this formula to the end of the script pivot_x = sum(all_x_centers) / len(selected)
# Get selected nodes selected = nuke.selectedNodes() # An empty list to store x center data all_x_centers = [] # Loop through selected nodes and get their centers, then add to all_x_centers list for n in selected: center_x = n.xpos() all_x_centers.append(center_x) # Calculate the collective center and store in variable pivot_x = sum(all_x_centers) / len(selected)
3) Calculating the New Positions
This is the magic formula! For every single node, we'll apply this logic to find its new scaled value:
new_center_x = pivot_x + (node_center_x - pivot_x) * scale_factor
Let's break that formula down:
(node_center_x - pivot_x) : This calculates the distance of the node's current center from the central pivot point. If the node is to the right of the pivot, this value is positive, if it's to the left, it's negative
* scale_factor : This takes that distance and multiplies it. With a factor of 1.1, it increases the distance by 10%, pushing the node away from the center. If the factor were 0.9, it would decrease the distance by 10%, pulling it closer
pivot_x + : The new scaled distance is added back to the pivot points position. This gives the node its new center position
new_center_x = : This is a variable to store the result of the formula for later use
So we add the following lines of code next
A scale_factor variable that stores the amount we want to scale by
Another for loop that iterates through the selected nodes again, but this time it calculates the above magic formula for each node, and puts it in the new_center_x variable
# Get selected nodes selected = nuke.selectedNodes() # An empty list to store x center data all_x_centers = [] # Loop through selected nodes and get their centers, then add to all_x_centers list for n in selected: center_x = n.xpos() all_x_centers.append(center_x) # Calculate the collective center and store in variable pivot_x = sum(all_x_centers) / len(selected) # Define scale amount. 1.1=10% larger, 0.9=10% smaller scale_factor = 1.1 # Loop through all the selected nodes and update scale for node in selected: # Get the center of the current node node_center_x = node.xpos() # Calculate the new center new_center_x = pivot_x + (node_center_x - pivot_x) * scale_factor
4) Applying the New Positions (Scale)
The last step is to simply apply the result of the for loop to the same node it is currently looping through
We do this with setXpos, instead of knob(‘xpos’).setValue(new_value) to preserve undo capability, after the script is run. We also use int to force the value to be an integer, instead of a float, otherwise we could get a type error when trying to apply the new value
Add the following line of code at the end of the second for loop. Then select some nodes, and run the full script in nukes script editor!
node.setXpos(int(new_center_x))
# Get selected nodes selected = nuke.selectedNodes() # An empty list to store x center data all_x_centers = [] # Loop through selected nodes and get their centers, then add to all_x_centers list for n in selected: center_x = n.xpos() all_x_centers.append(center_x) # Calculate the collective center and store in variable pivot_x = sum(all_x_centers) / len(selected) # Define scale amount. 1.1=10% larger, 0.9=10% smaller scale_factor = 1.1 # Loop through all the selected nodes and update scale for node in selected: # Get the center of the current node node_center_x = node.xpos() # Calculate the new center new_center_x = pivot_x + (node_center_x - pivot_x) * scale_factor # set the new x position node.setXpos(int(new_center_x))
The Plan Is Falling Apart :(
Oh No!
Our horizontal scaling script sometimes works, but with certain nodes like dots, the scaling isn’t correct, and seems to get worse the more we scale something…. why is this? Have the maths Gods forsaken us? This is something that confused me for a long, long time, and was the main stumbling point of fully developing this script. I was making the incorrect assumption that a nodes x/y positions were its geometric center, when it turns out that these values actually represent the top left corner instead. This horrible mistake on my part meant that the script worked for nodes of the same size, but if you mixed in a smaller dot, a labeled node, or something like a camera, the math would appear to fall apart. Thankfully the math was correct, and just needed a little bit more to get the real geometric center of each node, then everything magically fell into place!
1) Finding The Center Of Each Node For Real This Time. I Promise!
This is where the screenWidth and ScreenHeight knobs finally come into play
We can divide the width of the node as it is displayed to the user by 2, to get half of it, which is the local horizontal center of the node. Let’s add that to our previous script and see what happens!
In the first for loop add + n.screenWidth() / 2 at the end of the center_x line. This will add half the screen width to the x position, and give us the true global horizontal center of the node
In the second for loop add + n.screenWidth() / 2 at the end of the node_center_x line. This will add half the screen width to the x position, and give us the true global horizontal center of the node
In the second for loop add new_xpos = new_center_x - node.screenWidth() / 2 just before the final line. This will subtract half the screen width by the new center, and give us the the new scaled x position
# Get selected nodes selected = nuke.selectedNodes() # An empty list to store x center data all_x_centers = [] # Loop through selected nodes and get their centers, then add to all_x_centers list for n in selected: center_x = n.xpos() + n.screenWidth() / 2 all_x_centers.append(center_x) # Calculate the collective center and store in variable pivot_x = sum(all_x_centers) / len(selected) # Define scale amount. 1.1=10% larger, 0.9=10% smaller scale_factor = 1.1 # Loop through all the selected nodes and update scale for node in selected: # Get the center of the current node node_center_x = node.xpos() + node.screenWidth() / 2 # Calculate the new center new_center_x = pivot_x + (node_center_x - pivot_x) * scale_factor # Calculate the new x position new_xpos = new_center_x - node.screenWidth() / 2 # set the new x position node.setXpos(int(new_xpos))
The Plan Succeeds!
Let’s test that script again!
Look at that, it now scales nodes of all sizes correctly! That was the final missing piece of the puzzle. By calculating the true center of each node, and not directly using the top left corner, the script now treats every node uniformly, and nodes that are aligned before scaling, remain aligned after!
The Complete Script
Below is the entire script, containing the concepts we talked about above. I’ve duplicated the horizontal (xpos) logic to for the vertical axis (ypos) as well, but it is identical otherwise. Now when you run the script it will scale uniformly in both directions!
# Get selected nodes selected = nuke.selectedNodes() # Empty lists to hold all our center results all_x_centers = [] all_y_centers = [] # Loop through selected nodes, get their centers, then add to lists for n in selected: center_x = n.xpos() + n.screenWidth() / 2 center_y = n.ypos() + n.screenWidth() / 2 all_x_centers.append(center_x) all_y_centers.append(center_y) # Find the collective center pivot point pivot_x = sum(all_x_centers) / len(selected) pivot_y = sum(all_y_centers) / len(selected) # Set scale factor. 1.1 = 10% increase, 0.9 = 10% decrease scale_factor = 1.1 # Loop through each selected node and scale it for node in selected: # Get the current center of this specific node node_center_x = node.xpos() + node.screenWidth() / 2 node_center_y = node.ypos() + node.screenHeight() / 2 # Use the scaling formula to find the new center position new_center_x = pivot_x + (node_center_x - pivot_x) * scale_factor new_center_y = pivot_y + (node_center_y - pivot_y) * scale_factor # Calculate the node's new top-left corner from its new center new_xpos = new_center_x - node.screenWidth() / 2 new_ypos = new_center_y - node.screenHeight() / 2 # Set the final scaled position node.setXpos(int(new_xpos)) node.setYpos(int(new_ypos))
That’s All!
I hope this helps! If I had a walkthrough like this when developing NodeGraphBuddy, it wouldn’t have taken so long! Don’t forget to check out the full script to see all the other features, like scaling backdrops, zoom based scale factor, and biasing the scale to different sides of the selection!