nukescripts.snap3d

Package nukescripts :: Module snap3d |
[hide private]
[frames] | no frames]

# Source Code for Module nukescripts.snap3d

      1  # Copyright (c) 2010 The Foundry Visionmongers Ltd.  All Rights Reserved.
      2
      3  import math
      4  import nuke, nuke.geo, _nukemath
      5
      6  #
      7  # Predefined snapping functions.
      8  #
      9
     10  # Lazy functions to call on "thisNode"
     11 -def translateThisNodeToPoints():
     12    return translateToPoints(nuke.thisNode())
     13
     14 -def translateRotateThisNodeToPoints():
     15    return translateRotateToPoints(nuke.thisNode())
     16
     17 -def translateRotateScaleThisNodeToPoints():
     18    return translateRotateScaleToPoints(nuke.thisNode())
     19
     20  # Lazy functions to determine the vertex selection
     21  # These are the external user functions
     22  # Much hard work is done in obtaining the selection in these functions
     23 -def translateToPoints(nodeToSnap):
     24    '''
     25    Translate the specified node to the average position of the current vertex selection in the active viewer.
     26    The nodeToSnap must contain a 'translate' knob and the transform order must be 'SRT'.
     27
     28    @type nodeToSnap:   nuke.Node
     29    @param nodeToSnap:  Node to translate
     30    '''
     31    return translateSelectionToPoints(nodeToSnap, getSelection())
     32
     33 -def translateRotateToPoints(nodeToSnap):
     34    '''
     35    Translate the specified node to the average position of the current vertex selection in the active viewer
     36    and rotate to the orientation of the (mean squares) best fit plane for the selection.
     37    The nodeToSnap must contain 'translate' and 'rotate' knobs, the transform order must be 'SRT' and the
     38    rotation order must be 'ZXY'.
     39
     40    @type nodeToSnap:   nuke.Node
     41    @param nodeToSnap:  Node to translate and rotate
     42    '''
     43    return translateRotateSelectionToPoints(nodeToSnap, getSelection())
     44
     45 -def translateRotateScaleToPoints(nodeToSnap):
     46    '''
     47    Translate the specified node to the average position of the current vertex selection in the active viewer,
     48    rotate to the orientation of the (mean squares) best fit plane for the selection
     49    and scale to the extents of the selection.
     50    The nodeToSnap must contain 'translate', 'rotate' and 'scale' knobs, the transform order must be 'SRT' and
     51    the rotation order must be 'ZXY'.
     52
     53    @type nodeToSnap:   nuke.Node
     54    @param nodeToSnap:  Node to translate, rotate and scale
     55    '''
     56    return translateRotateScaleSelectionToPoints(nodeToSnap, getSelection())
     57
     58  # Verification wrappers
     59 -def translateSelectionToPoints(nodeToSnap, vertexSelection):
     60    try:
     61      verifyNodeToSnap(nodeToSnap, ["translate", "xform_order", "rot_order"])
     62      verifyVertexSelection(vertexSelection, 1)
     63      translateToPointsVerified(nodeToSnap, vertexSelection)
     64    except ValueError, e:
     65      nuke.message(str(e))
     66
     67 -def translateRotateSelectionToPoints(nodeToSnap, vertexSelection):
     68    try:
     69      verifyNodeToSnap(nodeToSnap, ["translate", "rotate", "xform_order", "rot_order"])
     70      verifyVertexSelection(vertexSelection, 1)  # This can use the normal direction
     71      translateRotateToPointsVerified(nodeToSnap, vertexSelection)
     72    except ValueError, e:
     73      nuke.message(str(e))
     74
     75 -def translateRotateScaleSelectionToPoints(nodeToSnap, vertexSelection):
     76    try:
     77      verifyNodeToSnap(nodeToSnap, ["translate", "rotate", "scaling", "xform_order", "rot_order"])
     78      verifyVertexSelection(vertexSelection, 3)
     79      translateRotateScaleToPointsVerified(nodeToSnap, vertexSelection)
     80    except ValueError, e:
     81      nuke.message(str(e))
     82
     83 -def verifyVertexSelection(vertexSelection, minLen):
     84    if len(vertexSelection) < minLen:
     85      raise ValueError('Selection must be at least %d points' % minLen)
     86
     87  # Verification functions
     88 -def verifyNodeToSnap(nodeToSnap, knobList):
     89    # Check the knobs
     90    nodeKnobs = nodeToSnap.knobs()
     91    for knob in knobList:
     92      if knob not in nodeKnobs:
     93        raise ValueError('Snap requires "%s" knob' % knob)
     94    # Verify the transform order
     95    verifyNodeOrder(nodeToSnap, "xform_order", "SRT")
     96    # Verify the rotation order as necessary
     97    if "rotate" in knobList:
     98      verifyNodeOrder(nodeToSnap, "rot_order", "ZXY")
     99
    100 -def verifyNodeOrder(node, knobName, orderName):
    101    orderKnob = node.knob(knobName)
    102    # Is there a better way than this?
    103    order = orderKnob.enumName( int(orderKnob.getValue()) )
    104    if orderName != order:
    105      raise ValueError('Snap requires "%s" %s' % (orderName, knobName))
    106
    107  # Info for each vertex
    108 -class VertexInfo:
    109 -  def __init__(self, objnum, index, value, position):
    110      self.objnum = objnum
    111      self.index = index
    112      self.value = value
    113      self.position = position  # This is updated on applying a transform
    114
    115  # Selection container
    116 -class VertexSelection:
    117 -  def __init__(self):
    118      self.vertexInfoSet = set()
    119      self.length = 0
    120
    121 -  def __len__(self):
    122      return self.length
    123
    124    # Convenience function to allow direct iteration
    125 -  def __iter__(self):
    126      # Since a set is not iterable use a generator function
    127      # Don't change the set while iterating!
    128      for info in self.vertexInfoSet:
    129        yield info
    130
    131 -  def add(self, vertexInfo):
    132      self.vertexInfoSet.add(vertexInfo)
    133      self.length += 1
    134
    135 -  def points(self):
    136      # Generate an iterable list of the positions
    137      points = []
    138      for info in self.vertexInfoSet:
    139        points += [info.position]
    140      return points
    141
    142 -  def indices(self):
    143      # Generate a searchable dictionary of the positions
    144      indices = {}
    145      i = 0
    146      for info in self.vertexInfoSet:
    147        indices[info.index] = i
    148        i += 1
    149      return indices
    150
    151 -  def translate(self, vector):
    152      for info in self.vertexInfoSet:
    153        info.position += vector
    154
    155 -  def inverseRotate(self, vector, order):
    156      #nuke.tprint(vector)
    157      #nuke.tprint(order)
    158      # Create a rotation matrix
    159      m = _nukemath.Matrix3()
    160      m.makeIdentity()
    161      for axis in order:
    162        if axis == "X":
    163          # Apply X rotation
    164          m.rotateX(vector[0])
    165          #nuke.tprint("rotateX: %f" % vector[0])
    166        elif axis == "Y":
    167          # Apply Y rotation
    168          m.rotateY(vector[1])
    169          #nuke.tprint("rotateY: %f" % vector[1])
    170        elif axis == "Z":
    171          # Apply Z rotation
    172          m.rotateZ(vector[2])
    173          #nuke.tprint("rotateZ: %f" % vector[2])
    174        else:
    175          raise ValueError("Invalid rotation axis: %s" % axis)
    176
    177      # Now determine the inverse/transpose matrix
    178      #nuke.tprint(m)
    179      transpose(m)
    180      #nuke.tprint(m)
    181
    182      # Apply the matrix to the vertices
    183      for info in self.vertexInfoSet:
    184        info.position = m * info.position
    185
    186 -  def scale(self, vector):
    187      for info in self.vertexInfoSet:
    188        info.position[0] *= vector[0]
    189        info.position[1] *= vector[1]
    190        info.position[2] *= vector[2]
    191
    192  # Helper function since Matrix3 has no transpose operation
    193 -def transpose(m):
    194    t = m[0+3*1]
    195    m[0+3*1] = m[1+3*0]
    196    m[1+3*0] = t
    197
    198    t = m[0+3*2]
    199    m[0+3*2] = m[2+3*0]
    200    m[2+3*0] = t
    201
    202    t = m[1+3*2]
    203    m[1+3*2] = m[2+3*1]
    204    m[2+3*1] = t
    205
    206    return
    207
    208  # Core snapping functions
    209 -def translateToPointsVerified(nodeToSnap, vertexSelection):
    210    # Find the average position
    211    centre = calcAveragePosition(vertexSelection)
    212    # Move the nodeToSnap to the average position
    213    nodeToSnap['translate'].setValue(centre)
    214    # Subtract this translation from the vertexSelection
    215    inverseTranslation = -centre
    216    vertexSelection.translate(inverseTranslation)
    217
    218 -def scaleToPointsVerified(nodeToScale, vertexSelection):
    219    # Scale the nodeToScale to fit the bounding box of the selected points
    220    scale = calcBounds(vertexSelection)
    221    nodeToScale['scaling'].setValue(scale)
    222    # Apply the inverse scale to the points
    223    inverseScale = _nukemath.Vector3(1/scale[0], 1/scale[1], 1/scale[2])
    224    vertexSelection.scale(inverseScale)
    225
    226 -def rotateToPointsVerified(nodeToSnap, vertexSelection):
    227    # Get the normal of the points
    228    norm = averageNormal(vertexSelection)
    229    # Calculate the rotation vector
    230    rotationVec = calcRotationVector(vertexSelection, norm)
    231    # Convert to degrees
    232    rotationDegrees = _nukemath.Vector3( math.degrees(rotationVec.x), math.degrees(rotationVec.y), math.degrees(rotationVec.z) )
    233    # Set the node transform
    234    nodeToSnap['rotate'].setValue(rotationDegrees)
    235    # Apply the reverse rotation to the points
    236    vertexSelection.inverseRotate(rotationVec, "YXZ")
    237
    238 -def translateRotateToPointsVerified(nodeToSnap, vertexSelection):
    239    # Note that the vertexSelection positions are updated as we go
    240    translateToPointsVerified(nodeToSnap, vertexSelection)
    241    rotateToPointsVerified(nodeToSnap, vertexSelection)
    242
    243 -def translateRotateScaleToPointsVerified(nodeToSnap, vertexSelection):
    244    # Note that the vertexSelection positions are updated as we go
    245    translateRotateToPointsVerified(nodeToSnap, vertexSelection)
    246    scaleToPointsVerified(nodeToSnap, vertexSelection)
    247
    248  # Get the rotation vector
    249 -def calcRotationVector(vertexSelection, norm):
    250    # Collate a point set from the vertex selection
    251    points = vertexSelection.points()
    252
    253    # Find a best fit plane with three or more points
    254    if len(vertexSelection) >= 3:
    255      planeTri = nuke.geo.bestFitPlane(*points)
    256
    257      rotationVec = planeRotation(planeTri, norm)
    258    elif len(vertexSelection) == 2:
    259      # Choose the axes dependent on the line direction
    260      u = points[1] - points[0]
    261      u.normalize()
    262
    263      w = norm
    264      v = w.cross(u)
    265      v.normalize()
    266      # Update w
    267      w = v.cross(u)
    268
    269      # Fabricate a tri (tuple)
    270      planeTri = (_nukemath.Vector3(0.0, 0.0, 0.0), u, v)
    271
    272      rotationVec = planeRotation(planeTri, norm)
    273    elif len(vertexSelection) == 1:
    274      # Choose the axes dependent on the normal direction
    275      w = norm
    276      w.normalize()
    277
    278      if abs(w.x) < abs(w.y):
    279        v = _nukemath.Vector3(0.0, 1.0, 0.0)
    280        u = w.cross(v)
    281        u.normalize()
    282        # Update v
    283        v = w.cross(u)
    284      else:
    285        u = _nukemath.Vector3(1.0, 0.0, 0.0)
    286        v = w.cross(u)
    287        v.normalize()
    288        # Update v
    289        u = w.cross(v)
    290
    291      # Fabricate a tri (tuple)
    292      planeTri = (_nukemath.Vector3(0.0, 0.0, 0.0), u, v)
    293
    294      rotationVec = planeRotation(planeTri, norm)
    295
    296      # In fact this only handles ZXY (see planeRotation)
    297      rotationVec.z = 0
    298
    299    return rotationVec
    300
    301  #
    302  # Geometric functions
    303  #
    304
    305 -def calcAveragePosition(vertexSelection):
    306    '''
    307    Calculate the average position of all points.
    308
    309    @param points: An iterable sequence of _nukemath.Vector3 objects.
    310    @return: A _nukemath.Vector3 containing the average of all the points.
    311    '''
    312    pos = _nukemath.Vector3(0, 0, 0)
    313    count = 0
    314    for info in vertexSelection:
    315      point = info.position
    316      pos += point
    317      count += 1
    318    if count == 0:
    319      return
    320    pos /= count
    321    return pos
    322
    323 -def calcBounds(vertexSelection):
    324    # Get the bounding box of all the selected points
    325    # Avoid zero size to allow inverse scaling (1/scale)
    326    high = _nukemath.Vector3(1e-20, 1e-20, 1e-20)
    327    low = _nukemath.Vector3(-1e-20, -1e-20, -1e-20)
    328    for info in vertexSelection:
    329      pos = info.position
    330      for i in xrange(len(pos)):
    331        if pos[i] < low[i]:
    332          low[i] = pos[i]
    333        elif pos[i] > high[i]:
    334          high[i] = pos[i]
    335
    336    bounds = high - low
    337    return bounds
    338
    339 -def planeRotation(tri, norm=None):
    340    '''
    341    Calculate the rotations around the X, Y and Z axes that will align a plane
    342    perpendicular to the Z axis with the given triangle.
    343
    344    @param tri: A list or tuple of 3 _nukemath.Vector3 objects. The 3 points must
    345     describe the plane (i.e. they must not be collinear).
    346    @return: A _nukemath.Vector3 object where the x coordinate is the angle of
    347     rotation around the x axis and so on.
    348    @raise ValueError: if the three points are collinear.
    349    '''
    350    # Get the vectors along two edges of the triangle described by the three points.
    351    a = tri[1] - tri[0]
    352    b = tri[2] - tri[0]
    353
    354    # Calculate the basis vectors for an orthonormal basis, where:
    355    # - u is parallel to a
    356    # - v is perpendicular to u and lies in the plane defined by a and b
    357    # - w is perpendicular to both u and v
    358    u = _nukemath.Vector3(a)
    359    u.normalize()
    360    w = a.cross(b)
    361    w.normalize()
    362    v = w.cross(u)
    363
    364    # If a normal was passed in, check to make sure that the one we're generating
    365    # is aligned close to the same way
    366    if norm != None:
    367      if w.dot(norm) < 0.0:
    368        w.x = -w.x
    369        w.y = -w.y
    370        w.z = -w.z
    371        # Don't mirror!
    372        v.x = -v.x
    373        v.y = -v.y
    374        v.z = -v.z
    375
    376    # Now find the rotation angles necessary to align a card to the uv plane.
    377    m = ( (u[0], v[0], w[0]),
    378          (u[1], v[1], w[1]),
    379          (u[2], v[2], w[2]) )
    380    if abs(m[1][2]) == 1.0:
    381      ry = 0.0
    382      rx = (math.pi / 2.0) * -m[1][2]
    383      rz = math.atan2(-m[0][1], m[0][0])
    384    else:
    385      cosx = math.sqrt(m[0][2] ** 2 + m[2][2] ** 2)
    386      if cosx == 0:
    387        cosx = 1.0
    388      rx = math.atan2(-m[1][2], cosx)
    389      rz = math.atan2(m[1][0] / cosx, m[1][1] / cosx)
    390      ry = math.atan2(m[0][2] / cosx, m[2][2] / cosx)
    391
    392    return _nukemath.Vector3(rx, ry, rz)
    393
    394  #
    395  # Helper functions
    396  #
    397 -def averageNormal(vertexSelection):
    398    '''
    399    averageNormal(selectionThreshold -> _nukemath.Vector3
    400    Return a _nukemath.Vector3 which is the average of the normals of all selected points
    401    '''
    402
    403    if not nuke.activeViewer():
    404      return None
    405
    406    # Put the indices in a dictionary for fast searching
    407    fastIndices = vertexSelection.indices()
    408
    409    found = False
    410    norm = _nukemath.Vector3(0.0, 0.0, 0.0)
    411
    412    for theNode in allNodesWithGeoSelectKnob():
    413      geoSelectKnob = theNode['geo_select']
    414      sel = geoSelectKnob.getSelection()
    415      objs = geoSelectKnob.getGeometry()
    416
    417      for o in xrange(len(sel)):
    418        objPrimitives = objs[o].primitives()
    419        objTransform = objs[o].transform()
    420
    421        # Use a dictionary for fast searching
    422        visitedPrimitives = {}
    423        for prim in objPrimitives:
    424          # This will be slow!
    425          if prim not in visitedPrimitives:
    426            for pt in prim.points():
    427              # This will be slow!
    428              if pt in fastIndices:
    429                found = True
    430                n = prim.normal()
    431                n = _nukemath.Vector3(n[0], n[1], n[2])
    432                n = objTransform.vtransform(n)
    433                norm += n
    434                visitedPrimitives[prim] = pt
    435                break
    436
    437    if found == False:
    438      return None
    439
    440    norm.normalize()
    441    return norm
    442
    443
    444 -def anySelectedPoint(selectionThreshold=0.5):
    445    '''
    446    anySelectedPoint(selectionThreshold) -> _nukemath.Vector3
    447    Return a selected point from the active viewer or the first viewer with a selection.
    448    The selectionThreshold parameter is used when working with a soft selection.
    449    Only points with a selection level >= the selection threshold will be returned by this function.
    450    '''
    451    if not nuke.activeViewer():
    452      return None
    453
    454    for n in allNodesWithGeoSelectKnob():
    455      geoSelectKnob = n['geo_select']
    456      sel = geoSelectKnob.getSelection()
    457      objs = geoSelectKnob.getGeometry()
    458      for o in xrange(len(sel)):
    459        objSelection = sel[o]
    460        for p in xrange(len(objSelection)):
    461          if objSelection[p] >= selectionThreshold:
    462            pos = objs[o].points()[p]
    463            tPos = objs[o].transform() * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
    464            return _nukemath.Vector3(tPos.x, tPos.y, tPos.z)
    465    return None
    466
    467
    468 -def selectedPoints(selectionThreshold=0.5):
    469    '''
    470    selectedPoints(selectionThreshold) -> iterator
    471
    472    Return an iterator which yields the position of every point currently
    473    selected in the Viewer in turn.
    474
    475    The selectionThreshold parameter is used when working with a soft selection.
    476    Only points with a selection level >= the selection threshold will be
    477    returned by this function.
    478    '''
    479    if not nuke.activeViewer():
    480      return
    481
    482    for info in selectedVertexInfos(selectionThreshold):
    483      yield info.position
    484
    485
    486 -def getSelection(selectionThreshold=0.5):
    487    # Build a VertexSelection from VertexInfos
    488    vertexSelection = VertexSelection()
    489    for info in selectedVertexInfos(selectionThreshold):
    490      vertexSelection.add(info)
    491    return vertexSelection
    492
    493
    494 -def selectedVertexInfos(selectionThreshold=0.5):
    495    '''
    496    selectedVertexInfos(selectionThreshold) -> iterator
    497
    498    Return an iterator which yields a tuple of the index and position of each
    499    point currently selected in the Viewer in turn.
    500
    501    The selectionThreshold parameter is used when working with a soft selection.
    502    Only points with a selection level >= the selection threshold will be
    503    returned by this function.
    504    '''
    505    if not nuke.activeViewer():
    506      return
    507
    508    for n in allNodesWithGeoSelectKnob():
    509      geoSelectKnob = n['geo_select']
    510      sel = geoSelectKnob.getSelection()
    511      objs = geoSelectKnob.getGeometry()
    512      for o in xrange(len(sel)):
    513        objSelection = sel[o]
    514        objPoints = objs[o].points()
    515        objTransform = objs[o].transform()
    516        for p in xrange(len(objSelection)):
    517          value = objSelection[p]
    518          if value >= selectionThreshold:
    519            pos = objPoints[p]
    520            tPos = objTransform * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
    521            yield VertexInfo(o, p, value, _nukemath.Vector3(tPos.x, tPos.y, tPos.z))
    522
    523
    524 -def anySelectedVertexInfo(selectionThreshold=0.5):
    525    '''
    526    anySelectedVertexInfo(selectionThreshold) -> VertexInfo
    527
    528    Returns a single VertexInfo for a selected point. If more than one point is
    529    selected, one of them will be chosen arbitrarily.
    530
    531    The selectionThreshold parameter is used when working with a soft selection.
    532    Only points with a selection level >= the selection threshold will be
    533    returned by this function.
    534    '''
    535    if not nuke.activeViewer():
    536      return None
    537
    538    for n in allNodesWithGeoSelectKnob():
    539      geoSelectKnob = n['geo_select']
    540      sel = geoSelectKnob.getSelection()
    541      objs = geoSelectKnob.getGeometry()
    542      for o in xrange(len(sel)):
    543        objSelection = sel[o]
    544        objPoints = objs[o].points()
    545        objTransform = objs[o].transform()
    546        for p in xrange(len(objSelection)):
    547          value = objSelection[p]
    548          if value >= selectionThreshold:
    549            pos = objPoints[p]
    550            tPos = objTransform * _nukemath.Vector4(pos.x, pos.y, pos.z, 1.0)
    551            return VertexInfo(o, p, value, _nukemath.Vector3(tPos.x, tPos.y, tPos.z))
    552    return None
    553
    554
    555 -def allNodes(node = nuke.root()):
    556    '''
    557    allNodes() -> iterator
    558    Return an iterator which yields all nodes in the current script.
    559
    560    This includes nodes inside groups. They will be returned in top-down,
    561    depth-first order.
    562    '''
    563    yield node
    564    if hasattr(node, "nodes"):
    565      for child in node.nodes():
    566        for n in allNodes(child):
    567          yield n
    568
    569
    570 -def allNodesWithGeoSelectKnob():
    571    if nuke.activeViewer():
    572      preferred_nodes = [n for n in nuke.activeViewer().getGeometryNodes() if 'geo_select' in n.knobs()]
    573    else:
    574      preferred_nodes = []
    575    nodes = preferred_nodes + [n for n in allNodes() if 'geo_select' in n.knobs() and n not in preferred_nodes]
    576    return nodes
    577
    578
    579 -def cameraProjectionMatrix(cameraNode):
    580    'Calculate the projection matrix for the camera based on its knob values.'
    581
    582    # Matrix to transform points into camera-relative coords.
    583    camTransform = cameraNode['transform'].value().inverse()
    584
    585    # Matrix to take the camera projection knobs into account
    586    roll = float(cameraNode['winroll'].getValue())
    587    scale_x, scale_y = [float(v) for v in cameraNode['win_scale'].getValue()]
    588    translate_x, translate_y = [float(v) for v in cameraNode['win_translate'].getValue()]
    589    m = _nukemath.Matrix4()
    590    m.makeIdentity()
    591    m.rotateZ(math.radians(roll))
    592    m.scale(1.0 / scale_x, 1.0 / scale_y, 1.0)
    593    m.translate(-translate_x, -translate_y, 0.0)
    594
    595    # Projection matrix based on the focal length, aperture and clipping planes of the camera
    596    focal_length = float(cameraNode['focal'].getValue())
    597    h_aperture = float(cameraNode['haperture'].getValue())
    598    near = float(cameraNode['near'].getValue())
    599    far = float(cameraNode['far'].getValue())
    600    projection_mode = int(cameraNode['projection_mode'].getValue())
    601    p = _nukemath.Matrix4()
    602    p.projection(focal_length / h_aperture, near, far, projection_mode == 0)
    603
    604    # Matrix to translate the projected points into normalised pixel coords
    605    format = nuke.root()['format'].value()
    606    imageAspect = float(format.height()) / float(format.width())
    607    t = _nukemath.Matrix4()
    608    t.makeIdentity()
    609    t.translate( 1.0, 1.0 - (1.0 - imageAspect / float(format.pixelAspect())), 0.0 )
    610
    611    # Matrix to scale normalised pixel coords into actual pixel coords.
    612    x_scale = float(format.width()) / 2.0
    613    y_scale = x_scale * format.pixelAspect()
    614    s = _nukemath.Matrix4()
    615    s.makeIdentity()
    616    s.scale(x_scale, y_scale, 1.0)
    617
    618    # The projection matrix transforms points into camera coords, modifies based
    619    # on the camera knob values, projects points into clip coords, translates the
    620    # clip coords so that they lie in the range 0,0 - 2,2 instead of -1,-1 - 1,1,
    621    # then scales the clip coords to proper pixel coords.
    622    return s * t * p * m * camTransform
    623
    624
    625 -def projectPoints(camera=None, points=None):
    626    '''
    627    projectPoint(camera, points) -> list of nuke.math.Vector2
    628
    629    Project the given 3D point through the camera to get 2D pixel coordinates.
    630
    631    @param camera: The Camera node or name of the Camera node to use for projecting
    632                   the point.
    633    @param points: A list or tuple of either nuke.math.Vector3 or of list/tuples of
    634                   three float values representing the 3D points.
    635    @raise ValueError: If camera or point is invalid.
    636    '''
    637
    638    camNode = None
    639    if isinstance(camera, nuke.Node):
    640      camNode = camera
    641    elif isinstance(camera, str):
    642      camNode = nuke.toNode(camera)
    643    else:
    644      raise ValueError, "Argument camera must be a node or the name of a node."
    645
    646    camMatrix = cameraProjectionMatrix(camNode)
    647    if camMatrix == None:
    648      raise RuntimeError, "snap3d.cameraProjectionMatrix() returned None for camera."
    649
    650    if not ( isinstance(points, list) or isinstance(points, tuple) ):
    651      raise ValueError, "Argument points must be a list or tuple."
    652
    653    for point in points:
    654      # Would be nice to not do this for every item but since lists/tuples can
    655      # containg anything...
    656      if isinstance(point, nuke.math.Vector3):
    657          pt = point
    658      elif isinstance(point, list) or isinstance(point, tuple):
    659        pt = nuke.math.Vector3(point[0], point[1], point[2])
    660      else:
    661        raise ValueError, "All items in points must be nuke.math.Vector3 or list/tuple of 3 floats."
    662
    663      tPos = camMatrix * nuke.math.Vector4(pt.x, pt.y, pt.z, 1.0)
    664      yield nuke.math.Vector2(tPos.x / tPos.w, tPos.y / tPos.w)
    665
    666
    667
    668 -def projectPoint(camera=None, point=None):
    669    '''
    670    projectPoint(camera, point) -> nuke.math.Vector2
    671
    672    Project the given 3D point through the camera to get 2D pixel coordinates.
    673
    674    @param camera: The Camera node or name of the Camera node to use for projecting
    675                   the point.
    676    @param point: A nuke.math.Vector3 or of list/tuple of three float values
    677                  representing the 3D point.
    678    @raise ValueError: If camera or point is invalid.
    679    '''
    680
    681    return projectPoints( camera, (point,) ).next()
    682
    683
    684
    685 -def projectSelectedPoints(cameraName='Camera1'):
    686    '''
    687    projectSelectedPoints(cameraName='Camera1') -> iterator yielding nuke.math.Vector2
    688
    689    Using the specified camera, project all of the selected points into 2D pixel
    690    coordinates and return their locations.
    691
    692    @param cameraName: Optional name of the Camera node to use for projecting the
    693                       points. If omitted, will look for a node called Camera1.
    694    '''
    695    camNode = nuke.toNode(cameraName)
    696    camMatrix = cameraProjectionMatrix(camNode)
    697    for pt in selectedPoints():
    698      tPos = camMatrix * nuke.math.Vector4(pt.x, pt.y, pt.z, 1.0)
    699      yield nuke.math.Vector2(tPos.x / tPos.w, tPos.y / tPos.w)
    700
    701
    702  #
    703  # Managing the list of snapping functions.
    704  #
    705
    706  # The list of snapping functions. Treat this as a read-only variable; if you
    707  # want to add a new snapping function call addSnapFunc (below)
    708  snapFuncs = []
    709
    710
    711 -def addSnapFunc(label, func):
    712    '''
    713    addSnapFunc(label, func) -> None
    714    Add a new snapping function to the list.
    715
    716    The label parameter is the text that will appear in (eg.) an Enumeration_Knob
    717    for the function. It cannot be the same as any existing snap function label
    718    (if it is, the function will abort without changing anything).
    719
    720    The func parameter is the snapping function. It must be a callable object
    721    taking a single parameter: the node to perform the snapping on.
    722    '''
    723    if label in dict(snapFuncs):
    724      return
    725    snapFuncs.append( (label, func) )
    726
    727
    728 -def callSnapFunc(nodeToSnap=None):
    729    '''
    730    callSnapFunc(nodeToSnap) -> None
    731    Call the snapping function on a node.
    732
    733    The nodeToSnap parameter is optional. If it's not specified, or is None, we
    734    use the result of nuke.thisNode() instead.
    735
    736    The node must have an Enumeration_Knob called "snapFunc" which selects the
    737    snapping function to call.
    738    '''
    739    if nodeToSnap is None:
    740      nodeToSnap = nuke.thisNode()
    741
    742    # Make sure that the nodeToSnap has a snapFunc knob
    743    if "snapFunc" not in nodeToSnap.knobs():
    744      # TODO: warn the user that we can't snap this node.
    745      return
    746
    747    snapFunc = dict(snapFuncs)[nodeToSnap['snapFunc'].value()]
    748    snapFunc(nodeToSnap)
    749