diff --git a/scripts/mgear/core/applyop.py b/scripts/mgear/core/applyop.py index 26b7848..81c1ef2 100644 --- a/scripts/mgear/core/applyop.py +++ b/scripts/mgear/core/applyop.py @@ -33,6 +33,41 @@ def curvecns_op(crv, inputs=[]): return node +def curve_skin_cns_op(crv, drivers=None): + """ + Arguments: + name (str): Name of the curve shape. + drivers (list of controls): List of driving transform. At less 2 control should be in + the list. + + Returns: + list: [nt.Joint, ], skinCluster + """ + import curve + import primitive + import transform + if drivers is None: + drivers = [] + + joints = [] + + for i, node in enumerate(drivers): + t = node.getTranslation(space="world") + param = curve.getCurveParamAtPosition(crv, t)[0] + p = crv.getPointAtParam(param) + m = transform.setMatrixPosition(pm.dt.Matrix(), p) + joints.append(primitive.addJoint(node, "{}_driver".format(node), m, vis=False)) + + skinName = crv.name().replace('|', '') + "_SkinCluster" + skinCluster = pm.skinCluster(joints, + crv, + tsb=True, + nw=1, + n=skinName) + + return joints, skinCluster + + def splineIK(name, chn, parent=None, cParent=None, curve=None): """Apply a splineIK solver to a chain. @@ -249,6 +284,39 @@ def aimCns(obj, ############################################# +def gear_raycast(in_mesh, + ray_source, + ray_direction, + out_obj=None, + connect_srt='t'): + """Create and connect mraycast node + + Args: + in_mesh (shape): Mesh shape + ray_source (transform): ray source + ray_direction (transform): ray direction + out_obj (transform, optional): Object to apply the raycast contact + connect_srt (str, optional): scale rotation traanslation flag + + Returns: + PyNode: The raycast node + """ + node = pm.createNode("mgear_rayCastPosition") + pm.connectAttr( + ray_source + ".worldMatrix[0]", node + ".raySource", force=True) + pm.connectAttr( + ray_direction + ".worldMatrix[0]", node + ".rayDirection", force=True) + pm.connectAttr( + in_mesh + ".outMesh", node + ".meshInput", force=True) + + if out_obj: + gear_matrix_cns(node.output, + out_obj=out_obj, + connect_srt=connect_srt) + + return node + + def gear_matrix_cns(in_obj, out_obj=None, connect_srt='srt', diff --git a/scripts/mgear/core/curve.py b/scripts/mgear/core/curve.py index e84e7f0..7706c22 100644 --- a/scripts/mgear/core/curve.py +++ b/scripts/mgear/core/curve.py @@ -5,18 +5,48 @@ ############################################# # GLOBAL ############################################# +from functools import wraps import pymel.core as pm from pymel.core import datatypes import json +import maya.mel as mel import maya.OpenMaya as om from mgear.core import applyop +from mgear.core import utils +from mgear.core import transform +reload(applyop) ############################################# # CURVE ############################################# +def addCnsCurve2(parent, name, points, drivers, degree): + # rebuild list to avoid input list modification + # points = points[:] + drivers = drivers[:] + if degree == 3: + if len(drivers) == 2: + # points.insert(0, points[0]) + # points.append(points[-1]) + drivers.insert(0, drivers[0]) + drivers.append(drivers[-1]) + elif len(drivers) == 3: + # points.append(points[-1]) + drivers.append(drivers[-1]) + + input_node = addCurve(parent, name, points, False, 1) + + if degree == 3: + node = pm.fitBspline(input_node, ch=0, tol=0.01)[0] + node.setParent(parent) + pm.delete(input_node) + node.rename(name) + pm.makeIdentity(node, t=1, r=0) + else: + node = input_node + return node def addCnsCurve(parent, name, centers, degree=1): """Create a curve attached to given centers. One point per center @@ -292,7 +322,7 @@ def get_color(node): return color - +@utils.one_undo def set_color(node, color): """Set the color in the Icons. @@ -339,18 +369,20 @@ def collect_curve_shapes(crv, rplStr=["", ""]): shapes_names.append(shape.name().replace(rplStr[0], rplStr[1])) c_form = shape.form() degree = shape.degree() + knots = list(shape.getKnots()) form = c_form.key form_id = c_form.index pnts = [[cv.x, cv.y, cv.z] for cv in shape.getCVs(space="object")] shapesDict[shape.name()] = {"points": pnts, "degree": degree, "form": form, - "form_id": form_id} + "form_id": form_id, + "knots": knots} return shapesDict, shapes_names -def collect_selected_curve_data(objs=None): +def collect_selected_curve_data(objs=None, rplStr=["", ""]): """Generate a dictionary descriving the curve data from selected objs Args: @@ -359,7 +391,7 @@ def collect_selected_curve_data(objs=None): if not objs: objs = pm.selected() - return collect_curve_data(objs) + return collect_curve_data(objs, rplStr=rplStr) def collect_curve_data(objs, rplStr=["", ""]): @@ -482,7 +514,6 @@ def create_curve_from_data_by_name(crv, parent, if several objects with the same name """ crv_dict = data[crv] - crv_transform = crv_dict["crv_transform"] shp_dict = crv_dict["shapes"] color = crv_dict["crv_color"] @@ -503,7 +534,10 @@ def create_curve_from_data_by_name(crv, points = shp_dict[sh]["points"] form = shp_dict[sh]["form"] degree = shp_dict[sh]["degree"] - knots = range(len(points) + degree - 1) + if "knots" in shp_dict[sh]: + knots = shp_dict[sh]["knots"] + else: + knots = range(len(points) + degree - 1) if form != "open": close = True else: @@ -627,7 +661,7 @@ def update_curve_from_data(data, rplStr=["", ""]): pm.rename(sh, sh.name().replace("ShapeShape", "Shape")) -def export_curve(filePath=None, objs=None): +def export_curve(filePath=None, objs=None, rplStr=["", ""]): """Export the curve data to a json file Args: @@ -651,7 +685,7 @@ def export_curve(filePath=None, objs=None): if not isinstance(filePath, basestring): filePath = filePath[0] - data = collect_selected_curve_data(objs) + data = collect_selected_curve_data(objs, rplStr=rplStr) data_string = json.dumps(data, indent=4, sort_keys=True) f = open(filePath, 'w') f.write(data_string) @@ -690,3 +724,219 @@ def import_curve(filePath=None, def update_curve_from_file(filePath=None, rplStr=["", ""]): # update a curve data from json file update_curve_from_data(_curve_from_file(filePath), rplStr) + + +# ----------------------------------------------------------------------------- +# Curve Decorators +# ----------------------------------------------------------------------------- + +def keep_lock_length_state(func): + @wraps(func) + def wrap(*args, **kwargs): + crvs = args[0] + state = {} + for crv in crvs: + if crv.getShape().hasAttr("lockLength"): + attr = crv.getShape().lockLength + state[crv.name()] = attr.get() + attr.set(False) + else: + state[crv.name()] = None + + try: + return func(*args, **kwargs) + + except Exception as e: + raise e + + finally: + for crv in crvs: + current_state = state[crv.name()] + if current_state: + crv.getShape().lockLength.set(current_state) + + return wrap + + +def keep_point_0_cnx_state(func): + @wraps(func) + def wrap(*args, **kwargs): + crvs = args[0] + cnx_dict = {} + for crv in crvs: + cnxs = crv.controlPoints[0].listConnections(p=True) + if cnxs: + cnx_dict[crv.name()] = cnxs[0] + pm.disconnectAttr(crv.controlPoints[0]) + else: + cnx_dict[crv.name()] = None + + try: + return func(*args, **kwargs) + + except Exception as e: + raise e + + finally: + for crv in crvs: + src_attr = cnx_dict[crv.name()] + if src_attr: + pm.connectAttr(src_attr, crv.controlPoints[0]) + + return wrap + +# ----------------------------------------------------------------------------- + +# add lock lenght attr + + +def lock_length(crv, lock=True): + crv_shape = crv.getShape() + if not crv_shape.hasAttr("lockLength"): + crv_shape.addAttr("lockLength", at=bool) + crv_shape.lockLength.set(lock) + return crv_shape.lockLength + + +# average curve shape +def average_curve(crv, + shapes, + average=2, + avg_shape=False, + avg_scl=False, + avg_rot=False): + """Average the shape, rotation and scale of the curve + bettwen n number of curves + + Args: + crv (dagNode): curve to average shape + shapes ([dagNode]]): imput curves to average the shapes + average (int, optional): Number of curves to use on the average + avg_shape (bool, optional): if True will interpolate curve shape + avg_scl (bool, optional): if True will interpolate curve scale + avg_rot (bool, optional): if True will interpolate curve rotation + + """ + if shapes and len(shapes) >= average: + shapes_by_distance = transform.get_closes_transform(crv, shapes) + bst = [] + bst_filtered = [] + bst_temp = [] + weights = [] + blends = [] + # calculate the average value based on distance + total_val = 0.0 + for x in range(average): + total_val += shapes_by_distance[x][1][1] + # setup the blendshape + for x in range(average): + blend = 1 - (shapes_by_distance[x][1][1] / total_val) + bst.append(shapes_by_distance[x][1][0]) + weights.append((x, blend)) + blends.append(blend) + + if avg_rot: + transform.interpolate_rotation(crv, bst, blends) + if avg_scl: + transform.interpolate_scale(crv, bst, blends) + if avg_shape: + # check the number of of points and rebuild to match number in + # order of make the blendshape + crv_len = len(crv.getCVs()) + for c in bst: + if len(c.getCVs()) == crv_len: + bst_filtered.append(c) + else: + t_c = pm.duplicate(c)[0] + bst_temp.append(t_c) + if bst_temp: + rebuild_curve(bst_temp, crv_len - 2) + bst_filtered = bst_filtered + bst_temp + # the blendshape is done with curves of the same number + pm.blendShape(bst_filtered, + crv, + name="_".join([crv.name(), "blendShape"]), + foc=True, + w=weights) + pm.delete(crv, ch=True) + pm.delete(bst_temp) + + # need to lock the first point after delete history + lock_first_point(crv) + else: + pm.displayWarning("Can average the curve with more" + " curves than exist") + + +# rebuild curve +@utils.one_undo +@utils.filter_nurbs_curve_selection +def rebuild_curve(crvs, spans): + for crv in crvs: + name = crv.name() + pm.rebuildCurve(crv, + ch=False, + rpo=True, + rt=0, + end=1, + kr=0, + kcp=0, + kep=1, + kt=0, + s=spans, + d=2, + tol=0.01, + name=name) + + +# smooth curve. +# Lockt lenght needs to be off for smooth correctly +@utils.one_undo +@keep_lock_length_state +@keep_point_0_cnx_state +def smooth_curve(crvs, smooth_factor=1): + + mel.eval("modifySelectedCurves smooth {} 0;".format(str(smooth_factor))) + +# straight curve. +# Need to unlock/diconect first point to work. +# also no length lock + + +@utils.one_undo +@keep_lock_length_state +@keep_point_0_cnx_state +def straighten_curve(crvs, straighteness=.1, keep_lenght=1): + + mel.eval( + "modifySelectedCurves straighten {0} {1};".format(str(straighteness)), + str(keep_lenght)) + +# Curl curve. +# Need to unlock/diconect first point to work. +# also no length lock + + +def curl_curve(crvs, amount=.3, frequency=10): + + mel.eval( + "modifySelectedCurves curl {0} {1};".format(str(amount)), + str(frequency)) + + +# ======================================== + + +def set_thickness(crv, thickness=-1): + crv.getShape().lineWidth.set(thickness) + + +def lock_first_point(crv): + # lock first point in the curve + mul_mtrx = pm.createNode("multMatrix") + dm_node = pm.createNode("decomposeMatrix") + pm.connectAttr(crv.worldMatrix[0], mul_mtrx.matrixIn[0]) + pm.connectAttr(crv.worldInverseMatrix[0], mul_mtrx.matrixIn[1]) + pm.connectAttr(mul_mtrx.matrixSum, dm_node.inputMatrix) + pm.connectAttr(dm_node.outputTranslate, + crv.getShape().controlPoints[0]) diff --git a/scripts/mgear/core/primitive.py b/scripts/mgear/core/primitive.py index c07d449..1a0f837 100644 --- a/scripts/mgear/core/primitive.py +++ b/scripts/mgear/core/primitive.py @@ -226,6 +226,49 @@ def add2DChain2(parent, name, positions, normal, negate=False, vis=True): return chain +def add2DChainNonPlanar(parent, name, positions, normal, negate=False, vis=True): + + """Create a 2D joint chain. Like Softimage 2D chain. + + Arguments: + parent (dagNode): The parent for the chain. + name (str): The node name. + positions(list of vectors): the positons to define the chain. + normal (vector): The normal vector to define the direction of + the chain. + negate (bool): If True will negate the direction of the chain + + Returns; + list of dagNodes: The list containg all the joints of the chain + + >>> self.rollRef = pri.add2DChain( + self.root, + self.getName("rollChain"), + self.guide.apos[:2], + self.normal, + self.negate) + + """ + if "%s" not in name: + name += "%s" + + transforms = transform.getChainTransform(positions, normal, negate) + t = transform.setMatrixPosition(transforms[-1], positions[-1]) + transforms.append(t) + + chain = [] + for i, t in enumerate(transforms): + node = addJoint(parent, name % i, t, vis) + chain.append(node) + parent = node + + # moving rotation value to joint orient + for i, jnt in enumerate(chain): + if i == 0: + pm.makeIdentity(jnt, apply=1, t=0, r=1, s=0, n=0, pn=1) + jnt.setAttr("radius", 1.5) + + return chain def add2DChain(parent, name, positions, normal, negate=False, vis=True): """Create a 2D joint chain. Like Softimage 2D chain. diff --git a/scripts/mgear/core/widgets.py b/scripts/mgear/core/widgets.py index 0c20c07..d33cd9a 100644 --- a/scripts/mgear/core/widgets.py +++ b/scripts/mgear/core/widgets.py @@ -1,8 +1,8 @@ """mGear Qt custom widgets""" -from mgear.vendor.Qt import QtCore, QtWidgets, QtGui +from mgear.vendor.Qt import QtCore, QtWidgets, QtGui, QtCompat import maya.OpenMaya as api - +import os, sys ################################################# # CUSTOM WIDGETS @@ -242,3 +242,58 @@ def setAction(self, action): def doAction(self, sel): self.theAction(sel) + + +class UIFileDialog(QtWidgets.QDialog): + """ + Automatically loads the ui file from the directory the executing python module is in. Defaults to settingsUI.ui + but can be set with set_ui_file("ui_file_name.ui") + + + Example Usage : + + class settingsTab(widgets.UIFileDialog): + def __init__(self, parent=None): + super(settingsTab, self).__init__(parent) + self.set_ui_file() + + """ + + def __getattribute__(self, attr): + """ + Always try to get properties from the internal ui file after the parent object + Now this basically gets over the error (Internal c++ Object Already Deleted) when you don't save the + objects on the widget stack by adding them to a layout or setting their parent. + + so : QtCompat.loadUi(path + "/{}".format(ui_file), self) works but causes the error Internal c++ Object Already Deleted + but : self.ui = QtCompat.loadUi(path + "/{}".format(ui_file)) is stable but incompatible with the way + widgets are being used from mgear as they are accessed directly on the parent object + """ + # return any functions straight away + try: + return object.__getattribute__(self, attr) + except Exception: + try: + ui = object.__getattribute__(self, "ui") + if ui: + try: + return object.__getattribute__(ui, attr) + except Exception as e: + return object.__getattribute__(self, attr) + else: + return object.__getattribute__(self, attr) + except Exception as e: + return object.__getattribute__(self, attr) + + def set_ui_file(self, ui_file=None): + if ui_file is None: + ui_file = "settingsUI.ui" + path = os.path.dirname(sys.modules[self.__module__].__file__) + self.ui = QtCompat.loadUi(path + "/{}".format(ui_file)) + self.layout().addWidget(self.ui) + + def __init__(self, parent=None): + super(UIFileDialog, self).__init__(parent) + self.ui = None + self.setLayout(QtWidgets.QVBoxLayout()) + self.layout().setContentsMargins(0, 0, 0, 0)