540 lines
15 KiB
Python
540 lines
15 KiB
Python
|
#
|
||
|
# duplicaeOnSurface.py
|
||
|
#
|
||
|
#
|
||
|
# ----------------------------------------------------------------------------
|
||
|
# "THE BEER-WARE LICENSE" (Revision 42):
|
||
|
# <michitaka_inoue@icloud.com> wrote this file. As long as you retain this
|
||
|
# notice you can do whatever you want with this stuff. If we meet some day,
|
||
|
# and you think this stuff is worth it, you can buy me a beer in return.
|
||
|
# -Michitaka Inoue
|
||
|
# ----------------------------------------------------------------------------
|
||
|
#
|
||
|
|
||
|
from maya import OpenMaya
|
||
|
from maya import OpenMayaUI
|
||
|
from maya import OpenMayaMPx
|
||
|
from maya import cmds
|
||
|
try:
|
||
|
from PySide.QtGui import QApplication
|
||
|
from PySide import QtCore
|
||
|
except ImportError:
|
||
|
from PySide2.QtWidgets import QApplication
|
||
|
from PySide2 import QtCore
|
||
|
import math
|
||
|
import sys
|
||
|
|
||
|
|
||
|
DRAGGER = "duplicateOverSurfaceDragger"
|
||
|
UTIL = OpenMaya.MScriptUtil()
|
||
|
|
||
|
|
||
|
kPluginCmdName = "duplicateOverSurface"
|
||
|
kRotationFlag = "-r"
|
||
|
kRotationFlagLong = "-rotation"
|
||
|
kDummyFlag = "-d"
|
||
|
kDummyFlagLong = "-dummy"
|
||
|
kInstanceFlag = "-ilf"
|
||
|
kInstanceFlagLong = "-instanceLeaf"
|
||
|
|
||
|
|
||
|
# Syntax creator
|
||
|
def syntaxCreator():
|
||
|
syntax = OpenMaya.MSyntax()
|
||
|
syntax.addArg(OpenMaya.MSyntax.kString)
|
||
|
syntax.addFlag(
|
||
|
kDummyFlag,
|
||
|
kDummyFlagLong,
|
||
|
OpenMaya.MSyntax.kBoolean)
|
||
|
syntax.addFlag(
|
||
|
kRotationFlag,
|
||
|
kRotationFlagLong,
|
||
|
OpenMaya.MSyntax.kBoolean)
|
||
|
syntax.addFlag(
|
||
|
kInstanceFlag,
|
||
|
kInstanceFlagLong,
|
||
|
OpenMaya.MSyntax.kBoolean)
|
||
|
return syntax
|
||
|
|
||
|
|
||
|
class DuplicateOverSurface(OpenMayaMPx.MPxCommand):
|
||
|
|
||
|
def __init__(self):
|
||
|
super(DuplicateOverSurface, self).__init__()
|
||
|
|
||
|
self.ANCHOR_POINT = None
|
||
|
self.DUPLICATED = None
|
||
|
self.SOURCE = None
|
||
|
self.SCALE_ORIG = None
|
||
|
self.MATRIX_ORIG = None
|
||
|
self.TARGET_FNMESH = None
|
||
|
self.MOD_FIRST = None
|
||
|
self.MOD_POINT = None
|
||
|
self.SPACE = OpenMaya.MSpace.kWorld
|
||
|
|
||
|
self.ROTATION = True
|
||
|
self.InstanceFlag = False
|
||
|
|
||
|
self.SHIFT = QtCore.Qt.ShiftModifier
|
||
|
self.CTRL = QtCore.Qt.ControlModifier
|
||
|
|
||
|
def doIt(self, args):
|
||
|
|
||
|
# Parse the arguments.
|
||
|
argData = OpenMaya.MArgDatabase(syntaxCreator(), args)
|
||
|
self.SOURCE = argData.commandArgumentString(0)
|
||
|
if argData.isFlagSet(kRotationFlag) is True:
|
||
|
self.ROTATION = argData.flagArgumentBool(kRotationFlag, 0)
|
||
|
|
||
|
if argData.isFlagSet(kInstanceFlag) is True:
|
||
|
self.InstanceFlag = argData.flagArgumentBool(kInstanceFlag, 0)
|
||
|
|
||
|
cmds.setToolTo(self.setupDragger())
|
||
|
|
||
|
def setupDragger(self):
|
||
|
""" Setup dragger context command """
|
||
|
|
||
|
try:
|
||
|
cmds.deleteUI(DRAGGER)
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
dragger = cmds.draggerContext(
|
||
|
DRAGGER,
|
||
|
pressCommand=self.pressEvent,
|
||
|
dragCommand=self.dragEvent,
|
||
|
releaseCommand=self.releaseEvent,
|
||
|
space='screen',
|
||
|
projection='viewPlane',
|
||
|
undoMode='step',
|
||
|
cursor='hand')
|
||
|
|
||
|
return dragger
|
||
|
|
||
|
def pressEvent(self):
|
||
|
button = cmds.draggerContext(DRAGGER, query=True, button=True)
|
||
|
|
||
|
# Leave the tool by middle click
|
||
|
if button == 2:
|
||
|
cmds.setToolTo('selectSuperContext')
|
||
|
return
|
||
|
|
||
|
# Get clicked point in viewport screen space
|
||
|
pressPosition = cmds.draggerContext(DRAGGER, query=True, ap=True)
|
||
|
x = pressPosition[0]
|
||
|
y = pressPosition[1]
|
||
|
|
||
|
self.ANCHOR_POINT = [x, y]
|
||
|
|
||
|
# Convert
|
||
|
point_in_3d, vector_in_3d = convertTo3D(x, y)
|
||
|
|
||
|
# Get MFnMesh of snap target
|
||
|
targetDagPath = getDagPathFromScreen(x, y)
|
||
|
|
||
|
# If draggin outside of objects
|
||
|
if targetDagPath is None:
|
||
|
return
|
||
|
|
||
|
# Get origianl scale information
|
||
|
self.SCALE_ORIG = cmds.getAttr(self.SOURCE + ".scale")[0]
|
||
|
self.MATRIX_ORIG = cmds.xform(self.SOURCE, q=True, matrix=True)
|
||
|
self.TARGET_FNMESH = OpenMaya.MFnMesh(targetDagPath)
|
||
|
|
||
|
transformMatrix = self.getMatrix(
|
||
|
point_in_3d,
|
||
|
vector_in_3d,
|
||
|
self.TARGET_FNMESH,
|
||
|
self.SCALE_ORIG,
|
||
|
self.MATRIX_ORIG)
|
||
|
|
||
|
if transformMatrix is None:
|
||
|
return
|
||
|
|
||
|
# Create new object to snap
|
||
|
self.DUPLICATED = self.getNewObject()
|
||
|
|
||
|
# Reset transform of current object
|
||
|
cmds.setAttr(self.DUPLICATED + ".translate", *[0, 0, 0])
|
||
|
|
||
|
location = [-i for i
|
||
|
in cmds.xform(self.DUPLICATED, q=True, ws=True, rp=True)]
|
||
|
cmds.setAttr(self.DUPLICATED + ".translate", *location)
|
||
|
|
||
|
# Can't apply freeze to instances
|
||
|
if self.InstanceFlag is not True:
|
||
|
cmds.makeIdentity(self.DUPLICATED, apply=True, t=True)
|
||
|
|
||
|
# Apply transformMatrix to the new object
|
||
|
cmds.xform(self.DUPLICATED, matrix=transformMatrix)
|
||
|
|
||
|
def getNewObject(self):
|
||
|
return cmds.duplicate(self.SOURCE, ilf=self.InstanceFlag)[0]
|
||
|
|
||
|
def dragEvent(self):
|
||
|
""" Event while dragging a 3d view """
|
||
|
|
||
|
if self.TARGET_FNMESH is None:
|
||
|
return
|
||
|
|
||
|
dragPosition = cmds.draggerContext(
|
||
|
DRAGGER,
|
||
|
query=True,
|
||
|
dragPoint=True)
|
||
|
|
||
|
x = dragPosition[0]
|
||
|
y = dragPosition[1]
|
||
|
|
||
|
modifier = cmds.draggerContext(
|
||
|
DRAGGER,
|
||
|
query=True,
|
||
|
modifier=True)
|
||
|
|
||
|
if modifier == "none":
|
||
|
self.MOD_FIRST = True
|
||
|
|
||
|
qtModifier = QApplication.keyboardModifiers()
|
||
|
|
||
|
if qtModifier == self.CTRL or qtModifier == self.SHIFT:
|
||
|
|
||
|
# If this is the first click of dragging
|
||
|
if self.MOD_FIRST is True:
|
||
|
self.MOD_POINT = [x, y]
|
||
|
|
||
|
# global MOD_FIRST
|
||
|
self.MOD_FIRST = False
|
||
|
|
||
|
length, degree = self.getDragInfo(x, y)
|
||
|
|
||
|
if qtModifier == self.CTRL:
|
||
|
length = 1.0
|
||
|
if qtModifier == self.SHIFT:
|
||
|
degree = 0.0
|
||
|
|
||
|
# Convert
|
||
|
point_in_3d, vector_in_3d = convertTo3D(
|
||
|
self.MOD_POINT[0],
|
||
|
self.MOD_POINT[1])
|
||
|
else:
|
||
|
point_in_3d, vector_in_3d = convertTo3D(x, y)
|
||
|
length = 1.0
|
||
|
degree = 0.0
|
||
|
|
||
|
# Get new transform matrix for new object
|
||
|
transformMatrix = self.getMatrix(
|
||
|
point_in_3d,
|
||
|
vector_in_3d,
|
||
|
self.TARGET_FNMESH,
|
||
|
self.SCALE_ORIG,
|
||
|
self.MATRIX_ORIG,
|
||
|
length,
|
||
|
degree
|
||
|
)
|
||
|
|
||
|
if transformMatrix is None:
|
||
|
return
|
||
|
|
||
|
# Apply new transform
|
||
|
cmds.xform(self.DUPLICATED, matrix=transformMatrix)
|
||
|
cmds.setAttr(self.DUPLICATED + ".shear", *[0, 0, 0])
|
||
|
|
||
|
cmds.refresh(currentView=True, force=True)
|
||
|
|
||
|
def releaseEvent(self):
|
||
|
self.MOD_FIRST = True
|
||
|
|
||
|
def getDragInfo(self, x, y):
|
||
|
""" Get distance and angle in screen space. """
|
||
|
|
||
|
start_x = self.MOD_POINT[0]
|
||
|
start_y = self.MOD_POINT[1]
|
||
|
end_x = x
|
||
|
end_y = y
|
||
|
|
||
|
cathetus = end_x - start_x
|
||
|
opposite = end_y - start_y
|
||
|
|
||
|
# Get distance using Pythagorean theorem
|
||
|
length = math.sqrt(
|
||
|
math.pow(cathetus, 2) + math.pow(opposite, 2))
|
||
|
|
||
|
try:
|
||
|
theta = cathetus / length
|
||
|
degree = math.degrees(math.acos(theta))
|
||
|
if opposite < 0:
|
||
|
degree = -degree
|
||
|
return cathetus, degree
|
||
|
except ZeroDivisionError:
|
||
|
return None, None
|
||
|
|
||
|
def getIntersection(self, point_in_3d, vector_in_3d, fnMesh):
|
||
|
""" Return a point Position of intersection..
|
||
|
Args:
|
||
|
point_in_3d (OpenMaya.MPoint)
|
||
|
vector_in_3d (OpenMaya.mVector)
|
||
|
Returns:
|
||
|
OpenMaya.MFloatPoint : hitPoint
|
||
|
"""
|
||
|
|
||
|
hitPoint = OpenMaya.MFloatPoint()
|
||
|
hitFacePtr = UTIL.asIntPtr()
|
||
|
idSorted = False
|
||
|
testBothDirections = False
|
||
|
faceIDs = None
|
||
|
triIDs = None
|
||
|
accelParam = None
|
||
|
hitRayParam = None
|
||
|
hitTriangle = None
|
||
|
hitBary1 = None
|
||
|
hitBary2 = None
|
||
|
maxParamPtr = 99999
|
||
|
|
||
|
# intersectPoint = OpenMaya.MFloatPoint(
|
||
|
result = fnMesh.closestIntersection(
|
||
|
OpenMaya.MFloatPoint(
|
||
|
point_in_3d.x,
|
||
|
point_in_3d.y,
|
||
|
point_in_3d.z),
|
||
|
OpenMaya.MFloatVector(vector_in_3d),
|
||
|
faceIDs,
|
||
|
triIDs,
|
||
|
idSorted,
|
||
|
self.SPACE,
|
||
|
maxParamPtr,
|
||
|
testBothDirections,
|
||
|
accelParam,
|
||
|
hitPoint,
|
||
|
hitRayParam,
|
||
|
hitFacePtr,
|
||
|
hitTriangle,
|
||
|
hitBary1,
|
||
|
hitBary2)
|
||
|
|
||
|
faceID = UTIL.getInt(hitFacePtr)
|
||
|
|
||
|
if result is True:
|
||
|
return hitPoint, faceID
|
||
|
else:
|
||
|
return None, None
|
||
|
|
||
|
def getMatrix(self,
|
||
|
mPoint,
|
||
|
mVector,
|
||
|
targetFnMesh,
|
||
|
scale_orig,
|
||
|
matrix_orig,
|
||
|
scale_plus=1,
|
||
|
degree_plus=0.0):
|
||
|
|
||
|
""" Return a list of values which consist a new transform matrix.
|
||
|
Args:
|
||
|
mPoint (OpenMaya.MPoint)
|
||
|
mVector (OpenMaya.MVector)
|
||
|
Returns:
|
||
|
list : 16 values for matrixs
|
||
|
"""
|
||
|
# Position of new object
|
||
|
OP, faceID = self.getIntersection(mPoint, mVector, targetFnMesh)
|
||
|
|
||
|
# If it doesn't intersect to any geometries, return None
|
||
|
if OP is None and faceID is None:
|
||
|
return None
|
||
|
|
||
|
qtMod = QApplication.keyboardModifiers()
|
||
|
if qtMod == (self.CTRL | self.SHIFT):
|
||
|
OP = getClosestVertex(OP, faceID, targetFnMesh)
|
||
|
|
||
|
# Get normal vector and tangent vector
|
||
|
if self.ROTATION is False:
|
||
|
NV = OpenMaya.MVector(
|
||
|
matrix_orig[4],
|
||
|
matrix_orig[5],
|
||
|
matrix_orig[6])
|
||
|
NV.normalize()
|
||
|
TV = OpenMaya.MVector(
|
||
|
matrix_orig[0],
|
||
|
matrix_orig[1],
|
||
|
matrix_orig[2])
|
||
|
TV.normalize()
|
||
|
else:
|
||
|
NV = self.getNormal(OP, targetFnMesh)
|
||
|
TV = self.getTangent(faceID, targetFnMesh)
|
||
|
|
||
|
# Ctrl-hold rotation
|
||
|
if qtMod == self.CTRL:
|
||
|
try:
|
||
|
rad = math.radians(degree_plus)
|
||
|
q1 = NV.x * math.sin(rad / 2)
|
||
|
q2 = NV.y * math.sin(rad / 2)
|
||
|
q3 = NV.z * math.sin(rad / 2)
|
||
|
q4 = math.cos(rad / 2)
|
||
|
TV = TV.rotateBy(q1, q2, q3, q4)
|
||
|
except TypeError:
|
||
|
pass
|
||
|
|
||
|
# Bitangent vector
|
||
|
BV = TV ^ NV
|
||
|
BV.normalize()
|
||
|
|
||
|
# 4x4 Transform Matrix
|
||
|
try:
|
||
|
x = scale_orig[0] * (scale_plus / 100 + 1.0)
|
||
|
y = scale_orig[1] * (scale_plus / 100 + 1.0)
|
||
|
z = scale_orig[2] * (scale_plus / 100 + 1.0)
|
||
|
TV *= x
|
||
|
NV *= y
|
||
|
BV *= z
|
||
|
except TypeError:
|
||
|
pass
|
||
|
finally:
|
||
|
matrix = [
|
||
|
TV.x, TV.y, TV.z, 0,
|
||
|
NV.x, NV.y, NV.z, 0,
|
||
|
BV.x, BV.y, BV.z, 0,
|
||
|
OP.x, OP.y, OP.z, 1
|
||
|
]
|
||
|
|
||
|
return matrix
|
||
|
|
||
|
def getTangent(self, faceID, targetFnMesh):
|
||
|
""" Return a tangent vector of a face.
|
||
|
Args:
|
||
|
faceID (int)
|
||
|
mVector (OpenMaya.MVector)
|
||
|
Returns:
|
||
|
OpenMaya.MVector : tangent vector
|
||
|
"""
|
||
|
|
||
|
tangentArray = OpenMaya.MFloatVectorArray()
|
||
|
targetFnMesh.getFaceVertexTangents(
|
||
|
faceID,
|
||
|
tangentArray,
|
||
|
self.SPACE)
|
||
|
numOfVtx = tangentArray.length()
|
||
|
x = sum([tangentArray[i].x for i in range(numOfVtx)]) / numOfVtx
|
||
|
y = sum([tangentArray[i].y for i in range(numOfVtx)]) / numOfVtx
|
||
|
z = sum([tangentArray[i].z for i in range(numOfVtx)]) / numOfVtx
|
||
|
tangentVector = OpenMaya.MVector()
|
||
|
tangentVector.x = x
|
||
|
tangentVector.y = y
|
||
|
tangentVector.z = z
|
||
|
tangentVector.normalize()
|
||
|
|
||
|
return tangentVector
|
||
|
|
||
|
def getNormal(self, pointPosition, targetFnMesh):
|
||
|
""" Return a normal vector of a face.
|
||
|
Args:
|
||
|
pointPosition (OpenMaya.MFloatPoint)
|
||
|
targetFnMesh (OpenMaya.MFnMesh)
|
||
|
Returns:
|
||
|
OpenMaya.MVector : tangent vector
|
||
|
int : faceID
|
||
|
"""
|
||
|
|
||
|
ptr_int = UTIL.asIntPtr()
|
||
|
origin = OpenMaya.MPoint(pointPosition)
|
||
|
normal = OpenMaya.MVector()
|
||
|
targetFnMesh.getClosestNormal(
|
||
|
origin,
|
||
|
normal,
|
||
|
self.SPACE,
|
||
|
ptr_int)
|
||
|
normal.normalize()
|
||
|
|
||
|
return normal
|
||
|
|
||
|
|
||
|
# Creator
|
||
|
def cmdCreator():
|
||
|
return OpenMayaMPx.asMPxPtr(DuplicateOverSurface())
|
||
|
|
||
|
|
||
|
def initializePlugin(mObject):
|
||
|
mPlugin = OpenMayaMPx.MFnPlugin(mObject, "Michitaka Inoue")
|
||
|
try:
|
||
|
mPlugin.registerCommand(kPluginCmdName, cmdCreator)
|
||
|
mPlugin.setVersion("0.10")
|
||
|
except:
|
||
|
sys.stderr.write("Failed to register command: %s\n" % kPluginCmdName)
|
||
|
raise
|
||
|
|
||
|
|
||
|
def uninitializePlugin(mObject):
|
||
|
mPlugin = OpenMayaMPx.MFnPlugin(mObject)
|
||
|
try:
|
||
|
mPlugin.deregisterCommand(kPluginCmdName)
|
||
|
except:
|
||
|
sys.stderr.write("Failed to unregister command: %s\n" % kPluginCmdName)
|
||
|
|
||
|
|
||
|
def convertTo3D(screen_x, screen_y):
|
||
|
""" Return point and vector of clicked point in 3d space.
|
||
|
Args:
|
||
|
screen_x (int)
|
||
|
screen_y (int)
|
||
|
Returns:
|
||
|
OpenMaya.MPoint : point_in_3d
|
||
|
OpenMaya.MVector : vector_in_3d
|
||
|
"""
|
||
|
point_in_3d = OpenMaya.MPoint()
|
||
|
vector_in_3d = OpenMaya.MVector()
|
||
|
|
||
|
OpenMayaUI.M3dView.active3dView().viewToWorld(
|
||
|
int(screen_x),
|
||
|
int(screen_y),
|
||
|
point_in_3d,
|
||
|
vector_in_3d)
|
||
|
|
||
|
return point_in_3d, vector_in_3d
|
||
|
|
||
|
|
||
|
def getDagPathFromScreen(x, y):
|
||
|
""" Args:
|
||
|
x (int or float)
|
||
|
y (int or float)
|
||
|
Returns:
|
||
|
dagpath : OpenMaya.MDagPath
|
||
|
"""
|
||
|
# Select from screen
|
||
|
OpenMaya.MGlobal.selectFromScreen(
|
||
|
int(x),
|
||
|
int(y),
|
||
|
OpenMaya.MGlobal.kReplaceList,
|
||
|
OpenMaya.MGlobal.kSurfaceSelectMethod)
|
||
|
|
||
|
# Get dagpath, or return None if fails
|
||
|
tempSel = OpenMaya.MSelectionList()
|
||
|
OpenMaya.MGlobal.getActiveSelectionList(tempSel)
|
||
|
dagpath = OpenMaya.MDagPath()
|
||
|
if tempSel.length() == 0:
|
||
|
return None
|
||
|
else:
|
||
|
tempSel.getDagPath(0, dagpath)
|
||
|
return dagpath
|
||
|
|
||
|
|
||
|
def getClosestVertex(point_orig, faceID, fnMesh):
|
||
|
""" Args:
|
||
|
point_orig (OpenMaya.MFloatPoint)
|
||
|
faceID (int)
|
||
|
fnMesh (OpenMaya.MFnMesh)
|
||
|
Returns:
|
||
|
closestPoint : OpenMaya.MPoint
|
||
|
"""
|
||
|
|
||
|
vertexIndexArray = OpenMaya.MIntArray()
|
||
|
fnMesh.getPolygonVertices(faceID, vertexIndexArray)
|
||
|
basePoint = OpenMaya.MPoint(point_orig)
|
||
|
closestPoint = OpenMaya.MPoint()
|
||
|
length = 99999.0
|
||
|
for index in vertexIndexArray:
|
||
|
point = OpenMaya.MPoint()
|
||
|
fnMesh.getPoint(index, point, OpenMaya.MSpace.kWorld)
|
||
|
lengthVector = point - basePoint
|
||
|
if lengthVector.length() < length:
|
||
|
length = lengthVector.length()
|
||
|
closestPoint = point
|
||
|
|
||
|
return closestPoint
|