# Copyright (c) 2024 The Foundry Visionmongers Ltd. All Rights Reserved.
import math
import nuke_internal as nuke
import _nukemath
from .geomath import transpose_matrix3, plane_rotation
from .geomath import translate_matrix, rotate_matrix_xyz, scaling_matrix, rotate_matrix_zxy
from .geomath import get_world_point_on_bbox, get_prim_world_rotation_zxy
from collections.abc import Iterable
"""
Predefined snapping functions for USD geometry
"""
def _split_knob_name_prefix(origin_knob):
'''
Get the knob name prefix so we can determine the names of the related knobs
Returns (prefix, knob name without prefix)
'''
origin_knob_name = origin_knob.name()
i = origin_knob_name.rfind("geosnap_action_")
if i != -1:
return (origin_knob_name[:i], origin_knob_name[i:])
raise ValueError("Invalid snap action knob")
[docs]def get_translate_enabled(node, knob_name_prefix):
'''
Get whether translation is enabled by the snap knob with the given prefix
'''
translate_knob_name = knob_name_prefix + "geosnap_translate"
return node[translate_knob_name].value()
[docs]def get_rotate_enabled(node, knob_name_prefix):
'''
Get whether rotation is enabled by the snap knob with the given prefix
'''
rotate_knob_name = knob_name_prefix + "geosnap_rotate"
return node[rotate_knob_name].value()
[docs]def get_scale_enabled(node, knob_name_prefix):
'''
Get whether scale is enabled by the snap knob with the given prefix
'''
scale_knob_name = knob_name_prefix + "geosnap_scale"
return node[scale_knob_name].value()
[docs]def get_stage_from_active_viewer(frame_range):
'''
Gets the stage from the active viewer with the specified frame range.
'''
if not nuke.activeViewer():
return None
return nuke.activeViewer().node().getStage(nuke.OutputContext(frame_range[0]), frame_range)
[docs]def ensure_frame_range(frames):
'''
If frames is not iterable (i.e it's a single number) we create an array with the single frame
'''
if not isinstance(frames, Iterable):
return [frames]
return frames
[docs]def need_stage_copy(frame_range, task=None):
'''
This method will either return true if a copy of the stage is needed, false otherwise.
Currently, updating the task progress can cause recomposition of the stage from the viewer if changes
have been made. We need a copy of the stage to ensure this wont change during the calculation.
'''
return len(frame_range) > 1 and task is not None
[docs]def should_animate(num_of_frames_to_affect, animate_single_frame):
'''
Returns whether we need to animate the knob by checking how many frames we are going to affect and
whether we want to animate even if only a single frame will be affected.
'''
return (num_of_frames_to_affect == 1 and animate_single_frame) or num_of_frames_to_affect > 1
[docs]def copy_stage(stage):
'''
Returns a copy of the given stage. This can be expensive so should be avoided where possible.
'''
from pxr import Usd, UsdUtils
return Usd.Stage.Open(UsdUtils.FlattenLayerStack(stage))
[docs]def on_snap_knob(origin_knob, command):
'''
Called by the snap knobs to perform a pivot_to_selection/pivot_to_bbox/geo_to_selection operation.
@type origin_knob: nuke.Knob
@param origin_knob: Knob that was used to start this operation. This is used to determine
the operation to perform as well as finding the parameters for the
operation from other related snap knobs on the node.
@type command: str
@param command: An additional command string currently used by the pivot_to_bbox operation
specifying the side of the bounding box to snap to. This must be one of:
Center/Top/Bottom/Left/Right/Front/Back if pivot_to_bbox operation, otherwise
this parameter is ignored.
'''
node_to_snap = origin_knob.node()
# Get the knob name prefix so we can determine the names of the related knobs
knob_name_prefix, origin_knob_name = _split_knob_name_prefix(origin_knob)
translate_enabled = get_translate_enabled(node_to_snap, knob_name_prefix)
rotate_enabled = get_rotate_enabled(node_to_snap, knob_name_prefix)
scale_enabled = get_scale_enabled(node_to_snap, knob_name_prefix)
pivot_to_sel_knob_name = "geosnap_action_pivot_to_sel"
pivot_to_sel_anim_knob_name = "geosnap_action_pivot_to_sel_anim"
pivot_to_bbox_knob_name = "geosnap_action_pivot_to_bounding_box"
geo_to_sel_knob_name = "geosnap_action_geo_to_sel"
geo_to_sel_anim_knob_name = "geosnap_action_geo_to_sel_anim"
animate_enabled = (origin_knob_name == pivot_to_sel_anim_knob_name
or origin_knob_name == geo_to_sel_anim_knob_name)
if animate_enabled:
frames = nuke.getInput('Frame Range', '%s-%s' % (nuke.root().firstFrame(), nuke.root().lastFrame()))
if frames is None:
# Cancelled
return
if frames:
frame_range = [t for t in nuke.FrameRange(frames)]
else:
nuke.message("Input error")
return
else:
frame_range = [nuke.frame()]
# Create progress bar if animating
task = None
if animate_enabled:
task = nuke.ProgressTask("Snap menu")
task.setMessage("Snapping over frame range")
if origin_knob_name == pivot_to_sel_knob_name or origin_knob_name == pivot_to_sel_anim_knob_name:
pivot_to_selection(node_to_snap, frame_range, translate_enabled, rotate_enabled, animate_enabled, task=task)
elif origin_knob_name == pivot_to_bbox_knob_name:
pivot_to_bbox(node_to_snap, frame_range, rotate_enabled, command, animate_enabled, task=task)
elif origin_knob_name == geo_to_sel_knob_name or origin_knob_name == geo_to_sel_anim_knob_name:
geo_to_selection(node_to_snap, frame_range, translate_enabled, rotate_enabled, scale_enabled,
animate_enabled, task=task)
if animate_enabled:
del task
del knob_name_prefix
del node_to_snap
del origin_knob
[docs]def verify_node_order(node, knob_name, order_name):
'''
Verify the transform/rotation order
'''
order_knob = node.knob(knob_name)
order = order_knob.enumName(int(order_knob.getValue()))
if order_name != order:
raise ValueError('Snap requires "%s" %s' % (order_name, knob_name))
[docs]def verify_node_to_snap(node_to_snap, knob_list):
'''
Check knobs exist and current transform/rotation order is supported
'''
node_knobs = node_to_snap.knobs()
for knob in knob_list:
if knob not in node_knobs:
raise ValueError('Snap requires "%s" knob' % knob)
verify_node_order(node_to_snap, "xform_order", "SRT")
if "rotate" in knob_list:
verify_node_order(node_to_snap, "rot_order", "ZXY")
def _set_knob_value(knob, value, animate_enabled, current_frame):
'''
Internal use only
'''
if animate_enabled:
knob.setAnimated()
knob.setValue(value, current_frame)
else:
knob.setValue(value)
[docs]class GeoVertexInfo:
'''
Information on a single vertex in the selection
'''
def __init__(self, objnum, index, value, position, normal):
self.objnum = objnum
self.index = index
self.value = value
# This is updated on applying a transform
self.position = position
self.normal = normal
def __lt__(self, other):
'''
The implementation of the Less Than operator serves the purpose of
proper sorting of class instances, where the Object ID, followed by
the Point Index are predominant over the actual position value of
the selected point.
This allows keeping the order of sorted points over changing location
of the points.
'''
if (self.objnum != other.objnum):
return self.objnum < other.objnum
if (self.index != other.index):
return self.index < other.index
return (self.position.x - other.position.x +
self.position.y - other.position.y +
self.position.z - other.position.z) < 0
[docs]def fuzzy_is_zero(value):
'''
Returns true if a value is very close to zero.
'''
return math.isclose(value, 0.0, abs_tol=0.0001)
[docs]def fuzzy_vector3_add(v1, v2):
'''
Adds two vectors ensuring the coordinates are actually 0.0 if they are 0.0001
of zero.
'''
v1 += v2
v1.x = 0.0 if fuzzy_is_zero(v1.x) else v1.x
v1.y = 0.0 if fuzzy_is_zero(v1.y) else v1.y
v1.z = 0.0 if fuzzy_is_zero(v1.z) else v1.z
return v1
[docs]class GeoVertexSelection:
'''
Selection container
'''
def __init__(self):
self.vertex_info_set = set()
def __len__(self):
return len(self.vertex_info_set)
# Convenience function to allow direct iteration
def __iter__(self):
# Since a set is not iterable use a generator function
# Don't change the set while iterating!
for info in self.vertex_info_set:
yield info
def add(self, vertex_info):
self.vertex_info_set.add(vertex_info)
def points(self):
# Generate an iterable list of the positions
points = []
for info in self.vertex_info_set:
points += [info.position]
return points
def unique_positions_sorted(self):
# The python bindings for Vector3 do not provide __hash()__ implementation,
# because of that we can't use set() and sorted() directly.
position_to_info_map = {}
for info in self.vertex_info_set:
position_to_info_map[(info.position.x, info.position.y, info.position.z)] = info
unique_points = []
for info in sorted(position_to_info_map.values()):
unique_points += [info.position]
return unique_points
def indices(self):
# Generate a searchable dictionary of the positions
indices = {}
i = 0
for info in self.vertex_info_set:
indices[info.index] = i
i += 1
return indices
def translate(self, vector):
for info in self.vertex_info_set:
info.position = fuzzy_vector3_add(info.position, vector)
def inverse_rotate(self, vector, order):
# nuke.tprint(vector)
# nuke.tprint(order)
# Create a rotation matrix
m = _nukemath.Matrix3()
m.makeIdentity()
for axis in order:
if axis == "X":
# Apply X rotation
m.rotateX(vector[0])
# nuke.tprint("rotateX: %f" % vector[0])
elif axis == "Y":
# Apply Y rotation
m.rotateY(vector[1])
# nuke.tprint("rotateY: %f" % vector[1])
elif axis == "Z":
# Apply Z rotation
m.rotateZ(vector[2])
# nuke.tprint("rotateZ: %f" % vector[2])
else:
raise ValueError("Invalid rotation axis: %s" % axis)
# Now determine the inverse/transpose matrix
# nuke.tprint(m)
transpose_matrix3(m)
# nuke.tprint(m)
# Apply the matrix to the vertices
for info in self.vertex_info_set:
info.position = m * info.position
def scale(self, vector):
for info in self.vertex_info_set:
info.position[0] *= vector[0]
info.position[1] *= vector[1]
info.position[2] *= vector[2]
def _all_nodes(node=nuke.root()):
'''
Internal use only
'''
yield node
if hasattr(node, "nodes"):
for child in node.nodes():
for n in _all_nodes(child):
yield n
def _all_nodes_with_geo_select_knob():
'''
Internal use only
'''
if nuke.activeViewer():
preferred_nodes = [n for n in nuke.activeViewer().getGeometryNodes() if 'geo_select' in n.knobs()]
else:
preferred_nodes = []
nodes = preferred_nodes + [n for n in _all_nodes() if 'geo_select' in n.knobs() and n not in preferred_nodes]
return nodes
def _selected_vertex_infos_from_new_3d(stage, selection_threshold, current_frame):
'''
Internal use only
'''
sel = nuke.getGeoSelection()
for o, s in enumerate(sel):
vertex_weights = s.getVertexWeights()
points = s.getWorldPoints(stage, current_frame)
# We may not have normals, e.g. for point clouds
normals = s.getWorldNormals(stage, current_frame)
if len(vertex_weights) == len(points):
for p in range(len(vertex_weights)):
value = vertex_weights[p]
if value >= selection_threshold:
yield GeoVertexInfo(o, p, value, points[p],
normals[p] if p < len(normals) else _nukemath.Vector3(0, 0, 1))
[docs]def selected_vertex_infos(stage, current_frame, selection_threshold=0.5):
'''
selectedVertexInfos(selectionThreshold) -> iterator
Return an iterator which yields a tuple of the index and position of each
point currently selected in the Viewer in turn.
The selectionThreshold parameter is used when working with a soft selection.
Only points with a selection level >= the selection threshold will be
returned by this function.
'''
if stage:
yield from _selected_vertex_infos_from_new_3d(stage, selection_threshold, current_frame)
# Old 3D system
for n in _all_nodes_with_geo_select_knob():
geo_select_knob = n['geo_select']
sel = geo_select_knob.getSelection()
objs = geo_select_knob.getGeometry()
if objs:
for o in range(len(sel)):
obj_selection = sel[o]
obj_points = objs[o].points()
obj_transform = objs[o].transform()
inv_transform = obj_transform.inverse()
obj_normals = objs[o].constructNormals()
for p in range(len(obj_selection)):
value = obj_selection[p]
if value >= selection_threshold:
pos = obj_points[p]
t_pos = obj_transform * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
normal = obj_normals[p] if (obj_normals is not None
and p < len(obj_normals)) else _nukemath.Vector3(0, 0, 1)
t_normal = inv_transform.ntransform(normal)
yield GeoVertexInfo(o, p, value, _nukemath.Vector3(t_pos.x, t_pos.y, t_pos.z), t_normal)
[docs]def get_vertex_selection(stage, current_frame, selection_threshold=0.5):
'''
Build a GeoVertexSelection from GeoVertexInfos
'''
vertex_selection = GeoVertexSelection()
for info in selected_vertex_infos(stage, current_frame, selection_threshold):
vertex_selection.add(info)
return vertex_selection
[docs]def verify_vertex_selection_not_empty(vertex_selection: GeoVertexSelection):
'''
GeoSelection should have at least 1 selected vertex.
'''
if len(vertex_selection) < 1:
raise ValueError('Selection must be at least 1 point.')
[docs]def verify_vertex_selection_for_rotation(vertex_selection: GeoVertexSelection):
'''
GeoSelection should have at least 3 selected vertices for rotation
'''
if len(vertex_selection) < 3:
raise ValueError('Selection must be at least 3 points so a rotation can be computed.')
[docs]def average_normal(vertex_selection: GeoVertexSelection) -> _nukemath.Vector3:
'''
Return a _nukemath.Vector3 which is the average of the normals of all selected points
'''
norm = _nukemath.Vector3(0.0, 0.0, 0.0)
for v in vertex_selection:
norm += v.normal
norm.normalize()
return norm
[docs]def calc_bounds(vertex_selection: GeoVertexSelection) -> _nukemath.Vector3:
'''
Get the size of the bounding box of all the selected points
Avoid zero size to allow inverse scaling (1/scale)
'''
high = None
low = None
for info in vertex_selection:
pos = info.position
if high is None:
high = _nukemath.Vector3(pos)
low = _nukemath.Vector3(pos)
else:
for i in range(len(pos)):
if pos[i] < low[i]:
low[i] = pos[i]
elif pos[i] > high[i]:
high[i] = pos[i]
if high is None:
bounds = _nukemath.Vector3(0)
else:
bounds = high - low
bounds = [bounds[i] if bounds[i] != 0.0 else 2e-20 for i in range(3)]
return _nukemath.Vector3(*bounds)
[docs]def check_all_point_collinear(points):
'''
Iterates through the points the point and checks if all of them are collinear.
The points must contain more than two points.
@param points: Points to check
'''
first_point = points[0]
second_point = points[1]
dir_first_to_second = second_point - first_point
dir_first_to_second.normalize()
for i in range(2, len(points)):
next_point = points[i]
dir_second_to_next = next_point - second_point
dir_second_to_next.normalize()
d = dir_first_to_second.dot(dir_second_to_next)
if abs(d) < 0.999:
return False
return True
[docs]def least_significant_axis(vector):
'''
Given a Vector3, returns the least significant axis position, being x, y and
z 0, 1 and 2 respectively.
@type vector: _nukemath.Vector3
@param vector: a Vector3
@return the position of the least significant component of the vector.
'''
least = 0 if vector.x < vector.y else 1
return 2 if vector.z < vector[least] else least
[docs]def calc_rotation_vector(vertex_selection: GeoVertexSelection, norm) -> _nukemath.Vector3:
'''
Get the rotation vector
'''
# Collate a point set from the vertex selection
points = vertex_selection.unique_positions_sorted()
if len(points) == 0:
return None
# Find a best fit plane with three or more non-collinear points
if len(points) > 2 and not check_all_point_collinear(points):
plane_tri = nuke.geo.bestFitPlane(*points)
return plane_rotation(plane_tri, norm)
w = norm
w.normalize()
if len(points) > 1:
direction = points[1] - points[0]
direction.normalize()
# If normal is zero, we would not be able to properly define a triangle
# for the plane alignment using only the direction. We invent a normal that
# is aligned to the least significant axis of the direction.
if fuzzy_is_zero(w.x) and fuzzy_is_zero(w.y) and fuzzy_is_zero(w.z):
w = _nukemath.Vector3()
w[least_significant_axis(direction)] = 1
if abs(w.dot(direction)) < 1.0:
# Choose the axes dependent on the line direction
u = direction
v = w.cross(u)
v.normalize()
# Fabricate a tri (tuple)
plane_tri = (_nukemath.Vector3(0.0, 0.0, 0.0), u, v)
return plane_rotation(plane_tri, norm)
y = _nukemath.Vector3(0.0, 1.0, 0.0)
if abs(w.dot(y)) < 0.5:
v = y
u = w.cross(v)
u.normalize()
# Update v
v = w.cross(u)
else:
u = _nukemath.Vector3(1.0, 0.0, 0.0)
v = w.cross(u)
v.normalize()
# Update v
u = w.cross(v)
# Fabricate a tri (tuple)
plane_tri = (_nukemath.Vector3(0.0, 0.0, 0.0), u, v)
rotation_vec = plane_rotation(plane_tri, norm)
# In fact this only handles ZXY (see plane_rotation)
rotation_vec.z = 0
return rotation_vec
[docs]def radians(vector) -> list:
'''Return a list of all degree values in argument converted to radians'''
return [math.radians(x) for x in vector]
[docs]def degrees(vector) -> list:
'''Return a list of all radians values in argument converted to degrees'''
return [math.degrees(x) for x in vector]
[docs]def get_axis_translate_knob(node_to_snap):
'''Get the axis translate knob'''
return node_to_snap['translate']
[docs]def get_axis_rotate_knob(node_to_snap):
'''Get the axis rotate knob'''
return node_to_snap['rotate']
[docs]def get_axis_scaling_knob(node_to_snap):
'''Get the axis scaling knob'''
return node_to_snap['scaling']
[docs]def get_axis_pivot_translate_knob(node_to_snap):
'''Get the axis pivot_translate knob'''
return node_to_snap['pivot_translate']
[docs]def get_axis_pivot_rotate_knob(node_to_snap):
'''Get the axis pivot_rotate knob'''
return node_to_snap['pivot_rotate']
[docs]def calc_average_position(vertex_selection: GeoVertexSelection):
'''
Calculate the average position of all points.
@param points: An iterable sequence of _nukemath.Vector3 objects.
@return: A _nukemath.Vector3 containing the average of all the points.
'''
pos = _nukemath.Vector3(0, 0, 0)
count = 0
for info in vertex_selection:
point = info.position
pos += point
count += 1
if count == 0:
return None
pos /= count
return pos
[docs]def translate_rotate_pivot(node_to_snap, translate, rotate, current_frame) -> tuple:
'''
Pivot translation and rotation must keep the object stationary and in
order to do that compensation values must be placed in the geometry translate
and rotate.
@type node_to_snap: nuke.Node
@param node_to_snap: Node to translate and rotate
@type translate: _nukemath.Vector3
@param translate: Target position for the pivot point.
@type rotate: _nukemath.Vector3
@param rotate: Target rotation for the pivot point.
@return: A tuple with the new geometry translation and rotation
respectively (_nukemath.Vector3, _nukemath.Vector3).
'''
p_t = translate_matrix(translate)
p_ti = p_t.inverse()
p_r = rotate_matrix_xyz(rotate)
p_ri = p_r.inverse()
scaling = get_axis_scaling_knob(node_to_snap).valueAt(current_frame)
uniform_scale = get_axis_uniform_scaling_knob(node_to_snap).valueAt(current_frame)
mat_s = scaling_matrix([v * uniform_scale for v in scaling])
mat_si = mat_s.inverse()
mat_node = get_node_transform_matrix(node_to_snap, current_frame)
mat_compensated = p_ri * p_ti * mat_node * p_t * p_r * mat_si
geo_translate = mat_compensated.translation()
geo_rotate = degrees(mat_compensated.rotationsZXY())
return (geo_translate, geo_rotate)
[docs]def translate_pivot_to_world_pos_verified(node_to_snap, world_position,
animate_enabled, current_frame):
'''
Translate the pivot to world position without affecting the geometry's position
'''
transformations = get_node_transform_matrix(node_to_snap, current_frame)
# Get the local point that would equal the requested world point through the node's transform
local_position = transformations.inverse().transform(world_position)
# Get current pivot rotate values of the node to snap in radians
pivot_rotate = radians(get_axis_pivot_rotate_knob(node_to_snap).valueAt(current_frame))
(geo_translate, geo_rotate) = translate_rotate_pivot(node_to_snap, local_position, pivot_rotate, current_frame)
_set_knob_value(get_axis_translate_knob(node_to_snap), geo_translate, animate_enabled, current_frame)
_set_knob_value(get_axis_rotate_knob(node_to_snap), geo_rotate, animate_enabled, current_frame)
_set_knob_value(get_axis_pivot_translate_knob(node_to_snap), local_position, animate_enabled, current_frame)
[docs]def translate_pivot_to_points_verified(node_to_snap, vertex_selection: GeoVertexSelection,
animate_enabled, current_frame):
'''
Translate the pivot to the vertex selection without affecting the geometry's position
'''
global_position = calc_average_position(vertex_selection)
translate_pivot_to_world_pos_verified(node_to_snap, global_position,
animate_enabled, current_frame)
[docs]def rotate_pivot_to_angles_verified(node_to_snap, pivot_rotate: _nukemath.Vector3,
animate_enabled, current_frame):
'''
Rotate the pivot to rotation angles without affecting the geometry's position
'''
# Calc rotation vector returns a global rotation vector, in order to transfer
# it to object space as pivot rotation should be we need to remove the object
# rotation from it and then extract the rotation in XYZ order.
mat_r = rotate_matrix_zxy(radians(get_axis_rotate_knob(node_to_snap).valueAt(current_frame)))
pivot_rotate = _nukemath.Vector3(*((mat_r.inverse() * rotate_matrix_zxy(pivot_rotate)).rotationsXYZ()))
p_t = get_axis_pivot_translate_knob(node_to_snap).valueAt(current_frame)
pivot_translate = _nukemath.Vector3(p_t[0], p_t[1], p_t[2])
(geo_translate, geo_rotate) = translate_rotate_pivot(node_to_snap, pivot_translate, pivot_rotate, current_frame)
pivot_rotation_degrees = _nukemath.Vector3(math.degrees(pivot_rotate[0]),
math.degrees(pivot_rotate[1]),
math.degrees(pivot_rotate[2]))
_set_knob_value(get_axis_pivot_rotate_knob(node_to_snap), pivot_rotation_degrees, animate_enabled, current_frame)
_set_knob_value(get_axis_rotate_knob(node_to_snap), geo_rotate, animate_enabled, current_frame)
_set_knob_value(get_axis_translate_knob(node_to_snap), geo_translate, animate_enabled, current_frame)
[docs]def rotate_pivot_to_points_verified(node_to_snap, vertex_selection: GeoVertexSelection,
animate_enabled, current_frame):
'''
Rotate the pivot to the vertex selection without affecting the geometry's position
'''
# The following translation is required to match the behaviour of Geo Match
# to selection. The function translatePivotToPointsVerified is finished with
# this exact operation and the Geo rotations are applied with this offset to
# all vertices, without this, pivot rotation diverges on behaviour from Geo.
# This can be done at the end of translatePivotToPointsVerified because
# unlike Geo options, pivot have rotation only.
vertex_selection.translate(-calc_average_position(vertex_selection))
global_normal = average_normal(vertex_selection)
pivot_rotate = calc_rotation_vector(vertex_selection, global_normal)
rotate_pivot_to_angles_verified(node_to_snap, pivot_rotate,
animate_enabled, current_frame)
[docs]def pivot_to_selection(node_to_snap, frames, translate_enabled, rotate_enabled,
animate_single_frame=False, stage=None, task=None):
'''
Translate and/or rotate the specified node's Pivot Point to the average position/rotation of the
current vertex selection on the stage.
@type node_to_snap: nuke.Node
@param node_to_snap: Node to translate/rotate
@type frames: int or int array
@param frames: The frame or array of frames to affect
@type translate_enabled: boolean
@param translate_enabled: Whether to apply the translation to the node
@type rotate_enabled: boolean
@param rotate_enabled: Whether to apply the rotation to the node
@type animate_single_frame: boolean
@param animate_single_frame: If true, will set key for animation if affecting 1 frame.
Always will set keys for animation if affecting multiple frames
@type stage: pxr.Usd.Stage
@param stage: The stage to get the selected vertex info from. If None, this
method will attempt to get the stage from the active viewer.
@type task: nuke.ProgressTask
@param task: An optional progress task that will update as frames are processed.
@return: True if this method was successful, False otherwise.
'''
success = True
if task is not None:
task.setProgress(0)
try:
frame_range = ensure_frame_range(frames)
animate_enabled = should_animate(len(frame_range), animate_single_frame)
if stage is None:
stage = get_stage_from_active_viewer(frame_range)
if stage is not None and need_stage_copy(frame_range, task):
stage = copy_stage(stage)
for i, current_frame in enumerate(frame_range):
if task is not None:
if task.isCancelled():
success = False
break
task.setProgress(int(i / len(frame_range) * 100))
vertex_selection = get_vertex_selection(stage, current_frame)
if not (translate_enabled or rotate_enabled):
raise ValueError('At least one of T or R knobs must be enabled')
verify_vertex_selection_not_empty(vertex_selection)
if translate_enabled:
verify_node_to_snap(node_to_snap, ["pivot_translate", "translate", "rotate",
"xform_order", "rot_order"])
if rotate_enabled:
verify_vertex_selection_for_rotation(vertex_selection)
verify_node_to_snap(node_to_snap, ["pivot_rotate", "pivot_translate",
"translate", "rotate", "xform_order",
"rot_order"])
if translate_enabled:
translate_pivot_to_points_verified(node_to_snap, vertex_selection,
animate_enabled, current_frame)
if rotate_enabled:
rotate_pivot_to_points_verified(node_to_snap, vertex_selection,
animate_enabled, current_frame)
except ValueError as e:
nuke.message(str(e))
success = False
return success
[docs]def translate_geo_to_points_verified(node_to_snap, vertex_selection: GeoVertexSelection, adjust_selection_only,
animate_enabled=False, current_frame=None):
'''
Translate the geometry to the vertex selection (does not check for required conditions)
'''
# Find the average position
center = calc_average_position(vertex_selection)
# Move the nodeToSnap to the average position
if not adjust_selection_only:
_set_knob_value(get_axis_translate_knob(node_to_snap), center, animate_enabled, current_frame)
# Subtract this translation from the vertexSelection
inverse_translation = -center
vertex_selection.translate(inverse_translation)
[docs]def rotate_geo_to_points_verified(node_to_snap, vertex_selection: GeoVertexSelection, adjust_selection_only,
animate_enabled=False, current_frame=None):
'''
Rotate the geometry to the vertex selection (does not check for required conditions)
'''
# Get the normal of the points
norm = average_normal(vertex_selection)
# Calculate the rotation vector
rotation_vec = calc_rotation_vector(vertex_selection, norm)
# Convert to degrees
rotation_degrees = _nukemath.Vector3(math.degrees(rotation_vec.x),
math.degrees(rotation_vec.y),
math.degrees(rotation_vec.z))
# Set the node transform
if not adjust_selection_only:
_set_knob_value(get_axis_rotate_knob(node_to_snap), rotation_degrees, animate_enabled, current_frame)
# Apply the reverse rotation to the points
vertex_selection.inverse_rotate(rotation_vec, "YXZ")
[docs]def scale_geo_to_points_verified(node_to_snap, vertex_selection: GeoVertexSelection,
animate_enabled=False, current_frame=None):
'''
Scale the geometry to the vertex selection (does not check for required conditions)
'''
# Scale to fit the bounding box of the selected points
scale = calc_bounds(vertex_selection)
# Adjust scale to take into account the uniform scale
uniform_scale = get_axis_uniform_scaling_knob(node_to_snap).valueAt(current_frame)
adjusted_scales = scale
if uniform_scale != 0:
adjusted_scales /= uniform_scale
_set_knob_value(get_axis_scaling_knob(node_to_snap), adjusted_scales, animate_enabled, current_frame)
# Apply the inverse scale to the points
inverse_scale = _nukemath.Vector3(1/adjusted_scales[0], 1/adjusted_scales[1], 1/adjusted_scales[2])
vertex_selection.scale(inverse_scale)
[docs]def geo_to_selection(node_to_snap, frames, translate_enabled, rotate_enabled, scale_enabled,
animate_single_frame=False, stage=None, task=None):
'''
Translate/rotate/scale the specified node to the average position of the current vertex
selection on the stage.
@type node_to_snap: nuke.Node
@param node_to_snap: Node to translate/rotate/scale
@type frames: int or int array
@param frames: The frame or array of frames to affect
@type translate_enabled: boolean
@param translate_enabled: Whether to apply the translation to the node
@type rotate_enabled: boolean
@param rotate_enabled: Whether to apply the rotation to the node
@type scale_enabled: boolean
@param scale_enabled: Whether to apply the scale to the node
@type animate_single_frame: boolean
@param animate_single_frame: If true, will set key for animation if affecting 1 frame.
Always will set keys for animation if affecting multiple frames
@type stage: pxr.Usd.Stage
@param stage: The stage to get the selected vertex info from. If None, this
method will attempt to get the stage from the active viewer.
@type task: nuke.ProgressTask
@param task: An optional progress task that will update as frames are processed.
@return: True if this method was successful, False otherwise.
'''
success = True
if task is not None:
task.setProgress(0)
try:
frame_range = ensure_frame_range(frames)
animate_enabled = should_animate(len(frame_range), animate_single_frame)
if stage is None:
stage = get_stage_from_active_viewer(frame_range)
if stage is not None and need_stage_copy(frame_range, task):
stage = copy_stage(stage)
for i, current_frame in enumerate(frame_range):
if task is not None:
if task.isCancelled():
success = False
break
task.setProgress(int(i / len(frame_range) * 100))
vertex_selection = get_vertex_selection(stage, current_frame)
if not (translate_enabled or rotate_enabled or scale_enabled):
raise ValueError('At least one of TRS knobs must be enabled')
# Ensure required conditions for all actions we are performing are in order first
verify_vertex_selection_not_empty(vertex_selection)
if translate_enabled:
verify_node_to_snap(node_to_snap, ["translate"])
if rotate_enabled:
verify_vertex_selection_for_rotation(vertex_selection)
verify_node_to_snap(node_to_snap, ["rotate", "xform_order", "rot_order"])
if scale_enabled:
verify_node_to_snap(node_to_snap, ["scaling", "uniform_scale"])
# Ensure the necessary side effects on the vertex selection are applied if we don't translate/rotate
translate_geo_to_points_verified(node_to_snap, vertex_selection, not translate_enabled,
animate_enabled, current_frame)
rotate_geo_to_points_verified(node_to_snap, vertex_selection, not rotate_enabled,
animate_enabled, current_frame)
if scale_enabled:
scale_geo_to_points_verified(node_to_snap, vertex_selection,
animate_enabled, current_frame)
except ValueError as e:
nuke.message(str(e))
success = False
return success
[docs]def get_selected_prim_path(geo_selection):
'''
Get the currently selected prim
'''
for s in geo_selection:
if s.getObject():
# Found an actively selected prim
return s.getPath()
return None
[docs]def pivot_to_bbox(node_to_snap, frames, rotate_enabled, command,
animate_single_frame=False, stage=None, task=None):
'''
Translate and optionally rotate the specified node's Pivot Point to a chosen point on the
currently selected prim's bounding box.
@type node_to_snap: nuke.Node
@param node_to_snap: Node to translate and optionally rotate
@type frames: int or int array
@param frames: The frame or array of frames to affect
@type rotate_enabled: boolean
@param rotate_enabled: Whether to apply the rotation to the node
@type command: str
@param command: The side of the bounding box to snap to. This must be one of:
Center/Top/Bottom/Left/Right/Front/Back
@type animate_single_frame: boolean
@param animate_single_frame: If true, will set key for animation if affecting 1 frame.
Always will set keys for animation if affecting multiple frames
@type stage: pxr.Usd.Stage
@param stage: The stage to get the selected prim from. If None, this
method will attempt to get the stage from the active viewer.
@type task: nuke.ProgressTask
@param task: An optional progress task that will update as frames are processed.
@return: True if this method was successful, False otherwise.
'''
success = True
if task is not None:
task.setProgress(0)
try:
frame_range = ensure_frame_range(frames)
animate_enabled = should_animate(len(frame_range), animate_single_frame)
if stage is None:
stage = get_stage_from_active_viewer(frame_range)
if stage is None:
raise ValueError('No stage available to perform the snap')
if stage is not None and need_stage_copy(frame_range, task):
stage = copy_stage(stage)
for i, current_frame in enumerate(frame_range):
if task is not None:
if task.isCancelled():
success = False
break
task.setProgress(int(i / len(frame_range) * 100))
geo_selection = nuke.getGeoSelection()
# Ensure required conditions for all actions we are performing are in order first
selected_prim_path = get_selected_prim_path(geo_selection)
if selected_prim_path is None:
raise ValueError("At least one prim must be selected")
if not stage:
raise ValueError("Stage invalid")
verify_node_to_snap(node_to_snap, ["pivot_translate", "translate", "rotate", "xform_order", "rot_order"])
if rotate_enabled:
verify_node_to_snap(node_to_snap, ["pivot_rotate", "pivot_translate",
"translate", "rotate", "xform_order",
"rot_order"])
prim = stage.GetPrimAtPath(selected_prim_path)
bbox_world_point = get_world_point_on_bbox(prim, command, current_frame)
translate_pivot_to_world_pos_verified(node_to_snap, bbox_world_point,
animate_enabled, current_frame)
if rotate_enabled:
prim_rotation = get_prim_world_rotation_zxy(prim, current_frame)
rotate_pivot_to_angles_verified(node_to_snap, prim_rotation,
animate_enabled, current_frame)
except ValueError as e:
nuke.message(str(e))
success = False
return success