Source code for nukescripts.geosnap3d

# 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_uniform_scaling_knob(node_to_snap): '''Get the axis uniform_scale knob''' return node_to_snap['uniform_scale']
[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 get_node_transform_matrix(node_to_snap, current_frame) -> _nukemath.Matrix4: ''' Generates the transformation matrix for a given node based on its knob values. @type node_to_snap: nuke.Node @param node_to_snap: Node from which the data will be extracted to generate its transformation matrix. @return: The matrix containg all node transformations. ''' t_mat = translate_matrix(get_axis_translate_knob(node_to_snap).valueAt(current_frame)) r_mat = rotate_matrix_zxy(radians(get_axis_rotate_knob(node_to_snap).valueAt(current_frame))) scaling = get_axis_scaling_knob(node_to_snap).valueAt(current_frame) uniform_scale = get_axis_uniform_scaling_knob(node_to_snap).valueAt(current_frame) s_mat = scaling_matrix([v * uniform_scale for v in scaling]) p_t = translate_matrix(get_axis_pivot_translate_knob(node_to_snap).valueAt(current_frame)) p_r = rotate_matrix_xyz(radians(get_axis_pivot_rotate_knob(node_to_snap).valueAt(current_frame))) p_ti = p_t.inverse() p_ri = p_r.inverse() return p_t * p_r * t_mat * r_mat * s_mat * p_ri * p_ti
[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