From e4fd7e9a1a7e112da8a1e88f9119fbe05a410a83 Mon Sep 17 00:00:00 2001 From: Jeffreytsai1004 Date: Tue, 14 Jan 2025 02:59:09 +0800 Subject: [PATCH] Update --- .../Edit/CreasePlus/CreasePlusBase.py | 479 ++ .../Edit/CreasePlus/CreasePlusCore.py | 2125 ++++++ .../Edit/CreasePlus/CreasePlusEULA.pdf | Bin 0 -> 61704 bytes .../Edit/CreasePlus/CreasePlusExcept.py | 6 + .../Edit/CreasePlus/CreasePlusMain.py | 1057 +++ .../Edit/CreasePlus/CreasePlusNodes.py | 507 ++ .../Edit/CreasePlus/CreasePlus_def_sheet.pdf | Bin 0 -> 13400 bytes .../Edit/CreasePlus/Icons/crep_bevel_ico.png | Bin 0 -> 2607 bytes .../CreasePlus/Icons/crep_bevellive_ico.png | Bin 0 -> 1696 bytes .../Edit/CreasePlus/Icons/crep_bool_ico.png | Bin 0 -> 2730 bytes .../Edit/CreasePlus/Icons/crep_close_ico.png | Bin 0 -> 1188 bytes .../Edit/CreasePlus/Icons/crep_crease_ico.png | Bin 0 -> 2425 bytes .../CreasePlus/Icons/crep_curveattach_ico.png | Bin 0 -> 2036 bytes .../CreasePlus/Icons/crep_curvebevel_ico.png | Bin 0 -> 1238 bytes .../CreasePlus/Icons/crep_curveclose_ico.png | Bin 0 -> 2508 bytes .../CreasePlus/Icons/crep_curvedraw_ico.png | Bin 0 -> 2056 bytes .../CreasePlus/Icons/crep_curveint_ico.png | Bin 0 -> 2039 bytes .../CreasePlus/Icons/crep_curvemult_ico.png | Bin 0 -> 2301 bytes .../CreasePlus/Icons/crep_curvepoly_ico.png | Bin 0 -> 2471 bytes .../CreasePlus/Icons/crep_displayhe_ico.png | Bin 0 -> 1947 bytes .../Edit/CreasePlus/Icons/crep_eye_ico.png | Bin 0 -> 1783 bytes .../CreasePlus/Icons/crep_hardedge_ico.png | Bin 0 -> 1951 bytes .../Edit/CreasePlus/Icons/crep_menu_ico.png | Bin 0 -> 676 bytes .../CreasePlus/Icons/crep_meshslicer_ico.png | Bin 0 -> 2717 bytes .../Edit/CreasePlus/Icons/crep_mirror_ico.png | Bin 0 -> 2640 bytes .../Edit/CreasePlus/Icons/crep_next_ico.png | Bin 0 -> 1334 bytes .../CreasePlus/Icons/crep_panelbool_ico.png | Bin 0 -> 2057 bytes .../CreasePlus/Icons/crep_physcrease_ico.png | Bin 0 -> 2069 bytes .../Icons/crep_shapeshifter_ico.png | Bin 0 -> 2289 bytes .../CreasePlus/Icons/crep_smoothangle_ico.png | Bin 0 -> 2178 bytes .../Edit/CreasePlus/Icons/crep_subd_ico.png | Bin 0 -> 502 bytes .../Edit/CreasePlus/Icons/crep_uv_ico.png | Bin 0 -> 769 bytes .../CreasePlus/Icons/crep_weighttool_ico.png | Bin 0 -> 1858 bytes .../Edit/CreasePlus/Icons/crep_zbrush_ico.png | Bin 0 -> 3005 bytes .../Modeling/Edit/CreasePlus/MayaUndoRun.py | 17 + Scripts/Modeling/Edit/CreasePlus/__init__.py | 5 + Scripts/Modeling/Edit/CreasePlus/delpyc.bat | 2 + Scripts/Modeling/Edit/CreasePlus/readit.txt | 40 + .../Modeling/Edit/gs_curvetools/LICENSE.txt | 201 + .../Modeling/Edit/gs_curvetools/README.txt | 35 + .../Modeling/Edit/gs_curvetools/__init__.py | 10 + .../Modeling/Edit/gs_curvetools/constants.py | 69 + Scripts/Modeling/Edit/gs_curvetools/core.py | 6729 +++++++++++++++++ .../Edit/gs_curvetools/fonts/Roboto-Bold.ttf | Bin 0 -> 167336 bytes .../gs_curvetools/fonts/Roboto-Italic.ttf | Bin 0 -> 170504 bytes .../gs_curvetools/fonts/Roboto-Regular.ttf | Bin 0 -> 171272 bytes .../gs_curvetools/icons/drop-down-arrow.png | Bin 0 -> 234 bytes .../icons/gsCurveToolsIcon_logo.png | Bin 0 -> 10203 bytes .../icons/gsCurveToolsIcon_reset.png | Bin 0 -> 7528 bytes .../icons/gsCurveToolsIcon_reset_legacy.png | Bin 0 -> 1329 bytes .../icons/gsCurveToolsIcon_stop.png | Bin 0 -> 7011 bytes .../icons/gsCurveToolsIcon_stop_legacy.png | Bin 0 -> 1310 bytes .../icons/gsCurveToolsIcon_ui.png | Bin 0 -> 6918 bytes .../icons/gsCurveToolsIcon_ui_legacy.png | Bin 0 -> 1240 bytes .../Edit/gs_curvetools/icons/marking.png | Bin 0 -> 118 bytes .../Modeling/Edit/gs_curvetools/icons/mod.png | Bin 0 -> 178 bytes .../Edit/gs_curvetools/icons/reset.png | Bin 0 -> 1923 bytes .../icons/sliderLock_en_reversed.png | Bin 0 -> 1555 bytes .../icons/sliderLock_reversed.png | Bin 0 -> 1458 bytes Scripts/Modeling/Edit/gs_curvetools/init.py | 161 + Scripts/Modeling/Edit/gs_curvetools/log.log | 132 + Scripts/Modeling/Edit/gs_curvetools/main.py | 878 +++ .../gs_curvetools/plugins/2018/cv_manip.mll | Bin 0 -> 90112 bytes .../gs_curvetools/plugins/2019/cv_manip.mll | Bin 0 -> 90624 bytes .../gs_curvetools/plugins/2020/cv_manip.mll | Bin 0 -> 91136 bytes .../gs_curvetools/plugins/2022/cv_manip.mll | Bin 0 -> 91136 bytes .../gs_curvetools/plugins/2023/cv_manip.mll | Bin 0 -> 91136 bytes .../gs_curvetools/plugins/2024/cv_manip.mll | Bin 0 -> 91136 bytes .../Edit/gs_curvetools/plugins/__init__.py | 0 .../Edit/gs_curvetools/plugins/cv_manip.py | 80 + .../gs_curvetools/plugins/cv_manip_src.py | 770 ++ Scripts/Modeling/Edit/gs_curvetools/ui.py | 2924 +++++++ .../Edit/gs_curvetools/utils/__init__.py | 0 .../Edit/gs_curvetools/utils/gs_math.py | 62 + .../Edit/gs_curvetools/utils/hotkeys.csv | 79 + .../Edit/gs_curvetools/utils/style.py | 786 ++ .../Edit/gs_curvetools/utils/tooltips.md | 888 +++ .../Edit/gs_curvetools/utils/tooltips.py | 102 + .../Edit/gs_curvetools/utils/utils.py | 1426 ++++ .../Modeling/Edit/gs_curvetools/utils/wrap.py | 1319 ++++ .../Modeling/Edit/gs_curvetools/uv_editor.py | 1336 ++++ 81 files changed, 22225 insertions(+) create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlusBase.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlusCore.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlusEULA.pdf create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlusExcept.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlusMain.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlusNodes.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/CreasePlus_def_sheet.pdf create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_bevel_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_bevellive_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_bool_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_close_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_crease_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveattach_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvebevel_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveclose_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvedraw_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveint_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvemult_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvepoly_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_displayhe_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_eye_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_hardedge_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_menu_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_meshslicer_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_mirror_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_next_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_panelbool_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_physcrease_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_shapeshifter_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_smoothangle_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_subd_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_uv_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_weighttool_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/Icons/crep_zbrush_ico.png create mode 100644 Scripts/Modeling/Edit/CreasePlus/MayaUndoRun.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/__init__.py create mode 100644 Scripts/Modeling/Edit/CreasePlus/delpyc.bat create mode 100644 Scripts/Modeling/Edit/CreasePlus/readit.txt create mode 100644 Scripts/Modeling/Edit/gs_curvetools/LICENSE.txt create mode 100644 Scripts/Modeling/Edit/gs_curvetools/README.txt create mode 100644 Scripts/Modeling/Edit/gs_curvetools/__init__.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/constants.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/core.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Bold.ttf create mode 100644 Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Italic.ttf create mode 100644 Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Regular.ttf create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/drop-down-arrow.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_logo.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_reset.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_reset_legacy.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_stop.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_stop_legacy.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_ui.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_ui_legacy.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/marking.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/mod.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/reset.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/sliderLock_en_reversed.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/icons/sliderLock_reversed.png create mode 100644 Scripts/Modeling/Edit/gs_curvetools/init.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/log.log create mode 100644 Scripts/Modeling/Edit/gs_curvetools/main.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/2018/cv_manip.mll create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/2019/cv_manip.mll create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/2020/cv_manip.mll create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/2022/cv_manip.mll create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/2023/cv_manip.mll create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/2024/cv_manip.mll create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/__init__.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip_src.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/ui.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/__init__.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/gs_math.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/hotkeys.csv create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/style.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.md create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/utils.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/utils/wrap.py create mode 100644 Scripts/Modeling/Edit/gs_curvetools/uv_editor.py diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlusBase.py b/Scripts/Modeling/Edit/CreasePlus/CreasePlusBase.py new file mode 100644 index 0000000..c667b7a --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/CreasePlusBase.py @@ -0,0 +1,479 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +_____ _____ ______ _____ ______ +/ ____| __ \| ____| /\ / ____| ____|_ +| | | |__) | |__ / \ | (___ | |__ _| |_ +| | | _ /| __| / /\ \ \___ \| __|_ _| +| |____| | \ \| |____ / ____ \ ____) | |____|_| +\_____|_| \_\______/_/ \_\_____/|______| + +""" + +import maya.cmds as cmds +import maya.api.OpenMaya as om + +# uses python maya 2 + +maya_useNewAPI = True + + +class MayaVerObj(): + def __init__(self): + self.num = None + self.extnum = 0 + + +def cPwhatmayaversion(): + verstring = cmds.about(v=True) + + splited = verstring.split() + num = None + extnum = 0 + + for r in splited: + if "20" in r: + num = int(r) + break + + i = -1 + for r in splited: + i += 1 + if r.lower() == "extension" or r.lower() == "ext": + j = i+1 + for j in range(len(splited)): + if splited[j].isdigit(): + extnum = int(splited[j]) + break + break + + if not num: + raise Exception("can't get maya version") + + mayavobj = MayaVerObj() + mayavobj.num = num + mayavobj.extnum = extnum + return mayavobj + +global_creasePlus_mayaver = cPwhatmayaversion() + +def getmayaver(): + global global_creasePlus_mayaver + return global_creasePlus_mayaver + + +class CpMsg: + + kNoSel = 'nothing is selected' + kNoSelCurve = 'no curve(s) selected' + kNoSelMesh = 'no mesh(es) selected' + kNoSelEdge = 'no edge(s) selected' + kNoSelVert = 'no vertice(s) selected' + kNoSelFace = 'no face(s) selected' + + kSelOneMesh = 'select one mesh' + kSelMesh = 'mesh(es) must be selected' + kSelEdge = 'edge(s) must be selected' + kSelFace = 'face(s) must be selected' + kSelVert = 'vertice(s) must be selected' + + kSelEdgeOrOneMesh = 'select edge(s) or a mesh' + + kSelCurveCv = 'curve cv(s) must be selected' + kWorksForOne = 'works just for one entity' + + kNoHardEdges = 'no hard edge(s) were found' + + kSelLeastTwoMesh = 'select at least two meshes' + kSelLeastTwoCurve = 'select at least two curves' + + kInvalidFuncArgs = 'function called with invalid arguments' + + kRequModelView = 'you are required to be in a modeling view/pan' + kWrongSel = 'wrong selection' + + kcPnodePluginNotLoaded = \ + 'creasePlus nodes plugin must be loaded to use this command' + + +# obj assumed to be a parent +def cPgotoChild(obj, mfntyp): + dagn = om.MFnDagNode(obj) + for i in range(dagn.childCount()): + cur = dagn.child(i) + if cur.hasFn(mfntyp): + return cur + return om.MObject.kNullObj + + +def cPshapeDagPath(obj): + dagn = om.MFnDagNode(obj) + return dagn.getPath() + + +def cPhardEdgesStrings(shape): + + sel = om.MSelectionList() + sel.add(shape) + + dagp = sel.getDagPath(0) + + edgeIter = om.MItMeshEdge(dagp) + selStrings = [] + shapeString = dagp.partialPathName() + while not edgeIter.isDone(): + if edgeIter.isSmooth == False: + selStrings.append(shapeString + '.e[' + + str(edgeIter.index()) + ']') + + edgeIter.next() + + return selStrings + + +def cPgetShapeStringsFromSel(mfntyp): + + sel = om.MGlobal.getActiveSelectionList() + selIt = om.MItSelectionList(sel) + + selStrings = [] + dagFn = om.MFnDagNode() + while not selIt.isDone(): + + if selIt.itemType() != selIt.kDagSelectionItem: + selIt.next() + continue + + obj = selIt.getDependNode() + + if not obj.hasFn(mfntyp): + + obj = cPgotoChild(obj, mfntyp) + + if not obj.hasFn(mfntyp): + selIt.next() + continue + + dagFn.setObject(obj) + selStrings.append(dagFn.partialPathName()) + selIt.next() + + return selStrings + + +def cPcameraDominantPlane(): + + activePan = cmds.getPanel(wf=True) + + if cmds.getPanel(to=activePan) != 'modelPanel': + cmds.error(CpMsg.kRequModelView) + + camPos = cmds.camera(cmds.modelEditor(activePan, q=True, cam=True), + q=True, p=True) + camTarget = cmds.camera(cmds.modelEditor(activePan, q=True, cam=True), + q=True, wci=True) + + camDir = om.MVector(abs(camTarget[0] - camPos[0]), abs(camTarget[1] + - camPos[1]), abs(camTarget[2] - camPos[2])) + + maxv = 0 + ddir = 'x' + if maxv < camDir.x: + maxv = camDir.x + ddir = 'x' + if maxv < camDir.y: + maxv = camDir.y + ddir = 'y' + if maxv < camDir.z: + maxv = camDir.z + ddir = 'z' + + return ddir + + +# cmds.ls(sl=True) +# get shape + v, e, f comps in a tuple + +def cPgetShapeAndCoStrings(mselit): + + shape = None + v = None + e = None + f = None + + if mselit.itemType() != mselit.kDagSelectionItem: + return (shape, v, e, f) + + hasComp = mselit.hasComponents() + + if hasComp == True: + + comp = mselit.getComponent() + + if comp[0].node().hasFn(om.MFn.kMesh): + shape = comp[0] + + if comp[1].hasFn(om.MFn.kMeshPolygonComponent): + f = comp[1] + elif comp[1].hasFn(om.MFn.kMeshEdgeComponent): + + e = comp[1] + elif comp[1].hasFn(om.MFn.kMeshVertComponent): + + v = comp[1] + else: + + obj = mselit.getDependNode() + + if not obj.hasFn(om.MFn.kMesh): + obj = cPgotoChild(obj, om.MFn.kMesh) + + if obj.hasFn(om.MFn.kMesh): + shape = cPshapeDagPath(obj) + + return (shape, v, e, f) + + +def cPfaceToHardEdgeStrings(dagp, faceComps): + + edgeIds = set() + + faceIt = om.MItMeshPolygon(dagp, faceComps) + meshFn = om.MFnMesh(dagp.node()) + + while not faceIt.isDone(): + + fEdges = faceIt.getEdges() + + for idx in fEdges: + if not meshFn.isEdgeSmooth(idx): + edgeIds.add(idx) + + faceIt.next(None) # maya api bug + + shapeString = dagp.partialPathName() + return [shapeString + '.e[' + str(idx) + ']' for idx in edgeIds] + + +def cPedgeToStrings(dagp, edgeComps): + + edgeStrings = [] + edgeIt = om.MItMeshEdge(dagp, edgeComps) + shapeString = dagp.partialPathName() + while not edgeIt.isDone(): + + edgeStrings.append(shapeString + '.e[' + str(edgeIt.index()) + + ']') + edgeIt.next() + + return edgeStrings + + +################################################################ CONTEXTS ################################################################# + +# draw curve ctx +try: + global_cPcurveCtxStr +except: + global_cPcurveCtxStr = cmds.curveCVCtx('cPcurveCtx', degree=1) + +# + +# booleanop context +try: + global_cPboolOpCtxStr +except: + global_cPboolOpCtxStr = cmds.dragAttrContext('cPboolOpCtx') + +global_cPboolOpAttrCnt = 3 +global_cPboolOpAttrIter = 0 + + +def cPboolOpIterSetVal(val): + global global_cPboolOpAttrIter + global_cPboolOpAttrIter = val + return global_cPboolOpAttrIter % global_cPboolOpAttrCnt + + +def cPboolOpIterVal(): + global global_cPboolOpAttrIter + return global_cPboolOpAttrIter % global_cPboolOpAttrCnt + + +def cPboolOpIterIncVal(): + global global_cPboolOpAttrIter + global_cPboolOpAttrIter += 1 + return global_cPboolOpAttrIter % global_cPboolOpAttrCnt + + +# + +# mirror context +try: + global_cPmirrorCtxStr +except: + global_cPmirrorCtxStr = cmds.dragAttrContext('cPmirrorCtx') + +global_cPmirrorAttrCnt = None +if getmayaver().num > 2016: + global_cPmirrorAttrCnt = 1 +else: + global_cPmirrorAttrCnt = 3 + +global_cPmirrorAttrIter = 0 + + +def cPmirrorIterSetVal(val): + global global_cPmirrorAttrIter + global_cPmirrorAttrIter = val + return global_cPmirrorAttrIter % global_cPmirrorAttrCnt + + +def cPmirrorIterVal(): + global global_cPmirrorAttrIter + return global_cPmirrorAttrIter % global_cPmirrorAttrCnt + + +def cPmirrorIterIncVal(): + global global_cPmirrorAttrIter + global_cPmirrorAttrIter += 1 + return global_cPmirrorAttrIter % global_cPmirrorAttrCnt + + +# + +# hbevel context +try: + global_hBevelCtxStr +except: + global_hBevelCtxStr = cmds.dragAttrContext('hBevelCtx') + +global_hBevelAttrCnt = 2 +global_hBevelAttrIter = 0 + + +def cPhBevelIterSetVal(val): + global global_hBevelAttrIter + global_hBevelAttrIter = val + return global_hBevelAttrIter % global_hBevelAttrCnt + + +def cPhBevelIterVal(): + global global_hBevelAttrIter + return global_hBevelAttrIter % global_hBevelAttrCnt + + +def cPhBevelIterIncVal(): + global global_hBevelAttrIter + global_hBevelAttrIter += 1 + return global_hBevelAttrIter % global_hBevelAttrCnt + + +# + +# curvebevel context +try: + global_cPcurveBevelCtxStr +except: + global_cPcurveBevelCtxStr = cmds.dragAttrContext('cPcurveBvlCtx') + +global_cPcurveBevelAttrCnt = 2 +global_cPcurveBevelAttrIter = 0 + + +def cPcurveBevelIterSetVal(val): + global global_cPcurveBevelAttrIter + global_cPcurveBevelAttrIter = val + return global_cPcurveBevelAttrIter % global_cPcurveBevelAttrCnt + + +def cPcurveBevelIterVal(): + global global_cPcurveBevelAttrIter + return global_cPcurveBevelAttrIter % global_cPcurveBevelAttrCnt + + +def cPcurveBevelIterIncVal(): + global global_cPcurveBevelAttrIter + global_cPcurveBevelAttrIter += 1 + return global_cPcurveBevelAttrIter % global_cPcurveBevelAttrCnt + + +# + +# crease tool context +try: + global_cPcreaseCtxStr +except: + global_cPcreaseCtxStr = cmds.polyCreaseCtx('cPcreaseCtx', es=True, r=True) + +# + +# physical crease context +try: + global_pCreaseCtxStr +except: + global_pCreaseCtxStr = cmds.dragAttrContext('pCreaseCtx') + +global_pCreaseAttrCnt = 2 +global_pCreaseAttrIter = 0 + + +def cPpCreaseIterSetVal(val): + global global_pCreaseAttrIter + global_pCreaseAttrIter = val + return global_pCreaseAttrIter % global_pCreaseAttrCnt + + +def cPpCreaseIterVal(): + global global_pCreaseAttrIter + return global_pCreaseAttrIter % global_pCreaseAttrCnt + + +def cPpCreaseIterIncVal(): + global global_pCreaseAttrIter + global_pCreaseAttrIter += 1 + return global_pCreaseAttrIter % global_pCreaseAttrCnt + + +# +def cPcontextUndo(): + if cmds.currentCtx() == global_cPboolOpCtxStr: + # cmds.setToolTo('moveSuperContext') + cmds.dragAttrContext(global_cPboolOpCtxStr, e=True, reset=True) + cmds.setToolTo(global_cPboolOpCtxStr) + elif cmds.currentCtx() == global_hBevelCtxStr: + + # cmds.setToolTo('moveSuperContext') + cmds.dragAttrContext(global_hBevelCtxStr, e=True, reset=True) + cmds.setToolTo(global_hBevelCtxStr) + elif cmds.currentCtx() == global_cPmirrorCtxStr: + + # cmds.setToolTo('moveSuperContext') + cmds.dragAttrContext(global_cPmirrorCtxStr, e=True, reset=True) + cmds.setToolTo(global_cPmirrorCtxStr) + elif cmds.currentCtx() == global_cPcurveBevelCtxStr: + + # cmds.setToolTo('moveSuperContext') + cmds.dragAttrContext(global_cPcurveBevelCtxStr, e=True, reset=True) + cmds.setToolTo(global_cPcurveBevelCtxStr) + elif cmds.currentCtx() == global_pCreaseCtxStr: + + # cmds.setToolTo('moveSuperContext') + cmds.dragAttrContext(global_pCreaseCtxStr, e=True, reset=True) + cmds.setToolTo(global_pCreaseCtxStr) + + +try: + global_creasePlusCtxUndoJob +except: + global_creasePlusCtxUndoJob = cmds.scriptJob(event=['Undo', cPcontextUndo]) + +############################################################################################################ + +def main(): + return None + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlusCore.py b/Scripts/Modeling/Edit/CreasePlus/CreasePlusCore.py new file mode 100644 index 0000000..a03262f --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/CreasePlusCore.py @@ -0,0 +1,2125 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# import sys +""" +_____ _____ ______ _____ ______ +/ ____| __ \| ____| /\ / ____| ____|_ +| | | |__) | |__ / \ | (___ | |__ _| |_ +| | | _ /| __| / /\ \ \___ \| __|_ _| +| |____| | \ \| |____ / ____ \ ____) | |____|_| +\_____|_| \_\______/_/ \_\_____/|______| + +""" + +import math + +from MayaUndoRun import mayaUndoRun +import maya.cmds as mc +import maya.mel as mel +import maya.api.OpenMaya as om + +import CreasePlusExcept as crepexcept +import CreasePlusBase as crep +import CreasePlusNodes +import importlib + +# TODO remove reloads +# crep = importlib.reload(crep) +# CreasePlusNodes = importlib.reload(CreasePlusNodes) + +# uses python maya 2 + +maya_useNewAPI = True + +def cPmayaScriptDir(): + return mc.internalVar(usd=True) + +def cPmainDir(): + return cPmayaScriptDir() + 'CreasePlus/' + +# GLOBALS + +__ = str(CreasePlusNodes) +__ = __[__.find(" \'") + 2:__.find("\' from")] +__ = __[__.rfind('.') + 1:] + +global_CP_Nodes_pluginstr = __ + + +# bool namespace +try: + global_cPboolNamespace +except: + global_cPboolNamespace = mc.namespace(add='cPbool', parent=':') + + +def cPaddToMayaNamespace(toRename, newname): + global global_cPboolNamespace + if not mc.namespace(exists=global_cPboolNamespace): + global_cPboolNamespace = mc.namespace(add='cPbool', parent=':') + + return mc.rename(toRename, newname) + + +##### + + +def creasePlusShapeShifter(): + + sdir = mc.internalVar( + usd=True) + 'AMTools/AMTScripts/StartShapeShifter.mel' + ssExist = mel.eval('filetest -f "' + sdir + '";') + + # welcome + + if not ssExist: + mc.warning( + 'unable to start ShapeShifter, you have to purchase or update ShapeShifter for CREASE+ support.\n' + ) + return + + mel.eval('source "' + sdir + '";') + + +def cPgetGoz(): + if mc.about(win=True): + # for windows + assumed = "C:/Users/Public/Pixologic/GoZApps/Maya/GoZBrushFromMaya.mel" + gozTest = mel.eval("filetest -f " + "\"" + assumed + "\";") + sGoz = "source " + "\"" + assumed + "\";" + if gozTest == 0: + mc.warning("To use this feature you need Goz script from Pixologic Zbrush.\n") + return + else: + mel.eval(sGoz) + else: + + # for mac + assumed = "/Users/Shared/Pixologic/GoZApps/Maya/GoZBrushFromMaya.mel" + gozTest = mel.eval("filetest -f " + "\"" + assumed + "\";") + sGoz = "source " + "\"" + assumed + "\";" + if gozTest == 0: + mc.warning("To use this feature you need Goz script from Pixologic Zbrush.\n") + return + else: + mel.eval(sGoz) + + + + +def creasePlusGoz(): + + osel = mc.ls(sl=True) + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelMesh) + + for shapeStr in sel: + mc.select(shapeStr, r=True) + mel.eval( + 'polyCleanupArgList 4 { "0","2","0","0","1","0","0","0","0","1e-005","0","1e-005","0","1e-005","0","-1","0","0" };' + ) + if len(mc.ls(sl=True, fl=True)): + mel.eval( + 'polyCleanupArgList 4 { "0","1","0","0","1","0","0","0","0","1e-005","0","1e-005","0","1e-005","0","-1","0","0" };' + ) + + mc.select(osel, r=True) + cPgetGoz() + + +@mayaUndoRun +def creasePlusSmooth30(): + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSel) + + for shapeStr in sel: + mc.polySoftEdge(shapeStr, a=30, ch=True) + mc.select( + mc.listRelatives(sel, parent=True, fullPath=True, typ='transform'), + r=True) + + +# creasePlusDisplayHardEdges(0) + + +def creasePlusDisplayHardEdges(disp=0): + + # disp = 0 for toggle + # = 1 to display hard edges + # = 2 for no display + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + for shapeStr in sel: + + if disp == 0: + nodisp = mc.polyOptions(shapeStr, q=True, ae=True)[0] + if nodisp: + mc.polyOptions(shapeStr, hec=True) + else: + mc.polyOptions(shapeStr, ae=True) + elif disp == 1: + mc.polyOptions(shapeStr, hec=True) + elif disp == 2: + + mc.polyOptions(shapeStr, ae=True) + else: + raise crepexcept.cPexcept('wrong kwarg value') + + +@mayaUndoRun +def creasePlusSelHardEdges(): + sel = om.MGlobal.getActiveSelectionList() + selIt = om.MItSelectionList(sel) + + finalSel = om.MSelectionList() + + while not selIt.isDone(): + + obj = selIt.getDependNode() + + if not obj.hasFn(om.MFn.kMesh): + + obj = crep.cPgotoChild(obj, om.MFn.kMesh) + + if not obj.hasFn(om.MFn.kMesh): + selIt.next() + continue + + dagp = crep.cPshapeDagPath(obj) + edgeIter = om.MItMeshEdge(dagp) + + while not edgeIter.isDone(): + + if not edgeIter.isSmooth: + + finalSel.add( + (dagp, edgeIter.currentItem()), mergeWithExisting=False) + + edgeIter.next() + + selIt.next() + + om.MGlobal.setActiveSelectionList(finalSel) + +# @mayaUndoRun +def creasePlusToggleEdgeSmooth(): + + sel = om.MGlobal.getActiveSelectionList() + selIt = om.MItSelectionList(sel) + selIt.setFilter(om.MFn.kMeshEdgeComponent) + + comps = [] + while not selIt.isDone(): + comps.append(selIt.getComponent()) + selIt.next() + + for comp in comps: + toHardList = [] + toSoftenList = [] + + edgeIter = om.MItMeshEdge(comp[0], comp[1]) + + while not edgeIter.isDone(): + + compstring = comp[0].partialPathName() + '.e[' + str( + edgeIter.index()) + ']' + + if edgeIter.isSmooth: + toHardList.append(compstring) + else: + toSoftenList.append(compstring) + + edgeIter.next() + + if len(toHardList): + mc.polySoftEdge(toHardList, a=0, ch=True) + + if len(toSoftenList): + mc.polySoftEdge(toSoftenList, a=180, ch=True) + + om.MGlobal.setActiveSelectionList(sel) + + +def cPdoPolyBevel(selStrings, nname): + if crep.getmayaver().num > 2016 or (crep.getmayaver().num == 2016 and crep.getmayaver().extnum == 2): + nodestr = mc.polyBevel3( + selStrings, + oaf=False, + af=True, + mia=0, + c=True, + f=0, + o=0, + sg=1, + ws=True, + sa=180, + sn=True, + ma=180, + at=180, + )[0] + else: + nodestr = mc.polyBevel3( + selStrings, + oaf=False, + af=True, + fn=1, + o=0, + sg=1, + ws=True, + sa=180, + )[0] + + + + return mc.rename(nodestr, nname) + + +# + +class CpMirrorStat: + + axisStr = 'axis' + sideStr = 'side' + posStr = 'position' + dxs = 'dX' + dys = 'dY' + dzs = 'dZ' + + +def cPmirrorAttrs(nodeString, mirrorNodeStr): + axisStr = CpMirrorStat.axisStr + sideStr = CpMirrorStat.sideStr + posStr = CpMirrorStat.posStr + dxs = CpMirrorStat.dxs + dys = CpMirrorStat.dys + dzs = CpMirrorStat.dzs + + if crep.getmayaver().num > 2016: + + if mc.attributeQuery(axisStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + axisStr) + if mc.attributeQuery(sideStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + sideStr) + if mc.attributeQuery(posStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + posStr) + + mc.addAttr( + nodeString, + ln=axisStr, + k=True, + at='enum', + en='X=0:Y=1:Z=2', + dv=0, + ) + mc.addAttr( + nodeString, + ln=sideStr, + k=True, + at='enum', + en='+=0:-=1', + dv=0, + ) + mc.addAttr( + nodeString, + ln=posStr, + k=True, + at='floatLinear', + dv=mc.getAttr(mirrorNodeStr + '.mirrorPosition')) + + mc.connectAttr(nodeString + '.' + axisStr, mirrorNodeStr + '.axis') + mc.connectAttr(nodeString + '.' + sideStr, + mirrorNodeStr + '.axisDirection') + mc.connectAttr(nodeString + '.' + posStr, + mirrorNodeStr + '.mirrorPosition') + + else: + + if mc.attributeQuery(axisStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + axisStr) + if mc.attributeQuery(dxs , node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + dxs ) + if mc.attributeQuery(dys , node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + dys ) + if mc.attributeQuery(dzs , node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + dzs ) + + mc.addAttr( + nodeString, + ln=axisStr, + k=True, + at='enum', + en='+X=0:-X=1:+Y=2:-Y=3:+Z=4:-Z=5', + dv=0, + ) + + mc.addAttr( + nodeString, + ln=dxs, + k=True, + at='doubleLinear', + dv=0.5) + # dv=mc.getAttr(mirrorNodeStr + '.pivotX')) + mc.addAttr( + nodeString, + ln=dys, + k=True, + at='doubleLinear', + dv=0.5) + # dv=mc.getAttr(mirrorNodeStr + '.pivotY')) + mc.addAttr( + nodeString, + ln=dzs, + k=True, + at='doubleLinear', + dv=0.5) + # dv=mc.getAttr(mirrorNodeStr + '.pivotZ')) + + mc.connectAttr(nodeString + '.' + axisStr, mirrorNodeStr + '.direction') + mc.connectAttr(nodeString + "." + dxs, mirrorNodeStr + '.pivotX') + mc.connectAttr(nodeString + "." + dys, mirrorNodeStr + '.pivotY') + mc.connectAttr(nodeString + "." + dzs, mirrorNodeStr + '.pivotZ') + + +def creasePlusMirrorCont(nodeString): + + crep.global_cPmirrorCtxStr + + axisStr = CpMirrorStat.axisStr + sideStr = CpMirrorStat.sideStr + posStr = CpMirrorStat.posStr + dxs = CpMirrorStat.dxs + dys = CpMirrorStat.dys + dzs = CpMirrorStat.dzs + + cur = None + if mc.currentCtx() == crep.global_cPmirrorCtxStr: + cur = crep.cPmirrorIterIncVal() + else: + cur = crep.cPmirrorIterSetVal(0) + + mc.dragAttrContext(crep.global_cPmirrorCtxStr, e=True, reset=True) + + if crep.getmayaver().num > 2016: + + if cur == 0: + mc.dragAttrContext( + crep.global_cPmirrorCtxStr, e=True, ct=nodeString + '.' + posStr) + else: + if cur == 0: + mc.setAttr(nodeString + "." + axisStr, 0) + elif cur == 1: + mc.setAttr(nodeString + "." + axisStr, 2) + elif cur == 2: + mc.setAttr(nodeString + "." + axisStr, 4) + + + mc.setToolTo(crep.global_cPmirrorCtxStr) + + +def cPhasMirrorHistory(shape): + + dummy = om.MSelectionList() + dummy.add(shape) + dagFn = om.MFnDagNode(dummy.getDependNode(0)) + dagFn.setObject(dagFn.parent(0)) + transfrmStr = dagFn.partialPathName() + + if mc.attributeQuery(CpMirrorStat.axisStr, node=transfrmStr, ex=True): + destPlugs = mc.connectionInfo( + transfrmStr + '.' + CpMirrorStat.axisStr, dfs=True) + for plugStr in destPlugs: + if mc.nodeType(cPplugNode(plugStr)) == 'polyMirror': + return True + else: + + return False + + return False + + +@mayaUndoRun +def creasePlusMirror(): + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelMesh) + + crep.global_cPmirrorCtxStr + ctxActive = mc.currentCtx() == crep.global_cPmirrorCtxStr + + if ctxActive and cPhasMirrorHistory(sel[0]): + transformStr = mc.listRelatives( + sel[0], parent=True, fullPath=True, typ='transform')[0] + creasePlusMirrorCont(transformStr) + return + + transformNodes = [] + for shapeStr in sel: + + # shapeStr = "pCubeShape1" + + transformStr = mc.listRelatives( + shapeStr, parent=True, fullPath=True, typ='transform')[0] + transformNodes.append(transformStr) + mirNode = mc.polyMirrorFace(shapeStr)[0] + # mc.polyMergeVertex(shapeStr, d=0.015, am=False) + cPmirrorAttrs(transformStr, mirNode) + + mc.select(transformNodes, r=True) + creasePlusMirrorCont(transformStr) + + +class CpBoolOpStat: + + title = 'boolOp' + + unionStr = 'union' + diffStr = 'difference' + intersecStr = 'intersect' + + +def cPboolOpAttrs(nodeString, boolNodeStr): + + mainAttrName = CpBoolOpStat.title + + unionStr = CpBoolOpStat.unionStr + diffStr = CpBoolOpStat.diffStr + intersecStr = CpBoolOpStat.intersecStr + + if mc.attributeQuery(mainAttrName, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + mainAttrName) + + mc.addAttr( + nodeString, + ln=mainAttrName, + k=True, + at='enum', + en=unionStr + '=1:' + diffStr + '=2:' + intersecStr + '=3', + dv=2, + ) + + mc.connectAttr(nodeString + '.' + mainAttrName, boolNodeStr + '.operation') + + +# creasePlusBoolOpCont("polySurface1") + + +def creasePlusBoolOpCont(nodeString): + + crep.global_cPboolOpCtxStr + + mainAttrName = CpBoolOpStat.title + + cur = None + if mc.currentCtx() == crep.global_cPboolOpCtxStr: + cur = crep.cPboolOpIterIncVal() + else: + cur = crep.cPboolOpIterSetVal(0) + + mc.dragAttrContext(crep.global_cPboolOpCtxStr, e=True, reset=True) + + if cur == 0: + mc.setAttr(nodeString + '.' + mainAttrName, 2) + elif cur == 1: + mc.setAttr(nodeString + '.' + mainAttrName, 1) + elif cur == 2: + mc.setAttr(nodeString + '.' + mainAttrName, 3) + + mc.setToolTo(crep.global_cPboolOpCtxStr) + + +def cPhasBoolHistory(shape): + dummy = om.MSelectionList() + dummy.add(shape) + dagFn = om.MFnDagNode(dummy.getDependNode(0)) + dagFn.setObject(dagFn.parent(0)) + transfrmStr = dagFn.partialPathName() + + if mc.attributeQuery(CpBoolOpStat.title, node=transfrmStr, ex=True): + destPlugs = mc.connectionInfo( + transfrmStr + '.' + CpBoolOpStat.title, dfs=True) + for plugStr in destPlugs: + if mc.nodeType(cPplugNode(plugStr)) == 'polyCBoolOp': + return True + else: + + return False + + return False + + +# @mayaUndoRun +def creasePlusToggleBoolGhost(): + global global_cPboolNamespace + vs = mc.ls(global_cPboolNamespace + ":*", o=True, v=True) + ivs = mc.ls(global_cPboolNamespace + ":*", o=True, iv=True) + + if (len(vs)): + mc.hide(vs) + elif (len(ivs)): + mc.showHidden(ivs) + + +@mayaUndoRun +def creasePlusBool(keepOperands=False): + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelMesh) + + crep.global_cPboolOpCtxStr + ctxActive = mc.currentCtx() == crep.global_cPboolOpCtxStr + + hasBoolHistory = cPhasBoolHistory(sel[0]) + + if ctxActive and hasBoolHistory or len(sel) == 1 and hasBoolHistory: + curTransfrm = mc.listRelatives( + sel[0], + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + creasePlusBoolOpCont(curTransfrm) + return + elif len(sel) < 2: + raise crepexcept.cPexcept(crep.CpMsg.kSelLeastTwoMesh) + + if keepOperands: + for curShape in sel[1:]: + mc.setAttr(curShape + '.visibility', True) + mc.setAttr(curShape + '.hiddenInOutliner', False) + mc.setAttr(curShape + '.intermediateObject', False) + mc.setAttr(curShape + '.overrideShading', True) + mc.setAttr(curShape + '.overrideColor', 0) + mc.setAttr(curShape + '.overrideEnabled', False) + sel[1:] = mc.listRelatives( + mc.duplicate(sel[1:], renameChildren=True), + children=True, + fullPath=True, + noIntermediate=False, + typ='mesh') + + newObj = mc.polyCBoolOp( + sel, + useCarveBooleans=True, + classification=1, + op=2, + preserveColor=False, + ch=True, + ) + + if mc.nodeType(newObj[0]) == 'mesh': + newObj[0] = mc.listRelatives( + newObj[0], + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + + dummy = om.MSelectionList() + dummy.add(newObj[1]) + + depFn = om.MFnDependencyNode(dummy.getDependNode(0)) + allPlugs = depFn.getConnections() + + inShapeStrings = set() + fstShapeStr = None + for i in range(len(allPlugs)): + curPlug = allPlugs[i] + + if not curPlug.isDestination: + continue + + srcPlug = mc.connectionInfo(curPlug.name(), sfd=True) + srcShapeStr = cPplugNode(srcPlug) + if mc.nodeType(srcShapeStr) == 'mesh': + if fstShapeStr == None: + fstShapeStr = srcShapeStr + inShapeStrings.add(srcShapeStr) + + global global_cPboolNamespace + + for curShape in inShapeStrings: + + # curShape = next(iter(inShapeStrings)) + + if curShape == fstShapeStr: + continue + + # def creasePlusBool() + + curTransfrm = mc.listRelatives( + curShape, + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + + mc.setAttr(curShape + '.visibility', True) + mc.setAttr(curShape + '.hiddenInOutliner', False) + mc.setAttr(curShape + '.intermediateObject', False) + mc.setAttr(curShape + '.overrideEnabled', True) + mc.setAttr(curShape + '.overrideShading', False) + mc.setAttr(curShape + '.overrideColor', 4) + mc.xform(curShape,cp=True) + + if not ((global_cPboolNamespace + ":") in curTransfrm): + curTransfrm = cPaddToMayaNamespace( + curTransfrm, global_cPboolNamespace + ":" + + curTransfrm[curTransfrm.rfind("|") + 1:]) + + mc.setAttr(curTransfrm + '.visibility', True) + + cPboolOpAttrs(newObj[0], newObj[1]) + creasePlusBoolOpCont(newObj[0]) + mc.select(newObj[0], r=True) + + # mel.eval("AEdagNodeCommonRefreshOutliners();") + + if crep.getmayaver().num > 2016: + mel.eval( + 'attributeEditorVisibilityStateChange(`workspaceControl -q -visible AttributeEditor`, "");' + ) + + +@mayaUndoRun +def creasePlusPanelBool(): + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + # + + if len(sel) < 2: + raise crepexcept.cPexcept(crep.CpMsg.kSelLeastTwoMesh) + + dupl = mc.duplicate(sel, renameChildren=True) + + newObj = mc.polyCBoolOp( + sel, + useCarveBooleans=True, + classification=1, + op=2, + preserveColor=False, + ch=True, + )[0] + newObj1 = mc.polyCBoolOp( + dupl, + useCarveBooleans=True, + classification=1, + op=3, + preserveColor=False, + ch=True, + )[0] + mc.select([newObj, newObj1], r=True) + mc.xform(mc.ls(sl=True),cp=True) + # mc.polyUnite() + + +class CpHBevelStat: + + offsetStr = 'hOffset' + divStr = 'hDivisions' + miterStr = 'hMitering' + + +def cPhBevelAttrs(nodeString, bevelNodeStr): + + offsetStr = CpHBevelStat.offsetStr + divStr = CpHBevelStat.divStr + miterStr = CpHBevelStat.miterStr + + + if crep.getmayaver().num > 2016 or (crep.getmayaver().num == 2016 and crep.getmayaver().extnum == 2): + if mc.attributeQuery(offsetStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + offsetStr) + + if mc.attributeQuery(divStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + divStr) + + if mc.attributeQuery(miterStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + miterStr) + + mc.addAttr( + nodeString, + ln=offsetStr, + k=True, + at='doubleLinear', + hnv=True, + min=0, + dv=0, + ) + mc.addAttr( + nodeString, + ln=divStr, + k=True, + at='long', + hnv=True, + min=0, + dv=1, + ) + mc.addAttr( + nodeString, + ln=miterStr, + k=True, + at='enum', + en='Auto=0:Star=2:Round=3', + dv=0, + ) + + mc.connectAttr(nodeString + '.' + offsetStr, bevelNodeStr + '.offset') + mc.connectAttr(nodeString + '.' + divStr, bevelNodeStr + '.segments') + mc.connectAttr(nodeString + '.' + miterStr, bevelNodeStr + '.mitering') + else: + if mc.attributeQuery(offsetStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + offsetStr) + + if mc.attributeQuery(divStr, node=nodeString, ex=True): + mc.deleteAttr(nodeString + '.' + divStr) + + mc.addAttr( + nodeString, + ln=offsetStr, + k=True, + at='doubleLinear', + hnv=True, + min=0, + dv=0, + ) + mc.addAttr( + nodeString, + ln=divStr, + k=True, + at='long', + hnv=True, + min=0, + dv=1, + ) + + mc.connectAttr(nodeString + '.' + offsetStr, bevelNodeStr + '.offset') + mc.connectAttr(nodeString + '.' + divStr, bevelNodeStr + '.segments') + +def cPhasHBevelHistory(shape): + dummy = om.MSelectionList() + dummy.add(shape) + dagFn = om.MFnDagNode(dummy.getDependNode(0)) + dagFn.setObject(dagFn.parent(0)) + transfrmStr = dagFn.partialPathName() + + if mc.attributeQuery(CpHBevelStat.offsetStr, node=transfrmStr, ex=True): + destPlugs = mc.connectionInfo( + transfrmStr + '.' + CpHBevelStat.offsetStr, dfs=True) + for plugStr in destPlugs: + if mc.nodeType(cPplugNode(plugStr)) == 'polyBevel3': + return True + else: + + return False + + return False + + +# creasePlusBool(keepOperands =True) +# creasePlusHBevelCont("polySurface1") +# creasePlusHBevelCont("pCube1") + + +def creasePlusHBevelCont(nodeString): + + crep.global_hBevelCtxStr + + offsetStr = CpHBevelStat.offsetStr + divStr = CpHBevelStat.divStr + + cur = None + if mc.currentCtx() == crep.global_hBevelCtxStr: + cur = crep.cPhBevelIterIncVal() + else: + cur = crep.cPhBevelIterSetVal(0) + + mc.dragAttrContext(crep.global_hBevelCtxStr, e=True, reset=True) + if cur == 0: + mc.dragAttrContext( + crep.global_hBevelCtxStr, e=True, ct=nodeString + '.' + offsetStr) + elif cur == 1: + mc.dragAttrContext( + crep.global_hBevelCtxStr, e=True, ct=nodeString + '.' + divStr) + + mc.setToolTo(crep.global_hBevelCtxStr) + + +@mayaUndoRun +def creasePlusBakeHBL(): + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSel) + + mc.setToolTo('moveSuperContext') + + + beveliTransforms = [] + for shapeStr in sel: + (cpheNode, bevelNode, beveli)= cPisbevelCage(shapeStr) + if cpheNode and bevelNode and beveli: + beveliTrans = mc.listRelatives(beveli, parent=True, noIntermediate=False, typ='transform')[0] + mc.delete(beveliTrans + '.translate', icn=True) + mc.delete(beveliTrans + '.rotate' , icn=True) + mc.delete(beveliTrans + '.scale' , icn=True) + mc.delete(beveli, ch=True) + mc.select(beveli,r=True) + cPcleanAttrs() + beveliTransforms.append(beveliTrans) + try: + mc.delete(shapeStr) + except: + pass + else: + (cpheNode, bevelNode, beveli, beveliCage) = cPhasbevelCage(shapeStr) + if cpheNode and bevelNode and beveli and beveliCage: + beveliTrans = mc.listRelatives(beveli, parent=True, noIntermediate=False, typ='transform')[0] + mc.delete(beveliTrans + '.translate', icn=True) + mc.delete(beveliTrans + '.rotate' , icn=True) + mc.delete(beveliTrans + '.scale' , icn=True) + mc.delete(beveli, ch=True) + mc.select( beveli,r=True) + cPcleanAttrs() + beveliTransforms.append(beveliTrans) + try: + mc.delete(beveliCage) + except: + pass + + if len(beveliTransforms) : + mc.select(beveliTransforms, r=True) + else: + mc.select( cl=True) + +@mayaUndoRun +def creasePlusHBevel(): + + sel = om.MGlobal.getActiveSelectionList() + + if sel.length() == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSel) + + crep.global_hBevelCtxStr + ctxActive = mc.currentCtx() == crep.global_hBevelCtxStr + + selIt = om.MItSelectionList(sel) + + principalObjIdx = None + transformNodes = [] + while not selIt.isDone(): + + (shape, __, edges, faces) = crep.cPgetShapeAndCoStrings(selIt) + + if not shape: + selIt.next() + continue + + shapeStr = shape.partialPathName() + + (cpheNode, bevelNode, beveli)= cPisbevelCage(shapeStr) + if cpheNode and bevelNode and beveli: + creasePlusHBevelCont( + mc.listRelatives( + beveli, + parent=True, + noIntermediate=False, + typ='transform')[0] + ) + return + + + dagFn = om.MFnDagNode(shape) + dagFn.setObject(dagFn.parent(0)) + transfrmStr = dagFn.partialPathName() + transformNodes.append(transfrmStr) + + if ctxActive and cPhasHBevelHistory(shapeStr): + creasePlusHBevelCont(transfrmStr) + return + else: + ctxActive = False + + edgeStrings = None + + if faces: + edgeStrings = crep.cPfaceToHardEdgeStrings(shape, faces) + elif edges: + + edgeStrings = crep.cPedgeToStrings(shape, edges) + else: + + edgeStrings = crep.cPhardEdgesStrings(shape) + + # + + if len(edgeStrings) == 0: + selIt.next() + continue + elif principalObjIdx == None: + principalObjIdx = len(transformNodes) - 1 + + # mc.polyMergeVertex(transfrmStr, d=0.015, am=False) + nname = 'hBevel' + nname = cPdoPolyBevel(edgeStrings, nname) + + cPhBevelAttrs(transfrmStr, nname) + + selIt.next() + + # set selection + + mc.select(transformNodes, r=True) + + # + + if principalObjIdx != None: + creasePlusHBevelCont(transformNodes[principalObjIdx]) + else: + raise crepexcept.cPexcept(crep.CpMsg.kNoHardEdges) + + +def cPplugNode(plugStr): + return plugStr[:plugStr.find('.')] + + +def cPhasbevelCage(shpStr): + + cpheNode = None + bevelNode = None + beveli = None + beveliCage = None + + hists = mc.listHistory(shpStr , bf=False, f=False, lf=True) + + for n in hists: + if mc.nodeType(n) == 'polyBevel3' and bevelNode == None: + bevelNode = n + elif mc.nodeType(n) == CreasePlusNodes.CpHeIds.kNodeName and cpheNode == None: + cpheNode= n + if cpheNode and bevelNode: + break + + if cpheNode and bevelNode: + beveli = shpStr + + if cpheNode and bevelNode and beveli: + plugs = mc.connectionInfo(bevelNode + '.inputPolymesh', sfd=True) + if isinstance(plugs, str) and mc.nodeType(cPplugNode(plugs)) == 'mesh': + beveliCage = cPplugNode(plugs) + else: + for plugStr in plugs: + if mc.nodeType(cPplugNode(plugStr)) == 'mesh': + beveliCage = cPplugNode(plugStr) + break + + return (cpheNode, bevelNode, beveli, beveliCage) + + + +def cPisbevelCage(cageshpstr): + + cpheNode = None + bevelNode = None + beveli = None + + plugs = mc.connectionInfo(cageshpstr + ".outMesh", dfs=True) + + if isinstance(plugs, str) and mc.nodeType(cPplugNode(plugs)) == CreasePlusNodes.CpHeIds.kNodeName: + cpheNode = cPplugNode(plugs) + else: + for plugStr in plugs: + if mc.nodeType(cPplugNode(plugStr)) == CreasePlusNodes.CpHeIds.kNodeName: + cpheNode = cPplugNode(plugStr) + break + + if cpheNode: + hists = mc.listHistory(cpheNode, bf=False, f=True, lf=True) + + for n in hists: + if mc.nodeType(n) == 'polyBevel3': + bevelNode = n + break + beveli = hists[-1:][0] + if mc.nodeType(beveli) != 'mesh': + beveli = None + + return (cpheNode, bevelNode, beveli) + + + +def creasePlusHBevelLiveCmd(): + global global_CP_Nodes_pluginstr + + # mel.eval("print(" + global_CP_Nodes_pluginstr + " ;") + + if not mc.pluginInfo(global_CP_Nodes_pluginstr, q=True, loaded=True): + try: + mc.loadPlugin(cPmainDir() + global_CP_Nodes_pluginstr + '.py') + except: + raise crepexcept.cPexcept(crep.CpMsg.kcPnodePluginNotLoaded) + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + if len(sel) != 1: + raise crepexcept.cPexcept(crep.CpMsg.kSelOneMesh) + sel = sel[0] + mc.select(sel, r=True) + + (cpheNode, bevelNode, beveli)= cPisbevelCage(sel) + if cpheNode and bevelNode and beveli: + creasePlusHBevelCont( + mc.listRelatives( + beveli, + parent=True, + noIntermediate=False, + typ='transform')[0] + ) + return + + mc.xform(sel,cp=True) + creasePlusHBevel() + srcPlugs = mc.connectionInfo(sel + '.inMesh', sfd=True) + bevelNode = None + if isinstance(srcPlugs, str) and mc.nodeType( + cPplugNode(srcPlugs)) == 'polyBevel3': + bevelNode = cPplugNode(srcPlugs) + else: + for plugStr in srcPlugs: + if mc.nodeType(cPplugNode(plugStr)) == 'polyBevel3': + bevelNode = cPplugNode(plugStr) + break + srcPlugs = mc.connectionInfo(bevelNode + '.inputPolymesh', sfd=True) + + sourceMesh = None + if isinstance(srcPlugs, str): + plugStr = srcPlugs + if mc.nodeType(cPplugNode(srcPlugs)) == 'mesh': + sourceMesh = cPplugNode(srcPlugs) + else: + plugStr = srcPlugs[0] + if mc.nodeType(cPplugNode(plugStr)) == 'mesh': + sourceMesh = cPplugNode(plugStr) + + if not sourceMesh: + sourceMesh = mc.createNode('mesh') + mc.disconnectAttr(plugStr, bevelNode + '.inputPolymesh') + mc.connectAttr(plugStr, sourceMesh + '.inMesh') + mc.connectAttr(sourceMesh + '.outMesh', bevelNode + '.inputPolymesh') + + sourcetrans = mc.listRelatives( + sourceMesh, + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + seltrans = mc.listRelatives( + sel, + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + mc.xform(sourcetrans,cp=True) + + + mc.connectAttr(seltrans + '.translate', sourcetrans + '.translate') + mc.connectAttr(seltrans + '.rotate', sourcetrans + '.rotate') + mc.connectAttr(seltrans + '.scale', sourcetrans + '.scale') + mc.disconnectAttr(seltrans + '.translate', sourcetrans + '.translate') + mc.disconnectAttr(seltrans + '.rotate', sourcetrans + '.rotate') + mc.disconnectAttr(seltrans + '.scale', sourcetrans + '.scale') + else: + mc.setAttr(sourceMesh + '.visibility', True) + mc.setAttr(sourceMesh + '.hiddenInOutliner', False) + mc.setAttr(sourceMesh + '.intermediateObject', False) + newtrans = mc.createNode('transform') + seltrans = mc.listRelatives( + sel, + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + + mc.connectAttr(seltrans + '.translate', newtrans + '.translate') + mc.connectAttr(seltrans + '.rotate', newtrans + '.rotate') + mc.connectAttr(seltrans + '.scale', newtrans + '.scale') + mc.disconnectAttr(seltrans + '.translate', newtrans + '.translate') + mc.disconnectAttr(seltrans + '.rotate', newtrans + '.rotate') + mc.disconnectAttr(seltrans + '.scale', newtrans + '.scale') + + mc.parent(sourceMesh, newtrans, shape=True, relative=True) + + mc.setAttr(sourceMesh + '.overrideEnabled', True) + mc.setAttr(sourceMesh + '.overrideShading', False) + mc.setAttr(sourceMesh + '.overrideColor', 5) + + idsnode = mc.createNode(CreasePlusNodes.CpHeIds.kNodeName) + mc.connectAttr(sourceMesh + '.outMesh', idsnode + '.i') + mc.connectAttr(idsnode + '.cl', bevelNode + '.inputComponents') + + sourcetrans = mc.listRelatives( + sourceMesh, + parent=True, + fullPath=True, + noIntermediate=False, + typ='transform')[0] + seltrans = mc.listRelatives( + sel, parent=True, fullPath=True, noIntermediate=False, + typ='transform')[0] + mc.connectAttr(sourcetrans + '.translate', seltrans + '.translate') + mc.connectAttr(sourcetrans + '.rotate', seltrans + '.rotate') + mc.connectAttr(sourcetrans + '.scale', seltrans + '.scale') + + mc.setAttr(seltrans + '.' + CpHBevelStat.offsetStr, 0.05) + mc.select(sourcetrans, r=True) + +@mayaUndoRun +def creasePlusHBevelLive(): + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kSelMesh) + + for i in range(len(sel)): + mc.select(sel[i],r=True) + creasePlusHBevelLiveCmd() + if i != len(sel)-1: + mc.setToolTo("moveSuperContext") + + +def cPautoCenterpiv(): + if len(mc.ls(sl=True)): + mc.xform(cp=True) + +@mayaUndoRun +def creasePlusDrawCurve(): + mc.setToolTo(crep.global_cPcurveCtxStr) + mc.scriptJob(cu=True, ro=True, e=['PostToolChanged', cPautoCenterpiv]) + +@mayaUndoRun +def creasePlusAttachCurve(): + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + + if len(selCurve) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kSelLeastTwoCurve) + + newCurve = mc.attachCurve( + selCurve, + ch=False, + rpo=True, + kmk=True, + m=0, + bb=0.5, + bki=False, + p=0.1, + )[0] + + # mc.delete(newCurve, ch=True) + + mc.select(newCurve, r=True) + + +@mayaUndoRun +def creasePlusCloseCurve(): + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + + if len(selCurve) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelCurve) + + for shapeStr in selCurve: + + curveForm = mc.getAttr(shapeStr + '.form') + + if mc.getAttr(shapeStr + '.degree') == 3: + + # mc.closeCurve( rpo=True) + + if curveForm == 0: + mc.closeCurve( + shapeStr, + ch=False, + ps=0, + rpo=True, + bb=0.5, + bki=True, + p=0.1, + ) + else: + if curveForm == 0: + mc.closeCurve( + shapeStr, + ch=False, + ps=1, + rpo=True, + bb=0.5, + bki=False, + p=0.1, + ) + + mc.select( + mc.listRelatives( + selCurve, parent=True, fullPath=True, typ='transform'), + r=True) + + +@mayaUndoRun +def creasePlusCurveIntersect(): + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + + if len(selCurve) < 2: + raise crepexcept.cPexcept(crep.CpMsg.kSelLeastTwoCurve) + + mel.eval('cutCurvePreset(0,1,0.01,6,0,1,0,1,2);') + mc.select(cl=True) + + +@mayaUndoRun +def creasePlusCurveDoubleCvs(): + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + + if len(selCurve) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelCurve) + + for shapeStr in selCurve: + dummy = om.MSelectionList() + dummy.add(shapeStr) + curveFn = om.MFnNurbsCurve(dummy.getDependNode(0)) + deg = curveFn.degree + + # mc.select(mc.listRelatives(shapeStr,parent=True,fullPath=True, typ="transform")[0], r=True) + + mel.eval('selectCurveCV all;') + + # newNumCv = (len(mc.ls(sl=True ,fl=True)) * 2) + + mc.rebuildCurve( + shapeStr, + ch=True, + rpo=True, + rt=0, + end=1, + kr=2, + kcp=False, + kep=True, + kt=False, + s=curveFn.numSpans * 2, + d=deg, + tol=0.01, + ) + + mc.select(mc.listRelatives(selCurve, parent=True, fullPath=True), r=True) + + +def cPsphereSegIntersect( + q, + v, + p, + r, +): + a = (v - q).length()**2 + b = 2 * ((v - q) * (q - p)) + c = p.length()**2 + q.length()**2 - 2 * (p * q) - r**2 + + sqr = b**2 - 4 * a * c + if sqr < 0 or a == 0: + return None + s = (-b + math.sqrt(sqr)) / (2 * a) + t = None + if sqr > 0: + t = (-b - math.sqrt(sqr)) / (2 * a) + + # intp = q + (s*(v-q)) + + return (1 * s, 1 * t) + + +def cPsegInsideSphere( + q, + v, + p, + r, +): + a = (q - p).length() + b = (v - p).length() + + if a < r and b < r: + return True + else: + return False + + +# do not use + + +def creasePlusCurveRebuild(numSeg, numCv=None): + + if numCv != None: + numSeg = numCv - 1 + + if numSeg < 1: + raise crepexcept.cPexcept(crep.CpMsg.kInvalidFuncArgs) + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + + if len(selCurve) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelCurve) + + dummy = om.MSelectionList() + dummy.add(selCurve[0]) + curveFn = om.MFnNurbsCurve(dummy.getDependNode(0)) + polyline = curveFn.cvPositions() + polyline = [om.MVector(pt) for pt in polyline] + + pllen = float(0) + for i in range(len(polyline) - 1): + p0 = polyline[i] + p1 = polyline[i + 1] + pllen += (p1 - p0).length() + + param = pllen / float(numSeg) + + # numCv = numSeg+1 + + i = 0 + newPolyline = [] + lastPt = polyline[i] + newPolyline.append(1 * lastPt) + while len(newPolyline) < numSeg and i < len(polyline) - 1: + while len(newPolyline) < numSeg and i < len(polyline) - 1: + + p0 = polyline[i] + p1 = polyline[i + 1] + + if cPsegInsideSphere(p0, p1, lastPt, 1 * param): + i += 1 + continue + + s = cPsphereSegIntersect(p0, p1, lastPt, 1 * param)[0] + + # respt = p0 + s * (p1-p0) + + if s == None: + i += 1 + continue + + if 0 <= s <= 1: + lastPt = p0 + s * (p1 - p0) + newPolyline.append(1 * lastPt) + break + + i += 1 + + newPolyline.append(1 * polyline[len(polyline) - 1]) + for pt in newPolyline: + mc.spaceLocator(p=[pt.x, pt.y, pt.z]) + + +# cPhasCurveBevelHistory("curveShape4") + + +def cPhasCurveBevelHistory(shape): + + # shape = "curveShape2" + + srcPlugs = mc.connectionInfo(shape + '.create', sfd=True) + if isinstance(srcPlugs, str) and mc.nodeType( + cPplugNode(srcPlugs)) == CreasePlusNodes.CpCurveBevel.kNodeName: + return cPplugNode(srcPlugs) + else: + for plugStr in srcPlugs: + if mc.nodeType(cPplugNode( + plugStr)) == CreasePlusNodes.CpCurveBevel.kNodeName: + return cPplugNode(plugStr) + + return None + + +def creasePlusCurveBevelCont(nodeString): + + # crep.global_cPcurveBevelCtxStr + + cur = None + if mc.currentCtx() == crep.global_cPcurveBevelCtxStr: + cur = crep.cPcurveBevelIterIncVal() + else: + cur = crep.cPcurveBevelIterSetVal(0) + + mc.dragAttrContext(crep.global_cPcurveBevelCtxStr, e=True, reset=True) + if cur == 0: + mc.dragAttrContext( + crep.global_cPcurveBevelCtxStr, e=True, ct=nodeString + '.offset') + elif cur == 1: + mc.dragAttrContext( + crep.global_cPcurveBevelCtxStr, + e=True, + ct=nodeString + '.segments') + + mc.setToolTo(crep.global_cPcurveBevelCtxStr) + + +@mayaUndoRun +def creasePlusCurveBevelCmd(): + + global global_CP_Nodes_pluginstr + if not mc.pluginInfo(global_CP_Nodes_pluginstr, q=True, loaded=True): + try: + mc.loadPlugin(cPmainDir() + global_CP_Nodes_pluginstr + '.py') + except: + raise crepexcept.cPexcept(crep.CpMsg.kcPnodePluginNotLoaded) + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + if len(selCurve) != 1: + raise crepexcept.cPexcept(crep.CpMsg.kWorksForOne) + + selCurve = selCurve[0] + + ctxActive = mc.currentCtx() == crep.global_cPcurveBevelCtxStr + node = None + try: + node = cPhasCurveBevelHistory(selCurve) + except: + node = None + + if ctxActive and node != None: + creasePlusCurveBevelCont(node) + return + + selCvs = mc.filterExpand(mc.ls(sl=True), sm=28, ex=True) + if selCvs == None: + raise crepexcept.cPexcept(crep.CpMsg.kSelCurveCv) + if len(selCvs) == 0: + if node != None: + creasePlusCurveBevelCont(node) + return + else: + raise crepexcept.cPexcept(crep.CpMsg.kSelCurveCv) + + node = mc.createNode(CreasePlusNodes.CpCurveBevel.kNodeName) + newcurveShape = mc.createNode('nurbsCurve') + + mc.setAttr( + node + '.cvs', + len(selCvs), + type='componentList', + *[curcv[curcv.find('.') + 1:] for curcv in selCvs]) + + mc.connectAttr(selCurve + '.worldSpace[0]', node + '.inc') + mc.connectAttr(node + '.out', newcurveShape + '.create') + + # + + creasePlusCurveBevelCont(node) + + +@mayaUndoRun +def creasePlusCurveToPolyCmd(): + + global global_CP_Nodes_pluginstr + if not mc.pluginInfo(global_CP_Nodes_pluginstr, q=True, loaded=True): + try: + mc.loadPlugin(cPmainDir() + global_CP_Nodes_pluginstr + '.py') + except: + raise crepexcept.cPexcept(crep.CpMsg.kcPnodePluginNotLoaded) + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + if len(selCurve) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelCurve) + + for shapeStr in selCurve: + node = mc.createNode(CreasePlusNodes.CpCurveToPoly.kNodeName) + npoly = mc.createNode('mesh') + + mc.connectAttr(shapeStr + '.worldSpace[0]', node + '.inc') + mc.connectAttr(node + '.out', npoly + '.inMesh') + + mc.sets(npoly, e=1, forceElement='initialShadingGroup') + + mc.select( + mc.listRelatives(npoly, parent=True, fullPath=True, typ='transform'), + r=True) + + +@mayaUndoRun +def creasePlusCurveSlice(): + + selCurve = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + selMesh = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(selCurve) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelCurve) + + if len(selMesh) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelMesh) + + dplane = crep.cPcameraDominantPlane() + + curveDupl = mc.listRelatives( + mc.duplicate(selCurve[0], renameChildren=True), + children=True, + fullPath=True, + typ='nurbsCurve')[0] + + dagFn = om.MFnDagNode() + dummy = om.MSelectionList() + + dummy.add(curveDupl) + dagFn.setObject(dummy.getDependNode(0)) + + curveBb = dagFn.boundingBox + curvemin = curveBb.min + curvemax = curveBb.max + + # print((curvemin) + + curveWhd = (abs(curvemax.x - curvemin.x), abs(curvemax.y - curvemin.y), + abs(curvemax.z - curvemin.z)) + + curveMaxOffset = 0 + if curveMaxOffset < curveWhd[0]: + curveMaxOffset = curveWhd[0] + if curveMaxOffset < curveWhd[1]: + curveMaxOffset = curveWhd[1] + if curveMaxOffset < curveWhd[2]: + curveMaxOffset = curveWhd[2] + + curveDepth = 0 + + i = 1 + finalSel = [] + for shapeStr in selMesh: + + # shapeStr = "pCubeShape1" + + dummy.add(shapeStr) + dagFn.setObject(dummy.getDependNode(i)) + + bb = dagFn.boundingBox + bbmin = bb.min + bbmax = bb.max + bbWhd = (abs(bbmax.x - bbmin.x), abs(bbmax.y - bbmin.y), + abs(bbmax.z - bbmin.z)) + + bbMaxOffset = 0 + if bbMaxOffset < bbWhd[0]: + bbMaxOffset = bbWhd[0] + if bbMaxOffset < bbWhd[1]: + bbMaxOffset = bbWhd[1] + if bbMaxOffset < bbWhd[2]: + bbMaxOffset = bbWhd[2] + + curveDepth = bbMaxOffset + curveDepth += curveMaxOffset + curveDepth * 0.1 + + dirIdx = None + extrudeVec = None + axisCenter = None + if dplane == 'x': + dirIdx = 0 + extrudeVec = [1, 0, 0] + axisCenter = bb.center.x + elif dplane == 'y': + dirIdx = 1 + extrudeVec = [0, 1, 0] + axisCenter = bb.center.y + elif dplane == 'z': + dirIdx = 2 + extrudeVec = [0, 0, 1] + axisCenter = bb.center.z + + # + + mel.eval( + 'nurbsToPolygonsPref -f 3 -ucr 0 -uch 0 -pt 0 -m 0 -mt 0.1 -mrt 0;' + ) + + mc.optionVar(iv=('extrudeDirectionType', dirIdx)) + mc.optionVar(fv=('extrudeLength', curveDepth)) + + nsurface = mc.extrude( + curveDupl, + ch=False, + rn=False, + po=1, + et=0, + upn=0, + direction=extrudeVec, + length=curveDepth, + ro=0, + sc=1, + dl=1, + )[0] + + if mc.getAttr(curveDupl + '.form') == 1: + mel.eval('polyCloseBorder -ch 0 ' + nsurface) + + mc.xform(nsurface, cp=True) + mc.delete(nsurface, ch=True) + + if dplane == 'x': + mel.eval('move -rpr -moveX ' + str(axisCenter) + ';') + elif dplane == 'y': + mel.eval('move -rpr -moveY ' + str(axisCenter) + ';') + elif dplane == 'z': + mel.eval('move -rpr -moveZ ' + str(axisCenter) + ';') + + mc.select([shapeStr, nsurface], r=True) + creasePlusPanelBool() + finalSel += mc.ls(sl=True) + i += 1 + + # creasePlusCurveSlice() + + mc.delete( + mc.listRelatives( + curveDupl, parent=True, fullPath=True, typ='transform')) + + # mc.delete(finalSel, ch=True) # delete final sel history + + mc.select(finalSel, r=True) + + +def cPdoUnfold(shapeStr): + if crep.getmayaver().num > 2016: + mel.eval( + 'u3dUnfold -ite 10 -p 1 -bi 1 -tf 1 -ms 1024 -rs 0 ' + shapeStr + ';') + else: + mel.eval( + 'Unfold3D -u -ite 10 -p 1 -bi 1 -tf 1 -ms 1024 -rs 0 ' + shapeStr + ';') + + +@mayaUndoRun +def creasePlusMakeUv(): + + if not mc.pluginInfo('Unfold3D', q=True, loaded=True): + raise crepexcept.cPexcept("\'Unfold3D\' plugin must be loaded.\n") + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kSelMesh) + + for shapeStr in sel: + + faces = mc.polyListComponentConversion(shapeStr, tf=True) + mc.polyProjection( + faces, + ch=True, + t='planar', + ibd=True, + kir=True, + md='c', + ) + + edgeStrings = crep.cPhardEdgesStrings(shapeStr) + + if len(edgeStrings) == 0: + raise crepexcept.cPexcept( + shapeStr + ' has no hard edges, UV generation failed.\n') + + mc.select(edgeStrings, r=True) + mc.polyMapCut(ch=True) + + cPdoUnfold(shapeStr) + + + mc.select( + mc.listRelatives(parent=True, fullPath=True, typ='transform'), r=True) + if len(mc.ls(sl=True)): + print('DONE!') + + +# SUBD TOOLS + + +def cPapplyCrease(sel, val, clearcrease=False): + if clearcrease: + mc.polyCrease(sel, op=2) + mc.polyCrease(sel, v=val) + + +def cPdisplayNotSmooth(shapeStr): + mc.setAttr(shapeStr + '.displaySmoothMesh', 0) + + +def cPdisplaySmooth(shapeStr): + mc.setAttr(shapeStr + '.displaySmoothMesh', 2) + + +def cPsmoothGroupsSubDAttrs(shapeStr, nodestr1, nodestr2): + + # shapeStr = "pCubeShape1" + + transfrmStr = mc.listRelatives( + shapeStr, parent=True, fullPath=True, typ='transform')[0] + mc.addAttr( + transfrmStr, + k=True, + ln='baseDiv', + at='short', + dv=2, + hnv=True, + min=0, + softMaxValue=5, + ) + mc.addAttr( + transfrmStr, + k=True, + ln='divisions', + at='short', + dv=1, + hnv=True, + min=0, + softMaxValue=5, + ) + mc.connectAttr(transfrmStr + '.baseDiv', nodestr1 + '.divisions') + mc.connectAttr(transfrmStr + '.divisions', nodestr2 + '.divisions') + + +def cPshowCreaseEd(): + mel.eval( + 'python ("from maya.app.general import creaseSetEditor; creaseSetEditor.showCreaseSetEditor()");' + ) + + +@mayaUndoRun +def creasePlusNocrease(): + objsel = mc.filterExpand(mc.ls(sl=True), ex=True, sm=12) + if objsel != None: + mc.polyCrease(op=2) + else: + mc.polyCrease(mc.ls(sl=True), op=1) + + +@mayaUndoRun +def creasePlusSmoothGroupsSubD(): + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kSelMesh) + + mel.eval('LowQualityDisplay;') + + for shapeStr in sel: + edgeStrings = crep.cPhardEdgesStrings(shapeStr) + if len(edgeStrings) == 0: + continue + + mc.polyCrease(shapeStr, op=2) + mc.polyCrease(edgeStrings, v=5.0) + + nnode1 = mc.polySmooth(shapeStr)[0] + + mc.polyCrease(shapeStr, op=2) + + nnode2 = mc.polySmooth(shapeStr)[0] + + cPsmoothGroupsSubDAttrs(shapeStr, nnode1, nnode2) + + mc.select( + mc.listRelatives(sel, parent=True, fullPath=True, typ='transform'), + r=True) + + +@mayaUndoRun +def creasePlusSubDpreset(): + + osel = mc.ls(sl=True) + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kSelMesh) + + mc.polyOptions(dce=False) + mel.eval('LowQualityDisplay;') + + for shapeStr in sel: + smtlvl = cPsetCreaseSmoothLevel(shapeStr) + mc.setAttr(shapeStr + '.smoothLevel', 2) + smoothNode = mc.polySmooth(shapeStr)[0] + + mc.setAttr(smoothNode + '.divisions', smtlvl) + + mc.select(osel, r=True) + + +def cPupdateSmoothLvl(): + + edgesel = mc.filterExpand(mc.ls(sl=True), ex=True, sm=32) + + if not edgesel: + raise crepexcept.cPexcept(crep.CpMsg.kNoSelEdge) + + # mc.polyOptions(dce = False) + + shapeStr = mc.listRelatives( + edgesel[0], parent=True, fullPath=True, typ='mesh')[0] + + mc.setAttr(shapeStr + '.osdSmoothTriangles', 1) + + cPsetCreaseSmoothLevel(shapeStr) + + +def cPsetCreaseSmoothLevel(shapeStr): + + # shapeStr = "pCube1" + + maxv = -1.0 + vals = mc.polyCrease(shapeStr + '.e[*]', q=True, v=True) + for cur in vals: + if maxv < cur: + maxv = cur + + # newlvl = math.ceil(maxv) + + newlvl = float(int(maxv) + 1) + + if newlvl < 1: + newlvl = 1 + + mc.setAttr(shapeStr + '.smoothLevel', newlvl) + + # mc.setAttr(shapeStr + ".smoothLevel", newlvl + 1) + + return newlvl + + +@mayaUndoRun +def creasePlusWeigthTool(): + + objsel = mc.filterExpand(mc.ls(sl=True), ex=True, sm=12) + edgesel = mc.filterExpand(mc.ls(sl=True), ex=True, sm=32) + + if objsel == None and edgesel == None: + raise crepexcept.cPexcept(crep.CpMsg.kSelEdgeOrOneMesh) + + if objsel != None: + edgesel = crep.cPhardEdgesStrings( + mc.listRelatives( + objsel[0], children=True, fullPath=True, typ='mesh')[0]) + if len(edgesel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoHardEdges) + + mc.polyOptions(dce=False) + shapeStr = mc.listRelatives(edgesel[0], parent=True, fullPath=True)[0] + mc.setAttr(shapeStr + '.osdSmoothTriangles', 1) + cPdisplaySmooth(shapeStr) + + crep.global_cPcreaseCtxStr + mc.setToolTo(crep.global_cPcreaseCtxStr) + mc.scriptJob(cu=True, ro=True, e=['PostToolChanged', cPupdateSmoothLvl]) + + +@mayaUndoRun +def creasePlusPhysicalCrease(): + # sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + # if len(sel) == 0: + # raise crepexcept.cPexcept(crep.CpMsg.kSelMesh) + + # mc.select(sel, r=True) + + if mc.currentCtx() == crep.global_hBevelCtxStr: + creasePlusHBevel() + return None + + creasePlusHBevel() + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + if len(sel) == 0: + return None + + for shapestr in sel: + transfrmStr = mc.listRelatives(shapestr, parent=True, type="transform")[0] + bevelNode = None + if mc.attributeQuery(CpHBevelStat.offsetStr, node=transfrmStr, ex=True): + destPlugs = mc.connectionInfo( + transfrmStr + '.' + CpHBevelStat.offsetStr, dfs=True) + for plugStr in destPlugs: + if mc.nodeType(cPplugNode(plugStr)) == 'polyBevel3': + bevelNode = cPplugNode(plugStr) + break + + if bevelNode: + nname = 'pCrease' + nname = mc.rename(bevelNode, nname) + bevelNode = nname + if crep.getmayaver().num > 2016 or (crep.getmayaver().num == 2016 and crep.getmayaver().extnum == 2): + mc.setAttr(bevelNode + ".chamfer" , False) + else: + mc.setAttr(bevelNode + ".segments", 2) + else: + pass + + + +@mayaUndoRun +def creasePlusCreasePreset(presetnum): + + # presetnum == 1, 2, 3 + + sel = om.MGlobal.getActiveSelectionList() + + if sel.length() == 0: + raise crepexcept.cPexcept(crep.CpMsg.kNoSel) + + selIt = om.MItSelectionList(sel) + + principalObjIdx = None + transformNodes = [] + while not selIt.isDone(): + + (shape, __, edges, faces) = crep.cPgetShapeAndCoStrings(selIt) + + if not shape: + selIt.next() + continue + + shapeStr = shape.partialPathName() + dagFn = om.MFnDagNode(shape) + dagFn.setObject(dagFn.parent(0)) + transfrmStr = dagFn.partialPathName() + transformNodes.append(transfrmStr) + + edgeStrings = None + + if faces: + edgeStrings = crep.cPfaceToHardEdgeStrings(shape, faces) + elif edges: + + edgeStrings = crep.cPedgeToStrings(shape, edges) + else: + + edgeStrings = crep.cPhardEdgesStrings(shape) + + # + + if len(edgeStrings) == 0: + selIt.next() + continue + elif principalObjIdx == None: + principalObjIdx = len(transformNodes) - 1 + + resetCrease = not(faces or edges) + if presetnum == 1: + cPapplyCrease(edgeStrings, 2.0, resetCrease) + cPsetCreaseSmoothLevel(shapeStr) + elif presetnum == 2: + cPapplyCrease(edgeStrings, 3.0, resetCrease) + cPsetCreaseSmoothLevel(shapeStr) + elif presetnum == 3: + cPapplyCrease(edgeStrings, 4.0, resetCrease) + cPsetCreaseSmoothLevel(shapeStr) + elif presetnum == 0: + cPapplyCrease(edgeStrings, 0.0, resetCrease) + cPsetCreaseSmoothLevel(shapeStr) + else: + raise crepexcept.cPexcept(crep.CpMsg.kInvalidFuncArgs) + + cPdisplaySmooth(shapeStr) + selIt.next() + + # set selection + + mc.select(transformNodes, r=True) + + # + + if principalObjIdx == None: + raise crepexcept.cPexcept(crep.CpMsg.kNoHardEdges) + else: + mc.polyOptions(dce=False) + + +############### + + +@mayaUndoRun +def cPcleanAttrs(): + sel = mel.eval( + 'listRelatives -p -f `eval("listRelatives -p -f `polyListComponentConversion -tv`")`' + ) + for trans in sel: + cusattr = mc.listAttr(trans, ud=True) + if cusattr != None: + for a in cusattr: + mel.eval('deleteAttr -at ' + a + ' ' + trans + ' ;') + + +def creasePlusLastCtx(): + meshsel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + curvesel = crep.cPgetShapeStringsFromSel(om.MFn.kNurbsCurve) + + if len(meshsel): + shapeStr = meshsel[0] + trnsfrm = mc.listRelatives( + meshsel[0], parent=True, fullPath=True, typ='transform')[0] + + if cPhasHBevelHistory(shapeStr): + creasePlusHBevelCont(trnsfrm) + elif cPhasBoolHistory(shapeStr): + creasePlusBoolOpCont(trnsfrm) + elif cPhasMirrorHistory(shapeStr): + creasePlusMirrorCont(trnsfrm) + elif len(curvesel): + shapeStr = curvesel[0] + trnsfrm = mc.listRelatives( + curvesel[0], parent=True, fullPath=True, typ='transform')[0] + curvebvlnode = cPhasCurveBevelHistory(shapeStr) + if curvebvlnode != None: + creasePlusCurveBevelCont(curvebvlnode) + + +def cPcopyHBevelAttrs(srcnode, targetnode): + offsetStr = CpHBevelStat.offsetStr + divStr = CpHBevelStat.divStr + miterStr = CpHBevelStat.miterStr + + a1 = mc.getAttr(srcnode + '.' + offsetStr) + a2 = mc.getAttr(srcnode + '.' + divStr) + a3 = mc.getAttr(srcnode + '.' + miterStr) + + mc.setAttr(targetnode + '.' + offsetStr, a1) + mc.setAttr(targetnode + '.' + divStr, a2) + mc.setAttr(targetnode + '.' + miterStr, a3) + + +@mayaUndoRun +def creasePlusTransferHBevel(): + + sel = crep.cPgetShapeStringsFromSel(om.MFn.kMesh) + + if len(sel) == 0: + raise crepexcept.cPexcept(crep.CpMsg.kSelMesh) + + if not cPhasHBevelHistory(sel[0]): + raise crepexcept.cPexcept('no Hbevel history found on first object') + + seltrans = mc.listRelatives( + sel[0], parent=True, fullPath=True, typ='transform')[0] + for shapeStr in sel[1:]: + if cPhasHBevelHistory(shapeStr): + cPcopyHBevelAttrs(seltrans, + mc.listRelatives( + shapeStr, + parent=True, + fullPath=True, + typ='transform')[0]) + + +############### + + +def main(): + return None + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlusEULA.pdf b/Scripts/Modeling/Edit/CreasePlus/CreasePlusEULA.pdf new file mode 100644 index 0000000000000000000000000000000000000000..347d4a66468033283cefb4d70b714ad28a1d0567 GIT binary patch literal 61704 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
APOZb_s3B(*`V&#ZLFae%{`2_g|q$CBUcx8mZ s{Cs@UV34E`HaUXeXFQTT;H!n`=w#&J^sB7FJVIa|f=7>}m1PM24=TF5mjD0& literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlusExcept.py b/Scripts/Modeling/Edit/CreasePlus/CreasePlusExcept.py new file mode 100644 index 0000000..733e6dd --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/CreasePlusExcept.py @@ -0,0 +1,6 @@ +import maya.api.OpenMaya as om + + +def cPexcept(excepstr=""): + om.MGlobal.displayError(excepstr) + return Exception(excepstr) \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlusMain.py b/Scripts/Modeling/Edit/CreasePlus/CreasePlusMain.py new file mode 100644 index 0000000..850ae0c --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/CreasePlusMain.py @@ -0,0 +1,1057 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +""" +_____ _____ ______ _____ ______ +/ ____| __ \| ____| /\ / ____| ____|_ +| | | |__) | |__ / \ | (___ | |__ _| |_ +| | | _ /| __| / /\ \ \___ \| __|_ _| +| |____| | \ \| |____ / ____ \ ____) | |____|_| +\_____|_| \_\______/_/ \_\_____/|______| + +""" + +from functools import partial +import math + +import os + +crease_plus_dir = os.path.dirname(os.path.abspath(__file__)).replace('\\', '/') +print(crease_plus_dir) +try: + from PySide2.QtGui import * + from PySide2.QtCore import * + from PySide2.QtWidgets import * + def whatpyside(): + return 2 + +except ImportError: + try: + from PySide.QtGui import * + from PySide.QtCore import * + def whatpyside(): + return 1 + except ImportError: + raise Exception("Couldn't import the PySide module.") + +import importlib + +# from shiboken2 import wrapInstance + +# import maya.OpenMayaUI as omui +import maya.api.OpenMaya as om +import maya.cmds as cmds + +from . import CreasePlusCore +crepcore = CreasePlusCore + +# TODO remove reloads +# crepcore = importlib.reload(crepcore) + +maya_useNewAPI = True + + +def cPgetScreenSz(): + rec = QApplication.desktop().screenGeometry() + w = rec.width() + h = rec.height() + return (w, h) + +global_cPscreensize = cPgetScreenSz() +def cPscreenSize(): + global global_cPscreensize + return global_cPscreensize + +def cPsizeRatio(w=None,h=None): + rw = None + rh = None + if w != None : + rw = float(w) / 1600 + if h != None : + rh=float(h) / 900 + + if rw != None and rh != None: + return (rw,rh) + elif rw != None: + return (rw) + elif rh != None: + return (rh) + + +def cPmayaMainWindow(): + # mayaPtr = omui.MQtUtil.mainWindow() + # mayaWindow = wrapInstance(int(mayaPtr), QWidget) + # return mayaWindow + try: + mainWindow = QApplication.activeWindow() + while True: + lastWin = mainWindow.parent() + if lastWin: + mainWindow = lastWin + else: + break + return mainWindow + except: + pass + + +# def cPmayaScriptDir(): +# return crease_plus_dir + +def cPiconDir(): + icon_dir = crease_plus_dir + '/Icons/' + return icon_dir + +# global_maya_script_dir = cmds.internalVar(usd=True) +# global_icons_dir = global_maya_script_dir + 'Icons/' + + +def icoStr(iconame): + # global global_icons_dir + # return global_icons_dir + 'crep_' + iconame + '_ico.png' + return cPiconDir() + 'crep_' + iconame + '_ico.png' + + +global_creasePlusMainUi = 'creasePlusMainUi' + +global_cPsideshapestyle1 = ''' +QWidget { +border: 0px solid #2e3234; +background: #db9456; +font-size: 0px; +border-radius: 0px; +color: #ffffff; +} +''' + +global_cPsideshapestyle2 = ''' +QWidget { +border: 0px solid #2e3234; +background: #d5703f; +font-size: 0px; +border-radius: 0px; +color: #ffffff; +} +''' + +global_cPsideshapestyle3 = ''' +QWidget { +border: 0px solid #2e3234; +background: #5ebae9; +font-size: 0px; +border-radius: 0px; +color: #ffffff; +} +''' + +global_defInfodic = { + 'toolName': 'SomeTool', + 'toolDesc': 'tool description', + 'toolHelp': 'tool help' +} + +class CreasePlusMainPage(QWidget): + def __init__(self): + super(CreasePlusMainPage, self).__init__() + self.mainLay = QVBoxLayout() + self.setLayout(self.mainLay) + self.layout().setContentsMargins(2, 2, 2, 2) + + +class CreasePlusInfopop(QDialog): + def __init__(self, parent=None): + super(CreasePlusInfopop, self).__init__(parent) + + self.setStyleSheet(''' +QWidget { +border: 2px solid #66696c; +background: #202122; +border-radius: 4px; +color: #ffffff; +} +''') + + self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup + | Qt.WA_TranslucentBackground) + self.setAttribute(Qt.WA_DeleteOnClose) + self.setWindowOpacity(1) + + self.mainLay = QVBoxLayout() + self.setLayout(self.mainLay) + self.mainLay.setSpacing(1) + self.mainLay.setContentsMargins(8, 8, 8, 8) + + self.titleico = QLabel() + self.titleico.setStyleSheet(''' +QWidget { +border: 0px solid #2e3234; +background: transparent; +font-size: 14px; +border-radius: 0px; +color: #ffffff; +}''') + self.mainLay.addWidget(self.titleico) + + self.description = QLabel() + self.description.setAlignment(Qt.AlignLeft) + self.description.setStyleSheet(''' +QWidget { +border: 0px solid #2e3234; +background: #454545; +font-size: 12px; +border-radius: 4px; +color: #ffffff; +} +''') + self.mainLay.addWidget(self.description) + + def resizeEvent(self, event): + super(CreasePlusInfopop, self).resizeEvent(event) + + radius = 5.0 + painterpath = QPainterPath() + painterpath.addRoundedRect(QRectF(self.rect()), radius, radius) + maskedRegion = QRegion(painterpath.toFillPolygon().toPolygon()) + self.setMask(maskedRegion) + + +class CreasePlusBtn(QWidget): + + pressed = Signal() + global global_defInfodic + + def __init__( + self, + pmap=QPixmap(), + lbl='', + infobubble=None, + infodic=global_defInfodic, + parent=None, + ): + + super(CreasePlusBtn, self).__init__(parent) + + self.setStyleSheet(''' + QWidget {border: 0px solid #ffffff;background: #444444;border-radius: 0px;color: #ffffff; + }\nQWidget :hover {border: 0px solid #ffffff;background: #5e7876;border-radius: 3px;color: #ffffff; + }''') + + self.infoBubble = infobubble + self.infodic = infodic + + self.mainLay = QVBoxLayout() + self.setLayout(self.mainLay) + self.mainLay.setSpacing(0) + self.mainLay.setContentsMargins(2, 2, 2, 2) + + self.btn = QPushButton() + self.btn.setStyleSheet(''' + QWidget {border: 0px solid #ffffff;background: transparent;border-radius: 0px;color: #ffffff; + }''') + + self.btn.setIcon(pmap) + self.btn.setSizePolicy(QSizePolicy.Ignored,QSizePolicy.MinimumExpanding) + self.mainLay.addWidget(self.btn) + + def setPixmap(self, pmap): + self.btn.setIcon(pmap) + + def setPixmapSize(self, sz): + self.btn.setIconSize(sz) + + def setInfoDic(self, infodic): + self.infodic = infodic + + # self.btnLabel.setText(self.infodic["toolName"]) + + def mousePressEvent(self, event): + super(CreasePlusBtn, self).mousePressEvent(event) + self.pressed.emit() + + def pressedConnect(self, func): + self.btn.pressed.connect(func) + + # self.pressed.connect(func) + + def enterEvent(self, event): + super(CreasePlusBtn, self).enterEvent(event) + if self.infoBubble: + self.infoBubble.setWindowOpacity(1) + + self.infoBubble.titleico.setText(self.infodic['toolName']) + self.infoBubble.description.setText(self.infodic['toolDesc']) + + def leaveEvent(self, event): + super(CreasePlusBtn, self).leaveEvent(event) + if self.infoBubble: + self.infoBubble.setWindowOpacity(0) + + def paintEvent(self, event): + if not isinstance(event, QCloseEvent): + + super(CreasePlusBtn, self).paintEvent(event) + + painter = QPainter(self) + + option = QStyleOption() + if whatpyside() == 1: + option.initFrom(self) + else: + option.init(self) + style = self.style() + style.drawPrimitive(QStyle.PE_Widget, option, painter, self) + + +class CreasePlusSideShape(QWidget): + + pressed = Signal() + + def __init__(self, parent=None): + + super(CreasePlusSideShape, self).__init__(parent) + self.maskShape = QPixmap() + + self.setWindowFlags(Qt.FramelessWindowHint | Qt.Popup + | Qt.WA_TranslucentBackground) + self.setAttribute(Qt.WA_DeleteOnClose) + self.setWindowOpacity(1) + global global_cPsideshapestyle1 + self.setStyleSheet(global_cPsideshapestyle1) + + self.mainLay = QVBoxLayout() + self.setLayout(self.mainLay) + + self.resize(32, 32) + + def setMaskShape(self, maskshape): + self.maskShape = maskshape + + def resizeEvent(self, event): + super(CreasePlusSideShape, self).resizeEvent(event) + + self.resize(self.maskShape.size()) + self.setMask(self.maskShape.mask()) + + def mousePressEvent(self, event): + super(CreasePlusSideShape, self).mousePressEvent(event) + self.pressed.emit() + +def creaseplusclosethefucknwinbefoh(clientobj = None): + global global_creasePlusMainUi + if cmds.window(global_creasePlusMainUi,ex=True): + # print("hey here!") + cmds.deleteUI(global_creasePlusMainUi, wnd=True) + + +class CreasePlusMain(QWidget): + + gripsz = cPsizeRatio(w=10) * cPscreenSize()[0] + gripszMarg = (2, 2) + + numPages = 3 + + def __init__(self, parent=None): + + super(CreasePlusMain, self).__init__(parent) + + self.noOpacity = False + self.origw = 35 + self.origh = 370 + self.hostApp = parent + self.countDown = 50 + global global_creasePlusMainUi + self.setObjectName(global_creasePlusMainUi) + + self.setStyleSheet(''' + QWidget {border: 0px solid #ffffff;background: #444444;border-radius: 0px;color: #ffffff; + }''') + + self.pageIndex = 0 + self.pages = [ + CreasePlusMainPage() for i in range(CreasePlusMain.numPages) + ] + self.pageBtns = {} + self.oldPos = QPoint() + self.resizing = False + self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint) + self.setAttribute(Qt.WA_DeleteOnClose) + # register callbacks + self.mayaCallbacks = [] + cbid = om.MSceneMessage.addCallback(om.MSceneMessage.kBeforeNew, creaseplusclosethefucknwinbefoh) + self.mayaCallbacks.append(cbid) + cbid = om.MSceneMessage.addCallback(om.MSceneMessage.kBeforeOpen, creaseplusclosethefucknwinbefoh) + self.mayaCallbacks.append(cbid) + cbid = om.MSceneMessage.addCallback(om.MSceneMessage.kMayaExiting, creaseplusclosethefucknwinbefoh) + self.mayaCallbacks.append(cbid) + + # + closeAction = QAction( + 'Close', self, shortcut='Alt+F4', triggered=self.close) + self.addAction(closeAction) + + self.setWindowTitle('CreasePlus') + + self.mainLay = QGridLayout() + self.setLayout(self.mainLay) + + self.mainLay.setContentsMargins(2, 16, 2, 16) + self.mainLay.setVerticalSpacing(4) + self.menuBar = QMenuBar() + self.menuBar.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) + self.mainLay.addWidget(self.menuBar, 0, 0) + + self.spaceitm1 = QSpacerItem(0, cPsizeRatio(h=20) * cPscreenSize()[1], QSizePolicy.Ignored, + QSizePolicy.MinimumExpanding) + self.mainLay.addItem(self.spaceitm1, 1, 0) + + self.doMenus() + + self.hoverDelay = 0 + self.infoBubble = CreasePlusInfopop(parent=self) + self.nextWid = CreasePlusSideShape(parent=self) + self.nextWid.pressed.connect(self.onClick_next) + + + nextWidSize = cPsizeRatio(w=10) * cPscreenSize()[0] + self.nextWid.setMaskShape( + QPixmap(icoStr('next')).scaled(nextWidSize, nextWidSize, Qt.IgnoreAspectRatio, + Qt.SmoothTransformation)) + + self.timer = QTimer() + self.timer.timeout.connect(self.opacityModifier) + self.timer.start(self.countDown) + + self.doFirstPage() + self.doSecondPage() + self.doThirdPage() + + self.stackedWidget = QStackedWidget() + for page in self.pages: + self.stackedWidget.addWidget(page) + self.mainLay.addWidget(self.stackedWidget, 2, 0) + + self.infoBubble.show() + self.infoBubble.setWindowOpacity(0) + self.nextWid.show() + + self.mainRecRatio = (cPsizeRatio(w=self.origw), cPsizeRatio(h=self.origh)) + w = self.mainRecRatio[0] * cPscreenSize()[0] + h = self.mainRecRatio[1] * cPscreenSize()[1] + self.setMaximumWidth(w) + self.setMinimumWidth(w) + + # print((self.maximumWidth() ) + + self.resize(w, h) + + + def doMenus(self): + + deleteCpAttrsAc = QAction( + 'Clean Attributes', self, triggered=crepcore.cPcleanAttrs) + + + cleanHBevelLiveAc = QAction( + 'Clean HBevel Live', self, triggered=crepcore.creasePlusBakeHBL) + + toggleLastAc = QAction( + 'Toggle Last', self, triggered=crepcore.creasePlusLastCtx) + transferHBevelAc = QAction( + 'Transfer HBevel', + self, + triggered=crepcore.creasePlusTransferHBevel) + invkCreaseSetAc = QAction( + 'Crease Set Editor', self, triggered=crepcore.cPshowCreaseEd) + + self.miscMenu = QMenu('Miscs', self) + self.miscMenu.setIcon(QPixmap(icoStr('menu'))) + self.miscMenu.addAction(cleanHBevelLiveAc) + self.miscMenu.addAction(toggleLastAc) + self.miscMenu.addAction(transferHBevelAc) + self.miscMenu.addAction(deleteCpAttrsAc) + self.miscMenu.addAction(invkCreaseSetAc) + + self.menuBar.addMenu(self.miscMenu) + + def opacityOverride(self, o): + if self.noOpacity == False: + self.setWindowOpacity(o) + else: + self.setWindowOpacity(1) + def opacityModifier(self): + + # curpage = self.pages[self.stackedWidget.currentIndex()] + + try: + if self.underMouse(): + self.timer.stop() + else: + + if not self.timer.isActive(): + self.timer.start() + if self.hoverDelay >= self.countDown: + self.hoverDelay = 0 + self.opacityOverride(0.1) + self.timer.stop() + else: + self.hoverDelay += 1 + opacity = 1.0 - float(self.hoverDelay) / float( + self.countDown) + self.opacityOverride(max(0.1, opacity)) + except: + + try: + self.timer.stop() + except: + pass + + def reposNextWid(self): + + # pass + + spaceitmLeft = self.spaceitm1.geometry().topLeft() + spaceitmHeight = abs( + self.spaceitm1.geometry().bottomLeft().y() - spaceitmLeft.y()) + self.nextWid.move( + self.mapToGlobal(self.rect().topRight()).x(), + self.mapToGlobal(spaceitmLeft + QPoint( + 0, spaceitmHeight / 2 - self.nextWid.height() * 0.25)).y()) + + + def doFirstPage(self): + + pageLay = self.pages[0].layout() + pageLay.setSpacing(0) + + numbtns = 12 + + listBtns = [] + + icosz = cPsizeRatio(w=20) * cPscreenSize()[0] + for i in range(numbtns): + listBtns.append(CreasePlusBtn(infobubble=self.infoBubble)) + listBtns[i].setPixmapSize(QSize(icosz, icosz)) + + i = 0 + + listBtns[i].setPixmap(QPixmap(icoStr('bool'))) + listBtns[i].setInfoDic({ + 'toolName': 'Bool Op', + 'toolDesc': 'Performs non destructive boolean.' + }) + listBtns[i].pressedConnect(partial(crepcore.creasePlusBool, False)) + i += 1 + + + listBtns[i].setPixmap(QPixmap(icoStr('panelbool'))) + listBtns[i].setInfoDic({ + 'toolName': 'Panel Bool', + 'toolDesc': 'Performs panel bool operation.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusPanelBool) + i += 1 + + + + # + + listBtns[i].setPixmap(QPixmap(icoStr('smoothangle'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Smooth 30', + 'toolDesc': + 'Fixes and Smooths normals by a 30 degree angle.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusSmooth30) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('displayhe'))) + listBtns[i].setInfoDic({ + 'toolName': 'Display HardEdges', + 'toolDesc': 'Toggles display of hard edges.' + }) + listBtns[i].pressedConnect( + partial(crepcore.creasePlusDisplayHardEdges, 0)) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('bevel'))) + listBtns[i].setInfoDic({ + 'toolName': + 'HBevel', + 'toolDesc': + 'Bevels hard edges , based on selection.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusHBevel) + i += 1 + # + + + listBtns[i].setPixmap(QPixmap(icoStr('bevellive'))) + listBtns[i].setInfoDic({ + 'toolName': + 'HBevel Live', + 'toolDesc': + 'HBevel as a node, with a cage mesh (interactive).' + }) + listBtns[i].pressedConnect(crepcore.creasePlusHBevelLive) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('shapeshifter'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Shape Shifter', + 'toolDesc': + 'Calls ShapeShifter script (3rd party).' + }) + listBtns[i].pressedConnect(crepcore.creasePlusShapeShifter) + i += 1 + + + + + listBtns[i].setPixmap(QPixmap(icoStr('meshslicer'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Mesh Slicer', + 'toolDesc': + 'Slices the mesh in x,y,z direction based on camera , with a curve.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusCurveSlice) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('hardedge'))) + listBtns[i].setInfoDic({ + 'toolName': 'Sel HardEdges', + 'toolDesc': 'Selects hard edges.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusSelHardEdges) + i += 1 + + + + listBtns[i].setPixmap(QPixmap(icoStr('mirror'))) + listBtns[i].setInfoDic({ + 'toolName': 'Mirror', + 'toolDesc': 'Mirrors selected meshes.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusMirror) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('uv'))) + listBtns[i].setInfoDic({ + 'toolName': 'Make UV', + 'toolDesc': "Makes UV's based on hard edges." + }) + listBtns[i].pressedConnect(crepcore.creasePlusMakeUv) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('zbrush'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Goz', + 'toolDesc': + 'Exports selection in Zbrush without ngons.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusGoz) + i += 1 + + for btn in listBtns: + pageLay.addWidget(btn) + + self.pageBtns['page1'] = listBtns + + def doSecondPage(self): + pageLay = self.pages[1].layout() + pageLay.setSpacing(0) + + numbtns = 8 + + listBtns = [] + + icosz = cPsizeRatio(w=20) * cPscreenSize()[0] + for i in range(numbtns): + listBtns.append(CreasePlusBtn(infobubble=self.infoBubble)) + listBtns[i].setPixmapSize(QSize(icosz, icosz)) + + i = 0 + listBtns[i].setPixmap(QPixmap(icoStr('crease'))) + listBtns[i].setInfoDic({ + 'toolName': 'Crease1', + 'toolDesc': 'Applies first creasing preset.' + }) + listBtns[i].pressedConnect(partial(crepcore.creasePlusCreasePreset, 1)) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('crease'))) + listBtns[i].setInfoDic({ + 'toolName': 'Crease2', + 'toolDesc': 'Applies second creasing preset.' + }) + listBtns[i].pressedConnect(partial(crepcore.creasePlusCreasePreset, 2)) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('crease'))) + listBtns[i].setInfoDic({ + 'toolName': 'Crease3', + 'toolDesc': 'Applies third creasing preset.' + }) + listBtns[i].pressedConnect(partial(crepcore.creasePlusCreasePreset, 3)) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('weighttool'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Creasing Tool', + 'toolDesc': + 'Invokes the interactive creasing tool.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusWeigthTool) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('displayhe'))) + listBtns[i].setInfoDic({ + 'toolName': 'NoCrease', + 'toolDesc': 'Remove creasing on selection.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusNocrease) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('hardedge'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Smooth SG', + 'toolDesc': + 'Subdivides based on smoothing groups (retains hard edges shape).' + }) + listBtns[i].pressedConnect(crepcore.creasePlusSmoothGroupsSubD) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('physcrease'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Physical Crease', + 'toolDesc': + 'Adds edge loops around selection or hard edges.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusPhysicalCrease) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('subd'))) + listBtns[i].setInfoDic({ + 'toolName': 'Smooth', + 'toolDesc': 'Invokes subdivision smooth preset.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusSubDpreset) + i += 1 + + for btn in listBtns: + pageLay.addWidget(btn) + + self.pageBtns['page2'] = listBtns + + def doThirdPage(self): + pageLay = self.pages[2].layout() + pageLay.setSpacing(0) + + numbtns = 7 + + listBtns = [] + + icosz = cPsizeRatio(w=20) * cPscreenSize()[0] + for i in range(numbtns): + listBtns.append(CreasePlusBtn(infobubble=self.infoBubble)) + listBtns[i].setPixmapSize(QSize(icosz, icosz)) + + i = 0 + listBtns[i].setPixmap(QPixmap(icoStr('curvedraw'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Draw Curve', + 'toolDesc': + 'Enter linear curve drawing context.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusDrawCurve) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('curvepoly'))) + listBtns[i].setInfoDic({ + 'toolName': 'Curve To Polygon', + 'toolDesc': 'Makes polygon out of curve.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusCurveToPolyCmd) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('curveclose'))) + listBtns[i].setInfoDic({ + 'toolName': 'Close Curve', + 'toolDesc': 'Closes Curve(s).' + }) + listBtns[i].pressedConnect(crepcore.creasePlusCloseCurve) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('curvebevel'))) + listBtns[i].setInfoDic({ + 'toolName': 'Bevel Curve', + 'toolDesc': 'Bevels Curve Cv.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusCurveBevelCmd) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('curveint'))) + listBtns[i].setInfoDic({ + 'toolName': 'Curve Cuts', + 'toolDesc': 'Cuts curve with selected curves.' + }) + listBtns[i].pressedConnect(crepcore.creasePlusCurveIntersect) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('curveattach'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Curve Attach', + 'toolDesc': + "Attaches two curves at joining/close CV's." + }) + listBtns[i].pressedConnect(crepcore.creasePlusAttachCurve) + i += 1 + + listBtns[i].setPixmap(QPixmap(icoStr('curvemult'))) + listBtns[i].setInfoDic({ + 'toolName': + 'Curve Multiply', + 'toolDesc': + "Rebuilds to double the number of CV's" + }) + listBtns[i].pressedConnect(crepcore.creasePlusCurveDoubleCvs) + i += 1 + + for btn in listBtns: + pageLay.addWidget(btn) + + self.pageBtns['page3'] = listBtns + + + def onClick_leftSqr(self): + self.setWindowOpacity(1) + self.noOpacity = not self.noOpacity + + def onClick_eye(self): + # print("eye clicked") + # cmds.select("pCube*", r=True) + crepcore.creasePlusToggleBoolGhost() + + def onClick_next(self): + # print("next clicked") + + self.pageIndex = (self.pageIndex + 1) % CreasePlusMain.numPages + + global global_cPsideshapestyle1 + global global_cPsideshapestyle2 + global global_cPsideshapestyle3 + + if self.pageIndex == 0 : + self.nextWid.setStyleSheet(global_cPsideshapestyle1) + elif self.pageIndex == 1 : + self.nextWid.setStyleSheet(global_cPsideshapestyle2) + elif self.pageIndex == 2 : + self.nextWid.setStyleSheet(global_cPsideshapestyle3) + + self.stackedWidget.setCurrentIndex(self.pageIndex) + + def mouseInEye(self, mousePos): + + icosz = cPsizeRatio(w=17) * cPscreenSize()[0] + spaceitmLeft = self.spaceitm1.geometry().topLeft() + spaceitmHeight = abs( + self.spaceitm1.geometry().bottomLeft().y() - spaceitmLeft.y()) + minx = self.width() / 2 - icosz / 2 + miny = spaceitmLeft.y() + spaceitmHeight / 2 - icosz / 2 + return mousePos.x() > minx and mousePos.x( + ) < minx + icosz and mousePos.y() > miny and mousePos.y( + ) < miny + icosz * 0.75 + + + + def mouseInLeftSqr(self, mousePos): + return mousePos.x() < CreasePlusMain.gripsz + CreasePlusMain.gripszMarg[0] and mousePos.y( + ) > self.height() - CreasePlusMain.gripsz - CreasePlusMain.gripszMarg[1] + + def mouseInGrip(self, mousePos): + return mousePos.x() > self.width() - CreasePlusMain.gripsz - CreasePlusMain.gripszMarg[0] and mousePos.y( + ) > self.height() - CreasePlusMain.gripsz - CreasePlusMain.gripszMarg[1] + + def mouseInCloseSqr(self, mousePos): + return mousePos.x() > self.width() - CreasePlusMain.gripsz - CreasePlusMain.gripszMarg[0] and mousePos.y( + ) < CreasePlusMain.gripsz + CreasePlusMain.gripszMarg[1] + + def mousePressEvent(self, event): + super(CreasePlusMain, self).mousePressEvent(event) + if event.button() == Qt.LeftButton: + self.resizing = False + self.dragPosition = event.globalPos() - self.frameGeometry().topLeft() + if self.mouseInEye(event.pos()): + self.onClick_eye() + elif self.mouseInGrip(event.pos()): + self.oldPos = event.pos() + self.resizing = True + elif self.mouseInLeftSqr(event.pos()): + self.onClick_leftSqr() + elif self.mouseInCloseSqr(event.pos()): + event.accept() + self.close() + event.accept() + + def enterEvent(self, event): + super(CreasePlusMain, self).enterEvent(event) + + + self.hoverDelay = 0 + + # curpage = self.pages[self.stackedWidget.currentIndex()] + + self.setWindowOpacity(1) + + # if self.mouseInCloseSqr(QCursor.pos()): + # self.infoBubble.titleico.setText('dummy') + # self.infoBubble.description.setText('dummydesc') + # self.infoBubble.setWindowOpacity(1) + + # print("enter") + + def leaveEvent(self, event): + super(CreasePlusMain, self).leaveEvent(event) + self.infoBubble.setWindowOpacity(0) + + if self.timer: + if not self.timer.isActive(): + + # print("leave, restart timer...") + + self.timer.start(self.countDown) + + def mouseMoveEvent(self, event): + super(CreasePlusMain, self).mouseMoveEvent(event) + if event.buttons() == Qt.LeftButton: + if self.resizing: + delta = event.pos() - self.oldPos + self.oldPos = event.pos() + self.setMaximumWidth(16777215) + self.resize(self.width() + delta.x(), + self.height() + delta.y()) + event.accept() + self.updateGeometry() + else: + + self.move(event.globalPos() - self.dragPosition) + self.reposNextWid() + event.accept() + + def resizeEvent(self, event): + super(CreasePlusMain, self).resizeEvent(event) + + radius = 2.0 + painterpath = QPainterPath() + painterpath.addRoundedRect(QRectF(self.rect()), radius, radius) + maskedRegion = QRegion(painterpath.toFillPolygon().toPolygon()) + self.setMask(maskedRegion) + + def paintEvent(self, event): + + super(CreasePlusMain, self).paintEvent(event) + + painter = QPainter(self) + painter.setRenderHint(QPainter.Antialiasing) + painter.setOpacity(0.3) + + + painter.drawRoundedRect( + self.width() - CreasePlusMain.gripsz - + CreasePlusMain.gripszMarg[0], + self.height() - CreasePlusMain.gripsz - + CreasePlusMain.gripszMarg[1], + CreasePlusMain.gripsz, + CreasePlusMain.gripsz, + 1, + 1) + + painter.setPen(Qt.NoPen) + painter.setOpacity(1) + painter.setBrush(QColor(32+7,32+7,32+7)) + + painter.drawRoundedRect( + CreasePlusMain.gripszMarg[0], + self.height() - CreasePlusMain.gripsz - CreasePlusMain.gripszMarg[1], + CreasePlusMain.gripsz, + CreasePlusMain.gripsz, 3,3) + + + painter.setOpacity(1) + painter.setPen(Qt.NoPen) + + # nextico = QPixmap(icoStr("next")) + # icosz = CreasePlusMain.gripsz + # painter.drawPixmap(CreasePlusMain.gripszMarg[0], self.height()-icosz-CreasePlusMain.gripszMarg[1], + # nextico.scaled(icosz, icosz, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)) + + closeico = QPixmap(icoStr('close')) + icosz = CreasePlusMain.gripsz + painter.drawPixmap(self.width() - icosz - CreasePlusMain.gripszMarg[0], + CreasePlusMain.gripszMarg[1], + closeico.scaled(icosz, icosz, Qt.IgnoreAspectRatio, + Qt.SmoothTransformation)) + + eyeico = QPixmap(icoStr('eye')) + icosz = cPsizeRatio(w=17) * cPscreenSize()[0] + + try: + self.spaceitm1 + except: + start() + return None + spaceitmLeft = self.spaceitm1.geometry().topLeft() + spaceitmHeight = abs( + self.spaceitm1.geometry().bottomLeft().y() - spaceitmLeft.y()) + painter.drawPixmap(self.width() / 2 - icosz / 2, + spaceitmLeft.y() + spaceitmHeight / 2 - icosz / 2, + eyeico.scaled(icosz, icosz, Qt.IgnoreAspectRatio, + Qt.SmoothTransformation)) + + infbubx = self.mapToGlobal(self.rect().topRight()).x() + 4 + self.infoBubble.resize(0, 0) + self.infoBubble.move(infbubx, QCursor.pos().y()) + + self.reposNextWid() + + def closeEvent(self, event): + super(CreasePlusMain, self).closeEvent(event) + + # print("CLOSEEVENT") + try: + self.mayaCallbacks + except: + pass + else: + for cb in self.mayaCallbacks: + om.MMessage.removeCallback(cb) + + try: + self.timer + except: + pass + else: + self.timer.stop() + self.timer = None + # self.infoBubble.close() + # self.nextWid.close() + + def dummy(self): + print(('dummy')) + + + +def start(): + # self = CreasePlusMain(parent=cPmayaMainWindow()) + # self.show() + creaseplusclosethefucknwinbefoh() + + mywin = CreasePlusMain(parent=cPmayaMainWindow()) + mywin.show() \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlusNodes.py b/Scripts/Modeling/Edit/CreasePlus/CreasePlusNodes.py new file mode 100644 index 0000000..7cf496e --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/CreasePlusNodes.py @@ -0,0 +1,507 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +""" +_____ _____ ______ _____ ______ +/ ____| __ \| ____| /\ / ____| ____|_ +| | | |__) | |__ / \ | (___ | |__ _| |_ +| | | _ /| __| / /\ \ \___ \| __|_ _| +| |____| | \ \| |____ / ____ \ ____) | |____|_| +\_____|_| \_\______/_/ \_\_____/|______| + +""" + +# import maya.cmds as mc +import maya.api.OpenMaya as om +# import copy + +# uses python maya 2 + +maya_useNewAPI = True + +################# + + +def cPhardEdgeIds(mesh): + edgeIter = om.MItMeshEdge(mesh) + ids = [] + while not edgeIter.isDone(): + if not edgeIter.isSmooth: + ids.append(edgeIter.index()) + edgeIter.next() + return ids + + +def cPcompToIds(compdata, cptyp): + compDataFn = om.MFnComponentListData(compdata) + # compDataFn + ids = [] + for i in range(compDataFn.length()): + curcomp = compDataFn.get(i) + if curcomp.hasFn(cptyp): + sic = om.MFnSingleIndexedComponent(curcomp) + for j in range(sic.elementCount): + curIdx = sic.element(j) + ids.append(curIdx) + return ids + + +def cPidsToComp(ids, cptyp): + sic = om.MFnSingleIndexedComponent() + sic.create(cptyp) # om.MFn.kMeshEdgeComponent + + sic.addElements(ids) + + compData = om.MFnComponentListData() + compObj = compData.create() + compData.add(sic.object()) + return (sic.object(), compObj) + + +def cPtransformMeshPoints(mesh, mat): + meshFn = om.MFnMesh(mesh) + pts = meshFn.getPoints() + for pt in pts: + pt *= mat + + meshFn.setPoints(pts) + + +""" +input: mesh +output: componentlist +""" + +MayaNodeT = om.MPxNode + + +class CpCurveBevel(MayaNodeT): + + kNodeName = "creasePlusCurveBevel" + kNodeId = om.MTypeId(0x1154) + + aCvs = None + aOffset = None + aSeg = None + aOffsetFrac = None + aInputCurve = None + aOutput = None + + def __init__(self): + super(CpCurveBevel, self).__init__() + + @classmethod + def creator(cls): + return cls() + + @classmethod + def initialize(cls): + + nAttr = om.MFnNumericAttribute() + tAttr = om.MFnTypedAttribute() + + cls.aCvs = tAttr.create("cvComponentList", "cvs", + om.MFnComponentListData.kComponentList) + + cls.aOffset = nAttr.create("offset", "off", om.MFnNumericData.kFloat) + nAttr.setMin(0) + nAttr.default = 0.5 + nAttr.keyable = True + cls.aSeg = nAttr.create("segments", "seg", om.MFnNumericData.kInt) + nAttr.setMin(1) + nAttr.default = 1 + nAttr.keyable = True + + cls.aOffsetFrac = nAttr.create("offsetAsFraction", "oaf", + om.MFnNumericData.kBoolean) + nAttr.default = False + nAttr.keyable = True + + cls.aInputCurve = tAttr.create("inCurve", "inc", + om.MFnData.kNurbsCurve) + cls.aOutput = tAttr.create("outCurve", "out", om.MFnData.kNurbsCurve) + tAttr.storable = False + tAttr.writable = False + + MayaNodeT.addAttribute(cls.aCvs) + MayaNodeT.addAttribute(cls.aOffset) + MayaNodeT.addAttribute(cls.aSeg) + MayaNodeT.addAttribute(cls.aOffsetFrac) + MayaNodeT.addAttribute(cls.aInputCurve) + MayaNodeT.addAttribute(cls.aOutput) + + MayaNodeT.attributeAffects(cls.aCvs, cls.aOutput) + MayaNodeT.attributeAffects(cls.aOffset, cls.aOutput) + MayaNodeT.attributeAffects(cls.aSeg, cls.aOutput) + MayaNodeT.attributeAffects(cls.aOffsetFrac, cls.aOutput) + MayaNodeT.attributeAffects(cls.aInputCurve, cls.aOutput) + + def setOutputToCopy(self, data): + + inCurveHandle = data.inputValue(CpCurveBevel.aInputCurve) + outHandle = data.outputValue(CpCurveBevel.aOutput) + outHandle.copy(inCurveHandle) + outHandle.setClean() + + def compute(self, plug, data): + + if plug != CpCurveBevel.aOutput: + return None + + inCurveHandle = data.inputValue(CpCurveBevel.aInputCurve) + + inCurve = inCurveHandle.asNurbsCurveTransformed() + + inCvsHandle = data.inputValue(CpCurveBevel.aCvs) + compData = inCvsHandle.data() + ids = cPcompToIds(compData, om.MFn.kCurveCVComponent) + # + inOffset = data.inputValue(CpCurveBevel.aOffset).asFloat() + inSeg = data.inputValue(CpCurveBevel.aSeg).asInt() + inFrac = data.inputValue(CpCurveBevel.aOffsetFrac).asBool() + + if len(ids) == 0 or inOffset == 0: + self.setOutputToCopy(data) + return + + curveFn = om.MFnNurbsCurve(inCurve) + curveDegree = curveFn.degree + curveForm = curveFn.form + # curveKnots = curveFn.knots() + curvePts = curveFn.cvPositions() # to be modified + numcv = len(curvePts) # to be modified + + bevelParam = 1 / float(inSeg) + bevelPts = [] + + ids.sort() + + for cvIdx in ids: + + mpos = om.MVector(curvePts[cvIdx]) + + if curveForm == curveFn.kPeriodic: + lpos = om.MVector(curvePts[(cvIdx + numcv - + (1 + curveDegree)) % + (numcv - curveDegree)]) + rpos = om.MVector(curvePts[(cvIdx + 1) % + (numcv - curveDegree)]) + else: + lpos = om.MVector(curvePts[(cvIdx + numcv - 1) % numcv]) + rpos = om.MVector(curvePts[(cvIdx + 1) % numcv]) + + lvec = lpos - mpos + rvec = rpos - mpos + + lanchor = None + ranchor = None + if inFrac: + lanchor = mpos + (inOffset * lvec) + ranchor = mpos + (inOffset * rvec) + else: + lanchor = mpos + (inOffset * lvec.normal()) + ranchor = mpos + (inOffset * rvec.normal()) + + bevelPts.append(lanchor) + t = 1 * bevelParam + for i in range(inSeg - 1): + # (1-t)^2A+2t(1-t)B+t^2C + res = (1 - t)**2 * lanchor + (2 * t * (1 - t) * mpos) + ( + t**2 * ranchor) + bevelPts.append(res) + t += bevelParam + bevelPts.append(ranchor) + + # + i = len(bevelPts) - (inSeg + 1) + ids.sort(reverse=True) + for cvIdx in ids: + curvePts[cvIdx] = bevelPts[i] + curvePts[cvIdx + 1:cvIdx + 1] = bevelPts[i + 1:i + inSeg + 1] + i -= (inSeg + 1) + + numcv = len(curvePts) + if curveForm == curveFn.kPeriodic: + curvePts[-curveDegree:] = curvePts[:curveDegree] + + if curveDegree == 1: + knots = [float(i) for i in range(numcv)] + + else: + knots = [None] * (numcv - curveDegree + (2 * curveDegree) - 1) + + if curveForm == curveFn.kPeriodic: + knots[:curveDegree] = [ + float(i) for i in reversed(range(0, -curveDegree, -1)) + ] + knots[curveDegree:] = [ + float(i) for i in range(1, + len(knots) - curveDegree + 1) + ] + + else: + knots[:curveDegree] = [float(0)] * curveDegree + knots[-curveDegree:] = [float(numcv - curveDegree) + ] * curveDegree + knots[curveDegree:-curveDegree] = [ + float(i) + for i in range(1, + len(knots) - (2 * curveDegree) + 1) + ] + + curveDataFn = om.MFnNurbsCurveData() + curveDataFn.create() + + knots = om.MDoubleArray(knots) + curveFn.create( + curvePts, + knots, + curveDegree, + curveForm, + False, + False, + parent=curveDataFn.object()) + + out = data.outputValue(CpCurveBevel.aOutput) + out.setMObject(curveDataFn.object()) + out.setClean() + + +class CpCurveToPoly(MayaNodeT): + + kNodeName = "creasePlusCurveToPoly" + kNodeId = om.MTypeId(0x12547) + + aCount = None + aRevNorm = None + aControlPts = None + aInputCurve = None + aOutput = None + + def __init__(self): + super(CpCurveToPoly, self).__init__() + + @classmethod + def creator(cls): + return cls() + + @classmethod + def initialize(cls): + + nAttr = om.MFnNumericAttribute() + tAttr = om.MFnTypedAttribute() + + cls.aRevNorm = nAttr.create("reverse", "rev", + om.MFnNumericData.kBoolean) + nAttr.default = False + nAttr.keyable = True + + cls.aCount = nAttr.create("count", "cnt", om.MFnNumericData.kInt) + nAttr.setMin(3) + nAttr.default = 12 + nAttr.keyable = True + + cls.aControlPts = nAttr.create("controlPts", "cv", + om.MFnNumericData.kBoolean) + nAttr.default = True + nAttr.keyable = True + + cls.aInputCurve = tAttr.create("inCurve", "inc", + om.MFnData.kNurbsCurve) + cls.aOutput = tAttr.create("outPoly", "out", om.MFnData.kMesh) + tAttr.storable = False + tAttr.writable = False + + MayaNodeT.addAttribute(cls.aRevNorm) + MayaNodeT.addAttribute(cls.aCount) + MayaNodeT.addAttribute(cls.aControlPts) + MayaNodeT.addAttribute(cls.aInputCurve) + MayaNodeT.addAttribute(cls.aOutput) + + MayaNodeT.attributeAffects(cls.aRevNorm, cls.aOutput) + MayaNodeT.attributeAffects(cls.aCount, cls.aOutput) + MayaNodeT.attributeAffects(cls.aControlPts, cls.aOutput) + MayaNodeT.attributeAffects(cls.aInputCurve, cls.aOutput) + + def setOutputToNull(self, data): + out = data.outputValue(CpCurveToPoly.aOutput) + out.setMObject(om.MObject.kNullObj) + out.setClean() + + def compute(self, plug, data): + + if plug != CpCurveToPoly.aOutput: + return None + + reverseNormal = data.inputValue(CpCurveToPoly.aRevNorm).asBool() + inCurveHandle = data.inputValue(CpCurveToPoly.aInputCurve) + + inCurve = inCurveHandle.asNurbsCurveTransformed() + + inCount = data.inputValue(CpCurveToPoly.aCount).asInt() # + useCvs = data.inputValue(CpCurveToPoly.aControlPts).asBool() + + curveFn = om.MFnNurbsCurve(inCurve) + curveForm = curveFn.form + curveDegree = curveFn.degree + polyPts = None + + meshDataFn = om.MFnMeshData() + meshDataFn.create() + + if useCvs: + if curveForm == curveFn.kPeriodic: + polyPts = curveFn.cvPositions()[:-curveDegree] + else: + polyPts = curveFn.cvPositions() + else: + polyPts = [] + domain = curveFn.knotDomain + param = domain[1] / float(inCount) + t = 0.0 + for i in range(inCount): + polyPts.append(curveFn.getPointAtParam(t)) + t += param + + if reverseNormal: + polyPts = [pt for pt in reversed(polyPts)] + # create(vertices, polygonCounts, polygonConnects, uValues=None, vValues=None, parent=kNullObj) -> MObject + meshFn = om.MFnMesh() + meshFn.create( + polyPts, [len(polyPts)], [i for i in range(len(polyPts))], + parent=meshDataFn.object()) + + out = data.outputValue(CpCurveToPoly.aOutput) + out.setMObject(meshDataFn.object()) + out.setClean() + + +class CpHeIds(MayaNodeT): + + kNodeName = "creasePlusBevelHe" + kNodeId = om.MTypeId(0x1157) + + aForceComp = None + aInputMesh = None + aIds = None + + def __init__(self): + + super(CpHeIds, self).__init__() + + self.numVertices = 0 + self.numPolygons = 0 + self.numNormals = 0 + self.dummycompute = False + + @classmethod + def creator(cls): + return cls() + + @classmethod + def initialize(cls): + tAttr = om.MFnTypedAttribute() + nAttr = om.MFnNumericAttribute() + + cls.aForceComp = nAttr.create("forceCompute", "fc", + om.MFnNumericData.kBoolean, 0) + nAttr.default = False + nAttr.keyable = True + + cls.aInputMesh = tAttr.create("inMesh", "i", om.MFnData.kMesh) + + cls.aIds = tAttr.create("componentList", "cl", + om.MFnComponentListData.kComponentList) + tAttr.storable = False + tAttr.writable = False + + MayaNodeT.addAttribute(cls.aForceComp) + MayaNodeT.addAttribute(cls.aInputMesh) + MayaNodeT.addAttribute(cls.aIds) + + def attrToPlug(self, attr): + return om.MPlug(self.thisMObject(), attr) + + def setDependentsDirty(self, plug, affect): + + if plug == CpHeIds.aForceComp: + if self.dummycompute == False: + self.dummycompute = True + affect.append(self.attrToPlug(CpHeIds.aIds)) + else: + self.dummycompute = False + elif plug == CpHeIds.aInputMesh: + affect.append(self.attrToPlug(CpHeIds.aIds)) + + def compute(self, plug, data): + if plug != CpHeIds.aIds: + return None + + doingit = False + + data.inputValue(CpHeIds.aForceComp) + inMeshHandle = data.inputValue(CpHeIds.aInputMesh) + inmesh = inMeshHandle.asMesh() + meshFn = om.MFnMesh(inmesh) + + if self.dummycompute == True: + doingit = True + elif (self.numVertices != meshFn.numVertices + or self.numPolygons != meshFn.numPolygons + or self.numNormals != meshFn.numNormals): + doingit = True + + if doingit == True: + heIds = cPhardEdgeIds(inmesh) + compObj = cPidsToComp(heIds, om.MFn.kMeshEdgeComponent)[1] + + outIdsHandle = data.outputValue(CpHeIds.aIds) + outIdsHandle.setMObject(compObj) + outIdsHandle.setClean() + + if self.dummycompute == True: + forceCompplug = self.attrToPlug(CpHeIds.aForceComp) + forceCompplug.setBool(False) + + +def initializePlugin(obj): + + mplugin = om.MFnPlugin(obj, "Baidhir Hidair", "1.0") + + nodeName = None + + try: + nodeName = CpHeIds.kNodeName + mplugin.registerNode(CpHeIds.kNodeName, CpHeIds.kNodeId, + CpHeIds.creator, CpHeIds.initialize) + + nodeName = CpCurveBevel.kNodeName + mplugin.registerNode(CpCurveBevel.kNodeName, CpCurveBevel.kNodeId, + CpCurveBevel.creator, CpCurveBevel.initialize) + + nodeName = CpCurveToPoly.kNodeName + mplugin.registerNode(CpCurveToPoly.kNodeName, CpCurveToPoly.kNodeId, + CpCurveToPoly.creator, CpCurveToPoly.initialize) + + except: + raise Exception('failed to register node: ' + nodeName) + + +def uninitializePlugin(obj): + + mplugin = om.MFnPlugin(obj) + + nodeName = None + + try: + nodeName = CpHeIds.kNodeName + mplugin.deregisterNode(CpHeIds.kNodeId) + + nodeName = CpCurveBevel.kNodeName + mplugin.deregisterNode(CpCurveBevel.kNodeId) + + nodeName = CpCurveToPoly.kNodeName + mplugin.deregisterNode(CpCurveToPoly.kNodeId) + + except: + raise Exception('failed to deregister node: ' + nodeName) \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/CreasePlus_def_sheet.pdf b/Scripts/Modeling/Edit/CreasePlus/CreasePlus_def_sheet.pdf new file mode 100644 index 0000000000000000000000000000000000000000..cf6315cedcaa90088e84128ae62ddad2e61f6dbd GIT binary patch literal 13400 zcmc(`bx<8$)-MbJf_rcX?s5cocMTBS;o$D>1a}GUfdmN&7BslKLm;?I(BSeNGBfYU zGxgnDb^my~=+nE`UTe$JZtB;xDiTtxAT~}E+OFcRmah7)JQOemkix;l8bv?=pa8MA zaJ8ghhf-7lQdYLE5NCjtt+6Xa0%Gc51`!fMadCBq7~7$EX18h#*e{Bpwcati{_5BQ zGpoG%Qttx;_YlM;f(e@`AJ7(`@?$!vqJu!S8jhvi(Z#=PiCESqA&6S2x-$ZD3 zSpW0T`PSmvQ1~ye({}G0r`@se^tca0g?-~WZSLe2zqFdpuavuKwB&-QXb06lns^lE zz9fD)hQEtZHY&^|5<-3*iSQlULi*eyEYFJ>QGk7!sc&p2FMQ9}vk8%oXxRpsjnxV3 zXl}9<3>?H0Ekr!}^g<*^^ZRiZ{UK}-5l^UT6LQ7UM!r5>>MIbe1#5{`DsXNx;b z0TRh&+hlw#e?tjG9M zy|WV0FgHE16}D+pCRap!&Qf%^rZzWetxlm5S?zay9JI^nDw7#w_mXN_g_sI~F7hztcE>%5E!t|7&m5FZ*eH=VCV2g}@|F-(;^f zEAQt+5b4(}LvqhPcx8%$n2*OVq(nC1s)dF-abbf(A2#ln6`U6`Xl>D#!u(p?9dcnPX)@y!LKKgGIQ4^;V*NZgLEqirvREPZ%Ye!Tz zW~nrgA4-p4ZeAj)cD1U?X5l0iE_CEa2CcjBgVsYhorxLbM|=xDMbd&aV|p3IQTsuc zr#KCyMw}i$@W)xj)RzJUeULD+fXLq7B<04f8hvh|)Aqja?&GfA6E(Wj*4y|e;JkIa zyY?ldB;(Te8^@B5bSPhyBQ7vH)-8*bnqm0*8MC$)mBO$d=%^>LrNsRrNunwL^j{49 zY{F;jvh(sh_x>?7&tDA9`+s5R9JLX-ZXnk89mefLN`LkY8a1qf=EYXPD__`v_K;Nr zUq5;cZM-@s#$pKF!y{PK%o}+OUM!RlZ<|NGgpcKTL%*RhzZ4QK+!y{OL!52mqg<5S zIjjD!3@yv=ZAq2Dl#WRBo;ldyLqoXe7>0SzISM^xADT#mv3TRmt~L71$&`zU;=_Y< zCYOUcjD4mSYZk*b{YvArJT)dqKNT}X+f=Hk(1*OhY=J4ER@RrMTeqTLO1MY@H1BM- z>Bo_`-;U`^Rbk9mPMTeKJBoCu?s;^$zl(0wTqKgoL#(^S#5|_43TG|zIN=g(5)#gI zsUiTDg=Ae73$x$}c1izcKW$z$zG*z!47tnL7O7IYn@0aH>V8(_vyQ>sT>qnP5a=)Z z20hn||5hRXUO_+T)TWVW*QU|SSbWa2T(f}9i zq7exkP8Mwp=p|flI;{v!r~M_Z6+Y4e>cZ~VAQ7>_B6&7i#Rf&o$>mzb^B=`lli#@p zY;f@J9NQe?%t++C9sG|UmW9XE`BsK~+0T!L-u#$pEDIy-m+HjzIG;2MU#qzSV2dr& zgl1-HdMps2ujnG3gabW%jinDJ4g1~;i6earNwAc+x%-K#I5u+2NcmM6Yu%C68ky~S z_`cOCH0Iqyu_Ly^(&CdiE2SwsVJVx=kGlGPX9>g&1wsc~!)4O!gnMZ%5*N}w`4Aia zlJrC1XqVev_3C6=lLbF3@Ow1Y#MD<`$QwW3&*$A-7Gg}n4^!=)9_AG`OsBfL}i zo*m|62dEFVP>Q5rN}9N^(9|TB&Lz8tI4^-`m`i|>w+AF9rzk(xPnxmi3fb9=BNn6s zwB>*K=y}+XG+FI@_O!Udcs-}s)I&ijhC>;ptmksBMtQtl8s*@4v|YOH5Jr>T^)}E! z7g4cQxIwoFvr-uTNh5hCh2csyWRmDLs%GO%VLNMDP<0Wc0nWmw zh;^EM2_t`Vk5i4kLKgh#yzbhkGR2etg)PHl5WfoZIJwfm0ja~>(C9s2dO+;9Vp)NB ztJ3y!3TLWN_;BeBiTNGOLj9D*`-6R>LG*cNkXVm@TdQ=>%P6oh6G!z>k)z)$6N8WE z3!=pQA@16SJE-k3WOEltcvR|yUl8v7+t7`shc%{yT))P36A_{Q>*DGLG`f{iw~Q@T@XD7Q#Se~D9eg|Khi@v=?%jQ`$! zL2v}ohATxrBE?qwAj!?Z05M!ygws}>+peo2xP>Q_*oZyAU{_uM{vYFt6KuWX0}OC$%xm~rn_XJV)B^Hua*t^j{eXij7YDjW<)5IJ# z%g2+_!8fdvKWrBc%RXPdRaT?7!${|@x#7{o1bLO-c`MLWDTKUU*p+w>H+6Q=y{gIL zJ1~vXv^ur#E65*v5dYfS~}wFn=M^l@SUr7t^gE@ETK&3Ii#q zF1G=xv}MSO1lGCcMGd}1J?Zg&!LdbxCa>BtQAx=Ta3bjTQuW;}l0 zz~@{f-;xGz*K60N;xANYiZBvv3od4S1V8C?-O;scF<3sU5Y&XTlwh*Bmqoi1B${Tg z^a9Vkb++(`$OiM^u4~}*sRM<5<=V-G)PjV4OJD=86%Zd!GGOo5*m1ABc4SsD^R%Iz zE_#=$=8cVP4P2B);k(js1|_;Dy`&t=Z`gR}7aTR!2}$#s|@Q{T6Bf1$iG z?QHrLIjdf{H&9gLEn)rXLey1)?fhND&Z6U){^biPFWBsob@j*doYgjuVuYQ@->>A; z@wQZ*l!fXjs=_ERjaEl5LgwJ3YqqE~m3*!0_3jM|g2TAsZnadfzm_bx9O8nw0xju} zmU!P}B6V~l5@B@$ic3u7Se+==oUpnpoCWfcu{5U$b>f3OH)X17AQo|E-`@WUWtX>? ztl9avGeCUu4y#@t!^qKT?oJ8CeZrofu{gTx{xw>ocf>NzBIR2Iv)C*5HguzTwW;J) zbQYMR&_k4=U~Wq|oZzbete{1irM@cT5WU-hgI=(dI7NBw{_z)$V*WY&50& zQnfV+8Y=Y2)iwTCs))xg#2L_sIx%WYyk+oJ86!T~8p+Pq{ARMEGKkV)A;+Qw7s=_+ z#eOICVKj3u7UHmyZW1d?;Isl}a@0n=N%f?+!L*VWOC}c%#c|Nmhplk6x8SWX;qHzn zO)*I&J^nqjK+*U^{()Dp$ddH>C{$O2#MbRwh4-(9E5oCcrcI~O05{_>c{ZnC@IS_T z13x)PB7|-C7a2*@ni3tc>!%HZ@)3W8|CJ1vTbgUQwLc*~iR^F_M<;mtl!iyG zOxPz!i}Y6fuxAZ(f*1S636BNzNxtJ__g;dSGqMvqMX)Tzi|GAJm9)U{ESF8Ole2*p zupxTi1W%0~yko5VYt(q16cgHrMFRvP9+pZ*HV(^1{Ps_u4aCY|KEef3sT}Wruq}aw z;bKR3qI)qO!GokAO$u*Lf$@S}pPmxm8}z%XpDMv`Z{#3xP><@vu!xYt>tG=Ug++vjFc+1-$d`;^3;jW;kHl*3 zN?98^5vIalgj(t*|psanvmWtI*dwZ#;=mbfyTTD;WEmcCV{yZ1FZ_1Z=+| z6sp^YFYKXTWD&59Yfz{<@!QFS9hWt@Om-L5u_nHiQxqNt=1_*(L&bBT;>~)ki51Fp*9h>B=2r$l+s zvcVFPmLB|?4rQ5Gd6gA46}D^PxRk5J*ujNeVoGUB9x`Sw7dD{+UMQ$YPdhHf7_luO;Vmj^Y%J>E#342$brTlmWm=&jpDi`CLv3lS zReIfgJL=qaBrL;xNarGVx4E?cwGos!YYbrro5Qo;v)ZsHC$pd8CO*AAq-uRA$GpLv zHSVhD<4jePFVW%Ts<8`o64SNP)r>jgG%4Z9>Bl2Neld?BSeLddo9Pppi$Z$B8rX

j`Y@iSyGN?^DcLSua&IgMjSE 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 U{&6J`n46aig_c%QSqkO<17WMkzW@LL literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_bevel_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_bevel_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..94b68c7c75ba19a62a8b56c839a335251ff0d4d6 GIT binary patch literal 2607 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 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_crease_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_crease_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..304dfed8c5c0d52be4de59a2a4a024807e6fc553 GIT binary patch literal 2425 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 P{|ylQNw{|36M6pyp4>lJ literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvebevel_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvebevel_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..b07b240de404c430b46ca340651ef8adc030167a GIT binary patch literal 1238 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeR!04SB5>XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$9hXgo70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`$q{6sLWFOC zf^&XRs)DJWnQpS7v4w)UrJkXwrG=4+j)IYap_#scrM{twu7RPIfu)s!p#l^r*>Ne@ z6s4qD1-ZCEEd%mwl`=|73as??%gf94%8m8%i_-NCEiEne4UF`SjC6r2bc-wVN)jt{ z^NN*0MnKGPNi9w;$}A|!%+FH*nVFcBUs__Tq@)kBL?3Dk(0lrD{{cDh5K7Gh21^mp z{|cV#tbk$1lJ4m1$iT3%pZiZDE0E7v9OUlAu|aUM{l_85EvD^{h5`GZN##39@(|FuAVNj*}=iV*0y$Y=FAD`SZe3t z7rx+_YeIh1sw;tADcTuME>&mH@q zDYUcFK&y{xifee$PPZjn##9qc^i zKJ1uSwJ4|YZ12~lb7BuDaHlLgaM$$dS$1E!hTh&subymSY2f|-(XhYo@WlWho`f?u z6J77|ur{2z9&|r1bH!v!7G`apj@(D}Y^S))U!P#s$U3ITZlkC1lH1DCAyRwAPf>@a oPepwTLUlZ>zUI5UUH^@D%F)|Tv^CA_fw9Nn>FVdQ&MBb@0A+;2`Tzg` literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveclose_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveclose_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..ca6b447c8885bb41d2eabd568164c1bd0c1c2cf6 GIT binary patch literal 2508 zcmeHH`#TeCAK!))Nu;+ztSRNRIV9}Fn8PrJvCVn5ne*1H%sEO9k2RvlVU#e*`Aken zDqf*@JUXL1$spp$H_xJjIuJ7l%?>m#=Vy_^lDhB`n6mSk$ zcgbqCHMUDh=F-lTcK`s;DU?X&lJU-P8Y>b?rL%$EqQDzEu6sx2QiqT zQ7FiA&n*Zzl#YUsO!09s84(o? z=b<2f^TH+jtuPD%{yT*mj)Gve1i@rH0c^wKFuX`8p$Eg=v35VjBnSlw;d0q<7%V0x1{z}mWpS7= zBMS=)n4vMu*w{dlVGtb~#ijBLqN267DE`ZVWkl0Bp=@p_D+;{DNeyD1;-VlB*p>+F zr;4OJ*gy42z&{Q#q9nF(B=v)F4RDEf(o75<0{}D&l~jH)jH0H7}f zXQHhn2!|v7-~R^({JR%=dwWqRbWcwY9D&4Oth>6p7z~EJy+dtnZB|wmpMRmVvy;hW zA`nQFos)};t9x#}-C1{w0AxfJR#e2M0GbH8nOive_q-lano-+>zFHHr~O$%`XZH3Oz2imY0{~ zlL`=ME6Vk85{Zm)_jNf_>{)!%>+&6}e>fpSDw~xDZGWlIodO0UDK`-GOH-n56PktuhM>`m5p!9 zOSi0Q&mwVLT%0Y9bLLD+dU|?JPOd{lQgwAT7Hb<45|WpfXG>+{qEg)j4Zdx&Rs;&4 z&v!b__iKNR@$e&84m(7qh{a-3`H)+74Z_meB}M4S&9VzVjboo9mEFU!)7%A(PCNnH z&e6MeGBPsK#mp!oQNnwpw~goJ>AK(8wUcG!|!$?Xr}y1RG)zF5o}&3OzM z4B9m8}O79TZZJnJKq-p72~(mApz3 z4vQg@?<|p+WTL%_pRbOtgRDq;?{4Y%%zLU%b(QNH9%DQ8d+YkLKoC$dxLgIZJK}MM zwyNo``{T2Zj0~*$J1_tBl{3Hej4>SFv?2T|bsn+&pvPqXxrrjbO0T;-+9>$u*i3w4 ze1L)22j(z47kg^Ms<6U-ee_PxIyf-M+oH)q#i*>6S8yzKQuL93G-ZB3d5Xu~_Wq4y z&c@P?l28BgE@LZxi=DQ*{{hu5O(x*kFT3T<2R`1MbgQ~nv0)fzUW>%dSh`exP`dXx zQ?4xe=-@C#Yeqg#MrJwzFW;n93)pABq9qeNWSzpoD+ z2UUA@aSBVby`?g-J_C!tbUb1Y9Gh^~EQ1J|H8X~j{^YrZ{_d42euWMHCXfd-5O#d- zGc?`si&54V*+N*E=AU!D`nP#0?U4mC_imy*cPCz^opb zST7|5rC6oHN52Y9yGBkb2dFf<)>lyT%V(H?eMfX`g&tB;yRVe^=eI2^waoG6@;R5+ zTDoIpfN9yLz@(}N;6NMQ0ZVSy`@PuT3v!Y|PYvDb z3ol_p>=ZN|6*aVuOywZdOnRF)5BO{QL$i<0x`InBd)33tb`P;pku#sX`zC(#yMfe_ zjXskn+?d|x{kD{k+-IiyI$M>~#FiRsVR_DClD^|s4 z`L>kA)%b>ziSkQp>g7c`i-Qw;io~8bCK?oEUuq>BdI6<5<6wtZM!GyJZs7XPY8<`1 z$ODOq5wBK~I0FI)y6Q6-4f)JM;}H0fH$koc2KA4k%RuNu+n!=|S(vyVZCCSG4twwI zWZNGug3qcAjl7R?Qd2Y<={Nztmxd5Z!W$2}}9$hcnP4^KRTs%IW3R>v?yT z?bYf7WxkG`*yP4duOrsSfzs2{w@GQ13kSPBZb8xUkguy(HE-2Eub84}h+^(f+a7Wu zC@hiFE%gnw0uvr6G*gwJR|V!@BF+DzE)A>{z9p|`0KaLdeJF!ev@`wGYa literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvedraw_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvedraw_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..e34fbea283a5eb6f1d8cf970fa8bfa06bd8f0f25 GIT binary patch literal 2056 zcmeHG`B&3d7XLC~_duQ6qD5xNgnF7*$pRY5z9%GPBMS)-!V)3`LJ~qSfdVFN*(yeq z;)37+MR1LjT@kU0LIo*Ow1X{+6tHQfXq}FfqBCDQp8f;#>)i9+eed3P&*yXQ=iFB= z60rSz1APGi@Z&@>#NbI;-`)_=*LB+;1AtqUR;n;7_&l82kcCK2HKb?|)+{6V4gdtQ z)tIc#)R>_unl!DR2wU#$he5TeM3{URAH_G)HR;;Oe3M3!FOaJ9Gu7Bs7@6ctu;M^~ zER8uCYR%H=b8uE7>~~xocwbK=VbI@8%$YK>e!!FojoMnqV;*=)oikrs;u zVF^bVOle3o7K=rq!jNHMpbQ&J;2aHd z0l=SnMLa1JOyY3(|F8d10o7#}4o|?7D9Vn9s!QJ|^ehr6^n}ju;+>OZzLaD92uC2| z2_!s`6nAza?%chYzYoN>e#6ZvCo(t`ks>mqFy`cqnA&TRx_z;y#}c~cS$iyugbYQ~ zFp144^Q7b`F;%Q2afC!BmlP?8Z~azM)j^f2q{n)B)=II%DX`b_3#th;CW*}xmNX`G zFA9qrl?KecissR{^Iyx3 z^-8O|xu#NPl7Xep;TIl>%H5AAQ{@eVv8V4U+ot0$enXS16-^_|WTWi(6?wyu;?xK& zCOM&Vj+b{x@=2%o;6?tvV+1ObDoc#6I47{3pa|vS%2rBroT#*k_x>k>59$)S7epVo zP=pGm+7$iqX^yeD^yd9ZaC~)Uu|NVK2U$ZmZw-V-Kid}|`2K5DoDNZpk%!~`ZvVD- z`0h;%_1G<3b)S_1^Tt?o!V3U#*MoU*!FQ?j_jxK}t8`7qjA$HwW`^56z}7M4_~f2J~c^{Gnrbhq-OkO`+?5C z7&2dMUiE>-PhKq_dV+Du8d~rDY;#~b@SSe;9t8MHmV2z5-YMR0<k?f4Uy`nB6|76I- zjuReM;Hi}IR_^wh;{#^j)}F6k1~pvxs-uV`k_{-*QM23r_2c;H+u_AGn^>lgCdZlq zCuQ40t$RR}U`!RQ&{f!aj1I%2c~1CN*OdK-mWoqMx9OCAOy#EQ|Cpxzd%MMB;DY)} zS?vDQ)w~jlUvW1iFs0wEZZ_cOmc#Is*bBA@)4`B{o+v&s0`+PbYp1o6Cn z2r=JyWtbNHH{_2iXD?Paj`K{_5X36X_Eu$IdDxA>!1z5w%wDnkKwVqX)$yG_ajQv< zXW@4lP4bo`^uiL5zos3z!|18;dcL7-YzqG?7cx};W&)kw?O7O{mJ}Y`;7#e6c~8BX zeISulV=E(t1c@fH9{iBuqPP0O%hWrFe;zyR&;L@d+?2Q0XtNgF5oX`7ww;3ypD_6! zdjGYtL>Z~Oxh5$48S9X(jJPp~Ygu}8_TAf0J+4Zgym6l0y;Ce{;_>GG5A)BdTovbM-z? z!fjLUJZ=q8p&@Aw>vK}2=B^-)qe}6eCl$bMV7eBzN6h8AWPxyPYT*m^^|sEzg7U(` z<}*8Hu^-lANEh}A7Dwtvf9VVK2&!$4z;?K*nlJ7a2@`)Erhy~eH6!R@xqd P{}bRa1q>%G>4X0O@NO$- literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveint_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curveint_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..ac13f1e28b67594372e5cc8bbbc7898cf13e421a GIT binary patch literal 2039 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 M*^Wl4we?s07iFoqyZ`_I literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvepoly_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_curvepoly_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..37051b6206443f5d4206b680c1713d5263ec10f7 GIT binary patch literal 2471 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 Gl)nM{?Fk0} literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_displayhe_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_displayhe_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..213c70ba8ab6d2f0b92cd91b87838f68742b0d94 GIT binary patch literal 1947 zcmc&z30IRx7k-gwE$C@fT6g;rwPM>OB+8N$$U*`E!VV>X0wE+J1V~5%L>4h@2N4BK zfJUIQFN!R(DFH&k5M>nzXe|iRPuhM;Mf+I=fqpNo$G^}yGk2bQ=bq=zea_5zHy8Wu z2Kx*E0NCz8qj*3&abs=Ohiq}X>^T7FIq|$05{5G!&k{tSm?45-HcA>Hgw6rLiXas- zS$wtx3}$nAk=BS8Dm4P+g;*ne%$!Y~g|_Ta9xYD9_Kb7!V#V=UxDW)v#=uI7hX^9r z5+*2(2#*xwrPheQdGQe52%{0;-zgHlHG;HJ5M(&Jfwlq>8#F_iBUzXemY@X=g)zff zVo!bzVofpTXj2>-a}sHa!JFdnrWo-3K-d_7R-zCN-h*QIz8K_LBSIw-As&s6jg3Xc zouU!=X*FXe<^9Wgx|8A|*^IGE#hagW?|!3R}z)@q`kdAQIf*WCjbOCDsT8 zdZP&XeHExZ^oRZ+=YvA*NJthD)IaD@kB78_aY@c30BFiKTxD*Cd_A#;JsGGUIq(|* zY>IKCdr_gFMsw$1pITMRXin6JYmB7@-fOD-#~rM^+^<$axc`Ul;VUvtUyrt{9kSyx zKPaf}XdWuLFns0QM1uQZWv)ikcdx!=JlVg$y8|K|xL$a-r4~vXKgSrldVVY;M02Na zL=iiF)@z`y2*ObOer70?aJTKoP)R14Oos5JkdTv;O`%Y@Ty9Cp)$;PPva(W%MC|VF z{_VHkN~N-~u^}-rfzRg`6%_^t2Nx9N*VI(sxzndm$O8fbzW=_suI{>2DisO^X=!P> zxw(FRzPE4Ra&~t1p^>YY#3*-?+@CluAaGqsj*gB!J?j2mbz)*7olbXkbycgo`}%qt z8XBlnYFAfhbMwvY>`VUs{uLGF_V)JS;e44)ri-?>wY7Dkk}0<204Gvo2vH{_xY&`Y zw&dafVyX{GC!y-Zlq7GGnn$QsoS%#$j%Ewm!t92NQbYW_A?e1V9R{oO?}ZauIYcin zuguKz!^)(g2*PbHF^l7wl$2ykCijF9}g zQC(ft*4A2CSs|BSZf$KTEG)dA#qaI?VM0nTC@6rm?ds~1NF;%Qfh{e~wY4>qQ6w6T zHYp-^>H_DWm**$(aPb6|7q*J1fCTZT(z#X z+|>SM9t}46Y;EP?qr}xw&%uYv#diyjC!VZNFOr_D>y}2}jr~44{J*uCx4*tznp#+2 zc{w~^^dB$*0Q9#wP)J^kzQqEzcs~Z7eo*wCQt1ix6ZslHG^DF?-p4UHu`yQ52k^&U zYedhdhb8#p|H?_Dho5ymI(^*eua%6fj0}Y=o_bu5ttg;O_iFdo)Bv4{Uso(0S|GX{ zHO|AGyRB`EvTy})g^jtz+oT=a4P!+ucAU@l?(tPN#z?UH+GC?O&+p2ZqGfz^>h$xA zy~xEPS(XGFTEPWIV)E0M8{NR8xk9^OgCMj1bG_!4u%nf!-^3)X!-85C;kT3*Kb^7q z{8(VOd&04`9p;?4r6tf4L|@FVPBZ}?pT6NVs83JnJ=3Yu&ru9&;}4BhzFf#$iH_Vk zH&>tXZh6ykZJc@FpULw#K3SQqUeND8iMlS7{hhPhq$l(|2hxo+AD|*0Zy#^kT||HX;}K9U=QT>fr1*rqxZ& zn@yw-@A?OFUZ{i^pm-`jZFS zk9-OD=)dVbB%E8q;XfKqwC>DVNX71IR5T_0W>94r*Va83xl6DteazemBM&gTURisd zx^<>7r63>9O+pP{Hif~ht(@+g*> zEc$*;#kIr07p@5dMh~(NU=cT-ZPhVtBCf$1v0>YGe|AvMn2nZc_-C;ir99nnzdeyQ z>tkXe2uYtv&-}`Vc=rCyPTtIxys(8Mwoi-E@PSt=Nd7N->ZmI)@~?Z`x4kyhy;wI~ zR}fXDIUv0K8h&|B^DrNX;sQ{vF^zb)nRggOdLbYMZDF literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_eye_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_eye_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..46d482a79985afb9d0dd2593f82399ec7fa40532 GIT binary patch literal 1783 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 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_mirror_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_mirror_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..4051db1eb4cfe8f0bb2d1f0b9adabec195c9641e GIT binary patch literal 2640 zcmZ8hc|4SR7k_Leg(6Xx8iNX%jWK3pFqXkEwlQYxW-J$DFtUZwrLvTrN@R(WB$p&a z+r83FDp||YEu@9GTDwd9y=(II!pt9_R-Ef>AWj+c${Ahxl;(1Gz-# zY-knm1oKc2(*k1=QzgNI{C&1`C{2nBSR@dG!7&P;qbj7(ILUy2wXG~ zy2*ZBd>#%CkBW+dMd`vq!~Nk%EEWq# zpx`K!4j7>m5yRzsN9%AS)HW!#ILMp`-|#>lKQNRF+2Hi{3610vp-}in5%`ZP;CS$# z^MS_C4RN^OTEfBkA(WdqaChSV##CbfsL$NF?kxe@A`uQ2Ccw?-%F_TK2GDGr%s^08 zRZU%81Bb)^ygw1_CWy!XzYD*K6YzKxN*Bz+;|ZJcM;MU+dVc6=3~q~7O-+4EA>#3R z{{uk;G`Db&6NTR30?p0*4Y}n~Q8@&*1nS^{KvcKDExFYR=-mp~h}jGy5;k(T*tE5e zVGMq>aqO6mrlvNCfZnYXaQ2_~O&)zHmmVJA6!!gjmm>&+)k@aB!{`qe zTn@twgxA~-YU>Z?(ATfJjpWk@a-1Q@Fn}B!9A6!V$@-O|jWobu@w1ni-%4Fib1V-W z)z{U>S(p$N52L?dbyLtlj~39OIv6{1qrqJI*Amy7oY??m>oNKO(DfSDhh-VG1>udnU z%G9to%h`xX;JT27weJ6vG2wdH`7-9)e0n6yEGNk7OR;M>lUy8T)1BcI;AHx)&?VK+ z(uQnkX<}$bA_{BVohhW%D%QjWM*Ru9U^@AZfM#Myi19Fcan9-CDTlw(9fjB3ug6du z6YMw+CV)KR&od65cE(6FPFU;VLo zId#3Z-)I*@t?S}!&zw0U6bkwLNG6jdyydY_&fE#neUk1N?`59sYhiC;G+o4~h@{?( zv+YcE^z~&Y`B+pPr~3N`R8?KQb*p}*+AY!BVx^L`SjAedV0k_=&j7yxCf~u<5y(6q z@-;lFtz>p?KBYm-AUpbo>FVFGbl%WoYR>bBi|g$?S#p<4^uTLQZExMUj_6HWH~mzZg^EfNKOh3Z;Ovi0ssjlg>3A^c=UB3 z%E{413Ha7%#?q-UBkxTwjcj7Ngxr2E)4G&U7cw8c;}nEVFf|c{?>MZAOjgezF+@aU zt?!pARh`^JHoKqXaJ~O!S!DlAK@&z^QY2R9Dv_wO)bU|tt#xT4Zl!&8kEC$^9cHDg z%h6n{Eo@?H)^oq~x7P!n@hgu51Z##X%N22*0-*|G)OS=;P}jL$Rv1T>@4D9OFC#|y zWSd`2w+X*@;yvFctgPx1G3t5N{drPKb2IL&u5VKMV#K7@V{~ilxYniS=n`wM+U+Iw z_D$LoZw~(Jn`r=Fa!;18k!!wTLs_q6A8&7+@*0<@3LhJK`|x}$?DMV?WOva0>8ruc z*JbBSW7v{1Lr`krRS8&>=(;Dh<{3rL?9pyA=Fh}f!P(kRYxM;ZS|2EKy$5-Fc6q4F z_2wkS5k_St=jmOzzp^A-INNqD&cOEJXC6h_CtZWeV*~CcU41i6;dIO3i9<618nIAolPI^w;0O1H1*w?j_U!`Kps{@s+%Gsk zd})Y$3$mu^QrgKArwh`ZjPm~Q?_Ilz@)G+>!?0j{AprE;uX z`SL&(y;mW9=K*~9PW0p(K?SuePFH;G5p|?Ij4r6Iavw_|)Gjnd%A?#L3WG0``ft^^ z`)xalDauk)OiN?>WLX{*gFPcQ3Y)bw>ox7p4kejWa~DvD148uodu_{1$*Q>-Xm>}I zT1noydue+x0)0oyps><=&)a8Ivtius`qhtLWxQCswB=qp?N#Vgd1rn_>A9|i#@8!* z%^5eOA;vsW;qmG(&8x`^C<|Ibr*i!h5mGX?$^Xud@_(&{`BQ^o59SsTI7yur_QWh$f|L$UbXqxCeD9(83*dJq` zSYqB_&W8X^*LrZzkn9LW{T@*Sp_hDD$r4`vZf&NZ9g>IH#=z`Gpy0 z6L$l$;=v=+%f}&Ed(qjX^9E4FOo!|fD=J%&M6~X#7gv}ufNV@vcFi=b>! z*Hn%K=*@ed(!KA?sdNMcPhT=pHO7|2o+TEwh|cB7c4J?F=-ZcbMq{%=GKa05y#O?kWMrFfw2{q2MK6BssY{(>~*ACvAsQigPY`LL*X8U7%( z$cN;8OO(xcIy+d+mKwOx)R%IhCfSMeUL`@*rb+a`zTc-1B}umNgZ&E29~I`ib4IiF z0)h_`F8<>h1yF9CjYqG8bKbtJj~PIzvAXG5CH8+J&kZ;6nh=+3+mrdui2c-8{Cg%H zuY0QX+rL!VX+NQ~#pdO_PDwsQ^D>$BUn?A#^3YPfE+b5Ew8e>}HSoGV8TZ9J*rj^3 zXJG%bV#oKx~7CfReYYHGTdJ4Ps(j4uHySe>7!|}+ogNeblgP!Zg}dm3nx~FrG$H08Y$X-!OB6A wor1*Gdvmqg9qS8|0fM>lk(kx7wb<`RIyFLsp<%wW;4d1WnAwtVns}Z3F9V84^Z)<= literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_next_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_next_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..86674ef46e108c2ab52357e47761035a446a518a GIT binary patch literal 1334 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeR!04SB5>XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$9hXgo70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`$q{6sLWFOC zf^&XRs)DJWnQpS7v4w)UrJkXwrG=4+j)IYap_#scrM{twu7RPIfu)s!p#l^r*>Ne@ z6s4qD1-ZCEEd%mwl`=|73as??%gf94%8m8%i_-NCEiEne4UF`SjC6r2bc-wVN)jt{ z^NN*0MnKGPNi9w;$}A|!%+FH*nVFcBUs__Tq@)kBL?3Dk(0lrD{{cDh5K7Gh21^mp z{|cV#tbk$1lJ4m1$iT3%pZiZDE0E7v9OUlAuSy$ZPGZxgYMU{G7BKlpH)=977@wpPha* z$T?8J<#C<>W0FcEL*s*YVDM$XvTLXZ$Ccgd#S6X_hkgI%qb>5ErFrX{1?%rARR0TU zE%(`J`RSSW*B>gzy918>`NJVJB}Igj`_#mieG$rL^QX6pBqje?TScHy!56>dRIraw+AjvnM>SexD_yl10`8mq(lei4S=-AfpD%nT}G zN-$gTnB_p^mnLC`6ZEzzcxB}{&Utd9NoLZyIH!oSuf4{xOv-*-wB)54#-HXdZAeS=!}ncr?R$*pqgIjj`hk% z*7igMn-{K4zVuS{7n9C4u`1@A&;@tW1^K$9~Z^tt}bIq!JyX}cGFgY-Iy85}Sb4q9e0LQ(HW&i*H literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_panelbool_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_panelbool_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..f509c57862e2c0a381be85526d7c029909bba233 GIT binary patch literal 2057 zcmds1i9gioAOAWgM=K<&jTnQ}T4s!^8N(dLFqoq`n94bWF|L`+OootAZn>%zn-b;a z?og9Nj;;KDDUw{vQd(I@Ohs&|cI>a${t>_D^<1CN^Lf4BulM_TUQafa?4hN(OA`P9 zEutr$rdTnXLtRBNm*18y0)Ub?kHHeLNIpmoKgK2`l+O;ck;DiT=Kz30O9UaDs4x+j z9meIwx=!s+oCozhH2!)_sHBl0z0w5+#6atpS zM8^t|5*NrnxJbo*GYy4+|0xkgxj?X+gkTnl3U=eig@GMx9IZLFcKg9j2pd}m#D2K_ zPhdFA))5LrKyB@MM^C$K3!B)MDsxBMG$kio&`K&LV)}IOxl@fuWLz^?hIObdirQFQz#Tw2z|1$ zvaqhGJ5p*>Q&UYmDV&b;!Mo;iiH(hog@uIyBy638{2+@qcG}k)?^=D7^xqshlZYJrEH7W~TLeyq~#P<#qN{c*5hu(eysP$q(Wi&q6?? z>W(*&@V&QMStSxdPa&_sVmxfLd0ra;R6Y^$SO)9YCw^;wdRQF-c%^#Bm3YtrDzCDJ z_4F6-mk~53rZ6AOJTv)AMXKo7SF7@NbAd*4Px@L?|6kU3KP8ur^QMr9&sJNB-#uD= zuayxT)G@JsKPq4P`zv;G=Wr5xdg501?`_Ib_(hnLwfs3?@R#Jk(ByN`a`m+_XO~+} zf#z55s9rV(t@-4#Ew?@lRF}zY_XzP<66&Vkn~m0XlchImak3wd5I`pLQp*cR)lmzQ zoKJnT339!cizXK|B0uEqMkJ@@mv39Uu#zWRu&TgKxwhirJJ4NpCt?Qp!>W7EHGO+8 zRks+eSdh+ob4y@|LBcD)N7>pf!Udf#8KA-NNM4iPLfK)Tm_;trs*9R#_`xaRNl=FN zwrn+-W0qBcm&TS2P40L^Ywz1k#EF`l(w;o2{L;~#FJvU1dGC7Uz-0KSAN6|Y>Rrt? zD{t^QUG2jTO?0QR6DF7THUeYEi{KlYX8KRIryf&V?VZ0mf!V%OzQ?e(8<0=Mo8}?p zhc3rx+2uUi($FA?(r8{9G=KNq`R^v;#18RF>fox+w0$21e}@?IaHoimavv8{C-o06{!jVz6Ex5 z8%%Ss+i`3Fe?)1d1VMka3-tH|I_^-H^4P$`1kO^Xg*Hsj5`;}W$tB6wf-zq&5vFxn zcD4f(5^r?#z3RLNR0Q8cRa!sY27}pk33Ubt0XwTFh5nf;_doM+yP%tKI3y2Xm-p9pj<0 z=6d$M*^uWy8+Fd$(AEa+UbYc!(N$5FV_}P&*aqS`F6a&3K<2e;0?v0fGmAuE%&Z48?gjUYgsq^)%4WA^YU!*AqTk8=^ d*s~|xj z1hNniMudP85RpZolubbjO6xY`P>K}=ky;cAUKU%|`_b$4FStM6Gw*iJd**rOJabM_ z2*ZD?xt%!x09$EP-%$7_Z|)CG;qy#S?jisn0uxwlh)oZ|^F&D~ZmcMVkCG>e;eP<| zB+12Go{$fLG5q+1WCC)z{|XXJh$SE++~^p(*oS{KftsGeKakE~@zRAnTr84AH20L_ zVSpq)#0BL^iOCYYoPhib7Z1NT!)PS#-q_P znG7X!L5Wi0(aty=4voR0u~;WK!%32s3~}X7$&!Cs*hff0{k?(K zTlNb8VA2v2#G=4KJl^B~>wl%d;NTz(^@~2dy}iA)wRNf@ip@S49v)FwSJ%?gQe9om zVub|-(aX!rX|#aO&d!Dgqlbqlfk5o&=eOeB(+%+P^>0lmIF*pCGFV}K7)qtRAYR%T^oDHQp^!NF~9Z8}|B4ilI=(0bt^v zCePa5&dwt~u=vN~Z+rS~T{`!M^We4QN&eke+MidJH*!AwbN!E{=YiEEcT{75oA(&S z^#{kV@%ELM7p_L^NWNnCc~ocE@wKxrxwEXGg zlo^54W?R-|iQ?K_+7bYmy3%~TSnQFdQ~ZcsOv{tUl==!}{-Y4{R9ID+tl#?3{XAGc z;q>%0=N;9h6QQ%5I@kDQ&2;tisf^`U#Q594-d1-Gchv)&iay{#g4lD?MTaa_su>%zfe1`y8PH~j7`TL#!ofzYPrQVg=VT^k5AmI z<3=ZmKiRD}yr@6u@B8rUQHPP?%*DB`g<;!9%sAD-c<>snc`<9O40BsOelq&AiU7{M;`fH}Qqw{q>bv}&!7td0vp_nqzTloYvbs~_{>GZT%pg;rd7Lo*)SPW`9_n9? z^f?FApGYX3J!9s4v)=oei5!T16cfv_;m1^aXV~*&Se!lEun150oSiuu>)7xwnp*M| z&6dYGsI=6aKL=zBklCf8v4eHRaih9X4<;e@EPGEIZe? zDg&{mGduFlzTKb9QnY7Wv=n(X_sFW|1_e*65#>=Z=FsiUbsL^TZl+tTWct<*HCH^6bb zHLLrV5r0klTchjDOG@>`?$O5C-F{W6K6d0Z7yYu9x0Z;dnvSVhPwsRMMVV+ec9Jt{ z`mTLjUK)Lc4;luSJlN>T$20QlE3!zBAvDIL-G8L)8Fz3uX?XSLX!~q+jc4|w>sJUh a?+_P==iLj#SvBzN0yGN4x7GW|iT?n5UQ96n literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_shapeshifter_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_shapeshifter_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..38bcbd9cbe0533b318f2c4e1e1a0afbdb615fb57 GIT binary patch literal 2289 zcmds1iC5D37XO)3CfZ{8oU)oBdgh!|z}yl+P;f;Qgc3D!0YpSl0T&Pz7bY>cR7A_Q zG1Odg-!*%hnWvhurYy^{?U`ERq>fgO_Hd>%f5f}r_1^D2pU?T+bI(^!^mN$)G6Vqt zu*1z2=dC_5n`^tKdN1uveg^;=cs7M9q!K(3^aw6Ah!GLYgvN0r)$ag+M8`!2(K$>Z zIGD*|^H7k5-fIvrn}LG(SP|faNGvmy?JDLo$zo3mUCg1|G9YMs5Hb#-2H-Mf;emvf~f=|7#qQ7f~}y|W^{9l6JQ%#sJWHxiQ|^0 z;Nx&}YZ%-XW^QQ)H%Gv25pZ+xSA*Dtz(_tL1mTTy{z|6yP>@ieFcJZS#m2@$V=bT& zd=|{y*47pVKMp&7+)SNeCJ^z2L2+h0!I4dfzcFx30iDl|6tW|D;7!b+;D~4;3Ic&` zlEA*QsO7=_(WiF)sSuN=E(>3+AMDnMP}lAomIJ{70Ge}j{tVix_B90FE{;Is-Gj3L zuth}lpg5_62!!4L)9ml>SIc=)aH_CS85kIJ@!~~)|I@uGq?QCP3Wd_w*Jo#k#A0zh zJw2VBoz>Mf_V(!B-rnF~I+f~s?%X*dk*H888XFs1T3X0tN<%{f4(B8k3ZtWAPMz{O zb0(mztt~Y*&D|YeRdtz0JJZzEgp;kF7EE`K7T$>E|)hq zH+OV&l$DibW@d@SVh)FM<;s=NP_{x*SWr+CAva+(Iq$Dvh34?KP!l3Hv>O4KYVq@cw zNK|BGRAFHui^U?5yn}**{rvoWef=nulVM?D-rn904vunphEyt*$z*uEM@~*Ikw_ww z$r6c#&1NSjrzCBfG$0_bwY8P#jLyo+PD@K`Z*Lcg#4#~3;o)2s8Cy`G$ji&i&CN|n zNFb3&7b5TtV$YP6R20&#wzgKKQn|Xi6>{-8VQ%W)ud1q|(P&sSQbcpAudk2xcTy^q zZHZoAy1W(Y=5rQ$dy;{qtDWP627QI;M6RFl{g7V{XBJboo9~q6Iuj;ZFWs@GAG5<~ z4!eIBaI2?uV0K5!{=5U*YM$yvkBDdoPqK!fVV9A03%bU!Zt9ljy5Srs)S>svRe!S2 zzMDrnbkA3I__@@b;&M%57X0j%B_>ryHge3iWQ-J?wtFR4FSk;fuFKq>B0a#J(yZ$c zE?lcwAnptJg25 zH(Y;Go|KwNsGi@mtbDi3xiL2V?&9;KWc+@?u{BPZtX})+K#Y0hhKISpq}y^w`V+mP z^`TV_g4Pm6evo=?{L6n_8Z}>Xif#UN=$mJvhzF9DljlNPjOU7BhZgG}|*NLAvt!sFZ^UCbE zFs1uCz;tD{gLYf%_C#rUxL(F-z*-(48x{PvrGZ<$dPFhl3!FZoG__>EgbaotjdpS# zx#%FHT^&ij}zTS*9?S(IRMLgYq$NS`AWf3#u6WP|9a~(qqj%Df$y||e$t%O7^g_uZ`ivgd;U>HXs<1ow9WYn_>x*2A#vkgPr)TVZ)&>@O#WThg z`1v^7=4 zkrXF;n7;J>o@_QW7~wIt+|#t%g$a!VkR!-#>Hz{CL|I(d*eKYdzg&zz zl9x0{vUBR!Wf8NmSSuC9qJZjQbSoLq|4h=Q%#=A2G|LarKOAUI8|FU@ThjUgipu`) z2J$7t@h)p-U#1NRrm`0pj93pzq$XwtMS`)?S6X}ZX6uPN4Z7VY_w0RxDwonz-|y=+ zW)yzjOQ{Th!70h2&`0sZ=>?AR&yC*>?l!Zuxo4((KS>jpG`ah`Bw1ag172t zjVePzX(#P)Z65!%4*s(2*@fMcx9-1bG_A3$MFPjN?kT9@&q>**`yRIbdb&Ghw~cJs ze3&T)5?}lVGMRFGRrX}M;StTif#yiD-OpX4$Y2fYgZ@(q#1^D{`zwZW6f_~QO9oIuQ%8O z%snC^o1lvgQYcFD0jaXW1BE-YsD@hq9*}tD0eMw7M1hk{J3g*o)!Ow%^l>`JezKFG y(@`x2lDAVLTsM4F`}kPrj{7Gx7p zkmV^w6a-O8!(!M)img5s6ma1w6rU@IqD831A}{oL`d7R;Gk50Rx##=N{pOy@4+>xy z!!6+e02q6F(U`j3eQm*@I$NX7ngalcuOO5yWBdCNxMCri6DN-4p%p@j?j8U@vO>b) zCh}y+SRP*>BBJJdE~AiwI3kMW?2q-APUmuRVH%raVRng4k`#b03lDt zK`MmbilhVu5%m`?L3dsYV^GMyQe=rl6lIMN$@ULIQpHI;q%(T2BNw;Z73qRUJ^U?Qz872v|G;i$i`MC=whACdI`Qm^9DNWIB(CN|4DU1Pn$lm!svo z(c&aN28YMvF<2*zlar$^!%>SP7Q>Q>*U*BB`Ss6qe7LtcWWH&dkrly8Ubh{}e-x890dV0uY_wIPo zh=|+5B*WGNVZWfOmhBB@kaQBnJQ z_#~-Rdi;1rQBhG!iaaYTOQ}ptPfyqNZ&=tj5fPDx4jmE-MR|F7IXO91RaF52foIR2 zt*)*V+ISL+1~$f! z1_k7QkyAvv=3~p40U^LBm%8z*yA`PI3S{Hnn9|x_9Tee@p-?o|Te|C^G zZQEq4xV3H0zL53h`HY6KnFIHrni^PNcj$=q1j9_-+|#G^u)T_oDUGScW=@UcT9sq{ zk+!mm&UHp+7uF}ful5;y`&JR&+p@HO%(KRS59Hk-85aD6x_o2yNlD50%PqmuPbU`P z`gMn$x7!Vjd7G){73F8Ph{2#$Nsa5wE*U*!4=o!ouF`V>s_v=8wt1EAc92%4hWzm4c9PFH-ZvI}`MyR0_&01R0IvCq0~vVJtis9=2%Ol+!h zJ>@gbx?q@FZ5Smf045}15M*22vlE;YKe(}xJ~p;4y$@4uePh~cT3}%~Bn9T6o)Cne z=-H^ifU^XN>vjvDiTtBMu(9R*188P>eiZIBq$K|OI*xrsis~?I# zczs;vynoXwZ-d)ij)1nk32pXR5rBA8Xe@vUtee0}P6WEQ}tEup1_ z0ePDC?RoFQ?l0?}U)5z%Y7Zj~PX%yb(%({+f^89}RX@$Gv>19Ww5RQK-*97kQ|V1; zm0@VxFRH1_Hrs$joS5cr(iaUX<+D__;i9TyoxAqZaR}jp62m}kxrSJ5ynefJ0cffv z*y}Cn-2s;y#y4t=QlR;u>gW!t+CWbMj8|H7;yqSWh{L2jqm?39r}rdcL@yrlG2GBa z6|>FVRZ$DP4G2Vx&3K-(+fj)!??n?yV2} s`qhjhjQ0Bb@JG1x+-n*TA>HIW;5GqpB!1xXLdi zxhgx^GDXSWj?1RP3TQxXYDuC(MQ%=Bu~mhw64+cTAR8pCucQE0Qj%?}lpi<;HsXMd|v6mX?_!@pqweYA7-DfcIYB|zV677)cZXnv6c?NJ!IT>-4kQRl^q#u7!XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$9hXgo70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`$q{6sLWFOC zf^&XRs)DJWnQpS7v4w)UrJkXwrG=4+j)IYap_#scrM{twu7RPIfu)s!p#l^r*>Ne@ z6s4qD1-ZCEEd%mwl`=|73as??%gf94%8m8%i_-NCEiEne4UF`SjC6r2bc-wVN)jt{ z^NN*0MnKGPNi9w;$}A|!%+FH*nVFcBUs__Tq@)kBL?3Dk(0lrD{{cDh5K7Gh21^mp z{|cV#tbk$1lJ4m1$iT3%pZiZDE0E7v9OUlAuCN3U{uHb4?3y+}YqC>1n^%U115xtdbzVU zME`9A^NL?5Lj)5U=I-|uW%*djXXD%>ks$HlXv3YJrM#-boq8hoH546gZ|(Sf?ub1@ z%)JT`2Hu{VM!O!(7k|JsN8mypqZHebteTBc3JfRSNzb!$TUa^a?)LE6=Yu#NTtDpR vtrk?Ge}1v@*(XM7m*-xP_Rn8uAIBf~FugGFrT!sc05N#F`njxgN@xNA1qu_; literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_weighttool_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_weighttool_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..4451d3ed568d99b58a574d6e5a0977ab1c412303 GIT binary patch literal 1858 zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I3?%1nZ+yeR!04SB5>XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$9hXgo70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`$q{6sLWFOC zf^&XRs)DJWnQpS7v4w)UrJkXwrG=4+j)IYap_#scrM{twu7RPIfu)s!p#l^r*>Ne@ z6s4qD1-ZCEEd%mwl`=|73as??%gf94%8m8%i_-NCEiEne4UF`SjC6r2bc-wVN)jt{ z^NN*0MnKGPNi9w;$}A|!%+FH*nVFcBUs__Tq@)kBL?3Dk(0lrD{{cDh5K7Gh21^mp z{|cV#tbk$1lJ4m1$iT3%pZiZDE0E7v9OUlAuJkmGvkZ*bul{ zU=MUaq^r%=RJ&L=o6Gt3)>hUP0k%=DHXWh1ts%CjbL=-J+f9tH4Rx{U4Y&1mwCRtq z4R^6A^S7N7W9Q>wJt@+5NxWT%v(3~f+eLA93*+p(9jyDpZEJ&UyTfeltgT(`tk31y z&yKO%k!I&&Ywc`nT^(q9BFBD0gzb!I+hqxMq0TmIlI#v-+U?D-s|>JRkznU$XT3Vn zF2>C!%*EzRuKku&yX6UXK~6RqUN#$3?5-5p2RPZ3_}L!Mwm*~!M0UQ8)EN3zP^wtw7Y+@CD)SYydo*59cQKMAc(*HlQ#jQqLL zbRZR%?`~J5Sm=n4^T^vIq4!@oLG+rc7q^VO`{HN4ZVnEP8+Y&C?b3a<|H<5$M_W_b(%$`j^8Wbmzm;YAbE?l(RBU-# z-xuE^AUE&*)&Pcl@$C{}bv7y2Y!W!l@7Wy|clZ|Mx99S_y%T~Cv>#tqKDF@7j8o0a zZx*Tkm@6xBYU7uVfS=p5>Ex<2a-`;pGEq3?5gi|sz z_tibtGWFuT5XdeoY!-jh{J3n^tZauA?JeKm`t^-t>gjx7LM~&JX!4(Q#>k@noSXw(h>AvY9P&8`r688FSgIzk&~fF`f6M|CE*}z%SoCOJX-(f9#A8)WYPKxdQF1Ky0GDUF>TLI($?KB>h5spQtNfj6 zQ#2*y*z&vE6iy%THY)QG2o5n3y7KA{({|gLv;kCElye~FB0us969!(jHjhR-$Uj!xlnr9|7bSx0BUX|SX?6boc zZb6|hM-?{m_k38;*fd|`?_n0!z0Itw^K4!0H&<<|&0O_dpV4BLM@75z17Id$@O1Ta JS?83{1OU?{uBHG0 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/Icons/crep_zbrush_ico.png b/Scripts/Modeling/Edit/CreasePlus/Icons/crep_zbrush_ico.png new file mode 100644 index 0000000000000000000000000000000000000000..3414e198f43d4dfa14ce1ba0743cc27ff516cb0f GIT binary patch literal 3005 zcmcgtXH-+^77Y*`1OXkf(F_3u0wE#vKmrLMiGWBMil`7Egg_t=dK09mD2mb)kWd_? zBW(cbReFGgUTqXb1QC@8;)^r0T=V12`}ywr?!D*iZ=b#IIrpx0kTbn5ZMI*vNb&q(x=jhAT=d*sI#()CP+g|Nm)%x z6QQaILco>PVQ?*&vMLm=jD%|;;mV-D4=|b!q(gIYMOqk~`P+<>p}=l_e%?qJEHE%o zDNsd;N+ZFPwY0Qga0Cp2fN~g6-(W94LJ-u;SL%nvUm6BPUuPQG+mB530{ze=I8pum zP+&0ZhY9R&7fySypZ#%?pA#Z_amGU9^bf+9Avv=Hk}#$i0H7j)f0wY2lXLl67-0cr z46!!=01x22iIpJ-L?X5SKTBI%=iiKUbWs1c^%Lim|FwQ1|E&6F{FCrsga5(Q(n9`K z($v(1LZO?Ro55fy4!^Onk%LP~Ni&(uxw$z_O|6!e7HMgj(a}+Pd5EMWSV;-i*48#Y zK0Y=!rmCv;?AbF74b9fpRvDSooM#pl6+eAiwzIQyWMo7_;?&H{41>X_t*zzQDJnwM z)YSX?`-g{z8yXtK#U&;tCRi*M1frmC^i9`pL=3-rn9*r$BvueFg@Gl9E!TrKKGm9aB?Nfq_9@ zUQ`DMM^jU?uCA_ufr07iX$%Gng(~6kXA27pySuy7)6)Y20_W%F&CShob8{yrCtO^J z#>OW8{{H3V6=%*EJ$Ufo@#DwI$tf>hys)#oU}0f-`}Xbd@Nfcw;Oa{9_V$j8i}UgE zq0xM?Sp6$kB0M}Ov9Yl@9Da0^b?a9AojVDho}P(`iPhEBk&%%XFJ9Eu)e8#?tEs8U z$;knM!1?+485tSZuV1&cv_hkGLqbBUs;XRETtq}f-Q3*D%F1G5Vv352Qd93oMMZ~) zU-|7fYinzpyu7@-ckj~a^vjnWGBfG3vomBe*~Z3pVq)CM$vHSUBsDda^JY3bJD^Z# zV`CHNn`bgdhKEPKeED25U-p8NU-Gj!X9-wYZe4F*y~fker!59&Sn}R2e>@JdJ9(_) z=9f@&#m1yk4dQznE+MDkUr1`Zh36b+4IUk);4i+K+GhEr`!60TSonzX*wc@yPO1BR zCi#W@C$AmB+c{lOrM;`N5gV*D0APOx-T-4|+p&0`B70EP?j8x5wR+wA`YSHx(#e#dR9qQ0G z`pz07biE<)>lQn!GrLel_p(kSO!;dHyA1>6ChRG`tdBdgC#gfRGFgG7OHM7P!cXvG z-wOf>+ipzx2q9h6>ot7&jSj~Kfl%Z3$7|QuC#wp}%FDei&7I?B1)JH&-C8!do5DN^YCmIJ{WVwLe&uX29`}nJKx4l1_0+?3gWE|@ z40L&t&8V;U6Fy|=iEZG_=dOC##G260V6a2=ecQVOtW52I_sz`h!90Z>`=inT_)cJu zemP&vNzF|A!Z7sM3ioj6N?I5lzRsZA%c5wAG|79o6ZelW+yOUl1)8A`6}P(Km9uWm z51TfaoJ@BvZCFi`CRrpJ)*mi@G*NoLz4DkIE{8fNnbYNX6jXVuB-sm@H4U1j?E;bn^~zkaczB;o~>oOFE`9o#Qn+=N#G`^ z@o>M`fidf|2^Jts+uq(8orOQJ0pDhprk@a@3W1W1=wubG%Q=d&-MJ6I9 zWVsf)2aObu^oT_GJWDAOk_^}opWWrp?zVVvuXKey34i#!ASmiUf}O$)*HE;>wF5EwXF_D3K^j_=DS80=8* zTaRrY_TGh5v4b6h370Y;JN2OEJ5_!OFxC$E^eWB2^k5ojn%i@`SNxjF!%+!zenI=pin0divvtJkoK5kRx;B~ZMf)3lBv$t3TK~()S6R4b%Y6l5VOGttW~Ubj-&^hq zVYf01!USfc%}!^H@@o{!>jkSk+=q%4%{o$=lp+^Yz|O2PraOj=$E!#SYp3{NKPgp; zA1v?M;(zyss@I!}YDAQd)b}PHY>o_GN-KCCD7(-0+|t#M&sR4^O7U#dD95Sm4@k%K zxoOsK_YKV3a@Q|rd@Iv*4GZV?Y=aN1M*CB`--kmB4dP$#fTF9q%n+pPCD|_Zmbh#q z|6Fz(HQ%bO>7)7Z$&oIaiu;v9iktdlY(=(CUcY-K*HQXN)1YP1v~guX|MAw{_T&23 z`ZuNoobn$Z;4=DzFiS5%)8qOk3_lZkG~B`*3fmN@!XKIJDH%JaGAc^|5|P1-k$3oh zLz19yoVf*IG~KqA%Dyv`A+t7>{R(r3GQ{)9n(dKcQohn;r0c#iY7P^WXm&elnoSC_ zgJu+^sh>^znjU@7biFNsOGaLsFXjTIwihcEFaG&mPg|9Z;K8prXaEo#vD>3sp~@ulMjJ^)r8Fbim)rwuu2wQ-*fV%Pm=Q>1MQpd zE+LJXb%jmUGRty&a>9CZdsU*n#B$Btq~A1VZU#scm7>?q?#3IbsrbQJJ#v(X+2}+d`=+)Lv%f5_U`B!HOvL;c~&&hGxl!i;QWnfnWA`E4E)G z8q>y3j0Eza9nuPLJC**+dC@6$@_6PjZs5&@L8Y*|88k%F#AVYYj~tviAsF#t?cU2Z zt^FFJg~Iqa<8~ffJ@4@krEXk_b)&dN<*Gqa0^MCVOaJ4fhp!xJ)RHR Y!d9=`bsyn$IR8Tc-te438P+lCKfeS!XaE2J literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/CreasePlus/MayaUndoRun.py b/Scripts/Modeling/Edit/CreasePlus/MayaUndoRun.py new file mode 100644 index 0000000..68b4ceb --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/MayaUndoRun.py @@ -0,0 +1,17 @@ +from functools import wraps +import maya.cmds as cmds + +def mayaUndoRun(func): + """ Puts the wrapped `func` into a single Maya Undo action, then + undoes it when the function enters the finally: block """ + @wraps(func) + def _undofunc(*args, **kwargs): + try: + # start an undo chunk + cmds.undoInfo(openChunk=True) + return func(*args, **kwargs) + finally: + # after calling the func, end the undo chunk + cmds.undoInfo(closeChunk=True) + + return _undofunc \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/__init__.py b/Scripts/Modeling/Edit/CreasePlus/__init__.py new file mode 100644 index 0000000..64f3743 --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/__init__.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# Empty Init file +from . import * \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/delpyc.bat b/Scripts/Modeling/Edit/CreasePlus/delpyc.bat new file mode 100644 index 0000000..7c2b498 --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/delpyc.bat @@ -0,0 +1,2 @@ +@echo off +del *.pyc \ No newline at end of file diff --git a/Scripts/Modeling/Edit/CreasePlus/readit.txt b/Scripts/Modeling/Edit/CreasePlus/readit.txt new file mode 100644 index 0000000..96a428c --- /dev/null +++ b/Scripts/Modeling/Edit/CreasePlus/readit.txt @@ -0,0 +1,40 @@ +_____ _____ ______ _____ ______ +/ ____| __ \| ____| /\ / ____| ____|_ +| | | |__) | |__ / \ | (___ | |__ _| |_ +| | | _ /| __| / /\ \ \___ \| __|_ _| +| |____| | \ \| |____ / ____ \ ____) | |____|_| +\_____|_| \_\______/_/ \_\_____/|______| + +This version of CreasePlus is rewritten entirely in python cmds(maya) and API's. +thus there is no longer support for MayaLT. And the run-in lang is Python. + +install : +extract, then just place icon and main folders in documents/maya/(maya_version)/scripts/ +restart maya if opened. + +# +# attach it as python script / runtime command to hotkey: +import maya.cmds as cmds +from CreasePlus import CreasePlusMain +CreasePlusMain.start() + +if not cmds.pluginInfo("CreasePlusNodes", q=True, loaded=True): + cmds.loadPlugin("CreasePlusNodes.py") + + +# attach it as python script / runtime command to hotkey, for attribute iteration in context (optional): +from CreasePlus import CreasePlusMain +CreasePlusMain.crepcore.creasePlusLastCtx() + +# attach it as python script / runtime command to hotkey, for edge soft/hard toggle (optional): +from CreasePlus import CreasePlusMain +CreasePlusMain.crepcore.creasePlusToggleEdgeSmooth() + +# attach it as python script / runtime command to hotkey, for edge makeUV (optional): +from CreasePlus import CreasePlusMain +CreasePlusMain.crepcore.creasePlusMakeUv() + + +# commands / defs for bindings / scripts can be found in def_sheet file + +# thank you \ No newline at end of file diff --git a/Scripts/Modeling/Edit/gs_curvetools/LICENSE.txt b/Scripts/Modeling/Edit/gs_curvetools/LICENSE.txt new file mode 100644 index 0000000..253c22f --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/LICENSE.txt @@ -0,0 +1,201 @@ +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +https://www.artstation.com/marketplace-product-eula + +Marketplace Product & Services Agreement +End User Agreement +This Marketplace End User Agreement applies to all downloadable products and professional services (e.g. mentorships, personal training, portfolio reviews) sold via the ArtStation Marketplace, unless a custom agreement or license is provided by the seller. + +The EUA is an agreement between the buyer and the seller providing the goods or services. + +PLEASE READ THIS DOCUMENT CAREFULLY. IT SIGNIFICANTLY ALTERS YOUR LEGAL RIGHTS AND REMEDIES. + +BY CLICKING “I AGREE” OR DOWNLOADING OR USING THE DIGITAL PRODUCT OR RECEIVING THE PROFESSIONAL SERVICES TO WHICH THIS AGREEMENT RELATES YOU ACCEPT ALL OF THIS AGREEMENT’S TERMS, INCLUDING THE DISCLAIMERS OF WARRANTIES AND LIMITATIONS ON DAMAGES, USE AND TRANSFERABILITY. IF YOU DO NOT ACCEPT THIS AGREEMENT’S TERMS, DO NOT DOWNLOAD, INSTALL OR USE THE DIGITAL PRODUCT OR RECEIVE OR USE THE PROFESSIONAL SERVICES. + +This end-user agreement (“Agreement”) is a legally binding agreement between you, the licensee and customer (“you” or “your”), and the provider (“we” or “us” or “our”) of the digital products (“Products”) or instructional, training, mentorship or other professional service packages (“Professional Services”) that you purchase through the ArtStation Marketplace, regarding your rights and obligations regarding those Products and Professional Services. + +1. Your Status +In this Agreement, “you” means the person or entity acquiring rights in the Products or purchasing Professional Services. That may be a natural person, or a corporate or business entity or organization. + +(a) If you are a natural person then you must be, and you confirm that you are, at least 13 years old. If you are between 13 years and the age of majority in your jurisdiction of residence, you confirm that your parent or legal guardian has reviewed and agrees to this Agreement and is happy for you to access and use the Product or receive the Professional Services. + +(b) If you are a corporate entity then: (i) the rights granted under this Agreement are granted to that entity; (ii) you represent and warrant that the individual completing and accepting this Agreement is an authorized your representative and has the authority to legally bind that you to the Agreement; and (iii) to the extent that one or more of your employees are granted any rights in the Product or to receive Professional Services under this Agreement, you will ensure that your employees comply with this Agreement and you will be responsible and liable for any breach of this Agreement by any employee. + +2. ArtStation +ArtStation is a division of Epic Games, Inc., You acknowledge and agree that Epic is a third-party beneficiary of this Agreement and therefore will be entitled to directly enforce and rely upon any provision in this Agreement that confers a benefit on, or rights in favour of, Epic. In addition, you authorize Epic to act as your authorized representative to file a lawsuit or other formal action against a licensor in a court or with any other governmental authority if Epic knows or suspects that a licensor breached any representations or warranties under this Agreement. The foregoing authorization is nonexclusive, and Epic shall be under no obligation to pursue any claim. Epic will not initiate any such action on your behalf without first consulting with and obtaining your approval. + +Products +The following sections 3 through 9 apply to any Products you acquire from us through the ArtStation Marketplace: + +3. Product Licence +Subject to this Agreement’s terms and conditions, we hereby grant you a limited, non-exclusive, worldwide, non-transferable right and licence to (which will be perpetual unless the licence terminates as set out in this Agreement): (a) download the Product; and (b) copy and use the Product. We reserve all rights not expressly granted to you under this Agreement. + +4. Licence Scope and Restrictions +(a) Tutorials +You are purchasing ONE licence to create ONE copy of the Product for use by you only (or, if you are a corporate entity, for use by a single authorized employee). + +If this Product is bundled with a stock digital asset then you receive a limited personal use licence regarding that stock digital asset, and you may use that stock digital asset for your personal use only. You will not use that stock digital asset in any commercial manner unless you purchase a separate commercial licence. + +(b) Installable Tools +You may purchase one or more licences for the Product. A single licence allows you to install the Product on a single computer at a time for use by a single authorized user. If you are a corporate entity and the authorized employee completing the transaction on your behalf purchases multiple licences, you may choose to store the Product on a single server or shared hard drive for use by a single authorized employee at a time for each licence purchased. + +Provided that you comply with the restrictions on users set out above, you may use the Product on an unlimited number of projects. + +(c) Stock Assets +Subject to the restrictions set out in this Agreement, you may copy, use, modify, adapt, translate, distribute, publicly display, transmit, broadcast, and create derivative works from the Product in works you create (“Works”), which may include things like films, videos, multi-media projects, computer games, models, images, publications, broadcasts, documents, and presentations. + +If you are a corporate entity, you may make the Product available for use by your employees in accordance with this Agreement (for example, by storing the Product on a network server). + +You may only share the Product with external people or entities where: + +You are collaborating with the external parties in the creation of your Work and you need to share the Product for that purpose, provided that any external party that receives the Product may only use it in your Work and must secure and limit access to the Product for that purpose; +You are working as a contractor for a client in the creation of a Work and need to share the Product with your client, or any external parties working with your client, provided that your client and any such external parties may use the Product only for your client’s Work, and all parties secure and limit access to the Product for that purpose. +For any other use of the Product by any other party, that party must purchase a licence to the Product. + +In addition to any other restrictions in this Agreement, you will not: + +publish, sell, license, offer or make available for sale or licensing, or otherwise distribute the Product except as part of a Work or through a form of sharing that is authorized in this Agreement; or +publish, distribute or make available the Product through any online clearinghouse platform. +FURTHER SPECIFIC TERMS +In addition to the restrictions set out above, the following terms and conditions apply to the following forms of commercial licences for the Product: + +Standard Commercial Licence +If you have purchased a Standard Commercial licence then you may exercise your rights under that licence: + +for personal use on an unlimited number of personal projects that are not used or distributed in any commercial manner; and +respect to one commercial Work, with up to a maximum of, as applicable, 2,000 sales of the Work or 20,000 monthly views of the Work. +Extended Commercial Licence +If you have purchased an Extended Commercial licence then you may exercise your rights under that licence: + +for personal use on an unlimited number of personal projects that are not used or distributed in any commercial manner; and +with respect to any number of commercial Works, with no limit on sales or views. +5. Additional Restrictions +Except as expressly permitted under this Agreement, you will not: + +(a) make any copy of the Product except for archival or backup purposes; + +(b) circumvent or disable any access control technology, security device, procedure, protocol, or technological protection mechanism that may be included or established in or as part of the Product; + +(c) hack, reverse engineer, decompile, disassemble, modify or create derivative works of the Product or any part of the Product; + +(d) publish, sell distribute or otherwise make the Product available to others to use, download or copy; + +(e) transfer or sub-license the Product or any rights under this Agreement to any third party, whether voluntarily or by operation of law; + +(f) use the Product for any purpose that may be defamatory, threatening, abusive, harmful or invasive of anyone’s privacy, or that may otherwise violate any law or give rise to civil or other liability; + +(g) misrepresent yourself as the creator or owner of the Property; + +(h) remove or modify any proprietary notice, symbol or label in or on the Product; + +(i) directly or indirectly assist, facilitate or encourage any third party to carry on any activity prohibited by this Agreement. + +6. Proprietary Rights +The Product is protected by copyright laws and international copyright treaties, as well as other intellectual property laws and treaties. You are licensing the Product and the right to access, install and use the Product in accordance with this Agreement, not buying the Product. As between you and us, we own all right, title and interest in and to the Product, and you are not acquiring any ownership of or rights in the Product except the limited rights granted under this Agreement. + +7. No Epic Support +You acknowledge and agree that you are licensing the Product from us (the Provider), not from Epic, and that Epic has no obligation to support the Product. + +8. Interruptions and Errors +Your use of the Product might be interrupted and might not be free of errors. + +9. Updates +We have no obligation to update the Product. + +Professional Services +The following sections 10 and 11 apply to any Professional Services you purchase from us through the ArtStation Marketplace: + +10. Provision of Professional Services +We will provide the Professional Services directly to you and, subject to this Agreement, will assume all responsibility for all aspects of the Professional Services. We represent and warrant that we have the right to offer and provide the Professional Services and that we have appropriate qualifications and experience to provide the Professional Services. + +11. Epic is not Involved +You acknowledge and agree that: + +(a) Epic is only a provider of the online ArtStation Marketplace where you purchased the Professional Services, and does not provide or exercise any control or oversight over us or the Professional Services, and is not responsible for us or the Professional Services or any shortcomings in them, including any damages, losses or legal issues caused by us or the Professional Services; + +(b) this Agreement (and any dispute under it) is an agreement between us and you only, and not with Epic, and Epic is not a party to this Agreement; + +(c) we are not Epic’s employee, agent or subcontractor; + +(d) Epic does not have any obligation to attempt to resolve any dispute between us and you; and + +(e) we will provide the Professional Services directly to you, and we (and not Epic) are solely responsible for the Professional Services, and Epic has no obligation or liability to you with respect to the Professional Services. + +Both Products and Services +The following sections 12 through 25 apply to all Products or Services you purchase from us through the ArtStation Marketplace: + +12. Disclaimer +ANY PRODUCTS OR PROFESSIONAL SERVICES ARE PROVIDED ON AN “AS IS” AND “AS AVAILABLE” BASIS, WITHOUT ANY REPRESENTATIONS, WARRANTIES OR CONDITIONS OF ANY KIND. + +TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW WE DISCLAIM, AND YOU WAIVE (WITH REGARD TO US AND ALSO TO EPIC, ITS AFFILIATES, AND ITS AND THEIR LICENSORS AND SERVICE PROVIDERS (COLLECTIVELY, THE “EPIC PARTIES”), ALL TERMS, CONDITIONS, GUARANTEES, REPRESENTATIONS AND WARRANTIES (EXPRESS, IMPLIED, STATUTORY AND OTHERWISE), IN RESPECT OF THE PRODUCTS AND PROFESSIONAL SERVICES, INCLUDING THOSE OF MERCHANTABILITY, NON-INFRINGEMENT, TITLE, QUALITY AND FITNESS FOR A PARTICULAR PURPOSE. + +NEITHER WE NOR ANY OF THE EPIC PARTIES REPRESENT OR WARRANT THAT: (A) ANY PRODUCT OR PROFESSIONAL SERVICE IS ACCURATE, COMPLETE, RELIABLE, CURRENT OR ERROR-FREE; (B) ANY PRODUCT OR PROFESSIONAL SERVICE WILL MEET YOUR REQUIREMENTS OR EXPECTATIONS; (C) ANY PRODUCT OR PROFESSIONAL SERVICES IS FREE OF VIRUSES OR OTHER HARMFUL COMPONENTS; OR (D) ANY DEFECTS IN ANY PRODUCT OR PROFESSIONAL SERVICE WILL BE CORRECTED. + +13. Exclusion and Limitation of Liability +(a) YOU DOWNLOAD, INSTALL AND OTHERWISE USE ALL PRODUCTS, AND RECEIVE AND USE ALL PROFESSIONAL SERVICES, AT YOUR OWN RISK. YOU AGREE TO, AND HEREBY DO: + +(i) WAIVE ANY CLAIMS THAT YOU MAY HAVE AGAINST US OR THE EPIC PARTIES OR OUR RESPECTIVE DIRECTORS, OFFICERS, EMPLOYEES, AGENTS, REPRESENTATIVES, LICENSORS, SUCCESSORS AND ASSIGNS (COLLECTIVELY THE “RELEASEES”) ARISING FROM OR RELATING TO ANY PRODUCTS OR PROFESSIONAL SERVICES, AND + +(ii) RELEASE THE RELEASEES FROM ANY LIABILITY FOR ANY LOSS, DAMAGE, EXPENSE OR INJURY ARISING FROM OR RELATING TO YOUR USE OF ANY PRODUCT OR PROFESSIONAL SERVICE, WHETHER ARISING IN TORT (INCLUDING NEGLIGENCE), CONTRACT OR OTHERWISE, EVEN IF THE RELEASEES ARE EXPRESSLY ADVISED OF THE POSSIBILITY OF SUCH LOSS, INJURY OR DAMAGE AND EVEN IF THAT LOSS, INJURY OR DAMAGE IS FORESEEABLE. + +(b) NEITHER WE NOR THE EPIC PARTIES WILL BE LIABLE FOR ANY LOSSES, DAMAGES, CLAIMS OR EXPENSES THAT CONSTITUTE: (I) LOSS OF INTEREST, PROFIT, BUSINESS, CUSTOMERS OR REVENUE; (II) BUSINESS INTERRUPTIONS; (III) COST OF REPLACEMENT PRODUCTS OR SERVICES; OR (IV) LOSS OF OR DAMAGE TO REPUTATION OR GOODWILL. + +(c) NEITHER WE NOR THE EPIC PARTIES WILL BE LIABLE FOR ANY LOSSES, DAMAGES, CLAIMS OR EXPENSES THAT CONSTITUTE INCIDENTAL, CONSEQUENTIAL, SPECIAL, PUNITIVE, EXEMPLARY, MULTIPLE OR INDIRECT DAMAGES, EVEN IF WE HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH LOSSES, DAMAGES, CLAIMS OR EXPENSES. + +(d) MAXIMUM LIABILITY: IF, DESPITE THE LIMITATIONS SET OUT ABOVE, WE OR ANY EPIC PARTY BECOME LIABLE TO YOU IN RESPECT OF ANY PRODUCT OR PROFESSIONAL SERVICE OR OTHERWISE UNDER THIS AGREEMENT, THE ENTIRE CUMULATIVE LIABILITY OF US AND THE EPIC PARTIES, AND YOUR EXCLUSIVE AND CUMULATIVE REMEDY, FOR ANY DAMAGES (REGARDLESS OF THE CAUSE OR FORM OR ACTION), WILL BE LIMITED TO CAD$10. + +14. Indemnity +As a condition of your use of any Product or any Professional Services, you agree to hold harmless and indemnify the Releasees from any liability for any loss or damage to any third party resulting from your access to, installation or use of the Product or your receipt and use of the Professional Services. + +15. Term and Termination +This Agreement is effective until terminated. Your rights under this Agreement will terminate automatically without notice if: (a) you breach any terms of this Agreement; or (b) you do not complete payment for the Product or Professional Services, or any payment you make is refunded, reversed or cancelled for any reason. Upon this Agreement’s termination, you will cease all use of the Product and destroy all copies, full or partial, of the Product in your possession. Sections 11 through 25 will survive the termination of this Agreement. + +16. Compliance with Laws +You will comply with all applicable laws when using any Product or Professional Services (including intellectual property and export control laws). + +17. Entire Agreement +This Agreement supersedes all prior agreements of the parties regarding the Product or Professional Services, and constitutes the whole agreement with respect to the Product or Professional Services. + +18. Disputes +If you have any concerns about the Product or Professional Services, please contact us through our ArtStation Marketplace account and we will work with you to try to resolve the issue. You acknowledge and agree that any such dispute is between you and us, and that Epic will not be involved in the dispute and has no obligation to try to resolve the dispute. + +19. Persons Bound +This Agreement will enure to the benefit of and be binding upon the parties and their heirs, executors, administrators, legal representatives, lawful successors and permitted assigns. + +20. Assignment +We may assign this Agreement without notice to you. You may not assign this Agreement or any of your rights under it without our prior written consent, which we will not withhold unreasonably. + +21. Waiver +No waiver, delay, or failure to act by us regarding any particular default or omission will prejudice or impair any of our rights or remedies regarding that or any subsequent default or omission that are not expressly waived in writing. + +22. Applicable Law and Jurisdiction +You agree that this Agreement will be deemed to have been made and executed in the State of North Carolina, U.S.A., and any dispute will be resolved in accordance with the laws of North Carolina, excluding that body of law related to choice of laws, and of the United States of America. Any action or proceeding brought to enforce the terms of this Agreement or to adjudicate any dispute must be brought in the Superior Court of Wake County, State of North Carolina or the United States District Court for the Eastern District of North Carolina. You agree to the exclusive jurisdiction and venue of these courts. You waive any claim of inconvenient forum and any right to a jury trial. The Convention on Contracts for the International Sale of Goods will not apply. Any law or regulation which provides that the language of a contract shall be construed against the drafter will not apply to this Agreement. + +23. Legal Effect +This Agreement describes certain legal rights. You may have other rights under the laws of your country. This Agreement does not change your rights under the laws of your country if the laws of your country do not permit it to do so. + +24. Interpretation +In this Agreement, "we", "us", and "our" refer to the licensor of the Product alone and never refer to the combination of you and that licensor (that combination is referred to as "the parties"), or the combination of you or the licensor with Epic. + +25. Artificial Intelligence +For purposes of this Agreement, “Generative AI Programs” means artificial intelligence, machine learning, deep learning, neural networks, or similar technologies designed to automate the generation of or aid in the creation of new content, including but not limited to audio, visual, or text-based content. + +We (the licensor of the Product) represent and warrant that where the Product was created using Generative AI Programs, we have applied the “CreatedWithAI” tag. Under this Agreement, a Product is considered to be created using Generative AI Programs where a material portion of a Product is generated with Generative AI Programs, whether characters, backgrounds, or other material elements. A Product is not considered to be created using Generative AI Programs merely for use of features that solely operate on a Product (e.g., AI-based upscaling or content-aware fill). diff --git a/Scripts/Modeling/Edit/gs_curvetools/README.txt b/Scripts/Modeling/Edit/gs_curvetools/README.txt new file mode 100644 index 0000000..95b782d --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/README.txt @@ -0,0 +1,35 @@ +GS CurveTools installation + +1. Copy gs_curvetools folder to {Path to Documents}\Documents\Maya\{Maya Version}\scripts\ + + Example of the final folder structure: + + Documents\Maya\2022\scripts\gs_curvetools\fonts + Documents\Maya\2022\scripts\gs_curvetools\icons + Documents\Maya\2022\scripts\gs_curvetools\utils + Documents\Maya\2022\scripts\gs_curvetools\__init__.py + Documents\Maya\2022\scripts\gs_curvetools\core.py + Documents\Maya\2022\scripts\gs_curvetools\init.py + Documents\Maya\2022\scripts\gs_curvetools\LICENSE.txt + Documents\Maya\2022\scripts\gs_curvetools\main.py + Documents\Maya\2022\scripts\gs_curvetools\README.txt + +2. Run Maya + +3. Copy and paste this line to "Python" command box and press "Enter": + +import gs_curvetools.init as ct_init;from imp import reload;reload(ct_init);ct_init.Init(); + +IMPORTANT: There should be no spaces or tabs before this command! + +4. Look for GS tab on your Shelf + +5. Click CT UI button to run the menu. Click again to hide the menu. + +NOTES: +>> To reset to factory defaults click CT with "refresh" arrow button. +>> To stop all scripts and close the menu press CT DEL button. +>> You can use middle-mouse button drag to move the buttons to any tab. +>> All the hotkeys are available in Hotkey Editor > Custom Scripts > GS > GS_CurveTools. +>> Always repeat initialization steps when updating the plug-in to a new version. +>> You can always repeat initialization steps if you lost control buttons or shelf. diff --git a/Scripts/Modeling/Edit/gs_curvetools/__init__.py b/Scripts/Modeling/Edit/gs_curvetools/__init__.py new file mode 100644 index 0000000..413c239 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/__init__.py @@ -0,0 +1,10 @@ +from . import * +from . import fonts +from . import icons +from . import plugins +from . import utils +from . import core +from . import init +from . import main +from . import ui +from . import uv_editor diff --git a/Scripts/Modeling/Edit/gs_curvetools/constants.py b/Scripts/Modeling/Edit/gs_curvetools/constants.py new file mode 100644 index 0000000..09c0b7c --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/constants.py @@ -0,0 +1,69 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" +import base64 as __B64 +from datetime import datetime as __DT + +import maya.cmds as __mc +from PySide2 import QtWidgets as __QTW + + +def __getMayaOS(): + """Get Maya version and parent OS""" + maya = str(__mc.about(api=1))[:4] + os = str(__mc.about(os=1)) + return [int(maya), os] + + +MAYA_VER = __getMayaOS()[0] +OS = __getMayaOS()[1] + +DEBUG = True + +VERSION = __B64.b64decode(b"R1MgQ3VydmVUb29scyB2MS4zLjEKU3R1ZGlvIEVkaXRpb24=").decode('utf-8') + +try: + YEAR = __DT.now().year +except BaseException: + YEAR = 2023 + +MAIN_WINDOW_NAME = 'GSCT_CurveTools' +MAIN_WINDOW_LABEL = 'GS CurveTools' +CURVE_CONTROL_NAME = 'GSCT_CurveControl' +CURVE_CONTROL_LABEL = 'GS Curve Control' +UV_EDITOR_NAME = 'GSCT_UVEditor' +UV_EDITOR_LABEL = 'GS Curve Tools UV Editor' +SCALE_FACTOR_UI = 'GSCT_ScaleFactorWindow' + +UI_SCRIPT = ''' +import gs_curvetools.utils.utils as ct_ut +ct_ut.logger.logger.info("Uninitializing Workspace Control") +maya.cmds.evalDeferred(ct_ut.stopUI) +''' + +FIXED_POLICY = __QTW.QSizePolicy(__QTW.QSizePolicy.Fixed, + __QTW.QSizePolicy.Fixed) +PREFERRED_POLICY = __QTW.QSizePolicy(__QTW.QSizePolicy.Preferred, + __QTW.QSizePolicy.Preferred) +EXPANDING_POLICY = __QTW.QSizePolicy(__QTW.QSizePolicy.Expanding, + __QTW.QSizePolicy.Expanding) diff --git a/Scripts/Modeling/Edit/gs_curvetools/core.py b/Scripts/Modeling/Edit/gs_curvetools/core.py new file mode 100644 index 0000000..377896b --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/core.py @@ -0,0 +1,6729 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import colorsys +import math +import os +import random +import re +from imp import reload + +import maya.api.OpenMaya as om +import maya.cmds as mc +import maya.mel as mel + +from .constants import * +from .utils import gs_math as mt +from .utils import style, utils, wrap +from .utils.wrap import WIDGETS + +reload(utils) +reload(mt) +reload(wrap) +reload(style) + +MESSAGE = utils.logger +LOGGER = utils.logger.logger + + +### Core Classes ### + +class Attributes: + + def __init__(self, name): + self.name = name + self.copyAttributesSourceCurve = None + self.copyUVsSourceCurve = None + + attrList = { + 'lengthDivisions', + 'dynamicDivisions', + 'widthDivisions', + 'Orientation', + 'Twist', + 'invTwist', + 'Width', + 'WidthX', + 'WidthZ', + 'LengthLock', + 'Length', + 'Taper', + 'Profile', + 'curveRefine', + 'autoRefine', + 'curveSmooth', + 'reverseNormals', + 'surfaceNormals', + 'flipUV', + 'moveU', + 'moveV', + 'rotateUV', + 'rotateRootUV', + 'rotateTipUV', + 'scaleU', + 'scaleV', + 'solidify', + 'solidifyThickness', + 'solidifyDivisions', + 'solidifyScaleX', + 'solidifyScaleY', + 'solidifyOffset', + 'solidifyNormals', + 'Axis', + 'AxisFlip', + 'Offset', + 'lineWidth', + 'samplingAccuracy', + 'Magnitude', + 'profileSmoothing', + 'profileMagnitude', + } + + uvAttr = { + 'moveU', + 'moveV', + 'rotateUV', + 'rotateRootUV', + 'rotateTipUV', + 'scaleU', + 'scaleV', + 'flipUV', + } + + checkBoxes = { + 'autoRefine', + 'dynamicDivisions', + 'LengthLock', + 'reverseNormals', + 'solidify', + 'Axis', + 'AxisFlip', + } + + multiInst = { + 'twistCurve', + 'scaleCurve' + } + + graphAttributes = { + 'twistCurve', + 'scaleCurve', + 'profileCurve', + } + + def getAttr(self, inputCurve, excludeUV=False, excludeCheckboxes=False, manualExclude=False): + getAttr = dict() + for attr in self.attrList: + if excludeUV and attr in self.uvAttr: + continue + if excludeCheckboxes and attr in self.checkBoxes: + continue + if manualExclude and attr in manualExclude: + continue + try: + getAttr[attr] = mc.getAttr(inputCurve + '.' + attr) + except Exception as e: + # LOGGER.exception(e) + pass + return getAttr + + def getCheckboxes(self, inputCurve): + checkboxes = dict() + for attr in self.checkBoxes: + try: + checkboxes[attr] = mc.getAttr(inputCurve + '.' + attr) + except BaseException: + pass + return checkboxes + + def getUVs(self, inputCurve): + getUVs = dict() + for attr in self.uvAttr: + try: + getUVs[attr] = mc.getAttr(inputCurve + '.' + attr) + except BaseException: + pass + return getUVs + + def getMultiInst(self, inputCurve): + if mc.attributeQuery('Length', n=inputCurve, ex=1): + try: + node = mc.ls(mc.listHistory(selectPart(2, True, inputCurve), ac=1, il=0), typ='curveWarp')[0] + except BaseException: + return None + returnList = list() + for attr in self.multiInst: + cList = list() + cIndex = mc.getAttr(node + '.' + attr, mi=1) + for i in cIndex: + cList.append(mc.getAttr(node + '.%s[%s]' % (attr, i))[0]) + cList.append(attr) + returnList.append(cList) + return returnList + + @staticmethod + def setMultiInst(targetCurve, inputList): + if mc.attributeQuery('Length', n=targetCurve, ex=1): + geo = selectPart(2, True, targetCurve) + targetNode = mc.ls(mc.listHistory(geo, ac=1, il=0), typ='curveWarp')[0] + attribute = inputList[-1] + + attributes.resetMultiInst(targetNode, attribute) + + for i in range(len(inputList) - 1): + mc.setAttr(targetNode + '.%s[%s]' % (attribute, i), inputList[i][0], inputList[i][1]) + + @staticmethod + def resetMultiInst(node, attr): + index = mc.getAttr(node + '.%s' % attr, mi=1) + for i in index: + if i == 0: + continue + mc.removeMultiInstance(node + '.%s[%s]' % (attr, i)) + for i in range(4): + mc.setAttr(node + '.%s[%s]' % (attr, i), i / 3.0, 0.5, typ='double2') + + @staticmethod + def blendMultInst(source, target, ratio): # TODO: Add better support for different number of points + returnList = list() + sourceL = len(source) + targetL = len(target) + if sourceL == targetL: + for i in range(sourceL - 1): + returnList.append((source[i][0], mt.lerp(ratio, source[i][1], target[i][1]))) + elif sourceL < targetL: + returnList.append((source[0][0], mt.lerp(ratio, source[0][1], target[0][1]))) + for i in range(1, sourceL - 2): + returnList.append((source[i][0], mt.lerp(ratio, source[i][1], target[i][1]))) + returnList.append((source[-2][0], mt.lerp(ratio, source[-2][1], target[-2][1]))) + else: + returnList.append((source[0][0], mt.lerp(ratio, source[0][1], target[0][1]))) + for i in range(1, targetL - 2): + returnList.append((source[i][0], mt.lerp(ratio, source[i][1], target[i][1]))) + returnList.append((source[-2][0], mt.lerp(ratio, source[-2][1], target[-2][1]))) + returnList.append(source[-1]) + return returnList + + @staticmethod + def setAttr(inputCurve, inputDict, exclude=None): + for attr in inputDict: + if exclude and attr in exclude: + continue + try: + mc.setAttr(inputCurve + "." + attr, inputDict[attr]) + except BaseException: + pass + + def copyAttributes(self): + """Select the last curve in selection list for attribute copy command""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + self.copyAttributesSourceCurve = None + if not sel: + MESSAGE.warningInView('Select at least one Curve') + return + self.copyAttributesSourceCurve = sel[-1] + mc.headsUpMessage('[Copied]', o=self.copyAttributesSourceCurve, time=1, vp=1) + + def pasteAttributes(self): + """Copy and paste the attributes from the source curve (copyAttributes function) to the target selection list""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one Curve.') + return + if not self.copyAttributesSourceCurve: + MESSAGE.warningInView('No copied attributes available.') + return + if not mc.objExists(self.copyAttributesSourceCurve): + MESSAGE.warningInView('Original curve was not found, copying cancelled.') + self.copyAttributesSourceCurve = None + return + sourceAttr = self.getAttr(self.copyAttributesSourceCurve, excludeUV=True) + filterAttrs = dict(eval(mc.optionVar(q='GSCT_AttributesFilter'))) + filteredSourceAttrs = sourceAttr.copy() + for attr in filterAttrs: + if filterAttrs and attr in filterAttrs and not filterAttrs[attr]: + filteredSourceAttrs.pop(attr, None) + multiInd = None + if 'Length' in sourceAttr: + multiInd = self.getMultiInst(self.copyAttributesSourceCurve) + for attr in multiInd: + if filterAttrs and attr[-1] in filterAttrs and not filterAttrs[attr[-1]]: + continue + for target in sel: + self.setMultiInst(target, attr) + for target in sel: + self.setAttr(target, filteredSourceAttrs) + if filterAttrs and 'profileCurve' in filterAttrs and filterAttrs['profileCurve']: + transferProfileCurve(self.copyAttributesSourceCurve, target) + LOGGER.info('Attributes transferred from "%s" to %s target curve(s).' % (self.copyAttributesSourceCurve, len(sel))) + curveControlUI.updateUI() + + def transferAttr(self, hk=None): # Transfer settings from curve to curve + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel or len(sel) < 2: + MESSAGE.warningInView('Select at least two Curves') + return -1 + # Init Vars + source = str() + targets = list() + # Get modifier + mod = True if hk == 2 or (hk is None and utils.getMod() == 'Shift') else False + # Set source and targets + if mod: + source = sel[-1] + targets = sel[0:-1] + else: + source = sel[0] + targets = sel[1:] + mc.headsUpMessage('[Source]', o=source, time=1, vp=1) + sourceAttr = self.getAttr(source, 1) + filterAttrs = dict(eval(mc.optionVar(q='GSCT_AttributesFilter'))) + filteredSourceAttrs = sourceAttr.copy() + for attr in filterAttrs: + if filterAttrs and attr in filterAttrs and not filterAttrs[attr]: + filteredSourceAttrs.pop(attr, None) + multiInd = None + if 'Length' in sourceAttr: + multiInd = self.getMultiInst(source) + for attr in multiInd: + if filterAttrs and attr[-1] in filterAttrs and not filterAttrs[attr[-1]]: + continue + for target in targets: + self.setMultiInst(target, attr) + for target in targets: + self.setAttr(target, filteredSourceAttrs) + if filterAttrs and 'profileCurve' in filterAttrs and filterAttrs['profileCurve']: + transferProfileCurve(source, target) + LOGGER.info('Attributes transferred from "%s" to %s target curve(s).' % (source, len(targets))) + curveControlUI.updateUI() + + def copyUVs(self): + """Select the last curve in selection list for UVs copy command""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + self.copyUVsSourceCurve = None + if not sel: + MESSAGE.warningInView('Select at least one Curve') + return + self.copyUVsSourceCurve = sel[-1] + mc.headsUpMessage('[Copied]', o=self.copyUVsSourceCurve, time=1, vp=1) + + def pasteUVs(self): + """Copy and paste the UVs from the source curve (copyUVs function) to the target selection list""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one Curve.') + return + if not self.copyUVsSourceCurve: + MESSAGE.warningInView('No copied UVs available.') + return + if not mc.objExists(self.copyUVsSourceCurve): + MESSAGE.warningInView('Original curve was not found, copying cancelled.') + self.copyUVsSourceCurve = None + return + source = self.copyUVsSourceCurve + targets = sel + self._transferUVs(source, targets) + LOGGER.info('UVs transferred from "' + str(source) + '" to ' + str(len(sel)) + ' target curve(s).') + curveControlUI.updateUI() + if mc.workspaceControl(UV_EDITOR_NAME, q=1, ex=1): + from . import ui + ui.uveditor.updateEditor() + + def transferUV(self, hk=None): # Transfer Curve UVs + # type: (int|bool|None) -> None + """Transfer UVs from last/first selected curve to all others""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel or len(sel) < 2: + MESSAGE.warningInView('Select at least two Curves') + return -1 + source = str() + targets = list() + # Get modifier + mod = True if hk == 2 or (hk is None and utils.getMod() == 'Shift') else False + # Set source and targets + if mod: + source = sel[-1] + targets = sel[0:-1] + else: + source = sel[0] + targets = sel[1:] + mc.headsUpMessage('[Source]', o=source, time=1, vp=1) + self._transferUVs(source, targets) + LOGGER.info('UVs transferred from "' + str(source) + '" to ' + str(len(targets)) + ' target curve(s).') + curveControlUI.updateUI() + if mc.workspaceControl(UV_EDITOR_NAME, q=1, ex=1): + from . import ui + ui.uveditor.updateEditor() + + def _transferUVs(self, source, targets): + # type: (str, list[str]) -> None + """Transfer UVs from source to target""" + # Check if source curve is bound curve + filterAttrs = dict(eval(mc.optionVar(q='GSCT_AttributesFilter'))) + if mc.attributeQuery('gsmessage', n=source, ex=1): + bindMessage = mc.listConnections(source + '.gsmessage', d=1, s=0) + if bindMessage: + source = bindMessage + # Get source UVs + sourceUVs = [] + if isinstance(source, list): + for s in source: + sourceUVs.append(self.getUVs(s)) + else: + sourceUVs.append(self.getUVs(source)) + # Filter source UVs + for uvs in sourceUVs: + for attr in uvs: + if filterAttrs and attr in filterAttrs and not filterAttrs[attr]: + uvs.pop(attr, None) + for target in targets: + # Check if target is a bound curve + bindMessage = None + if mc.attributeQuery('gsmessage', n=target, ex=1): + bindMessage = mc.listConnections(target + '.gsmessage', d=1, s=0) + if bindMessage: + target = bindMessage + if len(target) == len(sourceUVs): + for i, t in enumerate(target): + self.setAttr(t, sourceUVs[i]) + else: + for t in target: + self.setAttr(t, sourceUVs[-1]) + else: + self.setAttr(target, sourceUVs[-1]) + + def deleteAttr(self, inputCurve): # Delete Attributes from Curve + attrDict = self.attrList + for attr in attrDict: + if mc.attributeQuery(attr, n=inputCurve, ex=1): + mc.deleteAttr(inputCurve + '.' + attr) + + def storeGraphs(self, graph, *_): + """ Store graphs values on curves """ + graphName = graph.objName + graphString = WIDGETS[graphName].getGraph() + selection = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not selection: + selection = mc.filterExpand(mc.listRelatives(mc.ls(sl=1, o=1), p=1, pa=1), sm=9) + if not selection: + return + for sel in selection: + if not mc.attributeQuery(graphName, n=sel, ex=1): + mc.addAttr(sel, ln=graphName, dt='string', k=0) + mc.setAttr(sel + '.' + graphName, graphString, type='string') + + def propagateGraphs(self, graph): + """ Copy the graph changes to other selected curves """ + graphName = graph.objName.replace("_large", "") + selection = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not selection: + selection = mc.filterExpand(mc.listRelatives(mc.ls(sl=1, o=1), p=1, pa=1), sm=9) + if not selection: + return + sourceAttr = self.getAttr(selection[-1], 1) + if 'Length' in sourceAttr: + multiInd = self.getMultiInst(selection[-1]) + for sel in selection: + for attr in multiInd: + if attr[-1] == graphName: + self.setMultiInst(sel, attr) + + +attributes = Attributes("attributes") + + +class Create: + + def __init__(self, name): + self.name = name + self.globalThickness = mc.optionVar(q='GSCT_globalCurveThickness') + self.sf = 1.0 + self.nurbsTesselate = None + self.curveWarp = None + self.polyMoveUV_mid = None + self.polyMoveUV_tip = None + self.polyMoveUV_root = None + self.solidifyNode = None + self.solidifyChoice = None + self.extrude = None + self.lattice = None + self.twist = None + self.magnExpr = None + self.scaleExpr = None + self.uvExpr = None + self.polyNormalNode = None + self.polySoftEdge = None + self.autoRefineCalculate = None + + def currentLayerInd(self): + return WIDGETS['LayerGroup'].checkedId() * -1 - 2 + + def initialize(self): + self.__init__(self.name) + # Check if color mode is enabled + if utils.getAttr('gsColorShaderStorageNode', 'colorApplied'): + toggleColor.updateColors() + self.sf = getScaleFactor() + + # Check if layer is empty + ind = self.currentLayerInd() + self.collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + layer, _, _ = utils.getFormattedLayerNames(self.collectionID, ind) + if mc.objExists(layer): + test = mc.editDisplayLayerMembers(layer, q=1, nr=1, fn=1) + if not test: + deleteUnusedLayers() + + ### Common Creation Methods ### + + def new(self, mode, hk=None): + # type: (int, bool|None) -> None + """Creates a new card or tube of specified type""" + self.initialize() + warpRadio = WIDGETS['warpSwitch'].isChecked() + if warpRadio or hk: + mode = mode + 2 + pathCurve = mc.curve(d=3, + p=[(0, -0.0001, 0), (1.666667 * self.sf, 0, 0), (5 * self.sf, 0, 0), (10 * self.sf, 0, 0), + (15 * self.sf, 0, 0), (18.333333 * self.sf, 0, 0), (20 * self.sf, 0, 0)], + k=[0, 0, 0, 1, 2, 3, 4, 4, 4], n='pathCurve_inst#') + mc.rebuildCurve(pathCurve, kr=2) + finalCurve = [] + if mode == 0: + finalCurve = self.extrudeCard(pathCurve, False) + elif mode == 1: + finalCurve = self.extrudeTube(pathCurve, False) + elif mode == 2: + finalCurve = self.warpCard(pathCurve, False) + elif mode == 3: + finalCurve = self.warpTube(pathCurve, False) + else: + mc.delete(pathCurve) + LOGGER.error("Wrong mode parameter value") + raise ValueError("Wrong mode parameter value") + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + mc.select(finalCurve) + + def multiple(self, mode, hk=False, progressBar=True, keepAttrs=False): + # type: (int, bool, bool, bool) -> None + """Creates Curves from multiple curves selected""" + self.initialize() + selPool = mc.filterExpand(mc.ls(sl=1, l=1, tr=1), sm=9) + try: + selPool = utils.convertInstanceToObj(selPool) + except BaseException: + pass + + if selPool == -1: + return 0 + + if WIDGETS['warpSwitch'].isChecked() or hk: + mode = mode + 2 + + if mode == 0: + create = 'extrudeCards' + elif mode == 1: + create = 'extrudeTubes' + elif mode == 2: + create = 'warpCards' + elif mode == 3: + create = 'warpTubes' + else: + LOGGER.error("Wrong mode parameter value") + raise ValueError("Wrong mode parameter value") + progress = None + allCurves = list() + + if not selPool: + MESSAGE.warningInView('Select at least one curve') + return + + if progressBar: + progress = utils.ProgressBar('Creating ' + create, len(selPool)) + + for pathInst in selPool: + if progress and progress.tick(1): + break + prevAttrs = attributes.getAttr(pathInst) + graphAttrs = {} + for attr in attributes.graphAttributes: + if mc.attributeQuery(attr, n=pathInst, ex=1): + graphAttrs[attr] = mc.getAttr(pathInst + '.' + attr) + if utils.attrExists(pathInst, 'lengthDivisions'): + if mc.connectionInfo(pathInst + '.lengthDivisions', isSource=1): + LOGGER.info('%s is not a compatible curve. Skipped.' % pathInst) + continue + attributes.deleteAttr(pathInst) + if utils.attrExists(pathInst, 'Axis'): + if mc.connectionInfo(pathInst + '.Axis', isSource=1): + LOGGER.info('%s is not a compatible curve. Skipped.' % pathInst) + continue + attributes.deleteAttr(pathInst) + pathInst = mc.rename(pathInst, 'pathCurve_inst#') + returnCurve = list() + + # Bezier curve auto-check + pathInst = utils.checkIfBezier(pathInst) + + if mode == 0: + returnCurve = self.extrudeCard(pathInst) + elif mode == 1: + returnCurve = self.extrudeTube(pathInst) + elif mode == 2: + returnCurve = self.warpCard(pathInst) + elif mode == 3: + returnCurve = self.warpTube(pathInst) + if returnCurve: + if WIDGETS['keepCurveAttributes'].isChecked() and not keepAttrs: + for attr in graphAttrs: + values = graphAttrs[attr] + if attr == 'profileCurve': + updateLattice(values, returnCurve) + else: + rebuildCurve = mc.listConnections(returnCurve + '.curveSmooth')[0] + warp = mc.listConnections(rebuildCurve + '.outputCurve', et=True, t='curveWarp') + if warp: + graphValues = utils.fromStringToDouble2(values) + utils.setDouble2Attr(warp[0], attr, graphValues) + attributes.setAttr(returnCurve, prevAttrs) + allCurves.append(returnCurve) + if progress: + progress.end() + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + mc.select(allCurves, r=1) + return allCurves + + def populate(self, mode, hk=None): # + # type: (int, bool) -> None + """Creates new Curves in between existing curves""" + self.initialize() + sel = mc.filterExpand(mc.ls(sl=1, l=1, dag=1, tr=1), sm=9) + + if not sel or len(sel) < 2: + MESSAGE.warningInView('Select at least two curves') + return 0 + + topGrp = mc.listRelatives(mc.listRelatives(sel[0], p=1, pa=1), p=1, pa=1) + + if WIDGETS['warpSwitch'].isChecked(): + mode = mode + 2 + + # Get original shader + nodes = mc.listHistory(mc.listRelatives(sel[0], c=1, pa=1), f=1) + shapes = mc.ls(nodes, s=1, l=1) + shapeFilter = list() + if len(nodes) > 0: + for node in nodes: + shadingEngine = mc.listConnections(node, c=True, d=True, t='shadingEngine') + if shadingEngine: + break + shapeFilter = mc.ls(shadingEngine, et='shadingEngine') + + loft = mc.loft(sel, c=0, ch=1, d=3, ss=4, rsn=True, ar=1, u=1, rn=0, po=0) + + step = 1.0 / (mc.intSliderGrp('gsCurvesSlider', q=1, value=1) + 1.0) + cond = len(sel) - 1 + allCurves = list() + i = 0 + progress = utils.ProgressBar('Adding Cards', cond * 100) + while i < cond: + if progress.tick(step * 100): + break + roundI = round(i, 1) + crv0 = int(math.floor(roundI)) + crv = int(math.ceil(roundI)) + mod = math.fmod(roundI, 1) + if mod != 0: # Exclude whole numbers + + # Create curve from loft and convert it to curveCard + pathInst = mc.duplicateCurve((loft[0] + '.v[' + str(i) + ']'), rn=0, ch=0, local=0, n='pathCurve_inst#') + mc.rebuildCurve(pathInst, kr=2, kcp=1) + + # Get original scale factor for source curve + interpScaleFactor = scaleFactor0 = scaleFactor1 = getScaleFactor() + if WIDGETS['populateBlendAttributes'].isChecked(): + if mc.attributeQuery('scaleFactor', n=sel[crv0], ex=1): + scaleFactor0 = mc.getAttr(sel[crv0] + '.scaleFactor') + # Get original scale factor for target curve + if mc.attributeQuery('scaleFactor', n=sel[crv], ex=1): + scaleFactor1 = mc.getAttr(sel[crv] + '.scaleFactor') + # Interpolate scale factor + interpScaleFactor = mt.lerp(i - crv0, scaleFactor0, scaleFactor1) + + if interpScaleFactor: + mc.addAttr(pathInst[0], ln='scaleFactor', at='double', dv=interpScaleFactor) + + newCurve = str() + if mode == 0: + newCurve = self.extrudeCard(pathInst[0]) + elif mode == 1: + newCurve = self.extrudeTube(pathInst[0]) + elif mode == 2: + newCurve = self.warpCard(pathInst[0]) + elif mode == 3: + newCurve = self.warpTube(pathInst[0]) + + if WIDGETS['populateBlendAttributes'].isChecked(): + + # Getting all available attributes + crv0Attr = attributes.getAttr(sel[crv0]) + crv0ScaleX = mc.getAttr(sel[crv0] + '.scaleX') + + sourceMultInd = None + if 'Length' in crv0Attr and 'Taper' in crv0Attr and (mode == 2 or mode == 3): + sourceMultInd = attributes.getMultiInst(sel[crv0]) + + # Blending + mod = True if hk == 2 or (hk is None and utils.getMod() == 'Shift') else False + if not mod: + crvAttr = attributes.getAttr(sel[crv], 1, 1) + crv1ScaleX = mc.getAttr(sel[crv] + '.scaleX') + crv0ScaleX = mt.lerp(i - crv0, crv0ScaleX, crv1ScaleX) + for attr in crv0Attr: + if attr in crvAttr: + crv0Attr[attr] = mt.lerp(i - crv0, crv0Attr[attr], crvAttr[attr]) + if 'Length' in crvAttr and 'Taper' in crvAttr and (mode == 2 or mode == 3): + targetMultInd = attributes.getMultiInst(sel[crv]) + if sourceMultInd: + for attr in range(len(sourceMultInd)): + blendMultInst = attributes.blendMultInst(sourceMultInd[attr], + targetMultInd[attr], + i - crv0) + attributes.setMultiInst(newCurve, blendMultInst) + + profileCurve = blendProfileCurve(sel[crv0], sel[crv], i - crv0) + if profileCurve: + updateLattice(profileCurve, newCurve) + + # Applying Settings + attributes.setAttr(newCurve, crv0Attr, ['Width', 'WidthX', 'WidthZ']) + + if mc.attributeQuery('Width', n=newCurve, ex=1): + newWidth = 1 + if 'Width' in crv0Attr: + newWidth = crv0ScaleX * crv0Attr['Width'] + elif 'WidthX' in crv0Attr: + newWidth = (crv0Attr['WidthX'] + crv0Attr['WidthZ']) / 2 * crv0ScaleX + mc.setAttr(newCurve + '.Width', newWidth) + elif mc.attributeQuery('WidthX', n=newCurve, ex=1): + newWidthX = 1 + newWidthZ = 1 + if 'Width' in crv0Attr: + newWidthX = crv0ScaleX * crv0Attr['Width'] + newWidthZ = newWidthX + elif 'WidthX' in crv0Attr: + newWidthX = crv0ScaleX * crv0Attr['WidthX'] + newWidthZ = crv0ScaleX * crv0Attr['WidthZ'] + mc.setAttr(newCurve + '.WidthX', newWidthX) + mc.setAttr(newCurve + '.WidthZ', newWidthZ) + + try: + if shapes[1]: # Applying Shader + newSel = mc.listRelatives(newCurve, c=1, pa=1) + newHist = mc.listHistory(newSel[0], f=1) + newShape = mc.ls(newHist, shapes=1, l=1) + mc.sets(newShape, forceElement=shapeFilter[0], nw=1) + except Exception as e: + LOGGER.exception(e) + + allCurves.append(newCurve) + i += step + progress.end() + mc.delete(loft[0]) + + # Adding to group if exists + if topGrp: + mc.select(cl=1) + for curve in allCurves: + curveGrp = mc.listRelatives(curve, p=1, pa=1)[0] + mc.parent(curveGrp, topGrp) + mc.select(allCurves, r=1) + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + resetCurvePivotPoint() + + def fill(self, hk=None): + # type: (int) -> None + """Place curve duplicates in-between selected curves""" + self.initialize() + sel = mc.filterExpand(mc.ls(sl=1, l=1, dag=1, tr=1), sm=9) + if not sel or len(sel) < 2: + MESSAGE.warningInView('Select at least two curves') + return 0 + loft = mc.loft(sel, c=0, ch=1, d=3, ss=4, rsn=True, ar=1, u=1, rn=0, po=0) + step = 1.0 / (mc.intSliderGrp('gsCurvesSlider', q=1, value=1) + 1.0) + cond = len(sel) - 1 + allCurves = [] + i = 0 + progress = utils.ProgressBar('Adding Cards', cond * 100) + while i < cond: + if progress.tick(step * 100): + break + roundI = round(i, 1) + crv0 = int(math.floor(roundI)) + crv = int(math.ceil(roundI)) + mod = math.fmod(roundI, 1) + if mod != 0: # Exclude whole numbers + # Create curve from loft + pathInst = mc.duplicateCurve('%s.v[%s]' % (loft[0], i), rn=0, ch=0, local=0, n='pathCurve_inst#') + mc.rebuildCurve(pathInst, kr=2, s=mc.getAttr(sel[crv0] + '.spans')) + targetPoints = mc.getAttr(pathInst[0] + '.cp[:]') + mc.delete(pathInst) + newCurve = duplicateCurve(customSel=[sel[crv0]]) + if newCurve: + newCurve = newCurve[0] + else: + MESSAGE.warningInView('Wrong Selection Detected: %s' % sel[crv0]) + break + for p in range(0, len(targetPoints)): + mc.xform('%s.cp[%s]' % (newCurve, p), t=targetPoints[p], wd=1, ws=1) + + # Get Attributes + previousAttr = attributes.getAttr(sel[crv0], excludeCheckboxes=True, excludeUV=True) + targetAttr = attributes.getAttr(sel[crv], excludeCheckboxes=True, excludeUV=True) + newAttr = previousAttr + + previousMultiInst = attributes.getMultiInst(sel[crv0]) + targetMultiInst = attributes.getMultiInst(sel[crv]) + blendMultiInst = [] + + modifier = True if hk == 2 or (hk is None and utils.getMod() == 'Shift') else False + if not modifier: + # Interpolate Attributes + for attr in previousAttr: + if attr in targetAttr: + newAttr[attr] = mt.lerp(i - crv0, previousAttr[attr], targetAttr[attr]) + + if previousMultiInst and targetMultiInst and (len(previousMultiInst) == len(targetMultiInst)): + for attr in range(len(previousMultiInst)): + blendMultiInst.append(attributes.blendMultInst(previousMultiInst[attr], targetMultiInst[attr], i - crv0)) + + profileCurve = blendProfileCurve(sel[crv0], sel[crv], i - crv0) + if profileCurve: + updateLattice(profileCurve, newCurve) + + # Apply Attributes + attributes.setAttr(newCurve, newAttr) + allCurves.append(newCurve) + + for attr in blendMultiInst: + attributes.setMultiInst(newCurve, attr) + + i += step + mc.delete(loft) + progress.end() + mc.select(allCurves, r=1) + resetCurvePivotPoint(customCurves=allCurves) + + ### Curve Extrude Creation Methods ### + + def extrudeCard(self, pathInst, auto=True): + # type: (str, bool) -> str + """Creates Curve Card from pathInst""" + self.pathInst = mc.filterExpand(pathInst, sm=9)[0] + + if not self.pathInst: + MESSAGE.warningInView('Select nurbs or bezier curve') + return 0 + + spans = mc.getAttr(self.pathInst + '.spans') + self.ldiv = 10 + self.refine = 20 + if auto: + self.ldiv = spans * 2 if spans <= 10 else spans + self.refine = 20 if spans <= 20 else spans + + # Breaking if connection exist + try: + mc.disconnectAttr(self.pathInst + '.sx', self.pathInst + '.sy') + except BaseException: + pass + try: + mc.disconnectAttr(self.pathInst + '.sx', self.pathInst + '.sz') + except BaseException: + pass + + # Checking the stored scale factor + scaleFactor = self.sf + if mc.attributeQuery('scaleFactor', n=self.pathInst, ex=1) and WIDGETS['keepCurveAttributes'].isChecked(): + scaleFactor = mc.getAttr(self.pathInst + '.scaleFactor') + + # Creating profile + self.profileInst = mc.curve( + p=[(0, 0, -2.5 * scaleFactor), (0, 0.5 * scaleFactor, -1.666667 * scaleFactor), (0, 1 * scaleFactor, 0), + (0, 0.5 * scaleFactor, 1.666667 * scaleFactor), (0, 0, 2.5 * scaleFactor)], k=[0, 0, 0, 1, 2, 2, 2], d=3, + n='profileCurve_inst#') + mc.makeIdentity(self.pathInst, n=0, s=1, r=1, t=1, apply=True, pn=1) + curveOrigin = mc.pointPosition(self.pathInst + '.cv[0]', w=1) + mc.xform(self.pathInst, ws=1, piv=(curveOrigin[0], curveOrigin[1], curveOrigin[2])) + mc.move(0, 0, 0, self.pathInst, rpr=1, ws=1) + mc.makeIdentity(self.pathInst, n=0, s=1, r=1, t=1, apply=True, pn=1) + mc.nurbsToPolygonsPref(polyType=1, vType=1, format=2, uType=1, vNumber=10, uNumber=3) + self.hairCard = mc.extrude(self.profileInst, self.pathInst, upn=1, ch=True, rotation=0, ucp=1, n='geoCard#', fpt=1, scale=1, + et=2, rn=False, rsp=1, po=1)[0] + + # Turning off inherit transform + mc.setAttr(self.hairCard + '.inheritsTransform', 0) + + mc.xform(self.hairCard, t=(curveOrigin[0], curveOrigin[1], curveOrigin[2])) + + # Create actual path curve + self.pathCurve = str(mc.rename(mc.duplicate(self.pathInst, ilf=1, rr=1)[0], 'pathCurve#')) + pathInstShape = mc.ls(self.pathInst, s=1, dag=1, l=1)[0] + self.nurbsTesselate = mc.listConnections(mc.ls(self.hairCard, s=1, dag=1, l=1), s=True, d=False)[0] + self.extrude = mc.listConnections(self.nurbsTesselate, s=True, d=False)[0] + mc.setAttr(self.extrude + '.useProfileNormal', 1) + + # Connecting + + mc.connectAttr(self.pathCurve + '.translate', self.hairCard + '.translate', f=1) + mc.connectAttr(self.pathCurve + '.rotate', self.hairCard + '.rotate', f=1) + mc.connectAttr(self.pathCurve + '.scale', self.hairCard + '.scale', f=1) + mc.connectAttr(self.pathCurve + '.scaleX', self.pathCurve + '.scaleY', f=1) + mc.connectAttr(self.pathCurve + '.scaleX', self.pathCurve + '.scaleZ', f=1) + mc.connectAttr(self.pathCurve + '.rotatePivot', self.hairCard + '.rotatePivot', f=1) + mc.connectAttr(self.pathCurve + '.scalePivot', self.hairCard + '.scalePivot', f=1) + mc.connectAttr(self.pathCurve + '.rotatePivotTranslate', self.hairCard + '.rotatePivotTranslate', f=1) + mc.connectAttr(self.pathCurve + '.scalePivotTranslate', self.hairCard + '.scalePivotTranslate', f=1) + mc.connectAttr(pathInstShape + '.editPoints[0]', self.profileInst + '.translate', f=1) + mc.connectAttr(pathInstShape + '.editPoints[0]', self.extrude + '.pivot', f=1) + + # Moving path curve to original position + + mc.xform(self.pathCurve, ws=1, t=(curveOrigin[0], curveOrigin[1], curveOrigin[2])) + + # Attribute addition + mc.addAttr(self.pathCurve, ln='lengthDivisions', dv=self.ldiv, smx=500, at='long', min=2, k=1) + mc.addAttr(self.pathCurve, ln='widthDivisions', dv=3, smx=11, at='long', min=2, k=1) + mc.addAttr(self.pathCurve, ln='Orientation', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='Twist', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='Width', dv=1, smx=5, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln='Taper', dv=1, smx=5, at='double', min=0, k=1) + mc.addAttr(self.pathCurve, ln='Profile', smn=-2, dv=0, at='double', smx=2, k=1) + mc.connectAttr(self.pathCurve + '.lengthDivisions', (self.nurbsTesselate + '.vNumber'), f=1) + mc.connectAttr(self.pathCurve + '.widthDivisions', (self.nurbsTesselate + '.uNumber'), f=1) + mc.connectAttr(self.pathCurve + '.Orientation', (self.profileInst + '.rotateX'), f=1) + mc.connectAttr(self.pathCurve + '.Twist', (self.extrude + '.rotation'), f=1) + mc.connectAttr(self.pathCurve + '.Width', (self.profileInst + '.scaleZ'), f=1) + mc.connectAttr(self.pathCurve + '.Taper', (self.extrude + '.scale'), f=1) + mc.connectAttr(self.pathCurve + '.Profile', (self.profileInst + '.scaleY'), f=1) + + # Add Modules + + self.addRefine() + self.addAutoRefine() + self.addPolyNormal() + self.addUVs() + self.addSolidify() + self.addMessage() + self.addScaleFactor() + self.addDynamicDivisions() + self.setCurveThickness() + self.hideNodes() + self.group('curveCard#') + + return (self.hairCardGrp + '|' + self.pathCurve) + + def extrudeTube(self, pathInst, auto=True): # Creates Curve Tube from pathInst + self.pathInst = mc.filterExpand(pathInst, sm=9)[0] + + if not self.pathInst: + MESSAGE.warningInView('Select nurbs or bezier curve') + return 0 + + spans = mc.getAttr(self.pathInst + '.spans') + self.ldiv = 10 + self.refine = 20 + if auto: + self.ldiv = spans * 2 if spans <= 10 else spans + self.refine = 20 if spans <= 20 else spans + + # Breaking if connected + try: + mc.disconnectAttr(self.pathInst + '.sx', self.pathInst + '.sy') + except BaseException: + pass + try: + mc.disconnectAttr(self.pathInst + '.sx', self.pathInst + '.sz') + except BaseException: + pass + + # Checking the stored scale factor + scaleFactor = self.sf + if mc.attributeQuery('scaleFactor', n=self.pathInst, ex=1) and WIDGETS['keepCurveAttributes'].isChecked(): + scaleFactor = mc.getAttr(self.pathInst + '.scaleFactor') + + # Creating a profile + self.profileInst = mc.circle(c=(0, 0, 0), ch=1, d=3, ut=0, sw=360, n="profileCurve_inst#", + s=8, r=1 * scaleFactor, tol=0.01, nr=(0, 1, 0)) + mc.makeIdentity(self.pathInst, n=0, s=1, r=1, t=1, apply=True, pn=1) + curveOrigin = mc.pointPosition(self.pathInst + '.cv[0]', w=1) + mc.xform(self.pathInst, ws=1, piv=(curveOrigin[0], curveOrigin[1], curveOrigin[2])) + mc.move(0, 0, 0, self.pathInst, rpr=1, ws=1) + mc.makeIdentity(self.pathInst, n=0, s=1, r=1, t=1, apply=True, pn=1) + mc.nurbsToPolygonsPref(polyType=1, vType=1, format=2, uType=1, vNumber=10, uNumber=3) + self.hairCard = mc.extrude(self.profileInst[0], self.pathInst, upn=1, ch=True, rotation=0, + ucp=1, n='geoTube#', fpt=1, scale=1, et=2, rn=False, rsp=1, po=1)[0] + + # Turning off inherit transform + mc.setAttr(self.hairCard + '.inheritsTransform', 0) + + mc.xform(self.hairCard, t=(curveOrigin[0], curveOrigin[1], curveOrigin[2])) + self.pathCurve = str(mc.rename(mc.duplicate(self.pathInst, ilf=1, rr=1)[0], 'pathCurve#')) + pathInstShape = mc.ls(self.pathInst, s=1, dag=1, l=1)[0] + self.nurbsTesselate = mc.listConnections(mc.ls(self.hairCard, s=1, dag=1, l=1)[0], s=True, d=False)[0] + self.extrude = mc.listConnections(self.nurbsTesselate, s=True, d=False)[0] + mc.setAttr(self.extrude + '.useProfileNormal', 1) + + # Connecting + + mc.connectAttr(self.pathCurve + '.translate', self.hairCard + '.translate', f=1) + mc.connectAttr(self.pathCurve + '.rotate', self.hairCard + '.rotate', f=1) + mc.connectAttr(self.pathCurve + '.scale', self.hairCard + '.scale', f=1) + mc.connectAttr(self.pathCurve + '.scaleX', self.pathCurve + '.scaleY', f=1) + mc.connectAttr(self.pathCurve + '.scaleX', self.pathCurve + '.scaleZ', f=1) + mc.connectAttr(self.pathCurve + '.rotatePivot', self.hairCard + '.rotatePivot', f=1) + mc.connectAttr(self.pathCurve + '.scalePivot', self.hairCard + '.scalePivot', f=1) + mc.connectAttr(self.pathCurve + '.rotatePivotTranslate', self.hairCard + '.rotatePivotTranslate', f=1) + mc.connectAttr(self.pathCurve + '.scalePivotTranslate', self.hairCard + '.scalePivotTranslate', f=1) + mc.connectAttr(pathInstShape + '.editPoints[0]', self.profileInst[0] + '.translate', f=1) + mc.connectAttr(pathInstShape + '.editPoints[0]', self.extrude + '.pivot', f=1) + + # Moving path curve to original position + + mc.xform(self.pathCurve, ws=1, t=(curveOrigin[0], curveOrigin[1], curveOrigin[2])) + + # Attribute addition + mc.addAttr(self.pathCurve, ln="lengthDivisions", dv=self.ldiv, smx=500, at='long', min=2, k=1) + mc.addAttr(self.pathCurve, ln="widthDivisions", dv=7, smx=53, at='long', min=4, k=1) + mc.addAttr(self.pathCurve, ln="Orientation", smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln="Twist", smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln="WidthX", dv=1, smx=20, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln="WidthZ", dv=1, smx=20, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln="Taper", dv=1, smx=5, at='double', min=0, k=1) + mc.connectAttr(self.pathCurve + ".lengthDivisions", self.nurbsTesselate + ".vNumber", f=1) + mc.connectAttr(self.pathCurve + ".widthDivisions", self.nurbsTesselate + ".uNumber", f=1) + mc.connectAttr(self.pathCurve + ".Orientation", self.profileInst[0] + ".rotateY", f=1) + mc.connectAttr(self.pathCurve + ".Twist", self.extrude + ".rotation", f=1) + mc.connectAttr(self.pathCurve + ".WidthX", self.profileInst[0] + ".scaleX", f=1) + mc.connectAttr(self.pathCurve + ".WidthZ", self.profileInst[0] + ".scaleZ", f=1) + mc.connectAttr(self.pathCurve + ".Taper", self.extrude + ".scale", f=1) + + self.addRefine() + self.addAutoRefine() + self.addPolyNormal() + self.addUVs() + self.addSolidify() + self.addMessage() + self.addScaleFactor() + self.addDynamicDivisions() + self.setCurveThickness() + self.hideNodes() + self.group('curveTube#') + + return (self.hairCardGrp + '|' + self.pathCurve) + + ### Curve Warp Creation Methods ### + + def warpCard(self, pathCurve, auto=True): # Creates New Curve Warp Card from inputCurve + self.pathCurve = mc.filterExpand(pathCurve, sm=9) + if not self.pathCurve: + MESSAGE.warningInView('Select at least one curve') + return 0 + + self.pathCurve = mc.rename(self.pathCurve, 'pathCurve#') + self.pathCurveShape = mc.ls(self.pathCurve, dag=1, s=1, l=1)[0] + spans = mc.getAttr(self.pathCurveShape + '.spans') + self.ldiv = 10 + self.refine = 20 + if auto: + self.ldiv = spans * 2 if spans <= 10 else spans + self.refine = 20 if spans <= 20 else spans + + # Breaking + try: + mc.disconnectAttr(self.pathCurve + '.sx', self.pathCurve + '.sy') + except BaseException: + pass + try: + mc.disconnectAttr(self.pathCurve + '.sx', self.pathCurve + '.sz') + except BaseException: + pass + + # Checking the stored scale factor + scaleFactor = self.sf + if mc.attributeQuery('scaleFactor', n=self.pathCurve, ex=1) and WIDGETS['keepCurveAttributes'].isChecked(): + scaleFactor = mc.getAttr(self.pathCurve + '.scaleFactor') + + # Creating path and profile curves + self.pathInst = mc.curve(d=3, p=[(0, -0.0001, 0), (1.666667 * scaleFactor, 0, 0), (5 * scaleFactor, 0, 0), + (10 * scaleFactor, 0, 0), (15 * scaleFactor, 0, 0), (18.333333 * scaleFactor, 0, 0), + (20 * scaleFactor, 0, 0)], k=[0, 0, 0, 1, 2, 3, 4, 4, 4], n='pathCurve_inst#') + mc.rebuildCurve(self.pathInst, s=30) + + self.profileInst = mc.curve( + p=[(0, 0, -2.5 * scaleFactor), (0, 0.5 * scaleFactor, -1.666667 * scaleFactor), (0, 1 * scaleFactor, 0), + (0, 0.5 * scaleFactor, (1.666667 * scaleFactor)), (0, 0, (2.5 * scaleFactor))], k=[0, 0, 0, 1, 2, 2, 2], d=3, + n='profileCurve_inst#') + + mc.setAttr(self.profileInst + '.sy', 4) + + mc.nurbsToPolygonsPref(polyType=1, vType=1, format=2, uType=1, vNumber=10, uNumber=3) + extrudeCmd = mc.extrude(self.profileInst, self.pathInst, upn=1, ch=True, rotation=0, ucp=1, n='geoCard#', fpt=1, + scale=1, et=2, rn=False, rsp=1, po=1) + + self.hairCard = extrudeCmd[0] + self.extrude = extrudeCmd[1] + self.nurbsTesselate = mc.listConnections(self.extrude, s=False, d=True, et=1, t='nurbsTessellate')[0] + + # Creating deformers + + latticeDivisions = 10 + + self.lattice = mc.lattice(self.hairCard, dv=[latticeDivisions, 2, 2], oc=True, ldv=[2, 2, 2], ol=1) + mc.move(0, 0, 0, self.lattice[1] + '.scalePivot', self.lattice[1] + '.rotatePivot', a=1) + + points = [] + for i in range(latticeDivisions): + points.append(mc.pointPosition(self.lattice[1] + '.pt[%s][1][0]' % (i), l=1)) + + self.twist = mc.nonLinear(self.hairCard, type='twist') + mc.setAttr(self.twist[1] + '.rz', 90) + mc.move(0, 0, 0, self.twist, a=1, y=1) + + # Creating warp + + self.curveWarp = mc.deformer(ignoreSelected=1, type='curveWarp', n='curveWarp#')[0] + mc.deformer(self.curveWarp, e=1, g=self.hairCard) + mc.connectAttr(self.pathCurveShape + '.worldSpace[0]', self.curveWarp + '.inputCurve') + mc.setAttr(self.curveWarp + '.alignmentMode', 2) + + # Turning off inherit transform + + mc.setAttr(self.hairCard + '.inheritsTransform', 0) + mc.setAttr(self.extrude + '.useProfileNormal', 1) + if MAYA_VER >= 2018: + mc.setAttr(self.curveWarp + '.samplingAccuracy', 0.333) + mc.addAttr(self.pathCurve, ln='samplingAccuracy', dv=0.333, smx=2, at='double', min=0.001, k=1) + mc.connectAttr(self.pathCurve + '.samplingAccuracy', self.curveWarp + '.samplingAccuracy') + mc.setAttr(self.curveWarp + '.twistRotation', 720) + + # Adding and connecting main attributes + + mc.addAttr(self.pathCurve, ln='lengthDivisions', dv=self.ldiv, smx=500, at='long', min=2, k=1) + mc.addAttr(self.pathCurve, ln='widthDivisions', dv=3, smx=11, at='long', min=2, k=1) + mc.addAttr(self.pathCurve, ln='Orientation', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='Twist', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='invTwist', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='Width', dv=1, smx=5, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln='LengthLock', at='enum', dv=0, en="Locked:Unlocked:", k=1) + mc.addAttr(self.pathCurve, ln='Length', dv=1, smx=30, smn=-30, at='double', k=1) + mc.addAttr(self.pathCurve, ln='Offset', dv=0, smx=1, smn=-1, at='double', min=-30, max=30, k=1) + mc.addAttr(self.pathCurve, ln='Taper', dv=1, smx=5, at='double', min=0, k=1) + mc.addAttr(self.pathCurve, ln='Profile', smn=-2, dv=0, at='double', smx=2, k=1) + mc.addAttr(self.pathCurve, ln='profileSmoothing', dv=2, at='long', min=2, max=30, k=0) + mc.addAttr(self.pathCurve, ln='profileMagnitude', dv=1, at='double', min=-2, max=2, k=0) + + if not mc.attributeQuery('initialLatticePoints', n=self.pathCurve, ex=1): + mc.addAttr(self.pathCurve, ln='initialLatticePoints', dt='double3', m=1) + for i in range(len(points)): + mc.setAttr(self.pathCurve + '.initialLatticePoints[%s]' % i, *points[i], type='double3') + + if not mc.attributeQuery('latticeMessage', n=self.pathCurve, ex=1): + mc.addAttr(self.pathCurve, ln='latticeMessage', at='message', k=0) + mc.addAttr(self.lattice[1], ln='latticeMessage', at='message', k=0) + + mc.connectAttr(self.pathCurve + '.lengthDivisions', self.nurbsTesselate + '.vNumber', f=1) + mc.connectAttr(self.pathCurve + '.widthDivisions', self.nurbsTesselate + '.uNumber', f=1) + mc.connectAttr(self.pathCurve + '.Twist', self.twist[0] + '.startAngle', f=1) + mc.connectAttr(self.pathCurve + '.invTwist', self.twist[0] + '.endAngle', f=1) + mc.connectAttr(self.pathCurve + '.Width', self.profileInst + '.scaleZ', f=1) + mc.connectAttr(self.pathCurve + '.Taper', self.extrude + '.scale', f=1) + mc.connectAttr(self.pathCurve + '.Profile', self.profileInst + '.scaleY', f=1) + mc.connectAttr(self.pathCurve + '.LengthLock', self.curveWarp + '.keepLength', f=1) + mc.connectAttr(self.pathCurve + '.Length', self.curveWarp + '.lengthScale', f=1) + mc.connectAttr(self.pathCurve + '.Offset', self.curveWarp + '.offset', f=1) + mc.connectAttr(self.pathCurve + '.latticeMessage', self.lattice[1] + '.latticeMessage', f=1) + mc.connectAttr(self.pathCurve + '.profileSmoothing', self.lattice[0] + '.localInfluenceS', f=1) + mc.connectAttr(self.pathCurve + '.profileMagnitude', self.lattice[0] + '.envelope', f=1) + + # Fix Twist connections for Maya 2020.4 + twistHandle = mc.listRelatives(self.twist, c=1, pa=1) + connectionCheck = mc.isConnected(self.twist[0] + '.startAngle', twistHandle[0] + '.startAngle') + if not connectionCheck: + mc.connectAttr(self.twist[0] + '.startAngle', twistHandle[0] + '.startAngle', f=1) + mc.connectAttr(self.twist[0] + '.endAngle', twistHandle[0] + '.endAngle', f=1) + + # Fix Lattice connection for Maya 2020.4 + if MAYA_VER == 2020: + ffd = mc.listConnections(self.pathCurve + '.profileMagnitude', s=0, d=1) + origGeo = mc.listConnections(ffd[0] + '.originalGeometry[0]', s=1, d=0, p=1) + mc.disconnectAttr(origGeo[0], ffd[0] + '.originalGeometry[0]') + + # Twist Magnitude and Orientation Sum + + mc.addAttr(self.pathCurve, ln='Magnitude', dv=0.5, at='double', h=1) + ex = ''' + {2}.rx = {1}.Orientation + {1}.rx; + {0}.rotation = 360 * (1 - {1}.Magnitude); + {0}.twistRotation = 720 * {1}.Magnitude; + '''.format(self.curveWarp, self.pathCurve, self.lattice[1]) + self.magnExpr = mc.expression(ae=0, s=ex, n='twistOrienCalc#') + + self.addRefine(True) + self.addAutoRefine() + self.addPolyNormal() + self.addUVs() + self.addSolidify() + self.addMessage() + self.addScaleFactor() + self.addDynamicDivisions() + self.setCurveThickness() + self.hideNodes() + self.clearTweakNode() + self.group('warpCard#') + + return (self.hairCardGrp + '|' + self.pathCurve) + + def warpTube(self, pathCurve, auto=True): # Creates New Curve Warp Tube from inputCurve + self.pathCurve = mc.filterExpand(pathCurve, sm=9)[0] + if not self.pathCurve: + MESSAGE.warningInView('Select at least one curve') + return 0 + self.pathCurve = mc.rename(self.pathCurve, 'pathCurve#') + self.pathCurveShape = mc.ls(self.pathCurve, dag=1, s=1, l=1)[0] + spans = mc.getAttr(self.pathCurveShape + '.spans') + self.ldiv = 10 + self.refine = 20 + if auto: + self.ldiv = spans * 2 if spans <= 10 else spans + self.refine = 20 if spans <= 20 else spans + + try: + mc.disconnectAttr(self.pathCurve + '.sx', self.pathCurve + '.sy') + except BaseException: + pass + try: + mc.disconnectAttr(self.pathCurve + '.sx', self.pathCurve + '.sz') + except BaseException: + pass + + # Checking the stored scale factor + scaleFactor = self.sf + if mc.attributeQuery('scaleFactor', n=self.pathCurve, ex=1) and WIDGETS['keepCurveAttributes'].isChecked(): + scaleFactor = mc.getAttr(self.pathCurve + '.scaleFactor') + + # Creating path and profile curves + self.pathInst = mc.curve(d=3, p=[(0, -0.0001, 0), (1.666667 * scaleFactor, 0, 0), (5 * scaleFactor, 0, 0), + (10 * scaleFactor, 0, 0), (15 * scaleFactor, 0, 0), (18.333333 * scaleFactor, 0, 0), + (20 * scaleFactor, 0, 0)], k=[0, 0, 0, 1, 2, 3, 4, 4, 4], n='pathCurve_inst#') + mc.rebuildCurve(self.pathInst, s=30) + + self.profileInst = mc.circle(c=(0, 0, 0), ch=1, d=3, ut=0, sw=360, n="profileCurve_inst#", s=8, r=1 * scaleFactor, tol=0.01, + nr=(0, 1, 0))[0] + + mc.nurbsToPolygonsPref(polyType=1, vType=1, format=2, uType=1, vNumber=10, uNumber=7) + extrudeCmd = mc.extrude(self.profileInst, self.pathInst, upn=1, ch=True, rotation=0, ucp=1, n='geoTube#', fpt=1, + scale=1, et=2, rn=False, rsp=1, po=1) + + self.hairCard = extrudeCmd[0] + self.extrude = extrudeCmd[1] + self.nurbsTesselate = mc.listConnections(self.extrude, s=False, d=True, et=1, t='nurbsTessellate')[0] + + # Creating curveWarp deformer + + self.curveWarp = mc.deformer(ignoreSelected=1, type='curveWarp', n='curveWarp#')[0] + mc.deformer(self.curveWarp, e=1, g=self.hairCard) + mc.connectAttr(self.pathCurveShape + '.worldSpace[0]', self.curveWarp + '.inputCurve') + + # Turning off inherit transform + + mc.setAttr(self.hairCard + '.inheritsTransform', 0) + mc.setAttr(self.extrude + '.useProfileNormal', 1) + + if MAYA_VER >= 2018: + mc.setAttr(self.curveWarp + '.samplingAccuracy', 0.333) + mc.addAttr(self.pathCurve, ln='samplingAccuracy', dv=0.333, smx=2, at='double', min=0.001, k=1) + mc.connectAttr(self.pathCurve + '.samplingAccuracy', self.curveWarp + '.samplingAccuracy') + + mc.setAttr(self.curveWarp + '.twistRotation', 720) + + # Adding and connecting main attributes + + mc.addAttr(self.pathCurve, ln='lengthDivisions', dv=self.ldiv, smx=500, at='long', min=2, k=1) + mc.addAttr(self.pathCurve, ln='widthDivisions', dv=7, smx=53, at='long', min=4, k=1) + mc.addAttr(self.pathCurve, ln='Orientation', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='Twist', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln="WidthX", dv=1, smx=5, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln="WidthZ", dv=1, smx=5, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln='LengthLock', at='enum', dv=0, en="Locked:Unlocked:", k=1) + mc.addAttr(self.pathCurve, ln='Length', dv=1, smx=30, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln='Offset', dv=0, smx=1, smn=-1, at='double', min=-30, max=30, k=1) + mc.addAttr(self.pathCurve, ln='Taper', dv=1, max=1.9, at='double', min=0, k=1) + mc.connectAttr(self.pathCurve + '.lengthDivisions', self.nurbsTesselate + '.vNumber', f=1) + mc.connectAttr(self.pathCurve + '.widthDivisions', self.nurbsTesselate + '.uNumber', f=1) + mc.connectAttr(self.pathCurve + '.Twist', self.extrude + '.rotation', f=1) + mc.connectAttr(self.pathCurve + '.Taper', self.extrude + '.scale', f=1) + mc.connectAttr(self.pathCurve + '.LengthLock', self.curveWarp + '.keepLength', f=1) + mc.connectAttr(self.pathCurve + '.Length', self.curveWarp + '.lengthScale', f=1) + mc.connectAttr(self.pathCurve + '.Offset', self.curveWarp + '.offset', f=1) + + # Twist Magnitude + + mc.addAttr(self.pathCurve, ln='Magnitude', dv=0.5, at='double', h=1) + ex = ''' + {2}.rx = {1}.Orientation + {1}.rx; + {0}.rotation = 360 * (1 - {1}.Magnitude); + {0}.twistRotation = 720 * {1}.Magnitude; + '''.format(self.curveWarp, self.pathCurve, self.profileInst) + self.magnExpr = mc.expression(ae=0, s=ex, n='twistOrienCalc#') + # Handling Width to Scale Switch + + scaleEx = ''' + if(({0}.WidthX >= 5 && {0}.WidthZ >= 5)) + {{ + {1}.scaleZ = 5; + {1}.scaleX = 5; + {2}.maxScale = abs({0}.WidthZ - 5) + abs({0}.WidthX - 5) + 2; + }} + else if (({0}.WidthX >= 5)) + {{ + {1}.scaleX = 5; + {1}.scaleZ = {0}.WidthZ; + {2}.maxScale = abs({0}.WidthX - 5) + 2; + }} + else if (({0}.WidthZ >= 5)) + {{ + {1}.scaleZ = 5; + {1}.scaleX = {0}.WidthX; + {2}.maxScale = abs({0}.WidthZ - 5) + 2; + }} + else + {{ + {2}.maxScale = 2; + {1}.scaleX = {0}.WidthX; + {1}.scaleZ = {0}.WidthZ; + }} + '''.format(self.pathCurve, self.profileInst, self.curveWarp) + self.scaleExpr = mc.expression(ae=0, s=scaleEx, n='scaleManagement#') + + self.addRefine(True) + self.addAutoRefine() + self.addPolyNormal() + self.addUVs() + self.addSolidify() + self.addMessage() + self.addScaleFactor() + self.addDynamicDivisions() + self.setCurveThickness() + self.hideNodes() + self.clearTweakNode() + self.group('warpTube#') + + return (self.hairCardGrp + '|' + self.pathCurve) + + def bind(self, hk=None): # TODO: Check hotkey command # Attaches selected geo to selected curve + self.initialize() + # Sort for geoWarp + self.ldiv = 10 + self.refine = 20 + + sel = mc.ls(sl=1, tr=1) + if not sel or len(sel) < 2: + MESSAGE.warningInView( + 'Select one curve and one geometry or Select one target curve and any number of curveCards/Tubes') + return 0 + + curves = mc.filterExpand(sel, sm=9) + geo = mc.filterExpand(sel, sm=12) + + # Add gsmessage + for crv in curves: + if not mc.attributeQuery('gsmessage', n=crv, ex=1): + mc.addAttr(crv, ln='gsmessage', at='message', k=0) + + if not getOption('massBindOption'): + self.singleBind(sel, geo, hk) + else: + # Filter active curves + activeCurves = [] + inactiveCurves = [] + for curve in curves: + if (mc.attributeQuery('Orientation', n=curve, ex=1) and + mc.connectionInfo(curve + '.Orientation', isSource=1)): + activeCurves.append(curve) + else: + inactiveCurves.append(curve) + if not geo: + progress = utils.ProgressBar('Binding', len(inactiveCurves)) + try: + for i in range(len(inactiveCurves)): + progress.tick(1) + passedCurves = activeCurves + [inactiveCurves[i]] + self.singleBind(passedCurves, None, True) + except Exception as e: + LOGGER.exception(e) + finally: + progress.end() + else: + progress = utils.ProgressBar('Binding', len(inactiveCurves)) + try: + for i in range(len(inactiveCurves)): + progress.tick(1) + passedGeo = mc.duplicate(geo[0]) + passedCurves = [inactiveCurves[i]] + self.singleBind(passedCurves, passedGeo, True) + except Exception as e: + LOGGER.exception(e) + finally: + progress.end() + + def singleBind(self, curves, geometry=None, hk=None): + grpName = 'bindGeo#' + + sel = curves + self.pathCurve = [] + self.geo = [] + + self.pathCurve = mc.filterExpand(curves, sm=9) + self.geo = geometry + + isCurveWarp = False + origGroups = [] + targetCurve = None + sourceCurves = [] + + if not self.geo: + # Sort for curveWarp + isCurveWarp = True + grpName = 'bindGroup#' + for crv in sel: + attr = attributes.getAttr(crv, 1, 1) + if 'Orientation' in attr and mc.connectionInfo(crv + '.Orientation', isSource=1): + sourceCurves.append(crv) + else: + targetCurve = crv + if not sourceCurves or not targetCurve: + MESSAGE.warningInView('Wrong selection') + return 0 + + self.pathCurve = targetCurve + + # Duplicating the source curves if needed + mod = True if hk == 2 or (hk is None and utils.getMod() == 'Shift') else False + if getOption('bindDuplicatesCurves') or getOption('massBindOption') or mod: + sourceCurves = duplicateCurve(sourceCurves) + + # Original Groups + for curve in sourceCurves: + if (mc.attributeQuery('Orientation', n=curve, ex=1) and + mc.connectionInfo(curve + '.Orientation', isSource=1)): + origGroups += mc.listRelatives(curve, p=1, pa=1) + + # Adding source curves to a new group + hairGrp = self.currentLayerInd() + curveAddToLayer(hairGrp, inputCurves=sourceCurves) + + # Connect message + for crv in sourceCurves: + try: + mc.connectAttr(self.pathCurve + ".gsmessage", crv + '.gsmessage', f=1) + except BaseException: + LOGGER.info('"gsmessage" attribute not found. Possibly legacy curve.') + + # Flip original UVs before bind + if getOption('bindFlipUVs'): + for crv in sourceCurves: + if mc.attributeQuery('flipUV', n=crv, ex=1): + mc.setAttr(crv + '.flipUV', not mc.getAttr(crv + '.flipUV')) + + # Center and average pivots + wsPivX = list() + wsPivY = list() + wsPivZ = list() + lsPiv = list() + tr = list() + mc.xform(sourceCurves, cpc=1) + for ele in sourceCurves: + wsPiv = mc.xform(ele, q=1, rp=1, ws=1) + wsPivX.append(wsPiv[0]) + wsPivY.append(wsPiv[1]) + wsPivZ.append(wsPiv[2]) + lsPiv.append(mc.xform(ele, q=1, rp=1)) + tr.append(mc.xform(ele, q=1, t=1)) + wsPivX = sum(wsPivX) / len(wsPivX) + wsPivY = sum(wsPivY) / len(wsPivY) + wsPivZ = sum(wsPivZ) / len(wsPivZ) + + mc.xform(sourceCurves, piv=[wsPivX, wsPivY, wsPivZ], wd=1, ws=1) + + mc.move(0, 0, 0, sourceCurves, rpr=1) + findGeo = [] + for part in sourceCurves: + findGeo.append(selectPart(2, True, part)[0]) + + if len(findGeo) > 1: + unite = mc.polyUnite(findGeo, ch=1, muv=1, n='warpCard#') + mc.rename(unite[1], 'gsUniteNode#') + self.geo = unite[0] + else: + tempNode = mc.createNode('mesh') + unite = mc.polyUnite([findGeo[0], tempNode], ch=1, muv=1, n='warpCard#') + mc.rename(unite[1], 'gsUniteNode#') + self.geo = unite[0] + mc.delete(mc.listRelatives(mc.listRelatives(tempNode, p=1, pa=1), p=1, pa=1)) + + shader = utils.getShader(self.geo) + for key in shader: + mc.sets(shader[key], e=1, fe=key, nw=1) + mc.select(self.geo, r=1) + else: + self.pathCurve = self.pathCurve[0] + self.geo = self.geo[0] + mc.makeIdentity(self.geo, a=1, t=1, r=1, s=1, n=0, pn=1) + mc.makeIdentity(self.pathCurve, a=1, t=1, r=1, s=1, n=0, pn=1) + mc.makeIdentity(self.pathCurve, a=0, t=1, r=1, s=1, n=0, pn=1) + + # Cleaning previous attrs (Replace with remembering attrs) + prevAttrs = attributes.getAttr(self.pathCurve) + graphAttrs = {} + for attr in attributes.graphAttributes: + if mc.attributeQuery(attr, n=self.pathCurve, ex=1): + graphAttrs[attr] = mc.getAttr(self.pathCurve + '.' + attr) + for attr in attributes.attrList | attributes.checkBoxes | attributes.uvAttr: + if mc.attributeQuery(attr, n=self.pathCurve, ex=1): + mc.deleteAttr(self.pathCurve + '.' + attr) + + # Main warp + if not isCurveWarp: + mc.select(self.pathCurve, self.geo, r=1) + if MAYA_VER <= 2017: + mel.eval('MoveTool;') + mel.eval('BakeCustomPivot;') + attributes.deleteAttr(self.pathCurve) + self.pathCurve = mc.rename(self.pathCurve, 'pathCurve#') + self.geo = mc.rename(self.geo, 'geoCard#') + + self.polyNormalNode = mc.polyNormal(self.geo, ch=1, unm=0, nm=1)[0] + + self.curveWarp = mc.deformer(ignoreSelected=1, type='curveWarp', n='curveWarp#')[0] + mc.deformer(self.curveWarp, e=1, g=self.geo) + mc.connectAttr(self.pathCurve + '.worldSpace[0]', self.curveWarp + '.inputCurve') + + mc.setAttr(self.geo + '.inheritsTransform', 0) + if MAYA_VER >= 2018: + mc.setAttr(self.curveWarp + '.samplingAccuracy', 0.333) + mc.addAttr(self.pathCurve, ln='samplingAccuracy', dv=0.333, smx=2, at='double', min=0.001, k=1) + mc.connectAttr(self.pathCurve + '.samplingAccuracy', self.curveWarp + '.samplingAccuracy') + mc.setAttr(self.curveWarp + '.twistRotation', 720) + + # Optionally flip axis and normals and change orientation + axisFlipDefault = 0 + orientationDefault = 0 + reverseNormalsDefault = 1 + if getOption('bindFlipUVs'): + axisFlipDefault = 1 + orientationDefault = 90 + reverseNormalsDefault = 0 + + mc.addAttr(self.pathCurve, ln='Axis', dv=0, at='enum', en="Auto:X:Y:Z:", k=1) + mc.addAttr(self.pathCurve, ln='AxisFlip', dv=axisFlipDefault, at='bool', k=1) + mc.addAttr(self.pathCurve, ln='Orientation', smn=-360, dv=orientationDefault, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='Width', dv=2, smx=5, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln='LengthLock', at='enum', dv=0, en="Locked:Unlocked:", k=1) + mc.addAttr(self.pathCurve, ln='Length', dv=1, smx=200, at='double', min=0.001, k=1) + mc.addAttr(self.pathCurve, ln='Offset', dv=0, smx=1, smn=-1, at='double', min=-30, max=30, k=1) + mc.addAttr(self.pathCurve, ln="reverseNormals", dv=reverseNormalsDefault, en="reverse:off:", at="enum", k=1) + + mc.connectAttr(self.pathCurve + '.AxisFlip', self.curveWarp + '.flipAxis') + mc.connectAttr(self.pathCurve + '.Width', self.curveWarp + '.maxScale') + mc.connectAttr(self.pathCurve + '.LengthLock', self.curveWarp + '.keepLength') + mc.connectAttr(self.pathCurve + '.Length', self.curveWarp + '.lengthScale') + mc.connectAttr(self.pathCurve + '.Offset', self.curveWarp + '.offset') + mc.connectAttr(self.pathCurve + ".reverseNormals", self.polyNormalNode + ".normalMode", f=1) + + # Twist Magnitude + + mc.addAttr(self.pathCurve, ln='Magnitude', dv=0.5, at='double', h=1) + expr = ''' + {0}.rotation = 360 * (1 - {1}.Magnitude) + {1}.Orientation; + {0}.twistRotation = 720 * {1}.Magnitude; + {0}.alignmentMode = {1}.Axis + 1; + '''.format(self.curveWarp, self.pathCurve) + self.magnExpr = mc.expression(ae=0, s=expr, n='twistMagnitudeCalc#') + + self.addRefine(True) + self.addAutoRefine() + + # Set prev attributes + if WIDGETS['keepCurveAttributes'].isChecked(): + for attr in graphAttrs: + values = graphAttrs[attr] + if attr == 'profileCurve': + updateLattice(values, self.pathCurve) + else: + rebuildCurve = mc.listConnections(self.pathCurve + '.curveSmooth')[0] + warp = mc.listConnections(rebuildCurve + '.outputCurve')[0] + graphValues = utils.fromStringToDouble2(values) + utils.setDouble2Attr(warp, attr, graphValues) + attributes.setAttr(self.pathCurve, prevAttrs) + + self.setCurveThickness() + self.group(grpName, True) + + if isCurveWarp: + origCurves = mc.group(origGroups, n='origCurves#') + origCurvesGrp = mc.parent(origCurves, self.hairCardGrp) + mc.setAttr(origCurvesGrp[0] + '.visibility', 0) + + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + + mc.select(self.hairCardGrp + '|' + self.pathCurve, r=1) + resetCurvePivotPoint(self.hairCardGrp + '|' + self.pathCurve) + + def unbind(self): + self.initialize() + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, o=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one compatible curve (Bound/Warp curves or geometry)') + return + previousCurves = [] + newSel = [] + for obj in sel: + initialGroup = selectPart(0, True, obj) + origGeometry = selectPart(2, True, obj) + if not origGeometry: + continue + shader = utils.getShader(origGeometry) + topGroup = mc.listRelatives(initialGroup, p=1, pa=1) + origCurvesGrp = selectPart(3, True, obj) + if not origCurvesGrp: # If orig objects are geometry + if not mc.attributeQuery('Axis', n=obj, ex=1): + LOGGER.info('%s is not a bound group. Skipping.' % obj) + continue + mc.editDisplayLayerMembers("defaultLayer", origGeometry, nr=1) + newGeo = [] + if topGroup: + newGeo = mc.parent(origGeometry, topGroup) + previousCurves.append(mc.parent(mc.duplicate(obj), topGroup)) + else: + newGeo = mc.parent(origGeometry, w=1) + previousCurves.append(mc.parent(mc.duplicate(obj), w=1)) + mc.delete(initialGroup) + mc.delete(newGeo, ch=1) + newSel.append(newGeo[0]) + else: # If orig objects are curves + origGroups = mc.listRelatives(origCurvesGrp, c=1, pa=1) + newCards = [] + if not origGroups: + LOGGER.info('%s is not a bound card/geo. Skipping' % obj) + continue + if topGroup: + newCards = mc.parent(origGroups, topGroup) + previousCurves.append(mc.parent(mc.duplicate(obj), topGroup)) + else: + newCards = mc.parent(origGroups, w=1) + previousCurves.append(mc.parent(mc.duplicate(obj), w=1)) + mc.delete(initialGroup) + for card in newCards: + geo = selectPart(2, True, card) + nextNode = mc.listRelatives(geo, c=1, pa=1) + i = 0 + while mc.nodeType(nextNode) != 'mesh': + i += 1 + if i >= 100: + break + nextNode = mc.listRelatives(nextNode, c=1, pa=1) + mc.setAttr(nextNode[0] + '.intermediateObject', 0) + transformNode = mc.listRelatives(nextNode, p=1, pa=1) + transformNode = mc.parent(transformNode, card) + connections = mc.listConnections(geo, d=0, s=1, p=1) + for connection in connections: + attr = connection.split('.')[1] + target = '%s.%s' % (transformNode[0], attr) + if mc.attributeQuery(attr, n=transformNode[0], ex=1): + mc.connectAttr(connection, target, f=1) + # NOTE: Rerouting connections manually to avoid node deletions (Autodesk bug in 2022, 2023) + geoShape = mc.listRelatives(geo, c=1, pa=1) + if geoShape: + nurbsTesselate = mc.listConnections(geoShape[0] + '.inMesh', p=1) + destConnections1 = mc.listConnections(geoShape[0] + '.worldMesh[0]', p=1) + destConnections2 = mc.listConnections(geoShape[0] + '.outMesh', p=1) + if nurbsTesselate and destConnections1 and destConnections2: + for destCon in destConnections1 + destConnections2: + mc.connectAttr(nurbsTesselate[0], destCon, f=1) + mc.delete(geo) + transformNode = mc.rename(transformNode, geo) + mc.sets(transformNode, forceElement=list(shader.keys())[0]) + mc.setAttr(transformNode + '.v', 1) + curve = selectPart(1, True, card) + layerID = getCurveLayer(curve[0]) + curveAddToLayer(layerID, inputCurves=curve) + if getOption('bindFlipUVs'): + if mc.attributeQuery('flipUV', n=curve[0], ex=1): + mc.setAttr(curve[0] + '.flipUV', not mc.getAttr(curve[0] + '.flipUV')) + newSel.append(curve[0]) + for curve in previousCurves: + mc.editDisplayLayerMembers("defaultLayer", curve, nr=1) + toggleColor.resetSingleCurve(curve) + mc.rename(curve, 'unboundCurve#') + mc.select(newSel) + + ### Curve Creation Modules ### + + def addRefine(self, output=None): + inst = None + if output: + output = self.curveWarp + '.inputCurve' + inst = self.pathCurve + else: + output = self.extrude + '.path' + inst = self.pathInst + + self.rebuildCurve = mc.createNode('rebuildCurve') + mc.setAttr(self.rebuildCurve + '.keepRange', 2) + mc.addAttr(self.pathCurve, ln='curveRefine', smn=-1, dv=self.refine, at='long', smx=300, k=1) + mc.addAttr(self.pathCurve, ln='curveSmooth', dv=0, smx=10, at='double', min=-0, k=1) + mc.connectAttr(inst + '.worldSpace[0]', self.rebuildCurve + '.inputCurve', f=1) + mc.connectAttr(self.rebuildCurve + '.outputCurve', output, f=1) + mc.connectAttr(self.pathCurve + '.curveRefine', self.rebuildCurve + '.spans', f=1) + mc.connectAttr(self.pathCurve + '.curveSmooth', self.rebuildCurve + '.smooth', f=1) + + def addPolyNormal(self): + self.polyNormalNode = mc.polyNormal(self.hairCard, ch=1, nm=2)[0] + self.polySoftEdge = mc.polySoftEdge(self.hairCard, ch=1, a=180)[0] + mc.addAttr(self.pathCurve, ln="reverseNormals", dv=1, en="reverse:off:", at="enum", k=1) + mc.addAttr(self.pathCurve, ln="surfaceNormals", dv=180, smx=180, at='double', min=-0, k=1) + mc.connectAttr(self.pathCurve + ".reverseNormals", self.polyNormalNode + ".normalMode", f=1) + mc.connectAttr(self.pathCurve + ".surfaceNormals", self.polySoftEdge + ".angle", f=1) + + def addUVs(self): + self.polyFlipUV = mc.polyFlipUV(self.hairCard, ch=True, up=True, pu=0.5, pv=0.5)[0] + self.polyMoveUV_root = mc.polyMoveUV(self.hairCard, ch=True, pvu=0.5, pvv=0)[0] + self.polyMoveUV_mid = mc.polyMoveUV(self.hairCard, ch=True, pvu=0.5, pvv=0.5)[0] + + mc.setAttr(self.polyFlipUV + '.usePivot', 1) + mc.setAttr(self.polyFlipUV + '.pivotU', 0.5) + mc.setAttr(self.polyFlipUV + '.pivotV', 0.5) + + mc.addAttr(self.pathCurve, ln="flipUV", dv=1, en="flip:off:", at="enum", k=1) + mc.addAttr(self.pathCurve, ln='moveU', smn=-1, dv=0, at='double', smx=1, k=1) + mc.addAttr(self.pathCurve, ln='moveV', smn=-1, dv=0, at='double', smx=1, k=1) + mc.addAttr(self.pathCurve, ln='rotateUV', smn=-360, dv=0, at='double', smx=360, k=1) + mc.addAttr(self.pathCurve, ln='scaleU', smn=0, dv=1, at='double', smx=5, k=1) + mc.addAttr(self.pathCurve, ln='scaleV', smn=0, dv=1, at='double', smx=5, k=1) + + uv_rotate = ''' + {1}.pivotU = ({0}.translateU + 0.5); + {1}.pivotV = ({0}.translateV + 0.5); + '''.format(self.polyMoveUV_root, self.polyMoveUV_mid) + self.uvExpr = mc.expression(ae=0, n='UV_Rotation#', s=uv_rotate) + + mc.connectAttr(self.pathCurve + '.flipUV', self.polyFlipUV + '.nodeState', f=1) + mc.connectAttr(self.pathCurve + '.moveU', self.polyMoveUV_root + '.translateU', f=1) + mc.connectAttr(self.pathCurve + '.moveV', self.polyMoveUV_root + '.translateV', f=1) + mc.connectAttr(self.pathCurve + '.rotateUV', self.polyMoveUV_mid + '.rotationAngle', f=1) + mc.connectAttr(self.pathCurve + '.scaleU', self.polyMoveUV_root + '.scaleU', f=1) + mc.connectAttr(self.pathCurve + '.scaleV', self.polyMoveUV_root + '.scaleV', f=1) + + def addSolidify(self): + self.solidifyNode = mc.createNode('polyExtrudeFace') + self.solidifyChoice = mc.createNode('choice') + mc.addAttr(self.pathCurve, ln='solidify', dv=0, at='bool', k=1) + mc.addAttr(self.pathCurve, ln='solidifyThickness', smn=0, dv=.25, at='double', smx=10, k=1) + mc.addAttr(self.pathCurve, ln='solidifyDivisions', smn=0, dv=0, at='long', smx=10, k=1) + mc.addAttr(self.pathCurve, ln='solidifyScaleX', smn=-10, dv=1, at='double', smx=10, k=1) + mc.addAttr(self.pathCurve, ln='solidifyScaleY', smn=-10, dv=1, at='double', smx=10, k=1) + mc.addAttr(self.pathCurve, ln='solidifyOffset', smn=-10, dv=0, at='double', smx=10, k=1) + mc.addAttr(self.pathCurve, ln='solidifyNormals', smn=0, dv=30, at='double', smx=180, k=1) + mc.setAttr(self.solidifyNode + '.inputComponents', 1, 'f[*]', type='componentList') + mc.connectAttr(self.hairCard + '.worldMatrix[0]', self.solidifyNode + '.manipMatrix', f=1) + mc.connectAttr(self.polyMoveUV_mid + '.output', self.solidifyChoice + '.input[0]', f=1) + mc.connectAttr(self.polyMoveUV_mid + '.output', self.solidifyNode + '.inputPolymesh', f=1) + mc.connectAttr(self.solidifyNode + '.output', self.solidifyChoice + '.input[1]', f=1) + mc.connectAttr(self.solidifyChoice + '.output', self.hairCard + '.inMesh', f=1) + mc.connectAttr(self.pathCurve + '.solidify', self.solidifyChoice + '.selector', f=1) + mc.connectAttr(self.pathCurve + '.solidifyThickness', self.solidifyNode + '.localTranslateZ', f=1) + mc.connectAttr(self.pathCurve + '.solidifyDivisions', self.solidifyNode + '.divisions', f=1) + mc.connectAttr(self.pathCurve + '.solidifyScaleX', self.solidifyNode + '.localScaleX', f=1) + mc.connectAttr(self.pathCurve + '.solidifyScaleY', self.solidifyNode + '.localScaleY', f=1) + mc.connectAttr(self.pathCurve + '.solidifyOffset', self.solidifyNode + '.offset', f=1) + mc.connectAttr(self.pathCurve + '.solidifyNormals', self.solidifyNode + '.smoothingAngle', f=1) + + def addMessage(self): + if not mc.attributeQuery('gsmessage', n=self.pathCurve, ex=1): + mc.addAttr(self.pathCurve, ln='gsmessage', at='message', k=0) + + def hideNodes(self): + nodes = [ + self.nurbsTesselate, + self.curveWarp, + self.polyFlipUV, + self.polyMoveUV_mid, + self.polyMoveUV_tip, + self.polyMoveUV_root, + self.solidifyNode, + self.solidifyChoice, + self.extrude, + self.magnExpr, + self.scaleExpr, + self.uvExpr, + self.rebuildCurve, + self.polyNormalNode, + self.polySoftEdge, + self.lattice, + self.twist, + self.curveInfoNode, + self.dynamicDivisionsCalculate, + self.autoRefineCalculate, + ] + assetList = [] + for node in nodes: + if node: + if isinstance(node, list): + for n in node: + if mc.objExists(n): + assetList.append(n) + mc.setAttr(n + '.isHistoricallyInteresting', 0) + else: + if mc.objExists(node): + assetList.append(node) + mc.setAttr(node + '.isHistoricallyInteresting', 0) + + def setCurveThickness(self): + shape = mc.ls(self.pathCurve, dag=1, s=1, l=1)[0] + mc.setAttr(shape + '.lineWidth', self.globalThickness) + + def addScaleFactor(self): + # Delete the attribute if saving is not checked + if not WIDGETS['keepCurveAttributes'].isChecked(): + if mc.attributeQuery('scaleFactor', n=self.pathCurve, ex=1): + mc.deleteAttr(self.pathCurve + '.scaleFactor') + # Add the attribute + if not mc.attributeQuery('scaleFactor', n=self.pathCurve, ex=1): + mc.addAttr(self.pathCurve, ln='scaleFactor', at='double', dv=self.sf) + mc.setAttr(self.pathCurve + '.scaleFactor', self.sf) + + def addDynamicDivisions(self, curve=None, tesselateNode=None): + if not curve: + curve = self.pathCurve + if not tesselateNode: + tesselateNode = self.nurbsTesselate + curveShape = mc.listRelatives(curve, c=1, typ='nurbsCurve') + if not curveShape: + return + curveShape = curveShape[0] + self.curveInfoNode = mc.createNode('curveInfo', n='dynamicDivisionsInfo#', ss=1) + mc.connectAttr(curveShape + '.worldSpace[0]', self.curveInfoNode + '.inputCurve', f=1) + if not mc.attributeQuery('dynamicDivisions', n=curve, ex=1): + mc.addAttr(curve, ln='dynamicDivisions', dv=0, at='bool', k=1) + mc.disconnectAttr(curve + '.lengthDivisions', tesselateNode + '.vNumber') + + if not mc.attributeQuery('scaleFactor', n=curve, ex=1): + mc.addAttr(curve, ln='scaleFactor', at='double', dv=self.sf) + + if not mc.attributeQuery('dynamicDivMult', n=curve, ex=1): # NOTE: In case we need this in the future + mc.addAttr(curve, ln='dynamicDivMult', at='double', dv=1.0, min=0.01, max=1000, k=1) + + expr = ''' + if ({2}.dynamicDivisions) + {{ + {0}.vNumber = {1}.arcLength * {2}.lengthDivisions * 0.05 * {2}.dynamicDivMult / {2}.scaleFactor; + }} + else + {{ + {0}.vNumber = {2}.lengthDivisions; + }} + '''.format(tesselateNode, self.curveInfoNode, curve) + self.dynamicDivisionsCalculate = mc.expression(ae=0, s=expr, n='dynamicDivisionsCalculate#') + + def addAutoRefine(self, curve=None, rebuildCurve=None): + """Add automatic rebuild value calculation to existing curveRebuild node""" + useAutoRefine = True + if not curve: + curve = self.pathCurve + useAutoRefine = mc.optionVar(q="GSCT_useAutoRefineOnNewCurves") + if not rebuildCurve: + rebuildCurve = self.rebuildCurve + useAutoRefine = mc.optionVar(q="GSCT_useAutoRefineOnNewCurves") + + if not mc.attributeQuery('autoRefine', n=curve, ex=1): + mc.addAttr(curve, ln='autoRefine', dv=useAutoRefine, at='bool', k=1) + + # Check if curveRefine connection is correct + curveRefineTarget = mc.listConnections(curve + '.curveRefine') + for node in curveRefineTarget: + if mc.nodeType(node) == "expression": + continue + else: + mc.disconnectAttr(curve + '.curveRefine', node + '.spans') + + expr = ''' + if ({0}.autoRefine) + {{ + {1}.keepControlPoints = 1; + }} + else + {{ + {1}.keepControlPoints = 0; + {1}.spans = {0}.curveRefine; + }} + '''.format(curve, rebuildCurve) + self.autoRefineCalculate = mc.expression(ae=0, s=expr, n='autoRebuildCalculate#') + return self.autoRefineCalculate + + def clearTweakNode(self): # TODO: Check if this fix is needed + pass + # Possible fix for older Maya versions tweak node issue + # try: + # tweakNode = mc.listConnections(self.hairCard + '.tweakLocation') + # if tweakNode: + # mc.delete(tweakNode) + # except: + # pass + + def group(self, grpName, fake=False): + constructionGrp = None + if fake: + constructionGrp = mc.createNode('transform', n='instances#') + self.hairCard = self.geo + elif self.lattice: + constructionGrp = mc.group(self.pathInst, self.profileInst, self.lattice, self.twist, n='instances#') + else: + constructionGrp = mc.group(self.pathInst, self.profileInst, n='instances#') + mc.setAttr(constructionGrp + '.inheritsTransform', 0) + mc.setAttr(constructionGrp + '.tx', lock=True) + mc.setAttr(constructionGrp + '.ty', lock=True) + mc.setAttr(constructionGrp + '.tz', lock=True) + mc.setAttr(constructionGrp + '.rx', lock=True) + mc.setAttr(constructionGrp + '.ry', lock=True) + mc.setAttr(constructionGrp + '.rz', lock=True) + mc.setAttr(constructionGrp + '.sx', lock=True) + mc.setAttr(constructionGrp + '.sy', lock=True) + mc.setAttr(constructionGrp + '.sz', lock=True) + + # Layers + hairGrp = self.currentLayerInd() + curveGrp, geoGrp, instGrp = utils.getFormattedLayerNames(self.collectionID, hairGrp) + + if mc.objExists(instGrp) != 1: + utils.createNewDisplayLayer(name=instGrp, objects=constructionGrp) + mc.setAttr(instGrp + '.displayType', 2) + mc.setAttr(instGrp + '.visibility', 0) + else: + mc.editDisplayLayerMembers((instGrp), constructionGrp, nr=1) + mc.setAttr(instGrp + '.displayType', 2) + mc.setAttr(instGrp + '.visibility', 0) + + if mc.objExists(geoGrp) != 1: + utils.createNewDisplayLayer(name=geoGrp, objects=self.hairCard) + mc.setAttr(geoGrp + '.displayType', 2) + else: + mc.editDisplayLayerMembers(geoGrp, self.hairCard, nr=1) + + if mc.objExists(curveGrp) != 1: + utils.createNewDisplayLayer(name=curveGrp, objects=self.pathCurve) + else: + mc.editDisplayLayerMembers(curveGrp, self.pathCurve, nr=1) + + layerCollections.updateDefaultLayerNode() + layerCollections.updateCollectionNames() + + # Final Group Lock + self.hairCardGrp = str(mc.group(self.pathCurve, constructionGrp, self.hairCard, n=grpName)) + mc.setAttr(self.hairCardGrp + '|' + self.hairCard + '.inheritsTransform', 1) # TODO: Why do we need this? + mc.setAttr(self.hairCardGrp + '.inheritsTransform', 0) # TODO: Check if this breaks anything + mc.setAttr(self.hairCardGrp + '.tx', lock=True) + mc.setAttr(self.hairCardGrp + '.ty', lock=True) + mc.setAttr(self.hairCardGrp + '.tz', lock=True) + mc.setAttr(self.hairCardGrp + '.rx', lock=True) + mc.setAttr(self.hairCardGrp + '.ry', lock=True) + mc.setAttr(self.hairCardGrp + '.rz', lock=True) + mc.setAttr(self.hairCardGrp + '.sx', lock=True) + mc.setAttr(self.hairCardGrp + '.sy', lock=True) + mc.setAttr(self.hairCardGrp + '.sz', lock=True) + + +create = Create("create") + + +class Sliders: + + def __init__(self, name): + self.name = name + self.icons = utils.getFolder.icons() + self.timer = utils.Timer() + self.widthLock = False + self.rebuildDupTrans = list() + self.rebuildDupShape = list() + self.rebuildCurveNode = list() + self.rebuildTargetNode = list() + self.lastCV = list() + self.curveRepeat = int() + self.curveTrigger = int() + self.sliderCheck = int() + self.curveCVsName = list() + self.curveCVs = list() + self.curveRotate = list() + self.curveOrientation = list() + self.curveTwist = list() + self.curveWidth = list() + self.curveWidthX = list() + self.curveWidthZ = list() + self.curveTaper = list() + self.curveProfile = list() + self.curveRandDragSelList = [] + + def release(self, *_): # Close undo chunk on release + mc.undoInfo(cck=1) + self.sliderCheck = 0 + + def selectCVSlider(self, sliderValue): # Select CV Slider + if self.sliderCheck == 0: + mc.undoInfo(ock=1, cn='gsCVSlider') + self.sliderCheck = 1 + sel = mc.filterExpand(mc.ls(sl=1, o=1, fl=1, dag=1, s=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, o=1, fl=1, dag=1, s=1), sm=9) + if not sel: + return + modifier = utils.getMod() + selectionList = [] + for curve in sel: + shape = mc.listRelatives(curve, c=1, pa=1, ni=1, s=1, typ='nurbsCurve') + if not shape: + continue + shape = shape[0] + numCVs = mc.getAttr(shape + '.controlPoints', s=1) + position = math.floor(numCVs * sliderValue) + selectionList.append("{}.cv[{}]".format(shape, position)) + if modifier == "Ctrl": + return + if modifier == "Alt" or modifier == "Shift+Alt": + if len(mc.ls(sl=1, fl=1)) <= len(sel): + return + mc.select(selectionList, d=1) + elif modifier == "Shift": + mc.select(selectionList, add=1) + else: + mc.select(selectionList, r=1) + + def rebuildSliderDrag(self, *_): # Rebuild curve drag command # BUG: Rebuilding to small values can break advanced visibility curve drawing + if self.sliderCheck == 0: + mc.undoInfo(ock=1, cn='gsRebuildSlider') + self.sliderCheck = 1 + slider = mc.intSliderGrp('gsRebuildSlider', q=1, v=1) + sel = mc.filterExpand(mc.ls(sl=1, o=1, fl=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, o=1, fl=1), sm=9) + if not sel: + return 0 + j = int() + sha = mc.ls(sel, dag=1, s=1) + for obj in sha: + inputPlug = mc.listConnections(obj + '.create', d=0, s=1, p=1) + if inputPlug: # Rebuild Algorithm if rebuildCurve node is detected + if len(self.rebuildDupTrans) <= j or not mc.objExists(self.rebuildDupTrans[j]): + target = mc.listConnections(obj + '.worldSpace[1]', d=1, s=0, p=1) + nurbsCurve = mc.createNode('nurbsCurve', n='gsTempCurve') + mc.setAttr(nurbsCurve + '.dispCV', 1) + nurbsCurveTransform = mc.listRelatives(nurbsCurve, p=1, pa=1) + mc.setAttr(nurbsCurveTransform[0] + '.hiddenInOutliner', 1) + mc.matchTransform(nurbsCurveTransform[0], mc.listRelatives(obj, p=1, pa=1)[0]) + rebuildCurve = mc.createNode('rebuildCurve') + mc.connectAttr(obj + ".worldSpace[1]", rebuildCurve + ".inputCurve", f=1) + mc.connectAttr(rebuildCurve + ".outputCurve", nurbsCurve + ".create", f=1) + mc.connectAttr(nurbsCurve + '.local', target[0], f=1) + utils.addAtIndex(self.rebuildDupTrans, j, nurbsCurveTransform[0]) + utils.addAtIndex(self.rebuildDupShape, j, nurbsCurve) + utils.addAtIndex(self.rebuildCurveNode, j, rebuildCurve) + utils.addAtIndex(self.rebuildTargetNode, j, target[0]) + j += 1 + else: # Rebuild Algorithm if rebuildCurve node is not detected + if len(self.rebuildDupTrans) <= j or not mc.objExists(self.rebuildDupTrans[j]): + tra = mc.duplicate(obj, rc=1) + shp = mc.listRelatives(tra[0], c=1, pa=1) + rebuildCurve = mc.createNode('rebuildCurve') + mc.setAttr(rebuildCurve + '.keepRange', 1) + mc.setAttr(tra[0] + '.hiddenInOutliner', 1) + mc.setAttr(rebuildCurve + '.spans', (mc.getAttr(obj + '.spans'))) + mc.setAttr(rebuildCurve + '.degree', (mc.getAttr(obj + '.degree'))) + mc.connectAttr(shp[0] + '.worldSpace', rebuildCurve + '.inputCurve', f=1) + mc.connectAttr(rebuildCurve + '.outputCurve', obj + '.create', f=1) + utils.addAtIndex(self.rebuildDupTrans, j, tra[0]) + utils.addAtIndex(self.rebuildDupShape, j, shp[0]) + utils.addAtIndex(self.rebuildCurveNode, j, rebuildCurve) + j += 1 + mc.setAttr(obj + '.dispCV', 1) + + for node in self.rebuildCurveNode: + mc.setAttr(node + '.spans', slider) + # TODO: Add in v1.3 with different refine modes and auto refine values + # for curve in sel: + # if mc.attributeQuery('curveRefine', n=curve, ex=1): + # mc.setAttr(curve + '.curveRefine', 20 if slider <= 20 else 0) + mc.select(sel, r=1) + mc.headsUpMessage('[%s]' % slider, s=1) + + def rebuildSliderRelease(self, *_): # Rebuild curve slider release command + if self.sliderCheck == 0: + mc.undoInfo(ock=1, cn='gsRebuildSlider') + self.sliderCheck = 1 + selTemp = mc.ls(sl=1, o=1, fl=1, dag=1, s=1) + sel = list() + for obj in selTemp: + if utils.attrExists(obj, 'spans'): + sel.append(obj) + if len(sel) > 0: + slider = mc.intSliderGrp('gsRebuildSlider', q=1, v=1) + if len(self.rebuildDupTrans) == 0: + for i in range(len(sel)): + inputPlug = mc.listConnections(sel[i] + '.create', d=0, s=1, p=1) + rebuildCurve = list() + if not inputPlug: + rebuildCurve = mc.rebuildCurve(sel[i], s=slider, ch=0, kr=1) + else: + rebuildCurve = mc.rebuildCurve(sel[i], s=slider, ch=1, kr=1) + newName = mc.rename(rebuildCurve[1], 'gsRebuildCurveNode#') + hist = mc.listHistory(newName) + for ele in hist: + if 'gsRebuildCurveNode' in ele and ele != newName: + mc.delete(ele) + mc.rename(newName, 'gsRebuildCurveNode1') + mc.headsUpMessage('[%s]' % slider, s=1) + else: + try: + mc.delete(self.rebuildDupTrans) + except BaseException: + pass + try: + mc.delete(self.rebuildDupShape) + except BaseException: + pass + self.rebuildDupTrans *= 0 + self.rebuildDupShape *= 0 + self.rebuildCurveNode *= 0 + self.rebuildTargetNode *= 0 + + for obj in sel: + try: + mc.setAttr(obj + '.dispCV', 0) + except BaseException: + pass + mc.select(mc.listRelatives(sel, p=1, pa=1), r=1) + + def rebuildButtonClicked(self): + self.rebuildSliderDrag() + self.rebuildSliderRelease() + self.release() + + def randSliderDrag(self, sldr, *_): + """Randomize Curve sliders drag""" + # BUG: Fix width and taper slider to be changed based on the original values, not on some arbitrary ones + if self.sliderCheck == 0: + mc.undoInfo(ock=1, cn='curveRandSlider') + self.sliderCheck = 1 + sel = mc.filterExpand(mc.ls(sl=1, o=1, fl=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, o=1, fl=1), sm=9) + + if not sel: + return + + slid = list() + vect = str() + + for i in range(8): + slid.append(WIDGETS['curveRandomizeSlider' + str(i)].isChecked()) + + if slid[0] == 1 and (sldr == 0 or sldr == -1): + if self.curveRepeat == 1: + for i in range(len(self.curveCVsName)): + vect = self.curveCVs[i] + mc.move(vect[0], vect[1], vect[2], self.curveCVsName[i], a=1) + lockFirstCV = WIDGETS['gsLockFirstCV'].isChecked() + lockLastCV = WIDGETS['gsLockLastCV'].isChecked() + slider = mc.floatSliderGrp('gsCurveCVsRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveCVsRandMulti', q=1, v=1) + value = slider * mult + for obj in sel: + ind = mc.getAttr(obj + '.cp', mi=1) + CVs = list() + for i in range(len(ind)): + CVs.append(str(obj) + '.cv[' + str(i) + ']') + if self.curveTrigger == 0: + pp = mc.pointPosition(obj + '.cp[' + str(i) + ']') + self.curveCVsName.append(CVs[i]) + self.curveCVs.append([pp[0], pp[1], pp[2]]) + for i in range(len(CVs)): + if (i == 0 or i == 1) and lockFirstCV == 1: + continue + if i == len(CVs) - 1 and lockLastCV == 1: + continue + randX = random.uniform(value * -1, value) + randY = random.uniform(value * -1, value) + randZ = random.uniform(value * -1, value) + if not WIDGETS['gsRandAxisX'].isChecked(): + randX = 0 + if not WIDGETS['gsRandAxisY'].isChecked(): + randY = 0 + if not WIDGETS['gsRandAxisZ'].isChecked(): + randZ = 0 + mc.move(randX, randY, randZ, CVs[i], r=1, os=1, wd=1) + + if slid[1] == 1 and (sldr == 1 or sldr == -1): + slider = mc.floatSliderGrp('gsCurveRotationRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveRotationRandMulti', q=1, v=1) + value = slider * mult + for i in range(len(sel)): + if self.curveTrigger == 0: + rotX = mc.getAttr(sel[i] + '.rx') + rotY = mc.getAttr(sel[i] + '.ry') + rotZ = mc.getAttr(sel[i] + '.rz') + self.curveRotate.append([rotX, rotY, rotZ]) + if self.curveRepeat == 1: + rotate = self.curveRotate[i] + mc.setAttr(sel[i] + '.rx', rotate[0]) + mc.setAttr(sel[i] + '.ry', rotate[1]) + mc.setAttr(sel[i] + '.rz', rotate[2]) + for obj in sel: + randX = random.uniform(value * -1, value) + randY = random.uniform(value * -1, value) + randZ = random.uniform(value * -1, value) + if not WIDGETS['gsRandRotateAxisX'].isChecked(): + randX = 0 + if not WIDGETS['gsRandRotateAxisY'].isChecked(): + randY = 0 + if not WIDGETS['gsRandRotateAxisZ'].isChecked(): + randZ = 0 + mc.rotate(randX, randY, randZ, obj, r=1, os=1, fo=1) + + if slid[2] == 1 and (sldr == 2 or sldr == -1): + slider = mc.floatSliderGrp('gsCurveOrientationRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveOrientationRandMulti', q=1, v=1) + value = slider * mult + for i in range(len(sel)): + if self.curveTrigger == 0: + self.curveOrientation.append(mc.getAttr(sel[i] + '.Orientation')) + if self.curveRepeat == 1: + mc.setAttr(sel[i] + '.Orientation', self.curveOrientation[i]) + for obj in sel: + rand = random.uniform(value * -1, value) + mc.setAttr(obj + '.Orientation', rand) + + if slid[3] == 1 and (sldr == 3 or sldr == -1): + slider = mc.floatSliderGrp('gsCurveTwistRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveTwistRandMulti', q=1, v=1) + value = slider * mult + for i in range(len(sel)): + if self.curveTrigger == 0: + self.curveTwist.append(mc.getAttr(sel[i] + '.Twist')) + if self.curveRepeat == 1: + mc.setAttr(sel[i] + '.Twist', self.curveTwist[i]) + for obj in sel: + rand = random.uniform(value * -1, value) + mc.setAttr(obj + '.Twist', rand) + + if slid[4] == 1 and (sldr == 4 or sldr == -1): + slider = mc.floatSliderGrp('gsCurveWidthRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveWidthRandMulti', q=1, v=1) + value = slider * mult + for i in range(len(sel)): + if self.curveTrigger == 0: + if mc.attributeQuery('Width', n=sel[i], ex=1): + self.curveWidth.append(mc.getAttr(sel[i] + '.Width')) + else: + self.curveWidthX.append(mc.getAttr(sel[i] + '.WidthX')) + self.curveWidthZ.append(mc.getAttr(sel[i] + '.WidthZ')) + if self.curveRepeat == 1: + if mc.attributeQuery('Width', n=sel[i], ex=1): + mc.setAttr(sel[i] + '.Width', self.curveWidth[i]) + else: + mc.setAttr(sel[i] + '.WidthX', self.curveWidthX[i]) + mc.setAttr(sel[i] + '.WidthZ', self.curveWidthZ[i]) + rand = random.uniform(0.001, value) + if mc.attributeQuery('Width', n=sel[i], ex=1): + mc.setAttr(sel[i] + '.Width', rand) + else: + mc.setAttr(sel[i] + '.WidthX', rand) + if not WIDGETS['gsWidthCheckBoxUniform'].isChecked(): + rand = random.uniform(0.001, value) + mc.setAttr(sel[i] + '.WidthZ', rand) + + if slid[5] == 1 and (sldr == 5 or sldr == -1): + slider = mc.floatSliderGrp('gsCurveTaperRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveTaperRandMulti', q=1, v=1) + value = slider * mult + for i in range(len(sel)): + if self.curveTrigger == 0: + self.curveTaper.append(mc.getAttr(sel[i] + '.Taper')) + if self.curveRepeat == 1: + mc.setAttr(sel[i] + '.Taper', self.curveTaper[i]) + rand = random.uniform(0, value) + mc.setAttr(sel[i] + '.Taper', rand) + + if slid[6] == 1 and (sldr == 6 or sldr == -1): + slider = mc.floatSliderGrp('gsCurveProfileRand', q=1, v=1) + mult = mc.floatSliderGrp('gsCurveProfileRandMulti', q=1, v=1) + value = slider * mult + for i in range(len(sel)): + if mc.attributeQuery('Profile', n=sel[i], ex=1): + if self.curveTrigger == 0: + self.curveProfile.append(mc.getAttr(sel[i] + '.Profile')) + if self.curveRepeat == 1: + mc.setAttr(sel[i] + '.Profile', self.curveProfile[i]) + rand = random.uniform(0, value) + if WIDGETS['gsProfileCheckBoxNegative'].isChecked(): + rand = random.uniform(value * -1, value) + mc.setAttr(sel[i] + '.Profile', rand) + + if slid[7] == 1 and (sldr == 7 or sldr == -1): + if not self.curveRandDragSelList: + self.curveRandDragSelList = sel + if self.timer.increment(1.0 / 60.0): + sliderVal = math.ceil(mc.floatSliderGrp('gsCurveSelectRand', q=1, v=1) * len(self.curveRandDragSelList)) + shuffledList = self.curveRandDragSelList + random.shuffle(shuffledList) + newSel = [] + for i in range(sliderVal): + newSel.append(shuffledList[i]) + if not newSel: + mc.select(self.curveRandDragSelList, r=1) + else: + mc.select(newSel, r=1) + + self.curveRepeat = 1 + self.curveTrigger = 1 + + def randSliderRelease(self, sldr, *_): # Randomize Curve slider release + if self.sliderCheck == 0: + mc.undoInfo(ock=1, cn='curveRandSlider') + self.sliderCheck = 1 + sel = mc.ls(sl=1, fl=1, o=1) + slid = list() + for i in range(8): + slid.append(WIDGETS['curveRandomizeSlider' + str(i)].isChecked()) + + if slid[0] == 1 and sldr == 0: + for i in range(len(self.curveCVsName)): + vect = self.curveCVs[i] + mc.move(vect[0], vect[1], vect[2], self.curveCVsName[i], a=1) + + if slid[1] == 1 and sldr == 1: + for i in range(len(sel)): + rotate = self.curveRotate[i] + mc.setAttr(sel[i] + ".rx", rotate[0]) + mc.setAttr(sel[i] + ".ry", rotate[1]) + mc.setAttr(sel[i] + ".rz", rotate[2]) + + if slid[2] == 1 and sldr == 2: + for i in range(len(sel)): + mc.setAttr(sel[i] + '.Orientation', self.curveOrientation[i]) + + if slid[3] == 1 and sldr == 3: + for i in range(len(self.curveTwist)): + mc.setAttr(sel[i] + '.Twist', self.curveTwist[i]) + + if slid[4] == 1 and sldr == 4: + for i in range(len(sel)): + if mc.attributeQuery('Width', n=sel[i], ex=1): + mc.setAttr(sel[i] + '.Width', self.curveWidth[i]) + else: + mc.setAttr(sel[i] + '.WidthX', self.curveWidthX[i]) + mc.setAttr(sel[i] + '.WidthZ', self.curveWidthZ[i]) + + if slid[5] == 1 and sldr == 5: + for i in range(len(self.curveTaper)): + mc.setAttr(sel[i] + '.Taper', self.curveTaper[i]) + + if slid[6] == 1 and sldr == 6: + for i in range(len(self.curveProfile)): + mc.setAttr(sel[i] + '.Profile', self.curveProfile[i]) + + if slid[7] == 1 and sldr == 7: + if self.curveRandDragSelList: + mc.select(self.curveRandDragSelList, r=1) + + self.curveCVs *= 0 + self.curveCVsName *= 0 + self.curveRotate *= 0 + self.curveOrientation *= 0 + self.curveTwist *= 0 + self.curveWidth *= 0 + self.curveWidthX *= 0 + self.curveWidthZ *= 0 + self.curveTaper *= 0 + self.curveProfile *= 0 + self.curveRandDragSelList *= 0 + self.curveTrigger = 0 + self.curveRepeat = 0 + + def curveControlSliderDrag(self, sldr, *_): # Curve Control slider drag + + if self.sliderCheck == 0: + mc.undoInfo(ock=1, cn='curveControlSlider') + self.sliderCheck = 1 + + sel = mc.ls(sl=1, dag=1, tr=1) + if not sel: + sel = mc.listRelatives(mc.ls(sl=1, o=1), p=1, pa=1) + + if not sel: + return 0 + + sel = list(dict.fromkeys(sel)) + + if not self.timer.increment(1.0 / 30.0) and len(sel) > 10: + return 0 + + attr = sldr.getAttributeName() + value = sldr.getValue() + + for obj in sel: + lengthDivisoinsAttribute = mc.attributeQuery('lengthDivisions', n=obj, ex=1) + lengthAttribute = mc.attributeQuery('Length', n=obj, ex=1) + if not lengthDivisoinsAttribute and not lengthAttribute: + continue + + if attr == 'lineWidth': + mc.setAttr(mc.ls(obj, dag=1, s=1)[0] + '.lineWidth', value) + continue + + if attr == 'WidthX' and WIDGETS['widthLockSwitch'].isChecked(): + try: + mc.setAttr(obj + '.' + 'WidthX', value) + mc.setAttr(obj + '.' + 'WidthZ', value) + WIDGETS['WidthZ'].setValue(value) + continue + except BaseException: + continue + if attr == 'WidthZ' and WIDGETS['widthLockSwitch'].isChecked(): + try: + mc.setAttr(obj + '.' + 'WidthX', value) + mc.setAttr(obj + '.' + 'WidthZ', value) + WIDGETS['WidthX'].setValue(value) + continue + except BaseException: + continue + + if mc.attributeQuery(attr, n=obj, ex=1): + try: + mc.setAttr(obj + '.' + attr, value) + except BaseException: + pass + + def curveHighlightSliderDrag(self): + pass + + +sliders = Sliders("sliders") + + +class ToggleColor: + + COLOR_RANGE = (0.1, 1) + STORAGE_NODE = 'gsColorShaderStorageNode' + + def __init__(self, name): + self.name = name + + # IO + def writeColorDict(self, colorDict): + """Writes a color dict to color storage node""" + self.checkColorStorageNode() + mc.setAttr(self.STORAGE_NODE + '.layerColor', str(colorDict), typ='string') + + def readColorDict(self): + """Reads a color dict from color storage node""" + self.checkColorStorageNode() + dictString = mc.getAttr(self.STORAGE_NODE + '.layerColor') + return eval(dictString) + + def colorEnabled(self): + # type: () -> bool + """Is color enabled?""" + self.checkColorStorageNode() + return bool(mc.getAttr(self.STORAGE_NODE + '.colorApplied')) + + # Check nodes, materials and attributes + + def checkColorStorageNode(self): + """Checks if color storage node exists (and has default values)""" + # Create storage node if not found + if not mc.objExists(self.STORAGE_NODE): + mc.scriptNode(n=self.STORAGE_NODE) + + # Add colorApplied attribute if not found + if not mc.attributeQuery('colorApplied', n=self.STORAGE_NODE, ex=1): + mc.addAttr(self.STORAGE_NODE, ln='colorApplied', at='bool') + mc.setAttr(self.STORAGE_NODE + '.colorApplied', False) + + # Add layerColor attribute if not found + if not mc.attributeQuery('layerColor', n=self.STORAGE_NODE, ex=1): + mc.addAttr(self.STORAGE_NODE, ln='layerColor', dt='string') + mc.setAttr(self.STORAGE_NODE + '.layerColor', + str({k: self.generateBrightColor() for k in range(80)}), typ='string') + else: + colorDict = eval(mc.getAttr(self.STORAGE_NODE + '.layerColor')) + if len(colorDict) < 80: + mc.setAttr(self.STORAGE_NODE + '.layerColor', + str({k: self.generateBrightColor() for k in range(80)}), typ='string') + + # Add layerName attribute if not found + if not mc.attributeQuery('layerName', n=self.STORAGE_NODE, ex=1): + mc.addAttr(self.STORAGE_NODE, ln='layerName', dt='string') + mc.setAttr(self.STORAGE_NODE + '.layerName', str({k: "" for k in range(80)}), typ='string') + else: + if mc.objExists(self.STORAGE_NODE): + storageNode = mc.getAttr(self.STORAGE_NODE + '.layerName') + if storageNode: + layerNames = eval(storageNode) + if len(layerNames) < 80: + mc.setAttr(self.STORAGE_NODE + '.layerName', str({k: "" for k in range(80)}), typ='string') + + # Material creation utils + def getNodeNamesDict(self, layerName): + """Returns a dict with formatted material node names + Valid keys: 'engine', 'shader', 'switch', 'checker', 'tiling' """ + namesDict = {n: 'GSCTMAT_{}_{}'.format(layerName, n) for n in + ['engine', 'shader', 'switchChecker', 'switchDiffuse', 'switchAlpha', 'checker', 'tiling', 'color']} + return namesDict + + def deleteColorMaterial(self, layerName): + """Deletes the named material and all the appropriate nodes""" + nodes = self.getNodeNamesDict(layerName) + for node in nodes: + if mc.objExists(nodes[node]): + mc.delete(nodes[node]) + + def createColorMaterial(self, variantName, originalLayerName=None): + """Creates a single color material network based on the layerName name""" + nodes = self.getNodeNamesDict(variantName) + if not originalLayerName: + originalLayerName = variantName + self.deleteColorMaterial(variantName) # Just in case + # Create all the nodes mc.shadingNode('place2dTexture', au=1, n=place2d, ss=1) + shader = mc.shadingNode('lambert', n=nodes['shader'], ss=1, asShader=1) + engine = mc.sets(r=1, nss=1, em=1, n=nodes['engine']) + switchChecker = mc.shadingNode('condition', n=nodes['switchChecker'], ss=1, au=1) + switchDiffuse = mc.shadingNode('condition', n=nodes['switchDiffuse'], ss=1, au=1) + switchAlpha = mc.shadingNode('condition', n=nodes['switchAlpha'], ss=1, au=1) + checker = mc.shadingNode('checker', n=nodes['checker'], ss=1, at=1) + tiling = mc.shadingNode('place2dTexture', n=nodes['tiling'], ss=1, au=1) + color = mc.shadingNode('colorConstant', n=nodes['color'], ss=1, at=1) + # Set default attributes + mc.setAttr(tiling + '.repeatUV', 10, 10, typ='float2') + mc.setAttr(shader + '.diffuse', 1) + mc.setAttr(color + '.inColor', random.random(), random.random(), random.random(), typ='double3') + mc.setAttr(switchAlpha + '.colorIfFalse', 0, 0, 0, typ='float3') + # Connect stuff together + mc.connectAttr(tiling + '.outUV', checker + '.uvCoord', f=1) + mc.connectAttr(tiling + '.outUvFilterSize', checker + '.uvFilterSize', f=1) + mc.connectAttr(checker + '.outColor', switchChecker + '.colorIfTrue', f=1) + mc.connectAttr(switchChecker + '.outColor', switchDiffuse + '.colorIfTrue') + mc.connectAttr(switchDiffuse + '.outColor', shader + '.color', f=1) + mc.connectAttr(shader + '.outColor', engine + '.surfaceShader', f=1) + mc.connectAttr(color + '.outColor', checker + '.color1', f=1) + mc.connectAttr(color + '.outColor', switchChecker + '.colorIfFalse', f=1) + mc.connectAttr(originalLayerName + '.overrideColorRGB', color + '.inColor') + mc.connectAttr(switchAlpha + '.outColor', shader + '.transparency') + # Create messages + mc.addAttr(engine, ln='gs_shadermessage', at='message') + return engine + + def checkColorMaterial(self, variantName, originalLayerName=None): + """ + Checks if appropriately named material exists for the layer.\n + Also check if material has all the nodes it needs. + """ + if not originalLayerName: + originalLayerName = variantName + nodesDict = self.getNodeNamesDict(variantName) + if not mc.objExists(nodesDict['engine']): + # Create from scratch + self.deleteColorMaterial(variantName) + self.createColorMaterial(variantName, originalLayerName) + else: + # Check if not corrupted + for node in nodesDict: + if not mc.objExists(nodesDict[node]): + self.deleteColorMaterial(variantName) + self.createColorMaterial(variantName, originalLayerName) + break + return nodesDict['engine'] + + # Main methods + def toggleColorVis(self): + """Toggle colors on Color button click or hotkey""" + self.checkColorStorageNode() + if not mc.getAttr(self.STORAGE_NODE + '.colorApplied'): + currentViewport = mc.playblast(ae=1) + if currentViewport: + mc.modelEditor(currentViewport, e=1, displayTextures=1) + self.enableColors() + else: + self.disableColors() + utils.deferredLp(utils.noUndo(updateMainUI))() # TODO: Check if this breaks anything (mostly UNDO) + + def onLayerChange(self, curves, targetLayer): + """Called when curve is moved from layer to layer""" + if WIDGETS['colorMode'].isChecked(): + geo = selectPart(2, True, curves) + shadersDict = utils.getShader(geo) + # Set geometry to original material + for shader in shadersDict: + originalShader = mc.listConnections(shader + '.gs_shadermessage') + if originalShader: + mc.sets(shadersDict[shader], e=1, fe=originalShader[0]) + else: + mc.sets(shadersDict[shader], e=1, fe='initialShadingGroup') + # Disable/Enable target layer + self.disableColors(targetLayer) + self.enableColors(targetLayer) + + def updateColors(self): + """ + Update the colors on cards if color mode is on. Runs every time selection changes.\n + NOTE: Should be as fast as possible + """ + if not WIDGETS['colorMode'].isChecked(): + return + allLayers = [x for x in mc.ls(typ='displayLayer') if ('curveGrp_' in x and '_Geo' in x)] + # Update colors + self.checkColorStorageNode() + colorDict = self.readColorDict() + for layer in allLayers: + split = layer.split('_') + try: + layerID = int(split[1]) if len(split) == 3 else int(split[2]) + except ValueError: + error = 'Failed to extract layer ID from layer "{}". Display Layer name is corrupted. Please delete corrupted layers and curves.'.format(layer) + MESSAGE.warning(error) + continue + r, g, b = colorDict[layerID] + mc.setAttr(layer + '.overrideColorRGB', r, g, b) + + def updateColorOptions(self): + """Triggered when there is need to change checkered or alpha options""" + if not WIDGETS['colorMode'].isChecked(): + return + allMaterialNodes = [x for x in mc.ls() if 'GSCTMAT_' in x] + isChecker = getOption('checkerPattern') + isDiffuseOnly = getOption('colorOnlyDiffuse') + switchNodes = [x for x in allMaterialNodes if '_switchChecker' in x] + for node in switchNodes: + mc.setAttr(node + '.firstTerm', int(not isChecker)) + switchNodes = [x for x in allMaterialNodes if '_switchAlpha' in x] + for node in switchNodes: + mc.setAttr(node + '.firstTerm', int(not isDiffuseOnly)) + + def enableColors(self, oneLayer=None): + """Enable colors on layers""" + allLayers = [x for x in mc.ls(typ='displayLayer') if ('curveGrp_' in x and '_Geo' in x)] + colorDict = self.readColorDict() + isChecker = getOption('checkerPattern') + isDiffuseOnly = getOption('colorOnlyDiffuse') + if oneLayer: + allLayers = [oneLayer] + for layer in allLayers: + # Get layer ID + split = layer.split('_') + try: + layerID = int(split[1]) if len(split) == 3 else int(split[2]) + except ValueError: + error = 'Failed to extract layer ID from layer "{}". Display Layer name is corrupted. Please delete corrupted layers and curves.'.format(layer) + MESSAGE.warning(error) + continue + # Get layer colors and set them + r, g, b = colorDict[layerID] + mc.setAttr(layer + '.overrideColorRGB', r, g, b) + # Get all the shaders for geometry in the layer + layerGeo = mc.editDisplayLayerMembers(layer, q=1, fn=1, nr=1) + shaders = utils.getShader(layerGeo) + count = 0 + for shader in shaders: + # Modify name if there are multiple materials used in one layer + variantName = layer + if len(shaders) > 1: + variantName = variantName + "_variant{}".format(count) + count += 1 + engine = self.checkColorMaterial(variantName, layer) + # Create and connect shadermessage + utils.connectMessage(engine, shader, 'gs_shadermessage') + # Find alpha and color node in the original shader graph and apply it to new color material + namesDict = self.getNodeNamesDict(variantName) + colorNode = None + alphaNode = None + network = mc.hyperShade(lun=shader) + for node in network: + if mc.nodeType(node) == 'file' and mc.connectionInfo(node + '.outColor', isSource=1): + colorNode = node + break + for node in network: + if mc.nodeType(node) == 'file' and mc.connectionInfo(node + '.outTransparency', isSource=1): + alphaNode = node + break + if colorNode: # Just to enable UV editor functionality + mc.connectAttr(colorNode + '.outColor', namesDict['switchDiffuse'] + '.colorIfFalse', f=1) + if alphaNode: # To have transparency with solid color + mc.connectAttr(alphaNode + '.outTransparency', namesDict['switchAlpha'] + '.colorIfTrue', f=1) + # Apply material + mc.sets(shaders[shader], e=1, fe=engine, nw=1) + # Options + if isChecker: + mc.setAttr(namesDict['switchChecker'] + '.firstTerm', 0) + else: + mc.setAttr(namesDict['switchChecker'] + '.firstTerm', 1) + if isDiffuseOnly: + mc.setAttr(namesDict['switchAlpha'] + '.firstTerm', 0) + else: + mc.setAttr(namesDict['switchAlpha'] + '.firstTerm', 1) + mc.setAttr(self.STORAGE_NODE + '.colorApplied', 1) + + def disableColors(self, oneLayer=None): + """Disable colors on layers""" + allColorMaterials = [x for x in mc.ls() if 'GSCTMAT_' in x] + allEngines = [x for x in allColorMaterials if '_engine' in x] + for engine in allEngines: + if oneLayer and oneLayer not in engine: + continue + originalGeo = mc.listConnections(engine + '.dagSetMembers') + if mc.attributeQuery('gs_shadermessage', n=engine, ex=1): + originalShader = mc.listConnections(engine + '.gs_shadermessage') + if originalShader: + mc.sets(originalGeo, e=1, fe=originalShader[0], nw=1) + continue + mc.sets(originalGeo, e=1, fe='initialShadingGroup', nw=1) + for node in allColorMaterials: + if oneLayer and oneLayer not in node: + continue + if mc.objExists(node): + mc.delete(node) + mc.setAttr(self.STORAGE_NODE + '.colorApplied', 0) + + def changeLayerColor(self): + """Called when layer color picker is modified""" + sel = mc.ls(sl=1, tr=1) + clr = mc.colorSliderGrp('gsColorPicker', q=1, rgb=1) + layerList = [] + for obj in sel: + selPart = selectPart(2, True, obj)[0] + conn = mc.listConnections(selPart, s=1, d=0) + layer = mc.ls(conn, et='displayLayer')[0] + layerList.append(layer) + layerList = list(dict.fromkeys(layerList)) + colorDict = self.readColorDict() + for layer in layerList: + layerId = re.findall(r'\d+', layer)[0] + mc.setAttr(layer + '.overrideColorRGB', clr[0], clr[1], clr[2]) + colorDict[int(layerId)] = clr + self.writeColorDict(colorDict) + if WIDGETS['syncCurveColor'].isChecked(): + self.syncCurveColors() + + # Other + def randomizeColors(self, *_): + """Randomizes colors in color storage node and updates UI colors if needed""" + colorDict = self.readColorDict() + for key in colorDict: + geoLayer = 'curveGrp_%s_Geo' % key + r, g, b = self.generateBrightColor() + colorDict[int(key)] = [r, g, b] + if mc.objExists(geoLayer): + mc.setAttr(geoLayer + '.overrideColorRGB', r, g, b) + self.writeColorDict(colorDict) + + if WIDGETS['syncCurveColor'].isChecked(): + self.syncCurveColors() + if mc.objExists(self.STORAGE_NODE) and mc.getAttr(self.STORAGE_NODE + '.colorApplied'): + self.updateColors() + + def syncCurveColors(self, manualSync=False, *_): + """Syncs curve colors to match layer colors""" + self.checkColorStorageNode() + colorDict = self.readColorDict() + sync = WIDGETS['syncCurveColor'].isChecked() + if manualSync: + sync = True + + # Generate color dictionary (if needed) + for i in range(80): + if colorDict[i] == [0, 0, 0]: + colorDict[i] = self.generateBrightColor() + self.writeColorDict(colorDict) + + # Iterate over collections and layers and set colors to curves + layerCollectionsWidget = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + collectionCount = layerCollectionsWidget.count() + for collectionID in range(collectionCount): + collection = "%s_" % collectionID if collectionID > 0 else "" + for i in range(80): + curveLayer = 'curveGrp_%s%s_Curve' % (collection, i) + if not mc.objExists(curveLayer): + continue + curves = mc.editDisplayLayerMembers(curveLayer, q=1, nr=1, fn=1) + for curve in curves: + shape = mc.listRelatives(curve, c=1, typ='nurbsCurve', pa=1)[0] + if sync: + mc.setAttr(shape + '.overrideEnabled', 1) + mc.setAttr(shape + '.overrideRGBColors', 1) + mc.setAttr(shape + '.overrideColorRGB', colorDict[i][0], colorDict[i][1], colorDict[i][2]) + curveControlUI.updateUI() + + def resetCurveColors(self, *_): + """Resets curve colors to default value""" + self.checkColorStorageNode() + layerCollectionsWidget = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + collectionCount = layerCollectionsWidget.count() + for collectionID in range(collectionCount): + collection = "%s_" % collectionID if collectionID > 0 else "" + for i in range(80): + curveLayer = 'curveGrp_%s%s_Curve' % (collection, i) + if not mc.objExists(curveLayer): + continue + curves = mc.editDisplayLayerMembers(curveLayer, q=1, nr=1, fn=1) + for curve in curves: + shape = mc.listRelatives(curve, c=1, typ='nurbsCurve', pa=1)[0] + mc.setAttr(shape + '.overrideEnabled', 0) + curveControlUI.updateUI() + + def changeCurveColor(self, *_): + """Called when curve color picker is modified""" + sync = WIDGETS['syncCurveColor'].isChecked() + if sync: + updateMainUI() + curveControlUI.updateUI() + else: + sel = mc.ls(sl=1, dag=1, s=1, typ='nurbsCurve') + clr = mc.colorSliderGrp('gsCurveColorPicker', q=1, rgb=1) + for crv in sel: + if mc.nodeType(crv) != 'nurbsCurve': + continue + if clr != [0, 0, 0]: + mc.setAttr(crv + '.overrideEnabled', 1) + mc.setAttr(crv + '.overrideRGBColors', 1) + mc.setAttr(crv + '.overrideColorRGB', clr[0], clr[1], clr[2]) + else: + mc.setAttr(crv + '.overrideEnabled', 0) + + # Utility methods + def generateBrightColor(self, satMin=0.5, satMax=1.0): + """Create random bright color and return as RGB tuple""" + h, s, l = random.random(), random.uniform(satMin, satMax), random.uniform(0.3, 0.7) + r, g, b = colorsys.hls_to_rgb(h, l, s) + return (r, g, b) + + def resetSingleCurve(self, curve): + shape = mc.listRelatives(curve, c=1, typ='nurbsCurve', pa=1)[0] + mc.setAttr(shape + '.overrideEnabled', 0) + + +toggleColor = ToggleColor("toggleColor") + + +class CurveControlUI: + + extrudeCard = { + 'lengthDivisions', + 'dynamicDivisions', + 'widthDivisions', + 'Orientation', + 'Twist', + 'Width', + 'Taper', + 'Profile', + 'curveRefine', + 'autoRefine', + 'curveSmooth', + 'surfaceNormals', + 'reverseNormals', + } + + extrudeTube = { + 'lengthDivisions', + 'dynamicDivisions', + 'widthDivisions', + 'Orientation', + 'Twist', + 'widthComboSlider', + 'Taper', + 'curveRefine', + 'autoRefine', + 'curveSmooth', + 'surfaceNormals', + 'reverseNormals', + } + + warpCard = { + 'lengthDivisions', + 'dynamicDivisions', + 'widthDivisions', + 'Orientation', + 'Twist', + 'invTwist', + 'twistCurveFrame', + 'Width', + 'Taper', + 'widthCurveFrame', + 'LengthLock', + 'Length', + 'Offset', + 'Profile', + 'profileCurveGraph', + 'curveRefine', + 'autoRefine', + 'curveSmooth', + 'samplingAccuracy', + 'surfaceNormals', + 'reverseNormals', + 'Magnitude' + } + + warpTube = { + 'lengthDivisions', + 'dynamicDivisions', + 'widthDivisions', + 'Orientation', + 'Twist', + 'twistCurveFrame', + 'widthComboSlider', + 'Taper', + 'widthCurveFrame', + 'LengthLock', + 'Length', + 'Offset', + 'curveRefine', + 'autoRefine', + 'curveSmooth', + 'samplingAccuracy', + 'surfaceNormals', + 'reverseNormals', + 'Magnitude' + } + + bind = { + 'axisFrame', + 'editOrigObj', + 'Orientation', + 'twistCurveFrame', + 'Width', + 'widthCurveFrame', + 'LengthLock', + 'Length', + 'Offset', + 'curveRefine', + 'autoRefine', + 'curveSmooth', + 'samplingAccuracy', + 'reverseNormals', + 'Magnitude' + } + + # Set of all controls + allControls = extrudeCard \ + | extrudeTube \ + | warpCard \ + | warpTube \ + | bind + + def __init__(self, name): + self.name = name + + def updateUI(self): + """Updates all sliders and buttons in Curve Control Window and Connect Graphs""" + if not mc.workspaceControl(CURVE_CONTROL_NAME, q=1, ex=1): + return + + # Update advanced visibility window + advancedVisibility.updateUI() + + # Apply geometry highlight if enabled + if mc.optionVar(q='GSCT_GeometryHighlightEnabled'): + utils.noUndo(advancedVisibility.geoHighlight)() + + # Meshes Section + meshSel = mc.filterExpand(mc.ls(o=1, sl=1), sm=12) + + if meshSel: + WIDGETS['orientToNormalsFrame'].setVisible(True) + else: + WIDGETS['orientToNormalsFrame'].setVisible(False) + + # Curves Section + sel = mc.filterExpand(mc.ls(o=1, sl=1), sm=9) + if not sel: + sel = mc.ls(o=1, hl=1) + + if len(sel) == 0: + self.hideControls() + return + + curve = sel[-1] # Use only last curve to update the UI + + if mc.nodeType(curve) == 'nurbsCurve': + rel = mc.listRelatives(curve, p=1, pa=1) + curve = rel[0] + + # Get all attributes from the curve + self.sliderAttr = attributes.getAttr(curve) + + if 'Orientation' not in self.sliderAttr: + self.hideControls() + return + + if not mc.connectionInfo(curve + '.Orientation', isSource=1): + self.hideControls() + return + + # Enable orient to normals frame (if curves are selected but not the geo) + WIDGETS['orientToNormalsFrame'].setVisible(True) + + # Disable prompt + WIDGETS['selectCurvesPrompt'].setVisible(False) + + # Enable header and footer + WIDGETS['gsCurveControlHeader'].setEnabled(True) + WIDGETS['resetControlSliders'].setVisible(True) + + # Update Geometry Color and Layer Number + try: + layer = mc.ls(mc.listConnections(selectPart(2, True, curve)[0], s=1, d=0), et='displayLayer')[0] + + rgb = utils.getAttr(layer, 'overrideColorRGB') + + ind = int(re.findall(r'\d+', layer)[0]) + + # If RGB is zero, check the storage node + if rgb == [(0, 0, 0)]: + colorDict = toggleColor.readColorDict() + if ind in colorDict: + rgb = [colorDict[ind]] + WIDGETS['gsColorPicker'].setRGBColors(*rgb) + WIDGETS['gsLayerSelector'].setCurrentIndex(ind) + except Exception as e: + WIDGETS['gsLayerSelector'].setCurrentIndex(0) + WIDGETS['gsColorPicker'].setRGBColors([0, 0, 0]) + LOGGER.exception(e) + + # Update Curve Color + try: + shape = mc.ls(curve, dag=1, s=1)[0] + if mc.getAttr(shape + '.overrideEnabled'): + curveRGB = utils.getAttr(shape, 'overrideColorRGB') + WIDGETS['gsCurveColorPicker'].setRGBColors(*curveRGB) + else: + WIDGETS['gsCurveColorPicker'].setRGBColors([0, 0, 0]) + except Exception as e: + WIDGETS['gsCurveColorPicker'].setRGBColors([0, 0, 0]) + LOGGER.exception(e) + + # Update Text Field + try: + WIDGETS['selectedObjectName'].setText(selectPart(0, True, curve)[0]) + except BaseException: + WIDGETS['selectedObjectName'].setText('') + + # Set the dynamic divisions and auto refine false by default + WIDGETS['dynamicDivisions'].setChecked(False) + WIDGETS['autoRefine'].setChecked(False) + WIDGETS['curveRefine'].setEnabled(True) + WIDGETS['curveSmooth'].setEnabled(True) + + # Update main sliders and checkboxes + for attr in self.sliderAttr: + if attr == 'dynamicDivisions' and attr in WIDGETS: + WIDGETS['dynamicDivisions'].setChecked(bool(mc.getAttr(curve + '.dynamicDivisions'))) + continue + + # Set "Other" frame visibility + if attr == 'curveRefine' and attr in WIDGETS: + WIDGETS['otherFrame'].setVisible(True) + + # Update Auto-Refine, Curve Refine and Curve Smooth sliders and toggles + if attr == 'autoRefine' and attr in WIDGETS: + WIDGETS['autoRefine'].setChecked(bool(mc.getAttr(curve + '.autoRefine'))) + WIDGETS['curveRefine'].setEnabled(not bool(mc.getAttr(curve + '.autoRefine'))) + WIDGETS['curveSmooth'].setEnabled(not bool(mc.getAttr(curve + '.autoRefine'))) + continue + + # Update Axis switch and Edit Original Obj. checkbox + if attr == 'Axis' and attr in WIDGETS: + WIDGETS['Axis'].button(self.sliderAttr['Axis']).setChecked(True) + rebuildCurve = mc.listConnections(curve + '.curveSmooth', scn=1)[0] + warp = mc.listConnections(rebuildCurve + '.outputCurve', scn=1) + WIDGETS['editOrigObj'].setChecked(not mc.getAttr(warp[0] + '.envelope')) + continue + + # Update Reverse Normals Attribute + if attr == 'reverseNormals' and attr in WIDGETS: + WIDGETS['reverseNormals'].setValue(not self.sliderAttr[attr]) + continue + + # Update Flip UV Attribute + if attr == 'flipUV' and attr in WIDGETS: + WIDGETS['flipUV'].setValue(not self.sliderAttr[attr]) + continue + + # Update the rest of the attributes + if attr in WIDGETS: + WIDGETS[attr].setValue(self.sliderAttr[attr]) + + # Check for legacy UV attributes + if 'rotateTipUV' in self.sliderAttr: + WIDGETS['rotateTipUV'].setVisible(True) + WIDGETS['rotateRootUV'].setVisible(True) + else: + WIDGETS['rotateTipUV'].setVisible(False) + WIDGETS['rotateRootUV'].setVisible(False) + + # Update profile graph + if mc.attributeQuery('latticeMessage', n=curve, ex=1): + lattice = mc.listConnections(curve + '.latticeMessage') + if lattice: + latticeDiv = mc.getAttr(lattice[0] + '.sDivisions') + if latticeDiv: + currentPoints = [] + if lattice: + for i in range(latticeDiv): + currentPoints.append(mc.pointPosition(lattice[0] + '.pt[%s][1][0]' % (i), l=1)) + + newPoints = [] + for i in range(latticeDiv): + x = mt.lerp(currentPoints[i][0], 1, 0, 0.5, -0.5) + y = mt.lerp(currentPoints[i][1], 1, 0, 1.5, -0.5) + newPoints.append((x, y)) + + graphPoints = '' + + for i in range(latticeDiv): + graphPoints += '%s,%s,' % (newPoints[i][0], newPoints[i][1]) + + WIDGETS['profileCurve'].setGraph(graphPoints) + if mc.workspaceControl('GSCT_ProfileGraphPopOut', ex=1) and 'profileCurve_large' in WIDGETS: + WIDGETS['profileCurve_large'].setGraph(graphPoints) + + # Connect Warp Graphs + if 'Length' in self.sliderAttr: + rebuildCurve = mc.listConnections(curve + '.curveSmooth', scn=1) + if rebuildCurve: + warp = mc.listConnections(rebuildCurve[0] + '.outputCurve', scn=1) + if warp: + WIDGETS['twistCurve'].connectGraph(warp[0] + '.twistCurve') + WIDGETS['scaleCurve'].connectGraph(warp[0] + '.scaleCurve') + + # Connect Large Graphs if exist + if mc.workspaceControl("GSCT_TwistGraphPopOut", ex=1) and 'twistCurve_large' in WIDGETS: + if warp: + WIDGETS['twistCurve_large'].connectGraph(warp[0] + '.twistCurve') + WIDGETS['Magnitude_large'].setValue(self.sliderAttr['Magnitude']) + if mc.workspaceControl("GSCT_WidthGraphPopOut", ex=1) and 'scaleCurve_large' in WIDGETS: + if warp: + WIDGETS['scaleCurve_large'].connectGraph(warp[0] + '.scaleCurve') + + # Show/Hide current controls + currentControls = False + uvs = False + solidify = False + + if 'lengthDivisions' in self.sliderAttr and 'LengthLock' not in self.sliderAttr: + if 'Width' in self.sliderAttr: + currentControls = self.extrudeCard + else: + currentControls = self.extrudeTube + elif 'lengthDivisions' in self.sliderAttr and 'LengthLock' in self.sliderAttr: + if 'Width' in self.sliderAttr: + currentControls = self.warpCard + else: + currentControls = self.warpTube + elif 'AxisFlip' in self.sliderAttr: + currentControls = self.bind + else: + self.hideControls() + return 0 + + if 'solidify' in self.sliderAttr: + solidify = True + + if 'moveU' in self.sliderAttr: + uvs = True + + self.changeControls(currentControls, uvs, solidify) + + def changeControls(self, currentControls, uv=True, solidify=True): + if not currentControls: + self.hideControls() + return 0 + + hiddenControls = self.allControls - currentControls + for key in currentControls: + if key in WIDGETS: + WIDGETS[key].setVisible(True) + for key in hiddenControls: + if key in WIDGETS: + WIDGETS[key].setVisible(False) + + WIDGETS['UVFrame'].setVisible(True if uv else False) + WIDGETS['solidifyFrame'].setVisible(True if solidify else False) + + def hideControls(self): + for key in self.allControls: + if key in WIDGETS: + WIDGETS[key].setHidden(True) + WIDGETS['gsCurveControlHeader'].setEnabled(False) + WIDGETS['gsColorPicker'].setRGBColors([0, 0, 0]) + WIDGETS['gsCurveColorPicker'].setRGBColors([0, 0, 0]) + WIDGETS['gsLayerSelector'].setCurrentIndex(0) + WIDGETS['selectedObjectName'].setText('') + WIDGETS['otherFrame'].setVisible(False) + WIDGETS['UVFrame'].setVisible(False) + WIDGETS['solidifyFrame'].setVisible(False) + WIDGETS['resetControlSliders'].setVisible(False) + WIDGETS['selectCurvesPrompt'].setVisible(True) + + def selectOriginalObjects(self): + """Selects the curves that were attached to the bind curve""" + sel = mc.ls(sl=1, tr=1) + if not sel: + return + selectionList = [] + for curve in sel: + if not mc.attributeQuery('gsmessage', n=curve, ex=1): + continue + childCards = mc.listConnections(curve + '.gsmessage', d=1, s=0, t='transform', scn=1) + if not childCards: + continue + selectionList += childCards + if selectionList: + mc.select(selectionList, r=1) + + def editOriginalObjects(self): + """Temporarily unhides the original objects from the bind curve""" + sel = mc.ls(sl=1, tr=1) + if not sel: + return + checkBox = WIDGETS['editOrigObj'].isChecked() + axisFlip = WIDGETS['AxisFlip'].isChecked() + for curve in sel: + try: + getAttr = attributes.getAttr(curve) + if 'AxisFlip' in getAttr: + warpNode = mc.listConnections(curve + '.Width')[0] + message = mc.listConnections(curve + '.gsmessage') + if message: + grp = mc.listRelatives(mc.listRelatives(message[0], p=1, pa=1), p=1, pa=1) + if checkBox: + if grp: + mc.setAttr(warpNode + '.envelope', 0) + mc.setAttr(grp[0] + '.visibility', 1) + else: + if grp: + mc.setAttr(warpNode + '.envelope', 1) + mc.setAttr(grp[0] + '.visibility', 0) + if axisFlip and mc.attributeQuery('reverseNormals', n=curve, ex=1): + mc.setAttr(curve + '.reverseNormals', not mc.getAttr(curve + '.reverseNormals')) + elif not message and 'AxisFlip' in getAttr: + if checkBox: + mc.setAttr(warpNode + '.envelope', 0) + else: + mc.setAttr(warpNode + '.envelope', 1) + if axisFlip and mc.attributeQuery('reverseNormals', n=curve, ex=1): + mc.setAttr(curve + '.reverseNormals', not mc.getAttr(curve + '.reverseNormals')) + self.updateUI() + except Exception as e: + LOGGER.exception(e) + + +curveControlUI = CurveControlUI("curveControlUI") + + +class ImportExport: + """ Import and Export curves """ + + def __init__(self, name): + self.name = name + + def exportCurves(self): + """Export selected curves into a .curves or .ma file to import later""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, o=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one curve to export') + return + mc.select(sel) + sel = selectPart(0, True) + if not sel: + return + mc.select(sel, r=1) + mc.select(hi=1) + filters = "GS Curves (.curves) (*.curves);;Maya ASCII (.ma) (*.ma)" + dialog = mc.fileDialog2(fileFilter=filters, dialogStyle=2) + if not dialog: + return + mc.file(dialog, force=1, options="v=0;", typ="mayaAscii", pr=1, es=1, de=0) + + def importCurves(self): + """Import curves from .curve or .ma file that was exported using exportCurves function""" + filters = "GS Curves (.curves) (*.curves);;Maya ASCII (.ma) (*.ma)" + dialog = mc.fileDialog2(fileFilter=filters, fm=1, dialogStyle=2, okc='Open') + if not dialog: + return + nodes = mc.file(dialog, i=1, dns=1, rnn=1) + curveGroups = [] + geoGroups = [] + instGroups = [] + collection = None + if getOption('importIntoANewCollection') and getOption('showLayerCollectionsMenu'): + collection = layerCollections.createImportedCurvesCollection() + for node in nodes: + if 'curveGrp_' in node and '_Curve' in node: + curveGroups.append(node) + if 'curveGrp_' in node and '_Geo' in node: + geoGroups.append(node) + if 'curveGrp_' in node and '_Inst' in node: + instGroups.append(node) + for group in curveGroups: + split = group.split("_") + grpCurves = mc.editDisplayLayerMembers(group, q=1, fn=1) + try: + index = int(split[1]) if len(split) == 3 else int(split[2]) + except ValueError: + error = 'Failed to extract layer ID from layer "{}". Display Layer name is corrupted. Please delete corrupted layers and curves.'.format(group) + MESSAGE.warning(error) + continue + curveAddToLayer(index, inputCurves=grpCurves, targetCollection=collection) + utils.fixDuplicateNames(nodes) + # Check if there are some empty groups left after the import + grps = curveGroups + geoGroups + instGroups + for grp in grps: + if mc.objExists(grp) and not mc.editDisplayLayerMembers(grp, q=1, fn=1): + mc.delete(grp) + + +importExport = ImportExport("importExport") + + +class LayerCollections: + + def __init__(self, name): + self.name = name + self.copyIndex = None + + ### INTERFACE METHODS ### + + def toggleLayerCollectionsWidget(self): + """Toggles the visibility of the layer collections widget""" + if getOption('showLayerCollectionsMenu'): + WIDGETS['LayerCollectionsLayout'].setHidden(False) + else: + WIDGETS['LayerCollectionsLayout'].setHidden(True) + WIDGETS['layerCollectionsComboBox'].setCurrentIndex(0) + WIDGETS['LayerLayout'].update() + WIDGETS['LayerLayout'].parentWidget().update() + + ### PARAMETERS UPDATES ### + + def updateDefaultLayerNode(self): + """Updates layer collection dict attribute on defaultLayer node based on collections combo box items""" + if not mc.objExists('defaultLayer'): + MESSAGE.warning("Default layer not found!") + return + if not mc.attributeQuery('gsCollectionsDict', n='defaultLayer', ex=1): + mc.addAttr('defaultLayer', ln='gsCollectionsDict', dt='string') + comboBox = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + collectionsDict = {} + for i in range(1, comboBox.count()): + collectionsDict.update({i: comboBox.itemText(i)}) + mc.setAttr('defaultLayer.gsCollectionsDict', str(collectionsDict), typ='string') + + def updateCollectionNames(self): + """Updates all the active collection layers to have a proper name based gsCollectionName attr on curves""" + collectionSet = utils.getCollectionsSet() + if not collectionSet: + return + allLayers = [x for x in mc.ls(typ='displayLayer') if ('curveGrp_' in x and '_Curve' in x)] + filteredLayers = [x for x in allLayers if len(x.split("_")) == 4] + for c in filteredLayers: + if not mc.objExists(c): + continue + if not mc.attributeQuery('gsCollectionName', n=c, ex=1): + mc.addAttr(c, ln='gsCollectionName', dt='string') + collectionID = int(c.split("_")[1]) + collectionName = WIDGETS['layerCollectionsComboBox'].itemText(collectionID) + mc.setAttr(c + '.gsCollectionName', collectionName, typ='string') + + ### FUNCTIONAL METHODS ### + + def createImportedCurvesCollection(self): + # type: () -> int + """Creates and Imported Curves collection and returns its ID""" + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + collectionId = layerDropdownMenu.findText("ImportedCurves") + if collectionId == -1: + layerDropdownMenu.addItem("ImportedCurves") + collectionId = layerDropdownMenu.count() - 1 + layerDropdownMenu.setCurrentIndex(collectionId) + return collectionId + + def createLayerCollection(self, exists=False): + # type: (bool) -> None + """Create a named layer collection via the plus button on the interface""" + msg = 'Name already exists.\nEnter unique name:' if exists else 'Enter Collection Name:' + result = mc.promptDialog( + title='New Collection', + message=msg, + button=['OK', 'Cancel'], + defaultButton='OK', + cancelButton='Cancel', + dismissString='Cancel' + ) + if result == 'OK': + name = mc.promptDialog(q=1, t=1) + else: + return + if not name: + MESSAGE.warningInView("Invalid Name!") + self.createLayerCollection() + return + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + if layerDropdownMenu.findText(name) != -1: + self.createLayerCollection(True) + return + layerDropdownMenu.addItem(name) + layerDropdownMenu.setCurrentIndex(layerDropdownMenu.count() - 1) + self.updateDefaultLayerNode() + updateMainUI() + + def deleteLayerCollection(self): + """Delete currently active layer collection and transfer all the layers to the current-1 collection""" + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + index = layerDropdownMenu.currentIndex() + if index == 0: + return + name = layerDropdownMenu.itemText(index) + result = mc.confirmDialog( + title='Delete Collection', + message='Delete collection "{}" (#{})?\nAll the layers will be transferred one collection up.'.format(name, index), + icn='information', + button=['OK', 'Cancel'], + defaultButton='OK', + cancelButton='Cancel', + dismissString='Cancel' + ) + if result != 'OK': + return + self.transferLayerCollection(index, index - 1) + layerDropdownMenu.setCurrentIndex(index - 1) + layerDropdownMenu.removeItem(index) + self.updateDefaultLayerNode() + count = layerDropdownMenu.count() + for i in range(1, count): + self.transferLayerCollection(index + i, index - 1 + i) + self.updateDefaultLayerNode() + updateMainUI() + + def transferLayerCollection(self, sourceIndex, targetIndex): + # type: (int, int, bool) -> None + """Transfers all the layers from source collection to the target collection""" + allLayers = [x for x in mc.ls(typ='displayLayer') if ('curveGrp_' in x and '_Curve' in x)] + sourceLayers = [] + for layer in allLayers: + s = layer.split('_') + if len(s) == 4 and int(s[1]) == sourceIndex: + sourceLayers.append(layer) + for layer in sourceLayers: + s = layer.split('_') + targetLayer = s[2] if len(s) == 4 else s[1] + curves = mc.editDisplayLayerMembers(layer, q=1, fn=1) + if not curves: + continue + curveAddToLayer(targetLayer=targetLayer, inputCurves=curves, targetCollection=targetIndex) + + def mergeUp(self): + """Transfer all the layers from the current collection up and shifts other collections""" + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = layerDropdownMenu.currentIndex() + count = layerDropdownMenu.count() + if currentIndex > 0: + upIndex = currentIndex - 1 + self.transferLayerCollection(currentIndex, upIndex) + layerDropdownMenu.setCurrentIndex(upIndex) + layerDropdownMenu.removeItem(currentIndex) + self.updateDefaultLayerNode() + for i in range(1, count): + self.transferLayerCollection(currentIndex + i, upIndex + i) + self.updateDefaultLayerNode() + updateMainUI() + else: + MESSAGE.warningInView('This is the first collection in the list.') + + def mergeDown(self): + """Transfer all the layers from the current collection down and shift all the collections""" + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = layerDropdownMenu.currentIndex() + count = layerDropdownMenu.count() + if currentIndex == 0: + MESSAGE.warningInView("Main collection can't be merged.") + return + if currentIndex == count - 1: + MESSAGE.warningInView('This is the last collection in the list.') + return + downIndex = currentIndex + 1 + self.transferLayerCollection(currentIndex, downIndex) + layerDropdownMenu.setCurrentIndex(downIndex) + layerDropdownMenu.removeItem(currentIndex) + self.updateDefaultLayerNode() + for i in range(1, count): + self.transferLayerCollection(currentIndex + i, currentIndex - 1 + i) + self.updateDefaultLayerNode() + updateMainUI() + + def moveDown(self): + """Moves current collection index down, rearranging the collection""" + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = layerDropdownMenu.currentIndex() + count = layerDropdownMenu.count() + if currentIndex == 0: + MESSAGE.warningInView("Main collection can't be moved.") + return + if currentIndex == count - 1: + MESSAGE.warningInView('This is the last collection in the list.') + return + currentName = layerDropdownMenu.itemText(currentIndex) + downIndex = currentIndex + 1 + downName = layerDropdownMenu.itemText(downIndex) + layerDropdownMenu.addItem('GS_TEMP_COLLECTION_DELETE_THIS') + layerDropdownMenu.setItemText(downIndex, currentName) + layerDropdownMenu.setItemText(currentIndex, downName) + self.transferLayerCollection(currentIndex, layerDropdownMenu.count() - 1) + self.transferLayerCollection(downIndex, currentIndex) + self.transferLayerCollection(layerDropdownMenu.count() - 1, downIndex) + layerDropdownMenu.removeItem(layerDropdownMenu.count() - 1) + self.updateDefaultLayerNode() + updateMainUI() + + def moveUp(self): + """Moves current collection index down, rearranging the collection""" + layerDropdownMenu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = layerDropdownMenu.currentIndex() + if currentIndex == 0 or currentIndex == 1: + MESSAGE.warningInView("Main collection can't be moved.") + return + currentName = layerDropdownMenu.itemText(currentIndex) + upIndex = currentIndex - 1 + upName = layerDropdownMenu.itemText(upIndex) + layerDropdownMenu.addItem('GS_TEMP_COLLECTION_DELETE_THIS') + layerDropdownMenu.setItemText(upIndex, currentName) + layerDropdownMenu.setItemText(currentIndex, upName) + self.transferLayerCollection(currentIndex, layerDropdownMenu.count() - 1) + self.transferLayerCollection(upIndex, currentIndex) + self.transferLayerCollection(layerDropdownMenu.count() - 1, upIndex) + layerDropdownMenu.removeItem(layerDropdownMenu.count() - 1) + self.updateDefaultLayerNode() + updateMainUI() + + def rename(self, exists=False): + menu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = menu.currentIndex() + if currentIndex == 0: + MESSAGE.warningInView("Main collection can't be renamed.") + return + msg = "Name already exists.\nEnter unique name:" if exists else "Enter New Name:" + result = mc.promptDialog( + title='Rename Collection', + message=msg, + button=['OK', 'Cancel'], + defaultButton='OK', + cancelButton='Cancel', + dismissString='Cancel' + ) + if result == 'OK': + name = mc.promptDialog(q=1, t=1) + else: + return + if not name: + MESSAGE.warningInView("Invalid Name!") + self.rename() + return + if menu.findText(name) != -1: + self.rename(True) + return + menu.setItemText(currentIndex, name) + self.updateDefaultLayerNode() + updateMainUI() + + def clear(self): + menu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = menu.currentIndex() + itemName = menu.itemText(currentIndex) + msg = 'Clearing collection "{}".\nAll the layers and curves in this collection will be deleted.\nAre you sure?'.format(itemName) + result = mc.confirmDialog( + title='Clear Collection', + message=msg, + icn='warning', + button=['OK', 'Cancel'], + defaultButton='OK', + cancelButton='Cancel', + dismissString='Cancel' + ) + if result == 'OK': + for i in range(80): + curveGrp, _, _ = utils.getFormattedLayerNames(currentIndex, i) + if not mc.objExists(curveGrp): + continue + contents = mc.editDisplayLayerMembers(curveGrp, q=1, fn=1) + if not contents: + continue + for curve in contents: + if not mc.objExists(curve): + continue + if mc.attributeQuery('gsmessage', ex=1, n=curve): + if mc.connectionInfo(curve + '.gsmessage', isDestination=1): + continue + parentGrp = mc.listRelatives(curve, p=1) + if parentGrp: + mc.delete(parentGrp) + self.updateDefaultLayerNode() + updateMainUI() + + def copy(self): + menu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = menu.currentIndex() + self.copyIndex = currentIndex + MESSAGE.warningInView('Layer collection "%s" added to buffer' % menu.itemText(currentIndex)) + + def paste(self): + if self.copyIndex is None: + return + menu = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentIndex = menu.currentIndex() + if self.copyIndex == currentIndex: + return + MESSAGE.warningInView('Pasting from collection "%s" to "%s"' + % (menu.itemText(self.copyIndex), menu.itemText(currentIndex))) + + allLayers = [x for x in mc.ls(typ='displayLayer') if ('curveGrp_' in x and '_Curve' in x)] + sourceLayers = [] + if self.copyIndex == 0: + sourceLayers = [x for x in allLayers if len(x.split("_")) == 3] + else: + for layer in allLayers: + s = layer.split("_") + if len(s) == 4 and int(s[1]) == self.copyIndex: + sourceLayers.append(layer) + for layer in sourceLayers: + s = layer.split("_") + targetLayer = s[2] if len(s) == 4 else s[1] + curves = mc.editDisplayLayerMembers(layer, q=1, fn=1) + if not curves: + continue + curves = list(duplicateCurve(customSel=curves)) + curveAddToLayer(targetLayer=targetLayer, inputCurves=curves, targetCollection=currentIndex) + self.updateDefaultLayerNode() + self.copyIndex = None + + +layerCollections = LayerCollections("layerCollections") + + +class AdvancedVisibility(): + + CACHED_GEO = [] + + def __init__(self, name): + self.name = name + + def geoHighlight(self): + """ Update geometry hilite based on the selected curve """ + # Check if window exists + if not mc.workspaceControl(CURVE_CONTROL_NAME, q=1, ex=1): + return + if not mc.optionVar(q="GSCT_GeometryHighlightEnabled"): + mc.hilite(self.CACHED_GEO, u=1) + self.CACHED_GEO = [] + WIDGETS['geometryHighlight'].setChecked(False) + return + else: + WIDGETS['geometryHighlight'].setChecked(True) + sel = mc.ls(sl=1, dag=1, typ='nurbsCurve') + if not sel: + sel = mc.ls(sl=1, o=1, typ='nurbsCurve') + if not sel: + sel = mc.ls(hl=1, dag=1, typ='nurbsCurve') + if sel: + geo = selectPart(2, True, sel) + if geo: + mc.hilite(geo) + self.CACHED_GEO = geo + else: + self.CACHED_GEO = [] + else: + try: + mc.hilite(self.CACHED_GEO, u=1) + except BaseException: + mc.hilite([], r=1) + self.CACHED_GEO = [] + + def geometryHighlightCommand(self): + mc.optionVar(iv=["GSCT_GeometryHighlightEnabled", not mc.optionVar(q='GSCT_GeometryHighlightEnabled')]) + self.geoHighlight() + + def createNode(self): + tr = mc.createNode('transform', n='GSCT_CurveTools_DrawManager', ss=1) + # Lock transforms for node + for attr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz', 'shearXY', 'shearXZ', 'shearYZ']: + mc.setAttr(tr + '.' + attr, l=1, k=0) + # Create rest of the network + mc.createNode('GSCT_CurveTools_DrawManagerNode', + n='GSCT_CurveTools_DrawManagerNode', p=tr, ss=1) + mc.setAttr(tr + ".useOutlinerColor", 1) + mc.setAttr(tr + ".outlinerColor", 0, 1, 0, typ="double3") + mc.reorder(tr, r=1) + self.applySettingsToNode() + + def updateUI(self): + """Updates UI on selection change""" + if not all(mc.getClassification("GSCT_CurveTools_DrawManagerNode")): # Check if node type exists + return + button = WIDGETS['curveHighlight'] + frame = WIDGETS['gsCurveHighlightFrame'] + nodes = mc.ls(typ='GSCT_CurveTools_DrawManagerNode') + if not nodes: + button.setChecked(False) + frame.setEnabled(False) + nodeFound = False + for node in nodes: + if mc.referenceQuery(node, inr=1): + mc.setAttr(node + '.nodeState', 1) + continue + nodeFound = True + if nodeFound: + WIDGETS['curveHighlight'].setChecked(True) + WIDGETS['gsCurveHighlightFrame'].setEnabled(True) + else: + WIDGETS['curveHighlight'].setChecked(False) + WIDGETS['gsCurveHighlightFrame'].setEnabled(False) + + def removeUnknownNodes(self): + """Finds all the nodes that were possibly left out from the previous session""" + sel = mc.ls(typ="unknownDag") + for n in sel: + if "GSCT_CurveTools_DrawManager" in n: + mc.delete(mc.listRelatives(n, p=1, pa=1)) + + def toggleCurveHighlightFromUI(self, hotkey=False): + """Toggles the nodes in the scene from UI""" + # Check if there are leftover nodes + self.removeUnknownNodes() + # Check if control exists + if 'curveHighlight' not in WIDGETS: + from . import ui + ui.curveControlWorkspace() + return + # Check for Maya version and find an appropriate plug-in to load + button = WIDGETS['curveHighlight'] # type: wrap.Button + pluginPath = utils.getFolder.plugins() + winPath = os.path.join(pluginPath, str(MAYA_VER), "cv_manip.mll") + fallBackPath = os.path.join(pluginPath, "cv_manip.py") + result = False + if OS != "mac": + result = utils.loadCustomPlugin(winPath) + else: + result = utils.loadCustomPlugin(fallBackPath) + # If loading fails, try to load the fallback (python) version instead + if not result: + finalResult = utils.loadCustomPlugin(fallBackPath) + if not finalResult: + MESSAGE.warning( + "Can't load cv_manip plug-in. Check your installation or contact the developer: george.sladkovsky@gmail.com") + button.setChecked(False) + return + # Check if node type exists + if not all(mc.getClassification("GSCT_CurveTools_DrawManagerNode")): + button.setChecked(False) + MESSAGE.warning( + "No compatible node types found. Please reinstall GS CurveTools or send a bug report: george.sladkovsky@gmail.com") + return + # Proceed with node creation/activation + nodes = mc.ls(typ='GSCT_CurveTools_DrawManagerNode') + if button.isChecked() if hotkey else not button.isChecked(): + # Delete nodes from scene if found + for node in nodes: + if mc.referenceQuery(node, inr=1): + mc.setAttr(node + '.nodeState', 1) + else: + mc.delete(mc.listRelatives(node, p=1, pa=1)) + else: + # Just create a new node + self.createNode() + # Check if locator visibility is enabled in the current view + try: + currentView = mc.playblast(ae=1) + mc.modelEditor(currentView, e=1, locators=1) + except BaseException: + pass + self.updateUI() + + # ---- Saving, loading and applying settings ---- + + def loadSettingsFromOptionVar(self): + """Load settings from optionVars and apply them to the interface and to the node""" + if not mc.workspaceControl(CURVE_CONTROL_NAME, q=1, ex=1): + return + # Load ints (bools) + WIDGETS['curveVisibility'].setChecked(mc.optionVar(q='GSCT_' + 'gsCurveVisibilityToggle')) + WIDGETS['hullVisibility'].setChecked(mc.optionVar(q='GSCT_' + 'gsHullVisibilityToggle')) + WIDGETS['lazyUpdate'].setChecked(mc.optionVar(q='GSCT_' + 'gsLazyUpdateToggle')) + WIDGETS['alwaysOnTop'].setChecked(mc.optionVar(q='GSCT_' + 'gsAlwaysOnTopToggle')) + WIDGETS['cvDistanceColor'].setChecked(mc.optionVar(q='GSCT_' + 'gsCVDistanceColor')) + WIDGETS['hullDistanceColor'].setChecked(mc.optionVar(q='GSCT_' + 'gsHullDistanceColor')) + WIDGETS['curveDistanceColor'].setChecked(mc.optionVar(q='GSCT_' + 'gsCurveDistanceColor')) + WIDGETS['CVocclusion'].setChecked(mc.optionVar(q='GSCT_' + 'gsEnableCVOcclusion')) + # Load Floats + mc.floatSliderGrp('gsPointSizeSlider', e=1, v=(mc.optionVar(q='GSCT_' + 'gsPointSizeSlider'))) + mc.floatSliderGrp('gsCurveWidthSlider', e=1, v=(mc.optionVar(q='GSCT_' + 'gsCurveWidthSlider'))) + mc.floatSliderGrp('gsHullWidthSlider', e=1, v=(mc.optionVar(q='GSCT_' + 'gsHullWidthSlider'))) + WIDGETS['gsDeselectedCVAlpha'].setValue(mc.optionVar(q='GSCT_' + 'gsDeselectedCVAlpha')) + WIDGETS['gsSelectedCVAlpha'].setValue(mc.optionVar(q='GSCT_' + 'gsSelectedCVAlpha')) + WIDGETS['gsCurveHighlightAlpha'].setValue(mc.optionVar(q='GSCT_' + 'gsCurveHighlightAlpha')) + WIDGETS['gsHullHighlightAlpha'].setValue(mc.optionVar(q='GSCT_' + 'gsHullHighlightAlpha')) + WIDGETS['gsDistanceColorMinValue'].setValue(mc.optionVar(q='GSCT_' + 'gsDistanceColorMinValue')) + WIDGETS['gsDistanceColorMaxValue'].setValue(mc.optionVar(q='GSCT_' + 'gsDistanceColorMaxValue')) + # Load Colors + WIDGETS['gsDeselectedCVColor'].setRGBColors(eval(mc.optionVar(q='GSCT_' + 'gsDeselectedCVColor'))) + WIDGETS['gsSelectedCVColor'].setRGBColors(eval(mc.optionVar(q='GSCT_' + 'gsSelectedCVColor'))) + WIDGETS['gsCurveHighlightColor'].setRGBColors(eval(mc.optionVar(q='GSCT_' + 'gsCurveHighlightColor'))) + WIDGETS['gsHullHighlightColor'].setRGBColors(eval(mc.optionVar(q='GSCT_' + 'gsHullHighlightColor'))) + # String Values + WIDGETS['gsOccluderMeshName'].setText(mc.optionVar(q='GSCT_' + 'gsOccluderMeshName')) + self.applySettingsToNode() + + def saveSettingsFromUI(self): + """Save settings from UI to optionVars""" + mc.optionVar(fv=['GSCT_' + 'gsPointSizeSlider', mc.floatSliderGrp('gsPointSizeSlider', q=1, v=1)]) + mc.optionVar(fv=['GSCT_' + 'gsCurveWidthSlider', mc.floatSliderGrp('gsCurveWidthSlider', q=1, v=1)]) + mc.optionVar(fv=['GSCT_' + 'gsHullWidthSlider', mc.floatSliderGrp('gsHullWidthSlider', q=1, v=1)]) + mc.optionVar(sv=['GSCT_' + 'gsDeselectedCVColor', str(WIDGETS["gsDeselectedCVColor"].getRGBColors())]) + mc.optionVar(fv=['GSCT_' + 'gsDeselectedCVAlpha', WIDGETS['gsDeselectedCVAlpha'].getValue()]) + mc.optionVar(sv=['GSCT_' + 'gsSelectedCVColor', str(WIDGETS["gsSelectedCVColor"].getRGBColors())]) + mc.optionVar(fv=['GSCT_' + 'gsSelectedCVAlpha', WIDGETS['gsSelectedCVAlpha'].getValue()]) + mc.optionVar(sv=['GSCT_' + 'gsCurveHighlightColor', str(WIDGETS["gsCurveHighlightColor"].getRGBColors())]) + mc.optionVar(fv=['GSCT_' + 'gsCurveHighlightAlpha', WIDGETS['gsCurveHighlightAlpha'].getValue()]) + mc.optionVar(sv=['GSCT_' + 'gsHullHighlightColor', str(WIDGETS["gsHullHighlightColor"].getRGBColors())]) + mc.optionVar(fv=['GSCT_' + 'gsHullHighlightAlpha', WIDGETS['gsHullHighlightAlpha'].getValue()]) + mc.optionVar(iv=['GSCT_' + 'gsCurveVisibilityToggle', WIDGETS['curveVisibility'].isChecked()]) + mc.optionVar(iv=['GSCT_' + 'gsHullVisibilityToggle', WIDGETS['hullVisibility'].isChecked()]) + mc.optionVar(iv=['GSCT_' + 'gsLazyUpdateToggle', WIDGETS['lazyUpdate'].isChecked()]) + mc.optionVar(iv=['GSCT_' + 'gsAlwaysOnTopToggle', WIDGETS['alwaysOnTop'].isChecked()]) + mc.optionVar(iv=['GSCT_' + 'gsCVDistanceColor', WIDGETS['cvDistanceColor'].isChecked()]) + mc.optionVar(iv=['GSCT_' + 'gsHullDistanceColor', WIDGETS['hullDistanceColor'].isChecked()]) + mc.optionVar(iv=['GSCT_' + 'gsCurveDistanceColor', WIDGETS['curveDistanceColor'].isChecked()]) + mc.optionVar(fv=['GSCT_' + 'gsDistanceColorMinValue', WIDGETS['gsDistanceColorMinValue'].getValue()]) + mc.optionVar(fv=['GSCT_' + 'gsDistanceColorMaxValue', WIDGETS['gsDistanceColorMaxValue'].getValue()]) + mc.optionVar(iv=['GSCT_' + 'gsEnableCVOcclusion', WIDGETS['CVocclusion'].isChecked()]) + mc.optionVar(sv=['GSCT_' + 'gsOccluderMeshName', str(WIDGETS["gsOccluderMeshName"].text())]) + + def applySettingsToNode(self): + """Applies user settings from the UI to created node. A bit slow, but works for this.""" + if not all(mc.getClassification("GSCT_CurveTools_DrawManagerNode")): # Check if node type exists + return + nodes = mc.ls(typ='GSCT_CurveTools_DrawManagerNode') + for node in nodes: + if mc.referenceQuery(node, inr=1): + continue + mc.setAttr(node + '.pointSize', float(mc.floatSliderGrp('gsPointSizeSlider', q=1, v=1))) + mc.setAttr(node + '.lineWidth', float(mc.floatSliderGrp('gsCurveWidthSlider', q=1, v=1))) + mc.setAttr(node + '.hullWidth', float(mc.floatSliderGrp('gsHullWidthSlider', q=1, v=1))) + dpc_r, dpc_g, dpc_b = WIDGETS["gsDeselectedCVColor"].getRGBColors() + mc.setAttr(node + '.deselectedPointColor', dpc_r, dpc_g, dpc_b, typ='double3') + mc.setAttr(node + '.deselectedPointAlpha', WIDGETS['gsDeselectedCVAlpha'].getValue()) + spc_r, spc_g, spc_b = WIDGETS["gsSelectedCVColor"].getRGBColors() + mc.setAttr(node + '.selectedPointColor', spc_r, spc_g, spc_b, typ='double3') + mc.setAttr(node + '.selectedPointAlpha', WIDGETS['gsSelectedCVAlpha'].getValue()) + crvc_r, crvc_g, crvc_b = WIDGETS["gsCurveHighlightColor"].getRGBColors() + mc.setAttr(node + '.curveColor', crvc_r, crvc_g, crvc_b, typ='double3') + mc.setAttr(node + '.curveAlpha', WIDGETS['gsCurveHighlightAlpha'].getValue()) + hc_r, hc_g, hc_b = WIDGETS["gsHullHighlightColor"].getRGBColors() + mc.setAttr(node + '.hullColor', hc_r, hc_g, hc_b, typ='double3') + mc.setAttr(node + '.hullAlpha', WIDGETS['gsHullHighlightAlpha'].getValue()) + mc.setAttr(node + '.showCurve', WIDGETS['curveVisibility'].isChecked()) + mc.setAttr(node + '.showHull', WIDGETS['hullVisibility'].isChecked()) + mc.setAttr(node + '.lazyUpdate', WIDGETS['lazyUpdate'].isChecked()) + mc.setAttr(node + '.drawOnTop', WIDGETS['alwaysOnTop'].isChecked()) + mc.setAttr(node + '.useCVDistanceColor', WIDGETS['cvDistanceColor'].isChecked()) + mc.setAttr(node + '.useHullDistanceColor', WIDGETS['hullDistanceColor'].isChecked()) + mc.setAttr(node + '.useCurveDistanceColor', WIDGETS['curveDistanceColor'].isChecked()) + mc.setAttr(node + '.distanceColorMin', WIDGETS['gsDistanceColorMinValue'].getValue()) + mc.setAttr(node + '.distanceColorMax', WIDGETS['gsDistanceColorMaxValue'].getValue()) + mc.setAttr(node + '.useCVOcclusion', WIDGETS['CVocclusion'].isChecked()) + mc.setAttr(node + '.occluderMeshName', WIDGETS['gsOccluderMeshName'].text(), typ='string') + + def selectOccluderFromScene(self): + mesh = mc.ls(sl=1, tr=1) + if not mesh: + return + for obj in mesh: + if mc.nodeType(mc.listRelatives(obj, c=1, pa=1)) != "mesh": + continue + WIDGETS['gsOccluderMeshName'].setText(obj) + break + else: + MESSAGE.warningInView("No compatible meshes selected. Select a mesh object.") + self.applySettingsToNode() + + +advancedVisibility = AdvancedVisibility("advancedVisibility") + + +### Layers, Interface and Other Updates ### + +def updateMainUI(clearLayerCollections=False): + """Updates main UI controls""" + + # --- Change Layer number --- + num = 20 + if 'layerRowsActionGroup' in WIDGETS: + checkbox = WIDGETS['layerRowsActionGroup'].checkedAction().objName + else: + LOGGER.warning("'layerRowsActionGroup' not be found") + return + checkboxNum = re.findall(r'\d+', checkbox)[0] + num = int(checkboxNum) * 10 + active = set(range(int(checkboxNum))) + allLayers = set(range(8)) + for a in active: + if 'layerRow%s' % a in WIDGETS: + WIDGETS['layerRow%s' % a].setHidden(False) + for a in (allLayers - active): + WIDGETS['layerRow%s' % a].setHidden(True) + WIDGETS['curveGrp0'].setNumberOfLayers(num) + WIDGETS['LayerLayout'].update() + WIDGETS['LayerLayout'].parentWidget().update() + + # --- Update Layer Collections --- + comboBox = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + if clearLayerCollections: + comboBox.clear() + if mc.objExists('defaultLayer') and mc.attributeQuery('gsCollectionsDict', n='defaultLayer', ex=1): + collectionsString = mc.getAttr('defaultLayer.gsCollectionsDict') + collectionsDict = dict(eval(collectionsString)) + for key in collectionsDict: + if comboBox.findText(collectionsDict[key]) == -1: + comboBox.insertItem(key, collectionsDict[key]) + if WIDGETS['layerCollectionsComboBox'].currentIndex() == 0: + WIDGETS['layerCollectionsMinus'].setEnabled(False) + else: + WIDGETS['layerCollectionsMinus'].setEnabled(True) + updateVisibilityBasedOnActiveCollection() + + # --- Update layer buttons --- + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + for layerID in range(80): + button = 'curveGrp%s' % layerID + buttonWidget = WIDGETS[button] + grpCurve, grpGeo, grpInst = utils.getFormattedLayerNames(collectionID, layerID) + if not mc.objExists(grpCurve): + buttonWidget.setStyle('empty') + continue + layers = mc.editDisplayLayerMembers(grpCurve, q=1, fn=1) + if mc.objExists(grpCurve) and mc.objExists(grpGeo) and mc.objExists(grpInst) and layers: + curve = mc.getAttr(grpCurve + '.visibility') + geo = mc.getAttr(grpGeo + '.visibility') + geoEdit = utils.getAttr(grpGeo, 'displayType') + if not curve and not geo: + buttonWidget.setStyle('hidden') # Hidden + elif (curve and geo and not geoEdit) or (not curve and geo and not geoEdit): + buttonWidget.setStyle('edit') # Active + Editable + elif curve and geo: + buttonWidget.setStyle('active') # Active + elif curve and not geo: + buttonWidget.setStyle('curve') # Curve only + elif geo and not curve: + buttonWidget.setStyle('geo') # Geo only + else: + buttonWidget.setStyle('empty') + else: + buttonWidget.setStyle('empty') + + # --- Update color mode button --- + if utils.getAttr('gsColorShaderStorageNode', 'colorApplied'): + WIDGETS['colorMode'].setChecked(True) + else: + WIDGETS['colorMode'].setChecked(False) + + # --- Update scale factor window --- + if mc.workspaceControl(SCALE_FACTOR_UI, q=1, ex=1) and \ + mc.workspaceControl(SCALE_FACTOR_UI, q=1, vis=1): + sel = mc.ls(sl=1, tr=1, typ='nurbsCurve') + try: + if sel and mc.attributeQuery('scaleFactor', n=sel[-1], ex=1): + scaleFactor = mc.getAttr(sel[-1] + '.scaleFactor') + if scaleFactor and scaleFactor >= 0.001: + WIDGETS['scaleFactorSelectedValue'].setText(str(scaleFactor)) + else: + WIDGETS['scaleFactorSelectedValue'].setText(str('####')) + except BaseException: + if 'scaleFactorSelectedValue' in WIDGETS: + WIDGETS['scaleFactorSelectedValue'].setText(str('####')) + + +def onSceneOpenedUpdateLayerCount(): + # Check max layer ID present in the scene + allLayers = mc.ls(typ="displayLayer") + filtered = [x for x in allLayers if 'curveGrp_' in x] + if not filtered: + return + layersIDs = [] + for layer in filtered: + split = layer.split("_") + try: + layerID = int(split[1]) if len(split) == 3 else int(split[2]) + except ValueError: + error = 'Failed to extract layer ID from layer "{}". Display Layer name is corrupted. Please delete corrupted layers and curves.'.format(layer) + MESSAGE.warning(error) + continue + layersIDs.append(layerID) + maxLayerID = max(layersIDs) + # Check currently active layer count on the interface + checkbox = WIDGETS['layerRowsActionGroup'].checkedAction().objName + checkboxNum = re.findall(r'\d+', checkbox)[0] + activeID = int(checkboxNum) * 10 + # Check the required layer count + availableIDs = [20, 30, 40, 60, 80] + availableIDs.append(maxLayerID + 1) + availableIDs.sort() + requiredID = availableIDs.index(maxLayerID + 1) + try: + requiredLayerNumber = availableIDs[requiredID + 1] + except Exception as e: + LOGGER.exception(e) + return + if activeID >= requiredLayerNumber: + return + WIDGETS['curveGrp0'].setNumberOfLayers(requiredLayerNumber) + formatControlName = str(requiredLayerNumber)[:1] + "layerRows" + WIDGETS[formatControlName].setChecked(True) + updateMainUI() + + +def saveOptions(): # Save options to optionVar + LOGGER.info("Saving options") + qtBooleans = [ + 'syncCurveColor', + 'colorizedRegroup', + + 'checkerPattern', + 'ignoreLastLayer', + 'syncOutlinerLayerVis', + 'keepCurveAttributes', + 'massBindOption', + 'boundCurvesFollowParent', + + 'bindDuplicatesCurves', + 'bindFlipUVs', + 'replacingCurveLayerSelection', + 'populateBlendAttributes', + 'useAutoRefineOnNewCurves', + 'flipUVsAfterMirror', + 'convertInstances', + + 'layerNumbersOnly', + '2layerRows', + '3layerRows', + '4layerRows', + '6layerRows', + '8layerRows', + 'warpSwitch', + 'enableTooltips', + 'colorOnlyDiffuse', + + 'showLayerCollectionsMenu', + 'importIntoANewCollection', + 'ignoreTemplateCollections', + 'groupTemplateCollections', + ] + for option in qtBooleans: + mc.optionVar(iv=['GSCT_' + option, WIDGETS[option].isChecked()]) + + # Editor colors + if 'UVEditorBGColorPicker' in WIDGETS: + mc.optionVar(sv=['GSCT_UVEditorBGColor', + str(utils.colorFrom1to255(WIDGETS['UVEditorBGColorPicker'].getRGBColors()))]) + mc.optionVar(sv=['GSCT_UVEditorGridColor', + str(utils.colorFrom1to255(WIDGETS['UVEditorGridColorPicker'].getRGBColors()))]) + mc.optionVar(sv=['GSCT_UVEditorFrameColor', + str(utils.colorFrom1to255(WIDGETS['UVEditorFrameColorPicker'].getRGBColors()))]) + mc.optionVar(sv=['GSCT_UVEditorUVCardFillColor', + str(utils.colorFrom1to255(WIDGETS['UVEditorUVCardFillColorPicker'].getRGBColors()))]) + mc.optionVar(sv=['GSCT_UVEditorUVFrameSelectedColor', + str(utils.colorFrom1to255(WIDGETS['UVEditorUVFrameSelectedColorPicker'].getRGBColors()))]) + mc.optionVar(sv=['GSCT_UVEditorUVFrameDeselectedColor', + str(utils.colorFrom1to255(WIDGETS['UVEditorUVFrameDeselectedColorPicker'].getRGBColors()))]) + + +def saveScaleFactor(windowName, deleteUI=True): + sliderValue = mc.floatSliderGrp('GSCT_scaleFactorSlider', q=1, v=1) + mc.optionVar(fv=('GSCT_globalScaleFactor', sliderValue)) + + # Check if scaleFactor node and scale factor attribute exists + if not mc.objExists('gsScaleFactorStorageNode'): + mc.scriptNode(n='gsScaleFactorStorageNode') + if not mc.attributeQuery('scaleFactor', n='gsScaleFactorStorageNode', ex=1): + mc.addAttr('gsScaleFactorStorageNode', ln='scaleFactor', at='double') + + mc.setAttr('gsScaleFactorStorageNode.scaleFactor', sliderValue) + if deleteUI: + mc.deleteUI(windowName) + + +def getScaleFactor(*_): + scaleFactor = 1.0 + if mc.objExists('gsScaleFactorStorageNode') and mc.attributeQuery('scaleFactor', n='gsScaleFactorStorageNode', ex=1): + scaleFactor = mc.getAttr('gsScaleFactorStorageNode.scaleFactor') + else: + scaleFactor = mc.optionVar(q=('GSCT_globalScaleFactor')) + if not scaleFactor or scaleFactor < 0.001: + MESSAGE.warning("Invalid scale factor. Reverting to default (1.0).") + scaleFactor = 1.0 + return scaleFactor + + +def getOption(name): + return mc.optionVar(q='GSCT_' + name) + + +def updateLayerThickness(): + """Updates thickness values for all Curves""" + saveOptions() + value = mc.optionVar(q='GSCT_globalCurveThickness') + layerCollectionsWidget = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + collectionCount = layerCollectionsWidget.count() + for collectionID in range(collectionCount): + collection = "%s_" % collectionID if collectionID > 0 else "" + for layerID in range(80): + grpCurve = 'curveGrp_%s%s_Curve' % (collection, layerID) + if not mc.objExists(grpCurve): + continue + curves = mc.editDisplayLayerMembers(grpCurve, q=1, fn=1) + if curves: + for curve in curves: + shape = mc.ls(curve, dag=1, s=1, l=1)[0] + mc.setAttr(shape + '.lineWidth', value) + curveControlUI.updateUI() + + +def setCurveThickness(curves): + """Sets curve thickness for provided curves""" + if not isinstance(curves, list): + curves = [curves] + value = mc.optionVar(q='GSCT_globalCurveThickness') + for crv in curves: + if isinstance(crv, tuple): + origShape = mc.ls(crv[0], dag=1, s=1)[0] + targetShape = mc.ls(crv[1], dag=1, s=1)[0] + mc.setAttr(targetShape + '.lineWidth', mc.getAttr(origShape + '.lineWidth')) + else: + shape = mc.ls(crv, dag=1, s=1)[0] + mc.setAttr(shape + '.lineWidth', value) + + +def layerClicked(i): + """Called when layer button is clicked""" + mod = utils.getMod() + if mod == 'Alt': + toggleLayerVisibility(i) + updateMainUI() + elif mod == 'Ctrl+Alt': + toggleObjVisibility(i, 1) + updateMainUI() + elif mod == 'Shift+Ctrl': + toggleObjVisibility(i, 0) + updateMainUI() + elif mod == 'Shift+Alt': + layersFilterToggle(False, False, "Ctrl") + toggleLayerVisibility(i) + updateMainUI() + elif mod == 'Shift+Ctrl+Alt': + alwaysOnTopToggleLayer(i) + else: + curveLayerSelectObj(i, -1) + + +def updateScaleFactorWindow(): + if not mc.workspaceControl(SCALE_FACTOR_UI, q=1, ex=1): + return + if not mc.workspaceControl(SCALE_FACTOR_UI, q=1, vis=1): + return + + sel = mc.ls(sl=1, tr=1, typ='nurbsCurve') + try: + if sel and mc.attributeQuery('scaleFactor', n=sel[-1], ex=1): + scaleFactor = mc.getAttr(sel[-1] + '.scaleFactor') + if scaleFactor and scaleFactor >= 0.001: + WIDGETS['scaleFactorSelectedValue'].setText(str(scaleFactor)) + else: + WIDGETS['scaleFactorSelectedValue'].setText(str('####')) + except BaseException: + if 'scaleFactorSelectedValue' in WIDGETS: + WIDGETS['scaleFactorSelectedValue'].setText(str('####')) + + +def clearLayers(collection=None): + # type: (str|int) -> None + """Removes unused layers in selected collection""" + c = '' + if collection: + c = '%s_' % collection + for i in range(80): + grpCurve = 'curveGrp_%s%s_Curve' % (c, i) + grpGeo = 'curveGrp_%s%s_Geo' % (c, i) + grpInst = 'curveGrp_%s%s_Inst' % (c, i) + if mc.objExists(grpCurve) and mc.objExists(grpGeo) and mc.objExists(grpInst): + curves = mc.editDisplayLayerMembers(grpCurve, q=1, fn=1) + geo = mc.editDisplayLayerMembers(grpGeo, q=1, fn=1) + instances = mc.editDisplayLayerMembers(grpInst, q=1, fn=1) + if not curves or not geo or not instances: + mc.delete(grpCurve) + mc.delete(grpGeo) + mc.delete(grpInst) + else: + if mc.objExists(grpCurve): + mc.delete(grpCurve) + if mc.objExists(grpGeo): + mc.delete(grpGeo) + if mc.objExists(grpInst): + mc.delete(grpInst) + + +def deleteUnusedLayers(checkButtons=True): + # type: (bool) -> None + """Removes unused layers in all collections""" + + collections = utils.getCollectionsSet() + for i in collections: + clearLayers(i) + clearLayers() + if checkButtons: + updateMainUI() + + +def resetSlider(): # Reset Rebuild Curve Slider to default values + mc.intSliderGrp('gsRebuildSlider', e=1, min=1, max=50, fmx=999, v=1) + + +def resetControlSliders(): # Reset Curve Control window Sliders to default values + sliders = curveControlUI.allControls + for control in sliders: + if control in WIDGETS: + slider = WIDGETS[control] + if "resetMinMax" in dir(slider) and slider.isVisible(): + slider.resetMinMax() + + +def resetWarpCurvesControls(curve): # Reset curve control falloffCurveAttr + if curve == 1: + mc.falloffCurveAttr('gsCurveControlTwistCurve', e=1, asString="0, 0.5, 0.333, 0.5, 0.667, 0.5, 1, 0.5") + elif curve == 2: + mc.falloffCurveAttr('gsCurveControlWidthFalloff', e=1, asString="0, 0.5, 0.333, 0.5, 0.667, 0.5, 1, 0.5") + + +def curveControlCheckBoxes(box): # Updates check boxes in Curve Control window + sel = mc.filterExpand(mc.ls(sl=1, fl=1, o=1), sm=9) + if not sel: + curveControlUI.updateUI() + return 0 + for obj in sel: + if box == 0: + if utils.attrExists(obj, 'reverseNormals'): + mc.setAttr(obj + '.reverseNormals', not WIDGETS['reverseNormals'].isChecked()) + elif box == 1: + if utils.attrExists(obj, 'reverseNormals'): + mc.setAttr(obj + '.solidify', WIDGETS['solidify'].isChecked()) + elif box == 2: + if utils.attrExists(obj, 'LengthLock'): + mc.setAttr(obj + '.LengthLock', WIDGETS['LengthLock'].isChecked()) + elif box == 3: + if utils.attrExists(obj, 'flipUV'): + mc.setAttr(obj + '.flipUV', not WIDGETS['flipUV'].isChecked()) + + +def layersFilterToggle(curve, geo, mod=None, hotkey=False, ignore=None): + # type: (bool, bool, str|None, bool, None|list[str]) -> None + """ + Toggles the visibility of the components for all layers (and optionally collections) + mod - allows to manually pass "Ctrl, Shift etc" hotkey + hotkey - indicates that the call is a hotkey and should ignore user modifier keys + ignore - list of ignored hotkey combinations + """ + + if mod is None and not hotkey: + mod = utils.getMod() + + # Handle ignored hotkeys + if ignore and mod and mod in ignore: # TODO: Fix this hotkey-ignore-mod madness + mod = None + + if mod and (mod in ["Shift", "Shift+Ctrl"]): + curve = False + geo = False + + # Handle the hotkey state + allCollections = False + if mod and "Ctrl" in mod: + allCollections = True + + # Handle last layer ignore + ignoreLastLayer = bool(getOption('ignoreLastLayer')) + + # Sync visibility with the outliner + outlinerSync = getOption('syncOutlinerLayerVis') + + # Check available collections + collectionIDs = [] + if allCollections: + collectionIDs.append('0') + collectionIDs += list(utils.getCollectionsSet()) + else: + collectionIDs.append(WIDGETS['layerCollectionsComboBox'].currentIndex()) + + # Check for Template Ignore + if getOption('ignoreTemplateCollections'): + collectionsWidget = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + currentCollection = collectionsWidget.currentIndex() + collectionIDs_copy = list(collectionIDs) + for collection in collectionIDs_copy: + if "template" in collectionsWidget.itemText(int(collection)).lower() and int(collection) != currentCollection: + collectionIDs.remove(collection) + + for collection in collectionIDs: + formattedCollectionID = utils.getFormattedCollectionByID(collection) + currentLayerNum = WIDGETS['curveGrp0'].LAYERS - 1 + for i in range(80): + if ignoreLastLayer and i == currentLayerNum: + continue + curvesGrp = 'curveGrp_%s%s_Curve' % (formattedCollectionID, i) + geoGrp = 'curveGrp_%s%s_Geo' % (formattedCollectionID, i) + instGrp = 'curveGrp_%s%s_Inst' % (formattedCollectionID, i) + try: + mc.setAttr(curvesGrp + '.visibility', curve) + if mc.layerButton(curvesGrp, q=1, ex=1): + mc.layerButton(curvesGrp, e=1, lv=curve) + except BaseException: + pass + try: + mc.setAttr(geoGrp + '.visibility', geo) + if mc.layerButton(geoGrp, q=1, ex=1): + mc.layerButton(geoGrp, e=1, lv=geo) + except BaseException: + pass + try: + mc.setAttr(instGrp + '.visibility', 0) + if mc.layerButton(instGrp, q=1, ex=1): + mc.layerButton(instGrp, e=1, lv=0) + except BaseException: + pass + if outlinerSync and mc.objExists(curvesGrp): + groups = [] + layerCurves = mc.editDisplayLayerMembers(curvesGrp, q=1, fn=1) + if layerCurves: + for i in layerCurves: + parent = mc.listRelatives(i, p=1, pa=1) + if parent: + groups += parent + if not geo and not curve: + for grp in groups: + mc.setAttr(grp + '.v', 0) + else: + for grp in groups: + mc.setAttr(grp + '.v', 1) + updateMainUI() + + +def applyAxis(axis): + sel = mc.filterExpand(mc.ls(sl=1, o=1), sm=9) + if not sel: + return 0 + for curve in sel: + if axis == -1: + if mc.attributeQuery('AxisFlip', n=curve, ex=1): + mc.setAttr(curve + '.AxisFlip', not mc.getAttr(curve + '.AxisFlip')) + if mc.attributeQuery('reverseNormals', n=curve, ex=1): + mc.setAttr(curve + '.reverseNormals', not mc.getAttr(curve + '.reverseNormals')) + curveControlUI.updateUI() + elif axis == 0: + if mc.attributeQuery('Axis', n=curve, ex=1): + mc.setAttr(curve + '.Axis', 0) + elif axis == 1: + if mc.attributeQuery('Axis', n=curve, ex=1): + mc.setAttr(curve + '.Axis', 1) + elif axis == 2: + if mc.attributeQuery('Axis', n=curve, ex=1): + mc.setAttr(curve + '.Axis', 2) + elif axis == 3: + if mc.attributeQuery('Axis', n=curve, ex=1): + mc.setAttr(curve + '.Axis', 3) + + +def changeLayerViaOptionMenu(): + layer = WIDGETS['gsLayerSelector'].currentIndex() + curveAddToLayer(layer) + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + + +def moveLayers(source, target): + # type: (str, str) -> None + """Moves the curves from layer to layer within the same collection (via MMB drag)""" + mc.undoInfo(ock=1, cn='layerDragDropOP') + try: + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + collection = '' + if collectionID: + collection = '%s_' % collectionID + sourceGrp = re.findall(r'\d+', source)[0] + targetGrp = re.findall(r'\d+', target)[0] + curves = mc.editDisplayLayerMembers('curveGrp_%s%s_Curve' % (collection, sourceGrp), q=1, fn=1) + if utils.getMod() == 'Shift': + customSel = mc.editDisplayLayerMembers('curveGrp_%s%s_Curve' % (collection, sourceGrp), q=1, fn=1) + mc.select(customSel, r=1) + duplicateCurve() + curveAddToLayer(targetGrp) + else: + curveAddToLayer(targetGrp, sourceGrp) + if curves: + if mc.objExists('gsColorShaderStorageNode'): + if mc.getAttr('gsColorShaderStorageNode.colorApplied'): + toggleColor.updateColors() + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + except BaseException: + pass + mc.undoInfo(cck=1) + + +### General Functions ### + +def subdivideCurve(hk=None): + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, o=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one compatible card or tube curve.') + return + finalCurves = [] + for obj in sel: + if mc.attributeQuery('Axis', n=obj, ex=1): + MESSAGE.warning('%s is not compatible with Subdivide Curve command. Skipped.' % obj) + continue + geo = selectPart(2, True, obj) + geo = mc.duplicate(geo) + mc.sets(geo, forceElement='initialShadingGroup', nw=1) # set to default material to avoid errors + subd = mc.polyToSubdiv(geo, ch=0) + surface = mc.subdToNurbs(subd, ch=0) + nurbs = mc.listRelatives(surface, c=1, pa=1) + nurbs = mc.parent(nurbs, w=1) + mc.rebuildSurface(nurbs, ch=0, rpo=1, rt=6, end=1, kr=1, kcp=0, kc=0, su=16, du=0, sv=5, dv=0, tol=0.01, fr=0, + dir=2) + mc.delete(geo, surface, subd) + spansU, spansV = mc.getAttr(nurbs[0] + '.spansUV')[0] + span = 'v' if spansU > spansV else 'u' + attrs = attributes.getAttr(obj) + curves = [] + num = mc.intSliderGrp('gsCurvesSlider', q=1, v=1) + for i in range(0, num): + x = (float(i) + 1.0) / (float(num) + 1.0) + if 'WidthX' in attrs: + x = float(i) / float(num) + curves.append(mc.duplicateCurve(nurbs[0] + '.%sn[%s]' % (span, x))[0]) + widthValues = {1: 1, 2: 0.666, 3: 0.5, 4: 0.4, 5: 0.333, 6: 0.28, 7: 0.24, 8: 0.213, 9: 0.19, 10: 0.166} + if 'Width' in attrs: + attrs['Width'] = attrs['Width'] * widthValues[num] + attrs['Profile'] = attrs['Profile'] / num + elif 'WidthX' in attrs: + attrs['WidthX'] = attrs['WidthX'] * widthValues[num] + attrs['WidthZ'] = attrs['WidthZ'] * widthValues[num] + mc.delete(nurbs) + smoothCurve(curves) + for curve in curves: + mc.rebuildCurve(curve, kr=2, s=mc.getAttr(obj + '.spans')) + if 'Profile' in attrs: + mc.reverseCurve(curve) + targetPoints = mc.getAttr(curve + '.cp[:]') + newCurve = duplicateCurve(customSel=[obj])[0] + finalCurves.append(newCurve) + for p in range(0, len(targetPoints)): + mc.xform('%s.cp[%s]' % (newCurve, p), t=targetPoints[p], wd=1, ws=1) + attributes.setAttr(newCurve, attrs) + mc.delete(curve) + modifier = True if hk == 2 or (hk is None and utils.getMod() == 'Shift') else False + if not modifier: + mc.delete(selectPart(0, True, obj)) + mc.select(finalCurves) + if finalCurves: + resetCurvePivotPoint() + + +def cardToCurve(): + """Converts selected geo card to curve or (optionally) Curve Card""" + sel = mc.filterExpand(mc.ls(sl=1, tr=1, fl=1), sm=12) + if not sel: + sel = mc.filterExpand(mc.ls(hl=1, fl=1), sm=12) + if not sel: + MESSAGE.warningInView('Select a compatible one sided geometry to convert.') + return + finalSel = [] + progress = utils.ProgressBar("Converting cards to curves...", len(sel)) + options = eval(mc.optionVar(q='GSCT_CardToCurveOptions')) + + def getConvertOption(name): + return options[name] if name in options else True + + currentUnit = mc.currentUnit(q=1, linear=1) + if currentUnit != "cm" and getConvertOption('gsCardToCurve_profile'): + mc.confirmDialog( + title="Unit Warning", + message="Card to Curve 'Profile' matching is only supported with centimeters (cm) as scene units.\n\n" + + "You are using '{}'.\n\n".format(currentUnit) + + "Disable Profile Attribute matching or switch to centimiters in Maya Settings." + ) + progress.end() + return + + for obj in sel: + if progress.tick(1): + break + allFaces = "{}.f[*]".format(obj) + perimeterEdges = mc.polyListComponentConversion(allFaces, ff=1, te=1, bo=1) + perimeterVerts = mc.ls(mc.polyListComponentConversion(perimeterEdges, fe=1, tv=1), fl=1) + cornerVerts = [] + for vert in perimeterVerts: + expandedEdge = mc.ls(mc.polyListComponentConversion(vert, fv=1, te=1), fl=1) + if len(expandedEdge) == 2: + cornerVerts.append(vert) + + if not cornerVerts and len(cornerVerts) != 4: + MESSAGE.warningInView('Object "{}" is not a one-sided card. Skipping.'.format(obj)) + continue + + # Find closest point to cornerVerts[0] + closestPoint = None + om_distance = 0 + om_cornerVert = om.MFloatPoint(mc.pointPosition(cornerVerts[0], w=1)) + for i in range(1, len(cornerVerts)): + om_otherVert = om.MFloatPoint(mc.pointPosition(cornerVerts[i], w=1)) + om_distance_new = om_cornerVert.distanceTo(om_otherVert) + if i == 1 or om_distance_new < om_distance: + om_distance = om_distance_new + closestPoint = cornerVerts[i] + + # Closest corner verts + firstCornerVertPair = [cornerVerts[0], closestPoint] + secondCornerVertPair = list(set(cornerVerts) - set(firstCornerVertPair)) + + origFirstCornerDistance = om_cornerVert.distanceTo(om.MFloatPoint(mc.pointPosition(closestPoint, w=1))) + origSecondCornerDistance = om.MFloatPoint( + mc.pointPosition(secondCornerVertPair[0], w=1)).distanceTo(om.MFloatPoint(mc.pointPosition(secondCornerVertPair[1], w=1))) + + if len(firstCornerVertPair) != 2 or len(secondCornerVertPair) != 2: + MESSAGE.warningInView('Object "{}" has wrong topology. Skipped.'.format(obj)) + continue + + # Verts between closest corners + firstSideVertLoop = utils.polySelectSp(firstCornerVertPair, obj) + secondSideVertLoop = utils.polySelectSp(secondCornerVertPair, obj) + + # Edges between closest corners if there are no width spans + firstSideEdgeLoop = mc.ls(mc.polyListComponentConversion(firstCornerVertPair, fv=1, te=1, internal=1), fl=1) + secondSideEdgeLoop = mc.ls(mc.polyListComponentConversion(secondCornerVertPair, fv=1, te=1, internal=1), fl=1) + + # Edges between closest corners if there ARE width spans + if not firstSideEdgeLoop: + firstSideEdgeLoop = mc.ls(mc.polyListComponentConversion(firstSideVertLoop, fv=1, te=1, internal=1), fl=1) + if not secondSideEdgeLoop: + secondSideEdgeLoop = mc.ls(mc.polyListComponentConversion(secondSideVertLoop, fv=1, te=1, internal=1), fl=1) + + # Find the first long edge + # TODO: See if there is a need for those test loops + testVertLoop1 = utils.polySelectSp([firstCornerVertPair[0], secondCornerVertPair[0]], obj) + testVertLoop2 = utils.polySelectSp([firstCornerVertPair[0], secondCornerVertPair[1]], obj) + firstLongEdge = mc.ls(mc.polyListComponentConversion( + min(testVertLoop1, testVertLoop2, key=lambda v: len(v)), fv=1, te=1, internal=1), fl=1) + + # Open Edges + edges = mc.ls(mc.polyListComponentConversion(firstCornerVertPair[0], fv=1, te=1), fl=1) + openEdge = mc.ls(mc.polySelectSp(edges, q=1, loop=1), fl=1) + + # Second long edge + secondLongEdge = list(set(openEdge) - set(firstLongEdge + firstSideEdgeLoop + secondSideEdgeLoop)) + + # Select the longest edges and convert them + mc.select(firstLongEdge + secondLongEdge, r=1) + edgeToCurve(enableProgressBar=False) + curves = mc.ls(sl=1, tr=1) + pathCurve = None + if len(curves) >= 2: + loft = mc.loft(curves, ss=2) + finalCurve = mc.duplicateCurve(loft[0] + '.u[0.5]', ch=0) + finalCurve = mc.rename(finalCurve[0], 'convertedCurve#') + mc.delete(curves, loft) + pathCurve = finalCurve + + # ------------- Reversing ------------- + if getConvertOption('gsCardToCurve_reverseCurve'): + pathCurve = mc.reverseCurve(pathCurve, ch=0, rpo=1)[0] + + """Converting to cards, orienting and adjusting to the original (optional)""" + if not mc.optionVar(q='GSCT_CardToCurveOutputType'): + # ------------- Converting ------------- + mc.select(pathCurve, r=1) + if not mc.optionVar(q='GSCT_CardToCurveCardType'): # Warp + pathCurve = create.multiple(0, hk=True, progressBar=False, keepAttrs=False)[0] + else: # Extrude + pathCurve = create.multiple(-2, hk=True, progressBar=False, keepAttrs=False)[0] + + if mc.attributeQuery('scaleFactor', n=pathCurve, ex=1): + scaleFactor = mc.getAttr(pathCurve + '.scaleFactor') + else: + scaleFactor = 1.0 + + newGeo = selectPart(2, True, pathCurve)[0] + om_sel = om.MSelectionList() + om_sel.add(newGeo) + om_mesh = om.MFnMesh(om_sel.getDagPath(0)) + """Adjust the resulting card to closely match the target geo""" + # ------------- Orienting ------------- + firstCv = '%s.cv[0]' % pathCurve + if getConvertOption('gsCardToCurve_orientation'): + pos = mc.pointPosition(firstCv) + om_pos = om.MPoint(pos) + targetClosestPointNormal = utils.getClosestPointAndNormal(obj, pos)[1] + orientation = pathCurve + '.Orientation' + prevDiff = 360 + currentDiff = 360 + i = 0 + guesses = [] + while (currentDiff >= 1) and (i <= 5): + i += 1 + currentOrien = mc.getAttr(orientation) + om_cardFaceNormal = om_mesh.getClosestPointAndNormal(om_pos, space=om.MSpace.kWorld)[1] + currentDiff = om_cardFaceNormal.angle(targetClosestPointNormal) * 180 / math.pi % 360 + guesses.append((currentDiff, currentOrien)) + if currentDiff > prevDiff: + guess = currentOrien - currentDiff + else: + guess = currentOrien + currentDiff + prevDiff = currentDiff + nextOrien = guess + mc.setAttr(orientation, nextOrien % 360) + # Setting the best guess as the final orientation + finalOrientation = min(guesses, key=lambda t: t[0])[1] + mc.setAttr(orientation, finalOrientation) + + # ------------- Width ------------- + # BUG: Fix width calculation to be based on the widest part of the card, not on some average value + if getConvertOption('gsCardToCurve_width'): + allFaces = "{}.f[*]".format(newGeo) + perimeterEdges = mc.polyListComponentConversion(allFaces, ff=1, te=1, bo=1) + perimeterVerts = mc.ls(mc.polyListComponentConversion(perimeterEdges, fe=1, tv=1), fl=1) + cornerVerts = [] + for vert in perimeterVerts: + expandedEdge = mc.ls(mc.polyListComponentConversion(vert, fv=1, te=1), fl=1) + if len(expandedEdge) == 2: + cornerVerts.append(vert) + closestPoint = None + om_distance = 0 + om_cornerVert = om.MFloatPoint(mc.pointPosition(cornerVerts[0], w=1)) + for i in range(1, len(cornerVerts)): + om_otherVert = om.MFloatPoint(mc.pointPosition(cornerVerts[i], w=1)) + om_distance_new = om_cornerVert.distanceTo(om_otherVert) + if i == 1 or om_distance_new < om_distance: + om_distance = om_distance_new + closestPoint = cornerVerts[i] + # Closest corner verts + firstCornerVertPair = [cornerVerts[0], closestPoint] + secondCornerVertPair = list(set(cornerVerts) - set(firstCornerVertPair)) + newFirstCornerDistance = om_cornerVert.distanceTo(om.MFloatPoint(mc.pointPosition(closestPoint, w=1))) + newWidth = abs(origFirstCornerDistance / newFirstCornerDistance) + mc.setAttr(pathCurve + '.Width', newWidth) + # ------------- Taper ------------- + if getConvertOption('gsCardToCurve_taper'): + taper = origSecondCornerDistance / origFirstCornerDistance + mc.setAttr(pathCurve + '.Taper', taper) + + # ------------- Twist ------------- + lastCV = '%s.cv[%s]' % (pathCurve, mc.getAttr(pathCurve + '.spans') + 2) + if getConvertOption('gsCardToCurve_twist'): + pos = mc.pointPosition(lastCV) + om_pos = om.MPoint(pos) + targetClosestPointNormal = utils.getClosestPointAndNormal(obj, pos)[1] + twist = pathCurve + '.Twist' + prevDiff = 180 + currentDiff = 180 + i = 0 + guesses = [] + while (currentDiff >= 1) and (i <= 5): + i += 1 + currentTwist = mc.getAttr(twist) + om_cardFaceNormal = om_mesh.getClosestPointAndNormal(om_pos, space=om.MSpace.kWorld)[1] + currentDiff = om_cardFaceNormal.angle(targetClosestPointNormal) * 180 / math.pi + guesses.append((currentDiff, currentTwist)) + if currentDiff > prevDiff: + guess = currentTwist - currentDiff + else: + guess = currentTwist + currentDiff + prevDiff = currentDiff + nextTwist = guess + mc.setAttr(twist, nextTwist) + # Setting the best guess as the final orientation + finalTwist = min(guesses, key=lambda t: t[0])[1] + mc.setAttr(pathCurve + '.Twist', finalTwist) + + # ------------- Profile ------------- + # TODO: Possibly add profile curve matching for warp cards + if getConvertOption('gsCardToCurve_profile'): + # First iteration of profile + firstCVpos = mc.pointPosition(firstCv, w=1) + firstCVom_pos = om.MPoint(firstCVpos) + firstCVtargetClosestPoint = utils.getClosestPointAndNormal(obj, firstCVpos)[0] + firstProfileDistance = firstCVom_pos.distanceTo(firstCVtargetClosestPoint) + lastCVpos = mc.pointPosition(lastCV, w=1) + lastCVom_pos = om.MPoint(lastCVpos) + lastCVtargetClosestPoint = utils.getClosestPointAndNormal(obj, lastCVpos)[0] + lastProfileDistance = lastCVom_pos.distanceTo(lastCVtargetClosestPoint) + maxDist = max(firstProfileDistance, lastProfileDistance) + mc.setAttr(pathCurve + '.Profile', maxDist / scaleFactor * 1.4) + # Additional check in case the card was flipped in the wrong direction + firstCVtargetClosestPoint_2 = utils.getClosestPointAndNormal(newGeo, firstCVpos)[0] + lastCVtargetClosestPoint_2 = utils.getClosestPointAndNormal(newGeo, lastCVpos)[0] + firstTotalDistance = firstCVtargetClosestPoint.distanceTo(firstCVtargetClosestPoint_2) + lastTotalDistance = lastCVtargetClosestPoint.distanceTo(lastCVtargetClosestPoint_2) + if firstTotalDistance > firstProfileDistance and lastTotalDistance > lastProfileDistance: + mc.setAttr(pathCurve + '.Profile', mc.getAttr(pathCurve + '.Profile') * -1) + + # ------------- Material transfer ------------- + if getConvertOption('gsCardToCurve_material'): + mc.sets(newGeo, forceElement=list(utils.getShader(obj))[0]) + + # ------------- UV Approximation ------------- + if getConvertOption('gsCardToCurve_UVs'): + uVals, vVals = mc.polyEvaluate(obj, b2=True) + uMin, uMax = uVals + vMin, vMax = vVals + moveU = abs(uMax - uMin) / 2 + uMin - 0.5 + if getConvertOption('gsCardToCurve_verticalFlip'): + mc.setAttr(pathCurve + '.rotateUV', 180) + moveV = vMin + (vMax - vMin) - 1 + mc.setAttr(pathCurve + '.flipUV', not mc.getAttr(pathCurve + '.flipUV')) + else: + moveV = vMin + if getConvertOption('gsCardToCurve_horizontalFlip'): + mc.setAttr(pathCurve + '.flipUV', not mc.getAttr(pathCurve + '.flipUV')) + scaleU = abs(uMax - uMin) + scaleV = abs(vMax - vMin) + mc.setAttr(pathCurve + '.moveU', moveU) + mc.setAttr(pathCurve + '.moveV', moveV) + mc.setAttr(pathCurve + '.scaleU', scaleU) + mc.setAttr(pathCurve + '.scaleV', scaleV) + + if pathCurve: + finalSel.append(pathCurve) + progress.end() + mc.select(finalSel, r=1) + resetCurvePivotPoint() + + +def updateLattice(values, customTarget=None): + # TODO: Add curve fitting for better overall experience with the graph + + selection = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + + if not selection: + selection = mc.filterExpand(mc.ls(hl=1, o=1), sm=9) + if not selection and not customTarget: + return + if customTarget: + selection = [customTarget] + + for sel in selection: + if not mc.attributeQuery('latticeMessage', n=sel, ex=1): + continue + lattice = mc.listConnections(sel + '.latticeMessage') + if not lattice: + continue + else: + lattice = lattice[0] + + graphValues = utils.fromStringToDouble2(values) + + latticeDiv = mc.getAttr(lattice + '.sDivisions') + + if len(graphValues) != latticeDiv: + ffd = mc.listConnections(lattice + '.latticeOutput') + mc.lattice(ffd, e=1, rt=1) + mc.lattice(ffd, e=1, dv=[len(graphValues), 2, 2]) + latticeDiv = mc.getAttr(lattice + '.sDivisions') + curveControlUI.updateUI() + + currentPoints = [] + initialPoints = [] + for i in range(latticeDiv): + currentPoints.append(mc.pointPosition(lattice + '.pt[%s][1][0]' % (i), l=1)) + for i in range(mc.getAttr(sel + '.initialLatticePoints', s=1)): + initialPoints.append(mc.getAttr(sel + '.initialLatticePoints[%s]' % i)[0]) + + newPoints = [] + + for i in range(latticeDiv): + x = mt.lerp(graphValues[i][0], 0.5, -0.5, 1, 0) + y = mt.lerp(graphValues[i][1], 1.5, -0.5, 1, 0) + newPoints.append((x, y)) + + for i in range(len(newPoints)): + mc.move(newPoints[i][0], newPoints[i][1], lattice + '.pt[%s][1][0]' % i, xy=1, ls=1) + mc.move(newPoints[i][0], newPoints[i][1], lattice + '.pt[%s][1][1]' % i, xy=1, ls=1) + mc.move(newPoints[i][0], newPoints[i][1], lattice + '.pt[%s][0][0]' % i, x=1, ls=1) + mc.move(newPoints[i][0], newPoints[i][1], lattice + '.pt[%s][0][1]' % i, x=1, ls=1) + curveControlUI.updateUI() + + +def getLatticeValues(targetCurve): + if not mc.attributeQuery('latticeMessage', n=targetCurve, ex=1): + return + lattice = mc.listConnections(targetCurve + '.latticeMessage') + if lattice: + lattice = lattice[0] + else: + return + latticeDiv = mc.getAttr(lattice + '.sDivisions') + currentPoints = [] + initialPoints = [] + for i in range(latticeDiv): + currentPoints.append(mc.pointPosition(lattice + '.pt[%s][1][0]' % (i), l=1)) + for i in range(mc.getAttr(targetCurve + '.initialLatticePoints', s=1)): + initialPoints.append(mc.getAttr(targetCurve + '.initialLatticePoints[%s]' % i)[0]) + + newPoints = [] + + for i in range(latticeDiv): + x = mt.lerp(currentPoints[i][0], 1, 0, 0.5, -0.5) + y = mt.lerp(currentPoints[i][1], 1, 0, 1.5, -0.5) + newPoints.append((x, y)) + return newPoints + + +def equalizeProfileCurve(manual=False): + if WIDGETS['autoEqualizeSwitchOff'].isChecked() and not manual: + return + currentProfile = WIDGETS['profileCurve'].getGraph() + if currentProfile: + graphValues = utils.fromStringToDouble2(currentProfile) + for i in range(len(graphValues)): + graphValues[i][0] = float(i) / (len(graphValues) - 1) + newString = utils.fromDouble2ToString(graphValues) + WIDGETS['profileCurve'].setGraph(newString) + updateLattice(newString) + + +def resetProfileCurve(): + currentProfile = WIDGETS['profileCurve'].getGraph() + if currentProfile: + graphValues = utils.fromStringToDouble2(currentProfile) + for i in range(len(graphValues)): + graphValues[i][1] = 0.5 + newString = utils.fromDouble2ToString(graphValues) + WIDGETS['profileCurve'].setGraph(newString) + updateLattice(newString) + + +def transferProfileCurve(source, target): + if mc.attributeQuery('latticeMessage', n=target, ex=1) and mc.attributeQuery('latticeMessage', n=source, ex=1): + lattice = mc.listConnections(source + '.latticeMessage') + if lattice: + latticeDiv = mc.getAttr(lattice[0] + '.sDivisions') + if latticeDiv: + currentPoints = [] + if lattice: + for i in range(latticeDiv): + currentPoints.append(mc.pointPosition(lattice[0] + '.pt[%s][1][0]' % (i), l=1)) + newPoints = [] + for i in range(latticeDiv): + x = mt.lerp(currentPoints[i][0], 1, 0, 0.5, -0.5) + y = mt.lerp(currentPoints[i][1], 1, 0, 1.5, -0.5) + newPoints.append((x, y)) + + graphPoints = utils.fromDouble2ToString(newPoints) + + updateLattice(graphPoints, target) + + +def blendProfileCurve(source, target, ratio): + if mc.attributeQuery('latticeMessage', n=source, ex=1) and mc.attributeQuery('latticeMessage', n=target, ex=1): + + sourceLattice = mc.listConnections(source + '.latticeMessage') + if sourceLattice: + sourceLatticeDiv = mc.getAttr(sourceLattice[0] + '.sDivisions') + sourcePoints = [] + if sourceLatticeDiv: + if sourceLattice: + for i in range(sourceLatticeDiv): + point = mc.pointPosition(sourceLattice[0] + '.pt[%s][1][0]' % (i), l=1) + x = mt.lerp(point[0], 1, 0, 0.5, -0.5) + y = mt.lerp(point[1], 1, 0, 1.5, -0.5) + sourcePoints.append((x, y)) + + targetLattice = mc.listConnections(target + '.latticeMessage') + if not targetLattice: + return None + targetLatticeDiv = mc.getAttr(targetLattice[0] + '.sDivisions') + targetPoints = [] + if targetLatticeDiv: + if targetLattice: + for i in range(targetLatticeDiv): + point = mc.pointPosition(targetLattice[0] + '.pt[%s][1][0]' % (i), l=1) + x = mt.lerp(point[0], 1, 0, 0.5, -0.5) + y = mt.lerp(point[1], 1, 0, 1.5, -0.5) + targetPoints.append((x, y)) + + # Add placeholder nodes if needed + if sourceLatticeDiv > targetLatticeDiv: + for i in range(sourceLatticeDiv - targetLatticeDiv): + position = 1 / sourceLatticeDiv * (targetLatticeDiv + i) + targetPoints.append([position, 0.5]) + + blendResult = [[sourcePoints[i][0], mt.lerp(ratio, sourcePoints[i][1], targetPoints[i][1])] + for i in range(min(len(sourcePoints), len(targetPoints)))] + + finalPoints = utils.fromDouble2ToString(blendResult) + return finalPoints + + +def regroupByLayer(groupCustomCollections=None): + # type: (None|set) -> None + """ + Regroup all the curves in all layers and collections in the outliner + Optionally regroups only passed collection IDs + """ + def getCustomName(i, nameDict): + name = 'CT_Layer' + customLayerName = nameDict[i] + if customLayerName: + name = customLayerName + return name + + templateCollections = [] + if groupCustomCollections: + collections = set(groupCustomCollections) + else: + collections = utils.getCollectionsSet() + collections.add('0') + if getOption('groupTemplateCollections'): + collectionsWidget = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + for collection in collections.copy(): + if "template" in collectionsWidget.itemText(int(collection)).lower(): + collections.remove(collection) + templateCollections.append(collection) + + toggleColor.checkColorStorageNode() + counter = 0 + deletedGroupsCounter = 0 + previousGroups = set() + colorDict = dict(eval(mc.getAttr(toggleColor.STORAGE_NODE + '.layerColor'))) + nameDict = dict(eval(mc.getAttr(toggleColor.STORAGE_NODE + '.layerName'))) + hasCollections = len(collections) > 1 + for c in sorted(list(collections)): + for i in range(80): # Iterate over existing groups and layers + name = getCustomName(i, nameDict) + if hasCollections: + name = "%s_%s" % (WIDGETS['layerCollectionsComboBox'].itemText(int(c)).replace(' ', '_'), name) + layerName = '%s_%s' % (name, i) + if groupCustomCollections: + layerName = 'CT_Templates' + grpCurve = 'curveGrp_%s%s_Curve' % (utils.getFormattedCollectionByID(c), i) + if not mc.objExists(layerName): # Create groups if needed + mc.createNode('transform', n=layerName) + else: + if mc.nodeType(layerName) != 'transform': + mc.createNode('transform', n=layerName) + if mc.objExists(grpCurve): + curves = mc.editDisplayLayerMembers(grpCurve, q=1, fn=1) + if not curves: + continue + + if WIDGETS['colorizedRegroup'].isChecked(): + getClr = colorDict[i] + mc.setAttr(layerName + '.useOutlinerColor', 1) + mc.setAttr(layerName + '.outlinerColor', *getClr, typ='float3') + else: + mc.setAttr(layerName + '.useOutlinerColor', 0) + + for curve in curves: + curve = mc.ls(curve, l=1) + if curve: + curve = curve[0] + else: + continue + if mc.attributeQuery('gsmessage', n=curve, ex=1) and mc.connectionInfo(curve + '.gsmessage', id=1): + continue + counter += 1 + grp = selectPart(0, True, curve) + + topGrp = mc.listRelatives(grp, p=1, pa=1) + if topGrp: + previousGroups.add(topGrp[0]) + if topGrp[0] != layerName: + mc.parent(grp, layerName) + else: + mc.parent(grp, layerName) + mc.setAttr(toggleColor.STORAGE_NODE + '.layerColor', str(colorDict), typ='string') + + for ele in previousGroups: # Delete any previous empty groups that exist + children = mc.listRelatives(ele, c=1, pa=1) + if not children: + deletedGroupsCounter += 1 + mc.delete(ele) + for c in collections: + for i in range(80): # Remove Generated empty groups + name = getCustomName(i, nameDict) + if hasCollections: + name = "%s_%s" % (WIDGETS['layerCollectionsComboBox'].itemText(int(c)).replace(' ', '_'), name) + layerName = '%s_%s' % (name, i) + if mc.objExists(layerName) and (mc.nodeType(layerName) == 'transform'): + rel = mc.listRelatives(layerName, c=1, pa=1) + if rel and len(rel) == 0: + mc.delete(layerName) + elif not rel: + mc.delete(layerName) + + reorderingList = [] + for c in list(collections): + for i in range(80): # Reorder groups + name = getCustomName(i, nameDict) + if hasCollections: + name = "%s_%s" % (WIDGETS['layerCollectionsComboBox'].itemText(int(c)).replace(' ', '_'), name) + if mc.objExists('%s_%s' % (name, i)): + reorderingList.append('%s_%s' % (name, i)) + for ele in reorderingList: + mc.reorder(ele, b=1) + + mc.select(cl=1) + + deletedGroupsMsg = '' + if len(previousGroups) > 0: + deletedGroupsMsg = ' and deleted %s empty layer(s).' % deletedGroupsCounter + if templateCollections: + regroupByLayer(templateCollections) + if groupCustomCollections: + MESSAGE.printInView('Regrouped %s templates%s' % (counter, deletedGroupsMsg)) + else: + MESSAGE.printInView('Regrouped %s curve(s)%s' % (counter, deletedGroupsMsg)) + + +def mirrorHair(axis, flip=False): + sel = mc.filterExpand(mc.ls(sl=1), sm=9) + if not sel: + sel = mc.ls(hl=1, o=1) + if not sel: + MESSAGE.warningInView('Select compatible curves.') + return + mirrorRadio = WIDGETS['mirrorRadio'].isChecked() + option = 'Mirroring' if mirrorRadio else 'Flipping' + if flip: + option = 'Flipping' + axisDict = { + 0: [-1, 1, 1], + 1: [1, -1, 1], + 2: [1, 1, -1], + } + if option == 'Mirroring': + newCrv = duplicateCurve(sel) + else: + newCrv = sel + filteredCurves = [] + for crv in newCrv: + # originalProfile = None + originalProfileVector = None + if mc.attributeQuery("Profile", n=crv, ex=1): + originalProfile = mc.getAttr(crv + '.Profile') + mc.setAttr(crv + '.Profile', math.copysign(2, originalProfile)) + firstCv = '%s.cv[0]' % crv + pos = mc.pointPosition(firstCv) + originalProfileVector = utils.getMiddleVertAndNormal(crv, selectPart(2, True, crv)[0], pos) + mc.setAttr(crv + '.Profile', originalProfile) + attrs = utils.resetAndReturnAttrs(crv) + attrs.append(originalProfileVector) + # attrs.append(originalProfile) + filteredCurves.append(attrs) + + if filteredCurves: + mc.scale(axisDict[axis][0], + axisDict[axis][1], + axisDict[axis][2], + [i[0] for i in filteredCurves], + a=1, ws=1, p=[0, 0, 0]) + + # Init progress bar + progress = utils.ProgressBar("Mirroring...", len(filteredCurves)) + + # Fix orientation + for crv in filteredCurves: + if progress.tick(1): + break + curve = crv[0] + firstCv = '%s.cv[0]' % curve + pos = mc.pointPosition(firstCv) + om_pos = om.MPoint(pos) + targetVec = om.MVector( + crv[1][0] * axisDict[axis][0], + crv[1][1] * axisDict[axis][1], + crv[1][2] * axisDict[axis][2] + ) + om_sel = om.MSelectionList() + geo = selectPart(2, True, curve)[0] + om_sel.add(geo) + om_mesh = om.MFnMesh(om_sel.getDagPath(0)) + orientation = curve + '.Orientation' + prevDiff = 360 + currentDiff = 360 + iterator = 0 + maxIterations = 10 + angleTolerance = 0.1 + + guesses = [] + + while (prevDiff >= angleTolerance) and (iterator <= maxIterations): + iterator += 1 + om_cardFaceNormal = om_mesh.getClosestPointAndNormal(om_pos, space=om.MSpace.kWorld)[1] + currentDiff = om_cardFaceNormal.angle(targetVec) * 180.0 / math.pi + currentOrien = mc.getAttr(orientation) + + guesses.append((currentDiff, currentOrien)) + + if currentDiff > prevDiff: + guess = currentOrien - currentDiff + else: + guess = currentOrien + currentDiff + prevDiff = currentDiff + + mc.setAttr(orientation, guess % 360.0) + + # Setting the best guess as the final orientation + finalOrientation = min(guesses, key=lambda t: t[0])[1] + mc.setAttr(orientation, finalOrientation % 360.0) + + # Invert graph + if crv[6]: + invertedTwistGraph = crv[6][0] + widthGraph = crv[6][1] + if invertedTwistGraph[-1] != 'twistCurve': + invertedTwistGraph = crv[6][1] + widthGraph = crv[6][0] + for i in range(len(invertedTwistGraph) - 1): + invertedTwistGraph[i] = (invertedTwistGraph[i][0], 1 - invertedTwistGraph[i][1]) + attributes.setMultiInst(crv[0], invertedTwistGraph) # Inverting Twist Graph + attributes.setMultiInst(crv[0], widthGraph) # Setting Width Graph Back + + # Restore parameters + if crv[2] and crv[2] > 10: + mc.setAttr(curve + '.lengthDivisions', crv[2]) + if crv[3] and crv[3] > 2: + mc.setAttr(curve + '.widthDivisions', crv[3]) + if crv[4] and crv[4] != 0: + mc.setAttr(curve + '.Twist', crv[4] * -1) + if crv[5] and crv[5] != 0: + mc.setAttr(curve + '.invTwist', crv[5] * -1) + + # Fix profile if needed + if crv[7]: + originalProfile = mc.getAttr(curve + '.Profile') + mc.setAttr(curve + '.Profile', math.copysign(2, originalProfile)) + firstCvVec = om.MVector(om_pos) + + expectedVertex = om.MVector( + crv[7][0][0] * axisDict[axis][0], + crv[7][0][1] * axisDict[axis][1], + crv[7][0][2] * axisDict[axis][2] + ) + expectedVector = om.MVector( + crv[7][1][0] * axisDict[axis][0], + crv[7][1][1] * axisDict[axis][1], + crv[7][1][2] * axisDict[axis][2] + ) + profileClosestVertexAndNormal = utils.getMiddleVertAndNormal(curve, selectPart(2, True, curve)[0], firstCvVec) + + # Checking if normals of the flat card are correct. If not, flip the card 180 deg. + normalCheck = expectedVector.angle(profileClosestVertexAndNormal[1]) * 180.0 / math.pi + if abs(normalCheck) >= 90: + mc.setAttr(curve + ".Orientation", (mc.getAttr(curve + ".Orientation") + 180.0) % 360.0) + + # Checking if the angle between two middle vertices is correct. If not, invert the profile. + angleBetween = (firstCvVec - expectedVertex).angle(firstCvVec - om.MVector(profileClosestVertexAndNormal[0])) * 180.0 / math.pi + newProfile = originalProfile * -1 if abs(angleBetween) >= 90 else originalProfile + mc.setAttr(curve + ".Profile", newProfile) + + # Flip UVs + if getOption('flipUVsAfterMirror') and mc.attributeQuery('flipUV', n=curve, ex=1): + mc.setAttr(curve + '.flipUV', not mc.getAttr(curve + '.flipUV')) + + resetCurvePivotPoint() + progress.end() + LOGGER.info("Mirror Finished") + + +def renameSelected(): + sel = sorted(mc.ls(sl=1, tr=1)) + for obj in sel: + grp = selectPart(0, True, obj) + if grp: + try: + newName = WIDGETS['selectedObjectName'].text() + mc.rename(grp, newName + '#') + except BaseException: + MESSAGE.warning('Some items were skipped') + curveControlUI.updateUI() + + +def resetCurvePivotPoint(hk=None, customCurves=None): # Reset Curve Pivot + # type: (None|int, None|list[str]|str) -> None + """Resets pivot point on selected curves or passed as customCurves""" + if customCurves: + sel = customCurves + else: + sel = mc.filterExpand(mc.ls(sl=1, tr=1, l=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one Curve') + return + + # Get modifier + shift = utils.getMod() + mod = True if hk == 2 or (hk is None and shift == 'Shift') else False + + for selElement in sel: + nt = mc.nodeType(mc.listRelatives(selElement, c=1, pa=1)) + if nt != 'nurbsCurve' and nt != 'bezierCurve': + continue + mc.select(selElement) + transforms = mc.xform(selElement, q=1, t=1) + curve = mc.listRelatives(selElement, c=1, pa=1) + cvNum = '0' + if mod: + cvNum = str(mc.getAttr(selElement + '.controlPoints', s=1) - 1) + cv = mc.pointPosition((curve[0] + ".cv[" + cvNum + "]"), w=1) + mc.xform(ws=1, piv=(transforms[0], transforms[1], transforms[2])) + mc.xform(ws=1, piv=(cv[0], cv[1], cv[2])) + mc.manipMoveContext('Move', e=1, mode=0) + mc.manipRotateContext('Rotate', e=1, mode=0) + mc.manipScaleContext('Scale', e=1, mode=0) + mc.select(sel) + + +def selectGeoCurveUV(): + curve = mc.ls(sl=1, tr=1) + geo = selectPart(2, True, curve[0])[0] + mc.select(geo, r=1) + mc.select(curve, add=1) + + +def toggleGeoEdit(): # Toggle Geometry Edit Hotkey + value = None + for i in range(80): + geo = 'curveGrp_%s_Geo' % i + if (utils.attrExists(geo, 'displayType')): + if value is None: + value = mc.getAttr(geo + '.displayType') + if value == 2: + mc.setAttr(geo + '.displayType', 0) + else: + mc.setAttr(geo + '.displayType', 2) + updateMainUI() + + +def extractCurveGeo(layerNum, checkButtons=True): + # type: (str|int, bool) -> None + """Extract Geo from single Layer via marking menu""" + + create.initialize() + + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + collection = utils.getFormattedCollectionByID(collectionID) + + geo = "curveGrp_%s%s_Geo" % (collection, layerNum) + curve = "curveGrp_%s%s_Curve" % (collection, layerNum) + + if toggleColor.colorEnabled(): + toggleColor.disableColors() + + result = extractGeo(geo, curve, layerNum) + if not result: + MESSAGE.warning("Layer is Empty") + + if checkButtons: + updateMainUI() + + +def extractGeo(geoLayerName, curveLayerName, layerNum, hideLayer=True): + # type: (str, str, int, bool) -> list[str] + """Extracts geo from layer names, optionally hides the original layers""" + outlinerSync = getOption('syncOutlinerLayerVis') + if mc.objExists(geoLayerName): + groups = [] + if outlinerSync: + layerCurves = mc.editDisplayLayerMembers(curveLayerName, q=1, fn=1) + if not layerCurves: + return + for i in layerCurves: + parent = mc.listRelatives(i, p=1, pa=1) + if parent: + groups += parent + if hideLayer: + if outlinerSync: + for grp in groups: + mc.setAttr(grp + '.v', 0) + mc.setAttr(geoLayerName + '.visibility', 0) + mc.setAttr(curveLayerName + '.visibility', 0) + if mc.layerButton(geoLayerName, q=1, ex=1): + mc.layerButton(geoLayerName, e=1, lv=0) + if mc.layerButton(geoLayerName, q=1, ex=1): + mc.layerButton(geoLayerName, e=1, lv=0) + sel = mc.editDisplayLayerMembers(geoLayerName, q=1, fn=1) + sel = mc.filterExpand(sel, sm=12) + if sel: + dupList = list() + for ele in sel: + dup = mc.duplicate(ele, rc=1) + dupList.append(dup[0]) + mc.setAttr(dup[0] + '.inheritsTransform', 1) + mc.editDisplayLayerMembers('defaultLayer', dupList, nr=1) + split = curveLayerName.split("_") + collectionID = int(split[1]) if len(split) == 4 else 0 + collectionName = WIDGETS['layerCollectionsComboBox'].itemText(collectionID) + extracted = 'extractedGeo_%s_%s' % (collectionName, layerNum) + if mc.objExists(extracted): + finalGeo = mc.parent(dupList, extracted) + return [extracted + "|" + x for x in finalGeo] + else: + finalGeo = mc.group(dupList, w=1, n=extracted) + return [finalGeo + "|" + x for x in dupList] + return [] + + +def extractSelectedCurves(mod=None, hotkey=False): + # type: (str, bool) -> None + """Extract geo from selected curves""" + + sel = mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one Curve') + return 0 + + if not hotkey: + mod = utils.getMod() + + isColorEnabled = toggleColor.colorEnabled() + if isColorEnabled: + toggleColor.disableColors() + + geometry = [] + for part in sel: + geo = selectPart(2, True, part) + if geo: + geo = geo[0] + else: + MESSAGE.warningInView('Curve "%s" failed to extract. Check the outliner for any loose curves.' % part) + continue + geometry.append(geo) + geometry = mc.filterExpand(geometry, sm=12) + if not geometry: + return + allGeo = [] + progress = utils.ProgressBar('Extracting Geometry', len(geometry)) + try: + for geo in geometry: + progress.tick(1) + dup = mc.duplicate(geo, rc=1) + mc.setAttr(dup[0] + '.inheritsTransform', 1) + mc.editDisplayLayerMembers('defaultLayer', dup[0], nr=1) + allGeo += dup + except Exception as e: + LOGGER.exception(e) + finally: + progress.end() + + parentGrp = mc.group(allGeo, w=1, n='extractedGeo_#') + allGeo = mc.listRelatives(parentGrp, c=1, pa=1) + + if not mod or "Shift" not in mod: + if len(allGeo) > 1: + combinedGeo = mc.polyUnite(allGeo, cp=1) + mc.delete(combinedGeo, ch=1) + else: + combinedGeo = mc.parent(allGeo, w=1) + mc.delete(parentGrp) + combinedGeo = mc.filterExpand([x for x in combinedGeo if mc.objExists(x)], sm=12) + allGeo = mc.rename(combinedGeo[-1], 'extractedGeo_#') + + mc.select(allGeo, r=1) + + # Open export dialog + if mod and "Ctrl" in mod: + mel.eval("ExportSelection") + mc.delete(allGeo) + try: + mc.delete(parentGrp) + except BaseException: + pass + + if isColorEnabled: + toggleColor.enableColors() + + updateMainUI() + + +def extractAllCurves(mod=None, hotkey=False): + # type: (str|None, bool) -> None + """ + Extracts geo from all layers + Modifiers: + Default -> extract geo and combine + Shift -> extract geo + Ctrl -> extract geo, combine and open export menu, delete extracted + Shift+Ctrl -> extract geo and open export menu, delete extracted + """ + if not hotkey: + mod = utils.getMod() + + isColorEnabled = toggleColor.colorEnabled() + if isColorEnabled: + toggleColor.disableColors() + + numOfLayers = WIDGETS['curveGrp0'].LAYERS - 1 + if not WIDGETS['ignoreLastLayer'].isChecked(): + numOfLayers = WIDGETS['curveGrp0'].LAYERS + + allCollections = utils.getCollectionsSet() + allCollections.add('0') + if getOption('ignoreTemplateCollections'): + collectionsWidget = WIDGETS['layerCollectionsComboBox'] # type: wrap.LayerCollectionWidget + for collection in allCollections.copy(): + if "template" in collectionsWidget.itemText(int(collection)).lower(): + allCollections.remove(collection) + allGeo = [] + shouldHideLayers = (mod and "Ctrl" not in mod) or not mod + progress = utils.ProgressBar('Extracting Geometry', numOfLayers * len(allCollections)) + try: + for collection in allCollections: + formattedCollection = utils.getFormattedCollectionByID(collection) + for i in range(numOfLayers): + if progress.tick(1): + break + geo = "curveGrp_%s%s_Geo" % (formattedCollection, i) + curve = "curveGrp_%s%s_Curve" % (formattedCollection, i) + if mc.objExists(geo) and mc.objExists(curve): + allGeo += extractGeo(geo, curve, i, shouldHideLayers) + except Exception as e: + LOGGER.debug(e) + finally: + progress.end() + + # Delete parent groups + parentGrps = set(mc.listRelatives(allGeo, p=1)) + if not mod or "Shift" not in mod: + if len(allGeo) > 1: + combinedGeo = mc.polyUnite(allGeo, cp=1) + mc.delete(combinedGeo, ch=1) + combinedGeo = mc.filterExpand([x for x in combinedGeo if mc.objExists(x)], sm=12) + else: + combinedGeo = mc.parent(allGeo, w=1) + parentGrps = [x for x in parentGrps if mc.objExists(x)] + if parentGrps: + mc.delete(parentGrps) + allGeo = mc.rename(combinedGeo[-1], 'extractedGeo_#') + + mc.select(allGeo, r=1) + + # Open export dialog + if mod and "Ctrl" in mod: + mel.eval("ExportSelection") + mc.delete(allGeo) + parentGrps = [x for x in parentGrps if mc.objExists(x)] + if parentGrps: + mc.delete(parentGrps) + if isColorEnabled: + toggleColor.enableColors() + + updateMainUI() + + +def toggleLayerVisibility(layerNum): # Toggle Layer Visibility + outlinerSync = getOption('syncOutlinerLayerVis') + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + collection = utils.getFormattedCollectionByID(collectionID) + curve = "curveGrp_%s%s_Curve" % (collection, layerNum) + geo = "curveGrp_%s%s_Geo" % (collection, layerNum) + inst = "curveGrp_%s%s_Inst" % (collection, layerNum) + if (utils.attrExists(curve, 'visibility')): + groups = [] + if outlinerSync: + layerCurves = mc.editDisplayLayerMembers(curve, q=1, fn=1) + if not layerCurves: + MESSAGE.warningInView('Layer is Empty') + return + for i in layerCurves: + parent = mc.listRelatives(i, p=1, pa=1) + if parent: + groups += parent + current = mc.getAttr(curve + '.visibility') + if current == 1: + mc.setAttr(curve + '.visibility', 0) + if mc.layerButton(curve, q=1, ex=1): + mc.layerButton(curve, e=1, lv=0) + mc.setAttr(geo + '.visibility', 0) + if mc.layerButton(geo, q=1, ex=1): + mc.layerButton(geo, e=1, lv=0) + mc.setAttr(inst + '.visibility', 0) + if mc.layerButton(inst, q=1, ex=1): + mc.layerButton(inst, e=1, lv=0) + for grp in groups: + mc.setAttr(grp + '.v', 0) + else: + mc.setAttr(curve + '.visibility', 1) + if mc.layerButton(curve, q=1, ex=1): + mc.layerButton(curve, e=1, lv=1) + mc.setAttr(geo + '.visibility', 1) + if mc.layerButton(geo, q=1, ex=1): + mc.layerButton(geo, e=1, lv=1) + mc.setAttr(inst + '.visibility', 0) + if mc.layerButton(inst, q=1, ex=1): + mc.layerButton(inst, e=1, lv=0) + for grp in groups: + mc.setAttr(grp + '.v', 1) + else: + MESSAGE.warningInView('Layer is Empty') + + +def toggleObjVisibility(layerNum, obj): + # type: (str|int, int) -> None + """Toggle object type visibility in layer""" + name = str() + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + collection = utils.getFormattedCollectionByID(collectionID) + if obj == 0: + name = "curveGrp_%s%s_Curve" % (collection, layerNum) + elif obj == 1: + name = "curveGrp_%s%s_Geo" % (collection, layerNum) + if utils.attrExists(name, 'visibility'): + current = mc.getAttr(name + '.visibility') + if current == 1: + mc.setAttr(name + '.visibility', 0) + if mc.layerButton(name, q=1, ex=1): + mc.layerButton(name, e=1, lv=0) + else: + mc.setAttr(name + '.visibility', 1) + if mc.layerButton(name, q=1, ex=1): + mc.layerButton(name, e=1, lv=1) + else: + MESSAGE.warningInView('Layer is Empty') + + +def curveLayerSelectObj(layerNum, obj, *_): # Select Object from Layer + mod = utils.getMod() + if obj == -1: + if mod != 'Shift' and mod != 'Ctrl': + return 0 + else: + obj = 0 + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + collection = utils.getFormattedCollectionByID(collectionID) + geo = 'curveGrp_%s%s_Geo' % (collection, layerNum) + curve = 'curveGrp_%s%s_Curve' % (collection, layerNum) + if mc.objExists(geo): + sel = mc.ls(sl=1) + if (obj == 1): + mc.select(mc.editDisplayLayerMembers(geo, q=1, fn=1)) + if (obj == 0): + selection = [] + unfilteredCurves = mc.editDisplayLayerMembers(curve, q=1, fn=1) + if not unfilteredCurves: + MESSAGE.warningInView('Layer is Empty') + return + for ele in unfilteredCurves: + if mc.attributeQuery('gsmessage', n=ele, ex=1) and mc.connectionInfo(ele + '.gsmessage', id=1): + continue + selection.append(ele) + mc.select(selection) + if (not WIDGETS['replacingCurveLayerSelection'].isChecked() or (mod == 'Shift')): + mc.select(sel, add=1) + else: + MESSAGE.warningInView('Layer is Empty') + + +def curveGeometryEditToggle(layerNum): + # type: (str|int) -> None + """Toggle Geo Editing on Layer""" + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + collection = utils.getFormattedCollectionByID(collectionID) + geo = 'curveGrp_%s%s_Geo' % (collection, layerNum) + if (utils.attrExists(geo, 'displayType')): + currentValue = mc.getAttr(geo + '.displayType') + if currentValue == 2: + mc.setAttr(geo + '.displayType', 0) + curveLayerSelectObj(layerNum, 1) + else: + mc.setAttr(geo + '.displayType', 2) + else: + MESSAGE.warningInView('Layer is Empty') + + +def curveAddToLayer(targetLayer, sourceLayer=None, inputCurves=None, targetCollection=None): + # type: (str|int, str|int, list[str], int) -> None + """Add Selected Curves to Layer""" + create.initialize() + collectionID = WIDGETS['layerCollectionsComboBox'].currentIndex() + if targetCollection is not None: + collectionID = int(targetCollection) + curve, geo, inst = utils.getFormattedLayerNames(collectionID, targetLayer) + sel = mc.ls(sl=1, tr=1) + if inputCurves: + sel = inputCurves + if sourceLayer: + collection = utils.getFormattedCollectionByID(collectionID) + sourceLayer = "curveGrp_%s%s_Curve" % (collection, sourceLayer) + if mc.objExists(sourceLayer): + sel = mc.editDisplayLayerMembers(sourceLayer, q=1, fn=1) + else: + return 0 + origSel = sel + if sel: + for ele in sel: + if ('pathCurve' in ele) or ('geoCard' in ele) or ('geoTube' in ele): + eleGrp = mc.listRelatives(ele, p=1, pa=1) + if not eleGrp: + MESSAGE.warningInView('Wrong Selection: %s is skipped' % ele) + continue + components = separateComponents(eleGrp[0]) + if not components: + continue + if not mc.objExists(curve): + mc.select(cl=1) + utils.createNewDisplayLayer(curve) + if not mc.objExists(geo): + utils.createNewDisplayLayer(geo) + mc.setAttr(geo + '.enabled', 1) + mc.setAttr(geo + '.displayType', 2) + if not mc.objExists(inst): + utils.createNewDisplayLayer(inst) + mc.setAttr(inst + '.enabled', 1) + mc.setAttr(inst + '.visibility', 0) + mc.setAttr(inst + '.displayType', 2) + mc.editDisplayLayerMembers(curve, components[0], nr=1) + mc.editDisplayLayerMembers(inst, components[1], nr=1) + mc.editDisplayLayerMembers(geo, components[2], nr=1) + if WIDGETS['boundCurvesFollowParent'].isChecked(): + ele = getAllConnectedCurves(ele) + for i in ele: + if mc.attributeQuery('gsmessage', n=i, ex=1): + if mc.connectionInfo(i + '.gsmessage', isSource=1): + childCards = mc.listConnections(i + '.gsmessage', d=1, s=0, t='transform') + if childCards: + for card in childCards: + components = separateComponents(selectPart(0, True, card)) + mc.editDisplayLayerMembers(curve, components[0], nr=1) + mc.editDisplayLayerMembers(inst, components[1], nr=1) + mc.editDisplayLayerMembers(geo, components[2], nr=1) + else: + MESSAGE.warningInView('Wrong Selection: %s is skipped' % ele) + deleteUnusedLayers() + layerCollections.updateDefaultLayerNode() + layerCollections.updateCollectionNames() + if WIDGETS['syncCurveColor'].isChecked(): + toggleColor.syncCurveColors() + if WIDGETS['colorMode'].isChecked(): + toggleColor.onLayerChange(sel, geo) + mc.select(origSel, r=1) + else: + MESSAGE.warningInView('Nothing is Selected') + + +def separateComponents(inputGroup): + tr = mc.listRelatives(inputGroup, pa=1) + components = [None, None, None] + for comp in tr: + if 'pathCurve' in comp: + components[0] = comp + elif 'instances' in comp: + components[1] = comp + elif ('geoCard' in comp) or ('geoTube' in comp): + components[2] = comp + if len(components) != 3: + return + else: + return components + + +def getAllConnectedCurves(inputCurves): + returnCurves = [] + if isinstance(inputCurves, list): + currentCurves = inputCurves + else: + currentCurves = [inputCurves] + + returnCurves = currentCurves + + iterationCurves = currentCurves + safety = 0 + while iterationCurves: + safety = safety + 1 + if safety > 50: + break + boundCurves = [] + for curve in iterationCurves: + condition = (mc.attributeQuery('gsmessage', n=curve, ex=1) and + mc.connectionInfo(curve + '.gsmessage', isSource=1)) + if condition: + newCurves = mc.listConnections(curve + '.gsmessage', d=1, s=0, t='transform') + if newCurves: + boundCurves += newCurves + else: + continue + if boundCurves: + iterationCurves = False + iterationCurves = boundCurves + returnCurves += iterationCurves + else: + iterationCurves = False + break + + return returnCurves + + +def getCurveLayer(curve): + if not curve: + return + layer = mc.listConnections(curve, type='displayLayer') + layerID = re.findall(r'\d+', layer[0])[0] + return layerID + + +def filterBoundCurves(curves): + if isinstance(curves, list): + curves = curves + else: + curves = [curves] + + filteredCurves = [] + + for curve in curves: + if (mc.attributeQuery('gsmessage', n=curve, ex=1) and + mc.connectionInfo(curve + '.gsmessage', id=1)): + continue + filteredCurves.append(curve) + + return filteredCurves + + +def duplicateCurve(customSel=None): + # type: (list[str]|None) -> None + """Duplicate selected Curves or curves list passed as an argument""" + create.initialize() + sel = mc.ls(sl=1, fl=1) + if customSel: + sel = customSel + if not sel: + MESSAGE.warningInView('Select at least one curve') + return + fe = mc.filterExpand(sel, sm=(30, 28)) + if fe: + obj = mc.ls(sel, o=1) + sel = [] + for ele in obj: + rel = mc.listRelatives(ele, p=1, pa=1) + sel.append(rel[0]) + sel = list(set(sel)) + finalSel = list() + copyThickness = list() + mc.select(cl=1) + if not customSel: + progressBar = utils.ProgressBar('Duplicating Curves', len(sel)) + try: + sel = filterBoundCurves(sel) + try: + utils.deleteKeys(sel) + except BaseException: + pass + sel = list(set(selectPart(0, True, sel))) # Filter out duplicates + for ele in sel: + if not customSel: + progressBar.tick(1) + if ('pathCurve_inst' in ele) or ('profileCurve_inst' in ele): + ele = mc.listRelatives(ele, p=1, pa=1)[0] + if any([x in ele for x in ['geoCard', 'pathCurve', 'geoTube', 'instances', 'origCurves']]): + parent = mc.listRelatives(ele, p=1, pa=1) + origCrv = None + for curve in mc.listRelatives(parent, c=1, pa=1): + if 'pathCurve' in curve: + origCrv = curve + break + dup = mc.duplicate(parent, rr=1, rc=1, un=1) + dupCrv = None + for curve in mc.listRelatives(dup, c=1, pa=1): + if 'pathCurve' in curve: + dupCrv = curve + break + if origCrv and dupCrv: + copyThickness.append((origCrv, dupCrv)) + elif dupCrv: + copyThickness.append(dupCrv) + finalSel.append(dupCrv) + else: + rel = mc.listRelatives(ele, c=1, pa=1) + if not rel: + continue + dup = mc.duplicate(ele, rr=1, rc=1, un=1) + for curve in mc.listRelatives(dup, c=1, pa=1): + if 'pathCurve' in curve: + copyThickness.append(curve) + finalSel.append(curve) + break + except BaseException: + pass + finally: + if not customSel: + progressBar.end() + if copyThickness: + setCurveThickness(copyThickness) + mc.select(finalSel, r=1) + return (finalSel) + + +def deleteSelectedCurves(): + """Deletes selected curves in a safe way. Also deletes other objects""" + sel = mc.ls(sl=1, tr=1) + sel = selectPart(0, True, sel) + if not sel: + return + mc.delete(sel) + print(" ") # Just to avoid annoying errors during bind curve deletion + + +def smoothCurve(inputCurves=None): + """Smoothes selected curve""" + sel = mc.ls(sl=1) + if not sel: + MESSAGE.warningInView('Select at least one curve') + return 0 + if inputCurves: + sel = inputCurves + selCrv = mc.filterExpand(sel, sm=9) + selCV = mc.filterExpand(sel, sm=28) + if not selCrv and not selCV: + MESSAGE.warningInView('Select at least 1 curve to smooth') + return 0 + if selCV and len(selCV) < 3: + MESSAGE.warningInView('Select at least 3 CVs to smooth') + return 0 + mult = 1 + if mc.menuItem('gsSmoothMult3', q=1, rb=1): + mult = 3 + if mc.menuItem('gsSmoothMult5', q=1, rb=1): + mult = 5 + if mc.menuItem('gsSmoothMult10', q=1, rb=1): + mult = 10 + if inputCurves: + mult = 10 + smoothing = math.ceil((100 - mc.floatSliderGrp('gsFactorSlider', q=1, v=1)) / 20) + if selCrv: # If curve is selected + sel = mc.ls(selCrv, dag=1, s=1) + for obj in sel: + CVcount = mc.getAttr(obj + '.cp', s=1) + if CVcount >= 3: + for _ in range(0, mult): + cPoint = mc.getAttr(obj + '.cp[:]') + finalPoints = list() + finalPoints.append(tuple(cPoint[0])) + firstP = om.MFloatVector(cPoint[0][0], cPoint[0][1], cPoint[0][2]) + activeP = om.MFloatVector(cPoint[1][0], cPoint[1][1], cPoint[1][2]) + for i in range(2, CVcount): + nextP = om.MFloatVector(cPoint[i][0], cPoint[i][1], cPoint[i][2]) + activePpos = (activeP + firstP + nextP) / 3 + for _ in range(1, int(smoothing)): + activePpos = (activeP + activePpos) / 2 + finalPoints.append(tuple([activePpos.x, activePpos.y, activePpos.z])) + firstP = activeP + activeP = nextP + finalPoints.append(tuple(cPoint[-1])) + cmd = ('mc.setAttr("' + str(obj) + '.cp[:]"') + for i in range(len(finalPoints)): + for z in range(3): + cmd += (',' + str(finalPoints[i][z])) + cmd += ')' + eval(cmd) + else: # If CVs are selected + curveDict = dict() + for cv in selCV: + obj = mc.ls(cv, o=1)[0] + CVnum = re.findall(r'\d+', re.findall(r'\[\d+\]', cv)[0])[0] + if obj in curveDict: + curveDict[obj].append(int(CVnum)) + else: + curveDict[obj] = [int(CVnum)] + for obj in curveDict: + CVs = curveDict[obj] + if len(CVs) < 3: + continue + CVs.sort() + CVsFinal = [CVs[0]] + for i in range(len(CVs) - 1): + if (int(CVs[i + 1]) - int(CVs[i])) >= 2: + break + CVsFinal.append(CVs[i + 1]) + CVcount = len(CVs) + if CVcount >= 3: + for _ in range(0, mult): + cPoint = list() + for cv in CVsFinal: + cPoint.append(mc.getAttr(obj + '.cp[%s]' % cv)[0]) + finalPoints = list() + finalPoints.append(tuple(cPoint[0])) + firstP = om.MFloatVector(cPoint[0][0], cPoint[0][1], cPoint[0][2]) + activeP = om.MFloatVector(cPoint[1][0], cPoint[1][1], cPoint[1][2]) + for i in range(2, CVcount): + nextP = om.MFloatVector(cPoint[i][0], cPoint[i][1], cPoint[i][2]) + activePpos = (activeP + firstP + nextP) / 3 + for _ in range(1, int(smoothing)): + activePpos = (activeP + activePpos) / 2 + finalPoints.append(tuple([activePpos.x, activePpos.y, activePpos.z])) + firstP = activeP + activeP = nextP + finalPoints.append(tuple(cPoint[-1])) + cmd = ('mc.setAttr("' + str(obj) + '.cp[%s:%s]"' % (CVsFinal[0], CVsFinal[-1])) + for i in range(len(finalPoints)): + for z in range(3): + cmd += (',' + str(finalPoints[i][z])) + cmd += ')' + eval(cmd) + + +def extendCurve(): # Extends curve length + sel = mc.filterExpand(mc.ls(sl=1, fl=1, o=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one curve') + return 0 + factor = mc.floatSliderGrp('gsFactorSlider', q=1, v=1) + if factor < 10: + factor = 10 + for obj in sel: + distance = (mc.arclen(obj) * (float(factor) ** .5) / 100) / 10 + getCV = mc.getAttr(obj + '.spans') + deg = mc.getAttr(obj + '.degree') + if ((float(factor) ** .5) / 100 > .25): + getCV += math.ceil(float(factor) / 10) + mc.extendCurve(obj, et=2, d=(distance * factor), cos=0, em=0, s=0, jn=1, rmk=1, rpo=1) + mc.rebuildCurve(obj, d=deg, kt=1, rt=0, s=getCV) + mc.select(sel, r=1) + + +def reduceCurve(): # Reduces curve length + sel = mc.ls(sl=1, fl=1, o=1) + if not sel: + MESSAGE.warningInView('Select at least one curve') + return 0 + factor = (mc.floatSliderGrp('gsFactorSlider', q=1, v=1) / 100) ** 1.5 + for obj in sel: + degree = mc.getAttr(obj + '.degree') + spans = mc.getAttr(obj + '.spans') + minimum = mc.getAttr(obj + '.min') + maximum = mc.getAttr(obj + '.max') + posRange = maximum - (maximum - minimum) * factor + invRange = minimum + (maximum - minimum) * factor + iteration = int(math.floor(invRange)) + if invRange < .05: + posRange = maximum - (maximum - minimum) * 0.05 + mc.insertKnotCurve(obj + '.u[' + str(posRange) + ']', cos=1, nk=1, ib=0, rpo=1) + for _ in range(iteration + 1): + mc.delete(obj + '.ep[' + str(mc.getAttr(obj + '.spans')) + ']') + try: + mc.rebuildCurve(obj, s=spans, d=degree, tol=0.0001, rpo=1, rt=0, end=1, kr=1, kcp=0, kep=1, kt=0) + except BaseException: + pass + mc.select(sel, r=1) + + +def selectPart(selType, justReturn=False, inputObj=None): # Selects Curve, Geometry or Group + # type: (int, bool, list|str) -> (None|list) + """ selType: 0 - Group, 1 - Curve, 2 - Geo , 3 - Bound Curves""" + sel = mc.ls(sl=1, o=1, fl=1) + if not sel: + sel = mc.ls(hl=1, o=1, fl=1) + if inputObj: + if isinstance(inputObj, list): + sel = inputObj + else: + sel = [inputObj] + finSel = list() + for part in sel: + if mc.nodeType(part) != 'transform': + try: + part = mc.listRelatives(part, p=1, pa=1)[0] + except BaseException: + return + ch = list() + grp = str() + if ('pathCurve' in part) or ('instance' in part) or ('geoCard' in part) or ('geoTube' in part): + grp = mc.listRelatives(part, p=1, pa=1) + if grp: + grp = grp[0] + else: + return 0 + ch = mc.listRelatives(grp, c=1, pa=1) + else: + grp = part + ch = mc.listRelatives(part, c=1, pa=1) + if selType == 0: + finSel.append(grp) + else: + if not ch: + return + for ele in ch: + if selType == 1: + if 'pathCurve' in ele: + finSel.append(ele) + break + elif selType == 2: + if ('geoCard' in ele) or ('geoTube' in ele): + finSel.append(ele) + break + elif selType == 3: + if ('origCurves' in ele): + finSel.append(ele) + break + if justReturn: + return finSel + mc.select(finSel, r=1) + + +def groupCurves(): # Groups selected Curves in the Outliner + selectPart(0) + name = WIDGETS['gsGroupNameTextField'].text() + if not name: + name = 'crvGrp#' + sel = mc.ls(sl=1, fl=1) + if len(sel) > 0: + upGrp = mc.listRelatives(sel[0], p=1, pa=1) + grp = str() + if upGrp is None: + grp = mc.group(sel, n=name) + else: + grp = mc.group(sel, n=name, p=upGrp[0]) + mc.connectAttr(grp + '.sx', grp + '.sy', f=1) + mc.connectAttr(grp + '.sx', grp + '.sz', f=1) + else: + MESSAGE.warningInView('Select at least one Curve') + + +def controlCurveCreate(): + """Creates Control Curve from selected Curves""" + sel = mc.ls(sl=1, tr=1, fl=1) + if not sel: + sel = mc.listRelatives(mc.ls(sl=1, o=1), p=1, pa=1) + sel = mc.filterExpand(sel, sm=9) + if not sel: + MESSAGE.warningInView('No nurbs curves selected') + return + + # Finding closest points to ref CV + refCurve = sel[0] + refCV = "{}.ep[{}]".format(refCurve, 0) + closestEPs = [] + for curve in sel: + om_selList = om.MSelectionList() + om_selList.add(curve) + om_curve = om.MFnNurbsCurve(om_selList.getDagPath(0)) + om_closestPoint = om_curve.closestPoint(om.MPoint(mc.pointPosition(refCV)), space=om.MSpace.kWorld) + spans = mc.getAttr(curve + '.spans') + minmax = mc.getAttr(curve + '.minMaxValue')[0] + uMid = minmax[1] / 2.0 + finalEP = spans if om_closestPoint[1] > uMid else 0 + closestEPs.append((curve, finalEP)) + + # Finding average points between the curves + spans = mc.getAttr(sel[0] + '.spans') + step = 1.0 / float(spans + 4) + pp = [] + u = 0.0 + for _ in range(spans + 5): + point = om.MFloatVector() + for curve, ep in closestEPs: + minmax = mc.getAttr(curve + '.minMaxValue')[0] + if ep == 0: + uNorm = minmax[0] + (minmax[1] - minmax[0]) * u + else: + uNorm = minmax[1] - (minmax[1] - minmax[0]) * u + pp0 = mc.pointPosition(curve + '.u[%s]' % uNorm) + point += om.MFloatVector(pp0[0], pp0[1], pp0[2]) + u += step + pp.append(point / len(closestEPs)) + + # Constructing the curve + controlCurve = mc.curve(ep=pp, d=3) + controlCurve = mc.rebuildCurve(controlCurve, kr=2, rt=0, s=spans)[0] + controlCurve = mc.rename(controlCurve, 'controlCurve#') + mc.addAttr(controlCurve, ln='isControlCurve', dt='string') + + # Selection list for Warp deformer + selList = [] + for curve in sel: + selList.append(curve) + selList.append(controlCurve) + mc.select(selList, r=1) + + # Creating Warp deformer + mel.eval('CreateWrap;') + mc.setAttr(controlCurve + 'Base.hiddenInOutliner', 1) + mc.setAttr(controlCurve + '.wrapSamples', 100) + mc.setAttr(controlCurve + '.dropoff', 20) + mc.setAttr(mc.ls(controlCurve, dag=1, s=1)[0] + '.lineWidth', 3) + mc.setAttr(controlCurve + '.overrideEnabled', 1) + mc.setAttr(controlCurve + '.overrideRGBColors', 1) + mc.setAttr(controlCurve + '.overrideColorRGB', 0, 1, 0) + mc.select(controlCurve, r=1) + resetCurvePivotPoint() + LOGGER.info('Control Curve Created') + # mc.evalDeferred(lambda: print(), lp=1) + + +def controlCurveApply(): # Deletes selected Control Curve and clears History + sel = mc.ls(sl=1, fl=1, o=1, dag=1, s=1) + if not len(sel): + MESSAGE.warningInView('Select Control Curve to Apply') + return 0 + wraps = mc.listConnections(sel[0], t='wrap', et=1, d=1, s=0) + if not wraps: + MESSAGE.warningInView('Select Control Curve to Apply') + return 0 + back = mc.listConnections(wraps[0], s=1, d=1, et=1, t='nurbsCurve') + forth = mc.listConnections(back, s=0, d=1, et=1, t='wrap') + cleanWraps = list(dict.fromkeys(forth)) + curves = mc.listConnections(cleanWraps, s=1, d=0, et=1, t='nurbsCurve') + cleanCurves = list(dict.fromkeys(curves)) + other = [] + control = [] + for curve in cleanCurves: + if utils.attrExists(curve, 'isControlCurve'): + if utils.attrExists(curve, 'wrapSamples') and mc.listConnections(curve + '.wrapSamples'): + control.append(curve) + else: + other.append(curve) + else: + other.append(curve) + mc.delete(other, ch=1) + mc.delete(control) + mc.select(other, r=1) + + +def edgeToCurve(enableProgressBar=True): # Converts selected edge groups to curves + sel = mc.ls(sl=1, fl=1) + sel = mc.filterExpand(sel, sm=32) + if not sel: + MESSAGE.warningInView('Select Edges to Convert') + return 0 + result = str() + + if len(sel) > 1000: + message = 'Warning!\n\nMore than 1000 edges selected.\nComputation can take a long time ( > 10 seconds)\nContinue?\n\nNote: Selecting smaller edge groups is much faster\n' + result = mc.confirmDialog(ma='Center', icn='question', t='More than 1000 edges selected', m=message, + button=['OK', 'Cancel'], cancelButton='Cancel', ds='Cancel') + else: + result = 'OK' + if result != 'OK': + return 0 + + # Progress bar init + if enableProgressBar: + progressBar = utils.ProgressBar('Converting Edges', len(sel)) + + # Sort Edges to Edge Groups + obj = mc.ls(sl=1, o=1) + edges = dict() + for i in range(len(sel)): + polyInfo = mc.polyInfo(sel[i], ev=1)[0].format() + polyInfo = polyInfo.split() + edges[i] = (int(polyInfo[1][0:-1]), int(polyInfo[2]), int(polyInfo[3])) + + finalCollection = list() + while (len(edges) > 0): + edgeCollection = list() + edge = edges.popitem()[1] + origComp = edge[0] + # fe0 = edge[1] + # fe1 = fe0 + fv0 = (edge[1], edge[2]) + fv1 = fv0 + iteration = len(edges) + for i in range(iteration): + for key in edges: + nv = (edges[key][1], edges[key][2]) + if fv0[0] == nv[0] or fv0[0] == nv[1] or fv0[1] == nv[0] or fv0[1] == nv[1]: + edgeCollection.append(edges[key][0]) + # fe0 = edges[key][0] + fv0 = (nv[0], nv[1]) + edges.pop(key) + break + if fv1[0] == nv[0] or fv1[0] == nv[1] or fv1[1] == nv[0] or fv1[1] == nv[1]: + edgeCollection.append(edges[key][0]) + # fe1 = edges[key][0] + fv1 = (nv[0], nv[1]) + edges.pop(key) + break + edgeCollection.append(origComp) + finalCollection.append(edgeCollection) + + if enableProgressBar and progressBar.tick(len(edgeCollection)): + return + + if enableProgressBar: + progressBar.end() + + mode = 3 + if utils.getMod() == "Shift": + mode = 1 + + grp = list() + mc.select(cl=1) + + # Create cures from edge groups + for group in finalCollection: + edgeSelection = list() + for edge in group: + edgeSelection.append(obj[0] + '.e[' + str(edge) + ']') + mc.select(edgeSelection, r=1) + tmp = mel.eval('string $gsTempPolyToCurve[] = `polyToCurve -form 2 -degree ' + str( + mode) + ' -conformToSmoothMeshPreview 1`') + grp.append(tmp[0]) + mc.select(grp, r=1) + + +def orientToFaceNormals(): + mesh = WIDGETS['gsOrientMeshName'].text() + if not mesh: + MESSAGE.warningInView('Add the geo to the Target Field') + return + if not mc.objExists(mesh): + MESSAGE.warningInView('Target Mesh Not Found') + return + sel = mc.filterExpand(mc.ls(sl=1, fl=1, o=1), sm=9) + if not sel: + MESSAGE.warningInView('Select at least one curve') + return + N = WIDGETS['gsIterationsSlider'].getValue() # Number of iterations per curve + angleTolerance = WIDGETS['gsMinimumAngle'].getValue() + isRefresh = WIDGETS['orientRefreshViewport'].isChecked() + progress = None + if len(sel) > 10: + progress = utils.ProgressBar('Orienting Selection', len(sel)) + try: + for curve in sel: + if progress and progress.tick(1): + return + lDiv = mc.getAttr(curve + '.lengthDivisions') if mc.attributeQuery('lengthDivisions', n=curve, ex=1) else None + wDiv = mc.getAttr(curve + '.widthDivisions') if mc.attributeQuery('widthDivisions', n=curve, ex=1) else None + twist = mc.getAttr(curve + '.Twist') if mc.attributeQuery('Twist', n=curve, ex=1) else None + # invTwist = mc.getAttr(curve + '.invTwist') if mc.attributeQuery('invTwist', n=curve, ex=1) else None + if lDiv and lDiv > 10: + mc.setAttr(curve + '.lengthDivisions', 10) + if wDiv and wDiv > 2: + try: + mc.setAttr(curve + '.widthDivisions', 2) + except BaseException: + pass + if twist and twist != 0: + mc.setAttr(curve + '.Twist', 0) + # if invTwist and invTwist != 0: + # mc.setAttr(curve + '.invTwist', 0) + + firstCv = '%s.cv[0]' % curve + pos = mc.pointPosition(firstCv) + om_pos = om.MPoint(pos) + + targetClosestPointNormal = utils.getClosestPointAndNormal(mesh, pos)[1] + om_sel = om.MSelectionList() + geo = selectPart(2, True, curve)[0] + om_sel.add(geo) + om_mesh = om.MFnMesh(om_sel.getDagPath(0)) + + orientation = curve + '.Orientation' + prevDiff = 360 + currentDiff = 360 + i = 0 + + guesses = [] + while (currentDiff >= angleTolerance) and (i <= N): + i += 1 + currentOrien = mc.getAttr(orientation) + + om_cardFaceNormal = om_mesh.getClosestPointAndNormal(om_pos, space=om.MSpace.kWorld)[1] + currentDiff = om_cardFaceNormal.angle(targetClosestPointNormal) * 180 / math.pi % 360 + + guesses.append((currentDiff, currentOrien)) + + if currentDiff > prevDiff: + guess = currentOrien - currentDiff + else: + guess = currentOrien + currentDiff + + prevDiff = currentDiff + nextOrien = guess + + mc.setAttr(orientation, nextOrien % 360) + + # Setting the best guess as the final orientation + finalOrientation = min(guesses, key=lambda t: t[0])[1] + mc.setAttr(orientation, finalOrientation) + + if lDiv and lDiv > 10: + mc.setAttr(curve + '.lengthDivisions', lDiv) + if wDiv and wDiv > 2: + mc.setAttr(curve + '.widthDivisions', wDiv) + if twist and twist != 0: + mc.setAttr(curve + '.Twist', twist) + # if invTwist and invTwist != 0: + # mc.setAttr(curve + '.invTwist', invTwist) + if isRefresh: + mc.refresh() + + except Exception as e: + LOGGER.exception(e) + finally: + if progress: + progress.end() + curveControlUI.updateUI() + + +# TODO: Finish this alignment function for 1.3+ + +def alignTwistToMesh(targetMesh, curve, geo): # type: (str, str, str) -> None + """Attempts to align the twist graph to the normals of the target mesh on each step point""" + if not mc.attributeQuery('Offset', n=curve, ex=1): + return + + steps = 6 - 1 # How many points to probe along the curve minus the first point + targetMeshSel = om.MSelectionList() + targetMeshSel.add(targetMesh) + fnTargetMesh = om.MFnMesh(targetMeshSel.getDagPath(0)) + + geoSel = om.MSelectionList() + geoSel.add(geo) + fnGeo = om.MFnMesh(geoSel.getDagPath(0)) + curveSel = om.MSelectionList() + curveSel.add(curve) + fnCurve = om.MFnNurbsCurve(curveSel.getDagPath(0)) + k_min, k_max = fnCurve.knotDomain + try: + warpNode = mc.ls(mc.listHistory(selectPart(2, True, curve), ac=1, il=0), typ='curveWarp')[0] + except BaseException: + return + sel = mc.ls(sl=1) + attributes.resetMultiInst(warpNode, 'twistCurve') + finalList = [] + for i in range(steps + 1): + if i == 0: + param = 0.0 + diff = 0.0 + toSet = 0.5 + else: + param = i * (k_max - k_min) / steps + pointAtParam = fnCurve.getPointAtParam(param, space=om.MSpace.kWorld) + geo_p, geo_n, _ = fnGeo.getClosestPointAndNormal(pointAtParam, space=om.MSpace.kWorld) + target_p, target_n, _ = fnTargetMesh.getClosestPointAndNormal(geo_p, space=om.MSpace.kWorld) + mc.curve(ws=1, p=[list(geo_p)[:3], list(target_p)[:3]], d=1, n='GS_DEBUG:debugCurve#') + diff = geo_n.angle(target_n) + toSet = mt.lerp(diff, 0.5, 1.0, 0, math.pi) + finalList.append((i / steps, toSet)) + mc.select(sel, r=1) + finalList.append('twistCurve') + attributes.setMultiInst(curve, finalList) + + +def changeLayersToNumbers(): + if WIDGETS['layerNumbersOnly'].isChecked(): + for i in range(10, 20): + WIDGETS['curveGrp%s' % i].changeLabel(str(i)) + else: + letter = ord('A') + letters = [chr(i) for i in range(letter, letter + 10)] + for i in range(10, 20): + WIDGETS['curveGrp%s' % i].changeLabel(letters[i - 10]) + + +def setAOSettings(silent=False): + mc.setAttr('hardwareRenderingGlobals.ssaoAmount', 0) + mc.setAttr('hardwareRenderingGlobals.ssaoRadius', 1) + mc.setAttr('hardwareRenderingGlobals.ssaoFilterRadius', 1) + mc.setAttr('hardwareRenderingGlobals.ssaoSamples', 8) + if silent: + return + MESSAGE.printInView('AO Settings Applied') + LOGGER.info('AO Amount set to: %s' % 0) + LOGGER.info('AO Radius set to: %s' % 0) + LOGGER.info('AO Filter Radius set to: %s' % 1) + LOGGER.info('AO Samples set to: %s' % 8) + + +def setTransparencySettings(type=1): + """ + Type of transparency setup: + 0 - Simple (fast, but less detailed) + 1 - Object Sorting (average performance, better accuracy) + 2 - Depth Peeling (slow, but more accurate) + """ + if type == 0: + MESSAGE.printInView('Simple Transparency Settings Applied') + mc.setAttr('hardwareRenderingGlobals.transparencyAlgorithm', 0) + elif type == 1: + MESSAGE.printInView('Object Sorting Transparency Settings Applied') + mc.setAttr('hardwareRenderingGlobals.transparencyAlgorithm', 1) + elif type == 2: + MESSAGE.printInView('Depth Peeling Transparency Settings Applied') + mc.setAttr('hardwareRenderingGlobals.transparencyAlgorithm', 3) + mc.setAttr('hardwareRenderingGlobals.transparencyQuality', 1) + mc.setAttr('hardwareRenderingGlobals.transparentShadow', 1) + LOGGER.info('Transparency Quality set to: %s' % 1) + LOGGER.info('Transparent Shadow set to: %s' % 1) + + +def alwaysOnTopToggle(*_): + if MAYA_VER >= 2022: + allCurves = [] + for i in range(80): + if mc.objExists('curveGrp_%s_Curve' % i): + curves = mc.editDisplayLayerMembers('curveGrp_%s_Curve' % i, q=1, fn=1) + if curves: + allCurves += curves + if allCurves: + firstCurveAttr = 0 + if mc.attributeQuery('alwaysDrawOnTop', n=mc.listRelatives(allCurves[0], c=1, typ='nurbsCurve')[0], ex=1): + firstCurveAttr = mc.getAttr(allCurves[0] + '.alwaysDrawOnTop') + for obj in allCurves: + shape = mc.listRelatives(obj, c=1, typ='nurbsCurve', pa=1)[0] + if mc.attributeQuery('alwaysDrawOnTop', n=shape, ex=1): + mc.setAttr(shape + '.alwaysDrawOnTop', not firstCurveAttr) + else: + setAOSettings(silent=True) + utils.AOToggle() + + +def collectionVisibilityToggle(cb): + mc.optionVar(iv=['GSCT_AutoHideCurvesOnInactiveCollections', cb]) + updateVisibilityBasedOnActiveCollection(True) + + +def updateVisibilityBasedOnActiveCollection(force=None): + cb = mc.optionVar(q='GSCT_AutoHideCurvesOnInactiveCollections') + if not cb and not force: + return + currentCollection = WIDGETS['layerCollectionsComboBox'].currentIndex() + allLayers = [x for x in mc.ls(typ='displayLayer') if ('curveGrp_' in x and '_Curve' in x)] + mainLayers = [] + nonMainLayers = [] + for layer in allLayers: + split = layer.split("_") + if len(split) == 3: + mainLayers.append(layer) + elif len(split) == 4: + nonMainLayers.append(layer) + if cb: + if currentCollection == 0: + for layer in mainLayers: + mc.setAttr(layer + '.visibility', 1) + for layer in nonMainLayers: + mc.setAttr(layer + '.visibility', 0) + else: + for layer in mainLayers: + mc.setAttr(layer + '.visibility', 0) + for layer in nonMainLayers: + if int(layer.split("_")[1]) != currentCollection: + mc.setAttr(layer + '.visibility', 0) + else: + mc.setAttr(layer + '.visibility', 1) + + +def alwaysOnTopToggleLayer(layer, *_): + if MAYA_VER >= 2022: + allCurves = [] + if mc.objExists('curveGrp_%s_Curve' % layer): + allCurves = mc.editDisplayLayerMembers('curveGrp_%s_Curve' % layer, q=1, fn=1) + if allCurves: + firstCurveAttr = 0 + if mc.attributeQuery('alwaysDrawOnTop', n=mc.listRelatives(allCurves[0], c=1, typ='nurbsCurve')[0], ex=1): + firstCurveAttr = mc.getAttr(allCurves[0] + '.alwaysDrawOnTop') + for obj in allCurves: + shape = mc.listRelatives(obj, c=1, typ='nurbsCurve', pa=1)[0] + if mc.attributeQuery('alwaysDrawOnTop', n=shape, ex=1): + mc.setAttr(shape + '.alwaysDrawOnTop', not firstCurveAttr) + else: + MESSAGE.warning("Layer is Empty") + else: + setAOSettings(silent=True) + utils.AOToggle() + + +def convertSelectionTo(targetType): + # type: (int) -> None + """ + Converts selected curves to selected targetType + 0 - Warp Card, 1 - Warp Tube, 2 - Extrude Card, 3 - Extrude Tube + """ + sel = mc.filterExpand(mc.ls(sl=1, fl=1, tr=1), sm=9) + if not sel: + MESSAGE.warning('No curves selected.') + return + + # Init + dialog = 'Skip' + compatibleCurves = [] + allCurves = [] + finalCurves = [] + + # Check if bound + sel = [obj for obj in sel if not mc.attributeQuery('Axis', n=obj, ex=1)] + if not sel: + MESSAGE.warning('No compatible curves selected.') + return + + # Check for scale factor + for obj in sel: + if not mc.attributeQuery("Orientation", n=obj, ex=1): + continue + if mc.attributeQuery("scaleFactor", n=obj, ex=1): + compatibleCurves.append(obj) + allCurves.append(obj) + + # Check for dialog result + if allCurves != compatibleCurves: + from . import ui + dialog = ui.scaleFactorConversionDialog() + if not dialog: + MESSAGE.warningInView('Operation Cancelled') + return + if dialog == 'Skip': + finalCurves = compatibleCurves + elif dialog == 'All': + finalCurves = allCurves + + # Init progress bar + progress = utils.ProgressBar("Converting Curves", len(finalCurves)) + + finalSelection = [] + for curve in sel: + if progress.tick(1): + break + + # Find components + curve = selectPart(1, True, curve)[0] + geo = selectPart(2, True, curve)[0] + grp = selectPart(0, True, curve)[0] + firstCv = '%s.cv[0]' % curve + firstCvPos = mc.pointPosition(firstCv) + firstCvVec = om.MVector(firstCvPos) + om_sel = om.MSelectionList() + om_sel.add(geo) + om_mesh = om.MFnMesh(om_sel.getDagPath(0)) + + # Get original attributes + origProfile = None + if mc.attributeQuery('Profile', n=curve, ex=1): + origProfile = mc.getAttr(curve + '.Profile') + origLattice = getLatticeValues(curve) + origAttrs = attributes.getAttr(curve) + origGraphs = attributes.getMultiInst(curve) + + # Get original material + origMaterial = utils.getShader(geo) + + # Get original layer + layer = mc.listConnections(curve, type='displayLayer') + originalLayerID = re.findall(r'\d+', layer[0])[0] + + # Get a vertex position closest to the root CV of the curve + origProfileVert, origProfileVec = utils.getMiddleVertAndNormal(curve, geo, firstCvVec) + + # Resetting attrs before orientation aligning + utils.resetAttributes(curve, geo) + _, targetVec, _ = om_mesh.getClosestPointAndNormal(om.MPoint(firstCvPos), space=om.MSpace.kWorld) + + # Duplicating curve + topGrp = mc.listRelatives(grp, p=1, pa=1) + duplicatedCurve = mc.duplicate(curve) + if topGrp: + duplicatedCurve = mc.parent(duplicatedCurve, topGrp)[0] + else: + duplicatedCurve = mc.parent(duplicatedCurve, w=1)[0] + mc.delete(grp) + + # Cleanup + mc.select(duplicatedCurve, r=1) + filteredCurves = [] + + # Convert + if targetType == 0: # Warp Card + filteredCurves = create.multiple(0, hk=True, progressBar=False, keepAttrs=False) + elif targetType == 1: # Warp Tube + filteredCurves = create.multiple(1, hk=True, progressBar=False, keepAttrs=False) + elif targetType == 2: # Extrude Card + filteredCurves = create.multiple(-2, hk=True, progressBar=False, keepAttrs=False) + elif targetType == 3: # Extrude Tube + filteredCurves = create.multiple(-1, hk=True, progressBar=False, keepAttrs=False) + finalSelection.append(filteredCurves[0]) + + # Align vars + curve = filteredCurves[0] + firstCv = '%s.cv[0]' % curve + firstCvPos = mc.pointPosition(firstCv) + firstCvVec = om.MVector(firstCvPos) + om_pos = om.MPoint(firstCvPos) + om_sel = om.MSelectionList() + geo = selectPart(2, True, curve)[0] + om_sel.add(geo) + om_mesh = om.MFnMesh(om_sel.getDagPath(0)) + + # Iterate align normals + prevDiff = 360 + currentDiff = 360 + iteration = 0 + iterLimit = 10 + tolerance = 0.1 + guesses = [] + while (currentDiff >= tolerance) and (iteration <= iterLimit): + _, faceNormal, _ = om_mesh.getClosestPointAndNormal(om_pos, space=om.MSpace.kWorld) + currentDiff = faceNormal.angle(targetVec) * 180.0 / math.pi + currentOrien = mc.getAttr(curve + '.Orientation') + + guesses.append((currentDiff, currentOrien)) + + if currentDiff > prevDiff: + guess = currentOrien - currentDiff + elif currentDiff < prevDiff: + guess = currentOrien + currentDiff + else: + guess = currentOrien + nextOrien = guess + prevDiff = currentDiff + + mc.setAttr(curve + '.Orientation', nextOrien % 360.0) + iteration += 1 + + # Setting the best guess as the final orientation + finalOrientation = min(guesses, key=lambda t: t[0])[1] + mc.setAttr(curve + '.Orientation', finalOrientation % 360.0) + + # Restore the original attributes + attributes.setAttr(curve, origAttrs, exclude=["Orientation"]) + if origGraphs: + for graph in origGraphs: + attributes.setMultiInst(curve, graph) + + # Fix Profile + if mc.attributeQuery("Profile", n=curve, ex=1) and origProfileVert and 'Profile' in origAttrs: + newProfileVert, newProfileVec = utils.getMiddleVertAndNormal(curve, selectPart(2, True, curve)[0], firstCvVec) + + # Checking if normals of the flat card are correct. If not, flip the card 180 deg. + normalCheck = origProfileVec.angle(newProfileVec) * 180.0 / math.pi + if abs(normalCheck) >= 90: + mc.setAttr(curve + ".Orientation", (mc.getAttr(curve + ".Orientation") + 180.0) % 360.0) + + # Checking if the angle between two middle vertices is correct. If not, invert the profile. + angleBetween = (firstCvVec - origProfileVert).angle(firstCvVec - newProfileVert) * 180.0 / math.pi + newProfile = origProfile * -1 if abs(angleBetween) >= 90 else origProfile + mc.setAttr(curve + ".Profile", newProfile) + + # Setting the original Lattice + if origLattice and mc.attributeQuery("Length", n=curve, ex=1): + updateLattice(utils.fromDouble2ToString(origLattice), curve) + + # Apply original material + newGeo = selectPart(2, True, curve) + if newGeo and origMaterial: + mc.sets(newGeo[0], forceElement=list(origMaterial)[0]) + + # Sort to original layers + curveAddToLayer(originalLayerID, inputCurves=[curve]) + + progress.end() + mc.select(finalSelection, r=1) + + +def toggleDynamicDivisions(): + """ + Toggles dynamic divisions on selected curves + If dynamic node set is not available - create one + """ + sel = mc.ls(sl=1, tr=1) + sel = selectPart(1, True, mc.filterExpand(sel, sm=9)) + if not sel: + return + for curve in sel: + if not mc.attributeQuery('lengthDivisions', n=curve, ex=1): + continue + divConnections = mc.listConnections(curve + '.lengthDivisions', d=1, scn=1) + tesselateNode = None + for targetNode in divConnections: + if mc.nodeType(targetNode) == 'nurbsTessellate': + tesselateNode = targetNode + break + if tesselateNode: + create.addDynamicDivisions(curve, tesselateNode) + mc.setAttr(curve + '.dynamicDivisions', 1) + else: # Just toggle the dynamic divisions functionality + mc.setAttr(curve + '.dynamicDivisions', WIDGETS['dynamicDivisions'].isChecked()) + + +def toggleAutoRefine(): + """ + Toggles automatic curve refinement + If no auto-refine node found - create one + """ + sel = mc.ls(sl=1, tr=1) + sel = selectPart(1, True, mc.filterExpand(sel, sm=9)) + if not sel: + return + for curve in sel: + if not mc.attributeQuery('Orientation', n=curve, ex=1): + continue + curveRefineConnection = mc.listConnections(curve + '.curveRefine', d=1, scn=1) + rebuildCurveNode = None + for targetNode in curveRefineConnection: + if mc.nodeType(targetNode) == 'rebuildCurve': + rebuildCurveNode = targetNode + break + if rebuildCurveNode: + newNode = create.addAutoRefine(curve, rebuildCurveNode) + mc.setAttr(curve + '.autoRefine', 1) + mc.setAttr(newNode + '.isHistoricallyInteresting', 0) + utils.deferredLp(curveControlUI.updateUI)() + else: # Just toggle the dynamic auto refine checkbox + mc.setAttr(curve + '.autoRefine', WIDGETS['autoRefine'].isChecked()) + utils.deferredLp(curveControlUI.updateUI)() diff --git a/Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Bold.ttf b/Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..3742457900d51ae5c34ed53657ed8a72f17f19c6 GIT binary patch literal 167336 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 z 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 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Regular.ttf b/Scripts/Modeling/Edit/gs_curvetools/fonts/Roboto-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..2b6392ffe8712b9c5450733320cd220d6c0f4bce GIT binary patch literal 171272 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 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/icons/drop-down-arrow.png b/Scripts/Modeling/Edit/gs_curvetools/icons/drop-down-arrow.png new file mode 100644 index 0000000000000000000000000000000000000000..4452f3434c0de6b68bf395a27d3bd99e7816e7e2 GIT binary patch literal 234 zcmeAS@N?(olHy`uVBq!ia0vp^5(0r%1WBel`Z_W&Z0zU$lgJ9>a~60+7BevL9R^{>}V;X%kutCrW#= VyZk#lAbW681eDnYIxx4f1 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 ZgWaZ9WVdvH_NoyWhDEZLH5$+F~!{6fa>C$a zmTodlN_U1%ooRMDopvY5wB4DV>9pRR&Q3bnoirhvb{f*shF4xZS^@?G#$dp2{KA%P z`60>HwIxg1KZI01}q5V=>kS2>`xnG#dLF8ym0t zZZ24$`{H?r{S?D6u-R;wnVG@t>?~X^*Yb2X#)=@3NRW__0Hsoiq@*Os<#MiO-|$%? z5wPI%G=nv4nayUHOr~h}c>#uDFg!dAo6Ux#q$DIIB_TC6b=lq@gG3^cAT>1=I-QRF z(G-B1hK7dTxcfKW082dm{r#|7t;^6&FYWE^7#J8>xE>oD0{|o?C84aW3`t2z%hs0| zUR*90OePcTb~|!&bNxR`hF3E$itbyVyZ)aFVR!!S(5;*mR*k@3+ zUXNE_eU;aDGYo@6hYs<2CI&ZSV`ES%m297ApBO9w*=)9mUm_B5E-QuP$dMyd?LPYG zBi`RF7K?d56N4L<%f+RQePRIX{O`=n%tZW>xVX6Rjabn7S^oYJEinNJ3x_t;Vz9O+ zW@l$3et|?H3Ezl?N~MbUec_NwrBVCthY=2KR0o)hrL6O?1!Q3yc;lm{kk1T_(OC(7 z=_7dc{V&6|`}IF_LTU(wgs2en#_vQ66{FWE{h9w2acI3}RzBB+4kCaY z0p9=Ze=Xem?-Q5t#r5tb^+_lJ?b)*j>({S`R;z_Vp;-8jjoogC-EN1|>BQ936ncAm z(bm?6*49=*`mx#&R6y*FH^#xwA&-}m-I;UD^X~-zrKq6P)zzWAyc~MHp4yl47nWYH zM`>v(cJJN|tJR9p(NVOtwBW>v6P(Y!_S$PGEe-Dvt^_Afp5$D!i?V>6yq-^zlNM}x zc8+%+;{1kDkH-Ty4=d>7l~-OtRaF(Ig^3iEN(Gflg@S?t?Ao;pw{PFZ#ful$yk!zp zKv_Sj>q>zz+9J}34{p?B%ANTIvTh{kQgQyN+vrc{%NVP-($yoxT*_NrOsK9{( z2e@{cqt2;Qr*|TQCwV1h{t zpp=vp;(Bt1ej1*sC4bCO%QXM@Cz3u?Fbj0xzyWrMW>D;QJII&LEQtImJ!nTRz&kzp51SFA2kes|2R_*b4VBs-@txF@*(C&@% zkH=0|lS76tuG*i9iVAGqO1EQgHk)zy@L@rGl@ljU;KYd&sHmuTAbcMmf(a-yGn2SB zPxFf_%1u=h*S#ZCyq*_}L`YMSvp}wC^0UuAOSP(OHk9dQ20%-k+&aK(02((UMlH?a|vkOy5KQKKDxTPf^K%k2t*J8iA17>AIIbIz~k{i zuTdf^C6Q`#d_x{-y>9gKPb(_VCTD=1ot?z>fddD^`aVvl6CZ#4G4FRj%n0H{S5#Ci z1e5t90(X{WVP{zure~e#G)FFWbtY5!A?Dq^0566Zso(LkK9Xoae{VXv-iW);IY7E{S_sN+#beb&a9I>E%coL75 z8wmerWNI1{HhKpf0IJuK0^||_+ehQ#L0KJMP6QE9Mn=X$eZ`6 zo2;y?u-_AE+apGBmTZC8tGeyOlepYHip$-j0Dx4L97P$)D9TWyAYBE8-;d3%xG_6B zi!@aN(#o>%L|GQ*U2gQ5ZRi}apwnbQ|M)cKS*b$1Dzk~}#>U2gKC0E~fO~90ZM?<^ z&XNVh_J6MTlRw0mmBE;m!IvGw5Q{{})g_`>uR(cs3UYFCh_>uB=QxQN`RQupr>pU` zDsRqL&!}}lP;p{0vQrX?XSnv0Xle{CXC0&mi6sfByu6&)K%JR$;!4kG(DrVR2fbr9 z^p4q})g%-ClAW0in^rB0lb|G1gOW_|uK)b~cxUg>>A{^lchKL@&xi66Y_~Z^XqF@( zw)MHxJqnjQd>V}`PJ%rfi->#goNhr=#{i1;8Wd%yQKV0XS{}L{kS~DV=fHpQ#TT6I zXoj%Z^gS{n7LZD%3Wz_s)HOo&?5>&u$o+hw=~)N&6n|-WS)q;hYbQ zpB|c=!O-Lk&fdO`BUeW{;Gci~dBjPCe1h*|kVGt?#~yo(xW00i zZvVCbll^&0?b}GynOjGEEWr|?}#BByan|50^O%i6_h|U| z1ya%=|9Jh9}oLnY?~r_@D|X{ojZx^3!QXR_it<|Bd*6R zGq}{ouW;s56{PjKefu_shWLek1Y^&hJ%W7C!v-$_m6eqdnHTdeH!f4G{iqc2c&a*| zxcA}tR`8j0XK9ou){~hVw7w6`EK94^hHVog5MBZ@8jZyDm7Y=9DQftz#bdVG@kL8d zkk;HhSx#D?fq?*l|CXy6F}UpV&agBA>n4 zi&-9}>!cDfj0NQI^2sNkP(KfNH%e*zpeU@xryo&_7?Y_s7L|27q7LF!k0 z{O-sk@0|AytbqBmXU|4dO@DB3Fl-y0PDfRwAjTq80;;O2BCc=r(=E&ySqq0v9sEkV zNhM;GXOSgiBHc7SeE2ZcFh*N=A%>T*0uqTt0nTOH5MAMe4SD47)zlf@3iwlzlLCp~ zs`t>)5GE%0<;?rTm95G~rBdOw*ItYG9S<8}1!OQ7hyvnMGjkZHsJxM?RzjJuIBT18 z&ZEaP$*Xfk>0~6~wQJXSJc^Z;#aaRN1I5bJP!>GQ4K%gXl-r9xpU{Rd-rat_5}q6ICSU`y1Kg1*w`3R8M?Z< zIv5NF;t1^rrT$P@0kP{IPE3c6{BtlfxOw6*{`8Z-5%&u!YLWYR9rejO?0Sb>u`@7)j$8iaFMo;GUw@st*rpHv zGcRIL_(1G<;h7K%h}{=Xaf-)>b9M%e|MzREj!DI2k%njlzQ6hAn-M1>#_$r>0{IF? z`LalV72-_N%K!LY*caL-$MAAZGRn)#d2}1I#S&TODbPhJW~jvT@F zzyEzSHa13GNJ7jeR|{ds1*_Fc3}L~@$;p9ArJ}MzPe1)MeGl*Bt3ed)Gf>}t`)vT; znu^86#Z;BeXz-m_I5;@?z)ax*3IUCbj1U!AuihQQ`(JWL6lQJQ1?%b1k%H=sb@-p= zDWExYNQMZ&zyIUrg%f;aG8uAma+b~M_5YE5>o`iKGUR!wz+wi-)YKH5PUo_nnR?{N zk*FMH5RR|{x_kF7aji{C2+|VB|H{`ebo&Y{ChnP-T$zYR>JCzM%mbj;Y+ESh0s#5> z`71JOT8STKtq|4+V#gC_Ym|^mxF3_rmDv9BPmx>mSiomHcI?30AAH2?2=5+Nz8T+{ zTa4%v)&iMKCRnZ31t&R9B1UPZ1~&${_qJqmB{tT54^?{)Aki_2gS$7uU@*X7Fu>_@ z;aJ;8aD-Tl^rwAz3VW&-i-T8IRzfP3K2T2RT8Ap2j6Hs{i+Xb*lPlp+XJf<0%`h0e zR!1t4pn5&MQ`$d1O&nSm7Z(>09T{U~iAX?YS@df%mpaHGvd1duhcLifxWS*=+FD-E z$A~Ue0=jYI1~Gk3Ops!I8r{0jxtpX9wYfMO$;#l4IGdZSp|Yl?1_=rD&&P-^R03ic z1|1#b)V+dY7z2QgduH^HEe4^9MIt;^lON&si{E=YOYqY)e>yX zj>pr53HWMJ0$!+A;+YaTo+*)IgI0kOwSz5 zGvP(CWnDbJU8BUszQ`-JMFMqZD#I`VRp8kfqrq^@y8`mz_kX1vRrbHf6Ji*W^#MqgifS>JBEUgN)`I$jl2l7QLSxcy}uy9odbye$S_}MNscIFG`=ojgvcy*f!KYuzI z)p{~MYbC`4@Dxy2R~K=Rty~rdVet`jny~ofJjeR!8x4-v3lCT)2Sw`NirQYPk#>a_Opj z4VtZJ?ISCi?f(k>{#MtB_rxB5R)ZA^C!XVN41Rcg690bIiqqY5=rGR{v!y|c3mxWp zobH~(Pd~Nb7iTQ!G_Q^A8r}k87zWMF&BQ(Sfp4Ms^F=Z_@X?|yWTqqqX&uO^8>F?_ zxN+l(Ru_A|LLO8+EyE7{+h-OWYh!p#%Lgqk{OY_7AKnZ6PIp=4`+ZeocX_{&Z9*oo7 zb9k?Lb&Dt>0kyZcV`PLJinf)|Z<79nRo?gUN&^xT=&QwldUJ7q&ExT4>(;GA@z3Qz zAr;|34R=u4d(AUA*2eGL*I-=iTiqgxc)Gw|34U~a8YE(xL%xCYxB4({U5q=H#!2vW zZRja^yi|hHEDfHkHQ>pz#Um_5A`!%5aYSLVOJQ$u0_SY&cpJ0qi+L+a!~(i-;Q|qt zBbUXYiqe^OdptPSL>{p6bPXw>xcl1|dp6|a2VbqiFP`6w?>1iZ&D zEZ3PAI;Dhj=xP~u2xfVFtcIz2C?Za_Vi*ROFJC4KGHuFBLo-F?Rsi_?dN;ncqiP`o zHC3&|S8MX&bj+eKU5)i=q0a|nBbUpub?epu|1269bGU_ir1jwv53s~jo7uGrOS3dY zJ;PAg=bSiyiwyNR@OUMjtude^GyI9oj(Hcxtjxk`(Li7o{b*t2m~|a*W0vGFvsnw` z0hV~=WqW%&#>dBrD(~glnuQaa*=Q3JP>`WUVY(WH8ET^ZFW|Kz!BThJ9uEe_r_nJy zg$|Piy=EJf@o{+L9~Ns@XJ%%iq@*OO=bvV)C7gR(r@O-!-J)f!hzAhh)aJ8i&tm`n z{R`J+S&ITnOpu~TpNzsZl~*uHq~G9lIx#;#Poym;BqV@gyhFjLl|lRPBsxu2bd6ZR zu*w5l><+XJP7tw4n>TNcs(`pQH3z2VBMQIZVl9aW5I{iJu3cLYkVqs#iC%+!kK~~+ zLk+z~iE#5WhGB^F<82NPjx=_meP{|(_VBZrD?MX`fNE=N@$S3tM)YT)p-2}vL^loy zGwWJw;sFE@kjZ3%-EN0Mp@7HZLA+Fg9i{ZU>|H}<)D$lYNTHBZAKnW9ckWHV?e;)? ze+eNmF)^ycSPIARB}5RHLMnn*F2-?zXK~R+kdha}!^6nW4K=8Hreo-Z2|hJ!t@F?jZ$Kn#=F# zr7;@=vyC+WwDk{#AX`np;Q62-Sm<1Hb2G7iQ;#Y;~x?tptS3nTyo z17y&9s4_5I+%uB`%H}B`_Q}y&trkk9@M|g^9Fc-tTN@7xf>o|$AFWLrVCG}MObQ?; zm7joIZV#-C11kCAMrTe=j^NYz4s+lTmZOe?LMjq$HJXNMy$lC8lTitsW+#4i-nMLg z00gu^48vf~v6zCKm>>;WM~I8Tanb_i@U%dHck9C+b{3@&3|d?mw+Bw!g-qk2p{_D8 znC;?#gQI115j6kAY$lWcQdQjHo?Hx$kpeQL^N%nz+sIj8v?{wc_6Pn@drRcdDVN6= zdcGnsS#jL%T9H6DK?O8EP7Y&Q9>ds})lL+ilFOt}DfsyoYzq`M0R{P&zQ3IQA55eK zi^CQ&8h{Ui3TTW~m_eiDCxGDza{NwJ^K*j7SOSVxX<+k=3#YpRw;if8Wq76}dLqhJ zOE}}OJIzjuEZ)=IZr9=#v)c>63EXW*26 zZ;2dR)(Jl=%T`P9>eeLAIP5>(T)6`2f(mGMmNcXBJOwmnC9@CGlKEL62kS6i_CYa1 zVt0D*-ZkzY?%Op=>=n=wZCMwOpFE-F6wiCjGb_3m7hM7Ih-n4@GiwM+N#yq<%sH1& zKmc%k*ugoJ0l+gQavXd#aY>G~PAS2`M-zj_2wm)-Tk#AKK~FGY9}&QJ2%E5x&!_Ml z!q`VqMO#1s@L?+hxlGKNom!nKLv^MM$J!X2>7J!tC-T)5+POYHXrYEqvlH)KTlsu3 zK?Sr_A82x#Wr33TSs>>;>60v1n$MB@K1i?>XI~|<0n0oZ`yCjt%!BbOuAoy&pplDE zq?00BB|&v&=zd@9pIa5;0R$G1!$D4O`K09lzyw=BJclr+ixiMlx;zyuy#CM(7>7G( zsx#nSvCgSn+F$wUE(RY8oIbP^1QpQS9627-IMG)>`Yrmdom;s6&+mqA|H%G>SXaA) z^D~#593qxWKt4WfWzae9!nbP`kV}QF5j1Xh;k|1!oLea?NmK~oo_kT_# z5CZ|E$e%%r3xgKdifuUvfkYB$Ma2M! z!@ZVrjDXv{I17sQ$%3@doc9`o4i0U!hyf544we$dw^~HwPo&l~Mj#y8m=Xgx>VHS2 zQgNCp8vHHLYGE~sdMzT6NVpDGJ}RoOuQvc7&KxkC%`llv%hIviw{K%;Xo$LfSy>q} zGwD;OV+cb=Mg~`|U7on1p`jN5QH?;S(?$Il?-*+bl}g1Wo_8A>8hU+Gls5nnF{Qb= zxzOwN5f5iE);~k(ZZ;w6ruRmCBfy9s(p1 z36x4D($dn9mzPHr5A);2`tBn=_Q}C`49`*SL*2xn0wMF!N{t0000yAoASeQcTUKY95*Q2y(Ka+R#I3%* zUPdN`-EPO|=%`!(VR0qE*Q}ge9DnvW-aB(e)`tj)JlX<$J|AEI4^ahZG#UW&d~eig z;ayr0-=*%<;`*Q)wY9Y(`GpG?(9_eyq=kir-1__b;dZ;ladmZdFquqjr$(UuI-M?D z0FhHiSc3xc%qjT&=DiF_(`#|r?H1ijI5Z{;sRmbqLtq-!UT}}u3D!#;HnSi;DCBN9@Qq|&FA+*ov7p$ z3n@w~5mt6TP)OZR`#NxAhM&ueve5nWpZN6KF?`hhE8B$qizP$>L{6zU`|mRL?@4Fd zH^YDN^;m&f3@l;Q$;rtvks%5wFE7XS(CW(ycHz=MFj&a-`RRIgKSGs|K)Jt}RNIsQ znv5EGfIuD?n7GCFX*;!W&rFMH<#0H-A(URyA|%YU6>t!eJ11v?sW;Yp7G*u3U@5H# zsk!^`M2b#awSbO}4m3A6bIP{1wu-4Gq;W$-!`3`NAfon{OE7ljEc*KTfT{2K`B;HT zHi$ex0t8~(3Ti|ycxSUv3KJ5V&h+Ey>eEm-hmlutf}crPWhnI`MYOfG-78)j98%P8 zJ+J_ZY??6Nqz7;aC*Nv>R+Wg4FaF3|V<@%o-b%}>02(V0Cuaj#<65`;P4`9N* zipAA6w*5A}A{!gF1(asbWTx;vWdLOYodtw}8!l8}rT@lw799~|Am*9waor;E4Y z!=pQJqDag4>mf`4d4S5UYu?K>pkjY8&FFZJJa(mc!J@|D-AU-3TEgp>=J3k-S+rjZ zen}lPt7QG7dVpNXBGQ8eW@z#j%JO&NwIfgB-52-q89}uo8i<@+{MCq$T`p{gIw3Af z?{I{j1$2@%oGRwXUtA5q9|&M>X$^0G`4jY76<#eXL1$-Y*aUkF2~5;GwJh3*{D4`7 z3AbNHxS;?FCaQezpC9CseDcU$1fby6J6HTzT8rOQs(E~O(SGfY%pMI2sZGcRy*7!T z>02Q0aijR&IqQYl9R6A6nD>V~A^WyrPykYJt_MY=U3$cNm%~%%_MBH1h_vSvd=&Z8 zfTL}p44@RG*jNwIWruM2^A5yMW@GHT5#$`Mmc18n(YlCqostPDAbqQAFzMo1#Am;F z*R25rnE)y_hb%c<0Z+a21qQo=_NK-r{w)8)CYyu3KzmLe$TBFAtcho@{9s?^c-ROs z0hEdsk4H9S**3)Fkq46$L`ZEbkO`nn5K3!iW~OMDetgCfB)~>LiqZZLr2=9Nqmi@A nMgpqnFO@_*Ft+Hh0jT;9-c%t*{jria00000NkvXXu0mjfej0d6 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_stop.png b/Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_stop.png new file mode 100644 index 0000000000000000000000000000000000000000..14be446d5c643ed80ff29392f454070efd0f0b7d GIT binary patch literal 7011 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QfnV?{L%r;DM#O1dSjA3KQcFKkg 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 Udn}=-G5`Po07*qoM6N<$f&&q63;+NC literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_ui.png b/Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_ui.png new file mode 100644 index 0000000000000000000000000000000000000000..5e0b410602eee2218353ba155972d960eb5aef3e GIT binary patch literal 6918 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 JCGr-*{{h9&D2@OC literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_ui_legacy.png b/Scripts/Modeling/Edit/gs_curvetools/icons/gsCurveToolsIcon_ui_legacy.png new file mode 100644 index 0000000000000000000000000000000000000000..e5bc3dcadb0ec28d04bfc03cf13d51ce53e3b771 GIT binary patch literal 1240 zcmV;}1Sk86P)n61YC$> zyyraU+?&?c*0vYGSpbE2uy6yYR};C<=PUI2d=Hde5QIWO5YDP-zN^dN7k?!Plc6yij3Qw5_hLhQVNvVk09XTv}RM z6lC%6csv*z8&e7(EG|d*T4F83@n?_Y2xw_Wo0Fo!NEcJ{eJ1a=H_OYOeT&~Ay9jrPA3;Y zQhJHJx3^c3ek80DA|nI>0a;ujfYP?OyowgP1uB&a zMxzne|NIO3hcb|*Q6n=gBFh$ye5;TNp!8jG-Qoq;aL|gUvvTl=b_d>haWArVsCmSq zMQJ4B$hHDieEMl#ho)z_URPCu{$H-*)OX|fu>V(%iSw6ANCJqQtT*>>82k4WaO~UB zKlpm2!YnN;an!lFxhat$38<^9!{ylMYbti*+)#9}Q0i?3dQLxLJ0XF*zZt7-N&t06 zg)%^(3=B=);QRcYTKH#gNw%7qnc;$%z2Y|^v9J39FG5P^)NIsx;}_4M#`!6h@~dDh z+lR;Vbke9r^z`(gqoac@+tt-2SxdBWYisNMWq?3LAH7BAf9Y^6Dsd0%8$OrbrR-?SuasUcX#*Q&1-{0irTFQ z&H$}!>deZh1kg0Fh=S+_Gct7pHMZS&>+lnJ@1=cwM$lfdZ2}NG&1lts z@bUrFS|7z~B!c;+HN5-9&-lbSmh}AJDrAQdEhO6c-v4r#x8&Ck-$4W_9=&&Y6-#UB z|I2C{L6!m04SH=R*XdiJe3u|S_s#`jHk&u-Zi@mC_whgohL>f}Zq5f43-PLdg|D^6 znNqB_IJ){L{i_PXL?}MV;&J}79;8o|V*L9nu+}yxo)dobgqi%9Et_pedjR>L$R}gs zHiAL`9Xf|BR^9>MIPp1#`=jypws!t3|J^Nb9|+ySiEsYKbH$k`F=!A7MR3lw!oo!S zVU OGI+ZBxvX69a_%;8r0nkzgPgg&ebxsLQ0D}uT6951J literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/icons/reset.png b/Scripts/Modeling/Edit/gs_curvetools/icons/reset.png new file mode 100644 index 0000000000000000000000000000000000000000..4cba961e7b7c110b3f38975d47529ca2c596d3b0 GIT binary patch literal 1923 zcmbVN3s4kg9N$AF4<85yh@`WwFJ5>1IPQ*HJ%u<7&?CS@a6({jci~pzVd;lg*c{CYFha!DfnCbf_7ETK&dGIa_(xts(9E>@=Ca+lLBk>zHjhnECv(vu5n9a`xq>&M2nmtbG#c?7%4knCYuqU zYOf70!AI+synX^6L(3@v#kH7XQxA}#eK?^^boK~mXq0ubF4iqe0E_#uf`gZN$-(~% zweR?E0)TI=R-cTg>f&O@|o3$Lrkc0_m;*EMjqboARYcMv>q%r9zy(W&(#nTu= z7csQG@4khn%aq9W-Dm#i{Z~Z}losWDZXTuJl$0P-I0*!+!dnqD*;4Py$-zBYKvJ}l z-DZSV!pbs;_cQn09_Xd@I9NdXAK~tWNqmv)p+q*Z74XC1QKb%h?Lhx&V$azV zHQNUVjX+uYS`gUyni%T_ol68Q%9cCb1VMh2QZ0$L+~%|E&F+KPi&awf=uKf=11hWY z>%+PZ+m`7gV%K%O6`gI5`Jp9a>(1&HC?b4z>@M+LTLFD)T>PqA5`RWDsE=%}kJ zSQNjiUzoc2j@wXc-hS@Jr_Gn|lufBSl>G3Ei?^n{|K-&|Ck8KReyzs5<@EQP&-OQL zIC1?%>y^?N%AOQ-barg$rl?rqsLn59u)i_ZtotLL}Y}IEDD_I%Z*`~syhUV{Dv$UbB7A`DZG$A6PyY+)N8xOCm3f>_HS09{y z-qO9G@v#hr7>Yt_k6rs#&%%x~?cF)5O#dbAc};PNg+rGfNo&a7=@>2yNSTztU1*QX z88oahYwM`G67})pEsj)u?nHQG)uVoE_eUGg@8L#u1}BJX?F~DRNSdbcRfO@XfBcwA zs0Ioict}-0A1vDJI%9pz;u!n^d+X-d0vXfr3Z?e9N# z;`oL4$&WsJ;qrWLwOq=bY6=MqxP9*Ua^hxM_^dF`>$}rCu^N3_d%^ZuALmqL>>U

yzi9X OWNLD{Wyi~fi~atkT7V5RI10TBi}s zWi=HlWThf<%Zywr=29!2o%Qr@-}8Il_j#Z1|M!9$-a%D)voZhxP{ldg6XdBYR~=-H z+$ot_Ir0FJ?QotD`6WY$=jC02OmMIRa9`?7|9Xbmy4V5$_p;Zn27%=r8sm6`3;?Wa z{UwFx(QG0B04~JY+j=Gjd@2#*x8PvUA3QT}rl(|ppp7V6MyApEoD3djbkkGgap;(} z0^;F-xFrgl^Nt2AF@iLWIWjPT)y!-suY{!euBF2ISTas#7*RR6vRZYDbB7bAvdfw# z(AQC;i#|z;Q==*7N_4@VnV(P3jEQIdqxm$%0R<_9his4EIBF=>n`IT`W3ASgHF&GI z4)}k6rZk+qNiS)61-HP${|R{ZbMZo=HPa}x@`pgh1_n|yy$PGGRjl2cKF0YR#DpT< zrQ>R&X;jP<@|9=wM*9U-#9meLc)T=4hEMP*G~vL>b_F0=xBs6A%sSPXb{4w(!`eCXdc2V99V^) z6SjmYUps9R-T-S$|E{S& zcWW>LZ!W#Gy&=ATFceuj`*B}k6!&xL^;5(DFrtSF=~kB<9^ss;WdoH{gpG#CS0B}g zK#ecUC(|3XdF95$?yLJqfXlgq^!%TT`-KHgo4QF2sntV&pt*!T(P7Z&@x!Pt`%lfhhLqBFZe9W->S z|Hy@64k|-aI833^Rt_w$b3tk}KpsPbVBpK?E;sW0Ou!#B!AIu4m#1t)G?ptq@*uq+ z63V)9DHeJO08yzS?s9tbC3Wzet8!e{81ZFr$yv(R76vO9oY!?BZ(2G>>1)}6C7`Ar z^Kr_o*dFV+o$ceS&Vza;?Q8=`ObCy>PON1_(nm`ot@8t{p=G|{JcKSdDf~u${cb`L zKg7wM7YRX;T%on2CRt)(_zTbmOsY(3fUDaw+!draJ}+r2*hj)Hp6M_4V&HN;uN9N9 zuFmcpijV~~+09iwO6~{@2CW&$QSm~kZNF6%VaTK!oEd08tFWRd)!%UcThXKyzH|~h znu7$b8o0HiXO!ID`}aeBOq9KB?bt}VTB!tBp7lYGZ;j6Tdi}K2_j_;8JTb}bXcb2# z?=>@DkB){`Az z?Zk=)@JUD0JuZy99=5UoM(muRH&a`D_`VZ!fO*xLXL(TbqnD1~iFN<+ScvWtiK(`0 z723j~I*%=)+ixGempT=sQ896mY6v?vP6guItnX3CR5UHMQTfWwyG^&T&8@qUwe&q+ z>n&IhLHR6Cd+kG(STf_C3z8^R?Cui+%NL6p2%eefI-X8>ROWkHPn36EA275wFjt{e zq?lco2F)@IbKg33T~mN&8@)&A_9l9Gq$>u976VY7N59rTB8Z}v_0FInF;s&+xEm+N~H1E3Th@PZnMx=o{%E+N}w|`naa)g6(a} zyll6^Q%)bZ4BqDZx0O!itAoc^Ynr0Resj|Ey{1s!)z2X@6)wpm>~=Ka076M@b+B0z>NO^S0mx7 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/icons/sliderLock_reversed.png b/Scripts/Modeling/Edit/gs_curvetools/icons/sliderLock_reversed.png new file mode 100644 index 0000000000000000000000000000000000000000..638a11604cba7e3ab7febb9cc1dc368cae9485ef GIT binary patch literal 1458 zcmV;j1x@;iP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){ zsAK>D1vg1VK~#8N?VRmZ+b|S{ap?cH6VOfoouG7rrV~IHAYp>i3F5E-VS=U;G)z#K zpkV_#0Hp6LU%Qrc?bu1gAMSIGk0pQT)yLJA2gYZ0xYBE~P%AB!v9K;{1Oy|c5kTrPPuie6q`vSky= zZpzKm)6+0)wJJKDPG>ls6uGUrgYa=(IEwJQ2=Z489Q^fS&a+%@;t*Z!J>#_$V>rYE zKO`Fgff*yRx~~w6{3bjWBM3T>Bbt28M{`=yeJ=4XE4je?YvZI!R1*|2dZokSiZgjh zIp{ihe8h8HgKu!G{Yx&_y(nm27vvJf#T*6wLbejz6;8AsKv584?5bUbD-oU{)dY9m zx;ma{IDV<9L}@fQtqaIz82@9$I$3$Z0N>))koRy7E!XV=Lh6wNR|K_{D6mtE6w*|t z6P2k!OO1foEe<-1#%+QI%6x}1|46oi4RGCyn`-2M2Oe;6{`L|iDAR?TAsC+fa!gfG zJP^gWT|g^2mZKsUu_#5gplqWUNv53|f)bQDAMhc`humy9j*asiLq*?w>pA=YZ1 za?8LCFkPlMHQ1fB3lz$6Y}~)%{&$|8^`b!SFJHaCn!+=zM17xHt}vbUt`M9t`e)kn zNIh|@OOb2`f}D0$@V$*_Cq%HC=g!dFokT#4QNHI`F!p&8Q3!FYPcepcos}S3$t~nb zXHm?r3lx^)P@M&?(|gU4q}Y2H;dODRfYy&zUNPM^Wjs)0TCX>aI8}JYy+*GkbC*u$ zb8%%$L!wxlptO!ozY-*=tyc}wQj12=$evxazO*F)ty064q@rk*>ipY8o0hyzl+#J! z)`@aryR6UJ?ZF!KNHEI^LqdXkkdQ-8Y)SV9^HYtH_(8f9^*l`pUfgJpipsK=qUeD= zO3&s*p{|O86BM9+=W7Db^ZOuua0IUZ$<#~u6|CNUjG0?ET^vgs zT+H3#;EA%v5T)=|b2J1!OP>0yse-obdbwD)LeyG{-&m(ocwKZndJ5 zXb25%e}De~PURi~y?Vkh48t&t-Oy_h=i-f1pZ$&cM>J}T)C2=+9KU)T>dv5-=&mr) zq?l+@Of)GbniLaFiisx0M3Z8oNqIS<_TRmdVxmbg(WIDYLiYCl0l^uwl>m3OF8}}l M07*qoM6N<$g4m#=UjP6A literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/init.py b/Scripts/Modeling/Edit/gs_curvetools/init.py new file mode 100644 index 0000000..a48273c --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/init.py @@ -0,0 +1,161 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import csv +import io +import os +from imp import reload + +import maya.cmds as mc +import maya.mel as mel + +from .constants import * +from .utils import utils + +reload(utils) + + +def getHotkeys(): + path = os.path.dirname(os.path.realpath(__file__)) + '/utils/hotkeys.csv' + with io.open(path, 'r', encoding='utf-8') as f: + reader = csv.reader(f, delimiter='|') + return list(reader) + + +class Init: + + def __init__(self): + if mc.workspaceControl('GSCT_CurveTools', q=1, ex=1): + utils.stopUI(True) + if mc.workspaceControlState('GSCT_CurveTools', q=1, ex=1): + mc.workspaceControlState('GSCT_CurveTools', r=1) + + utils.resetOptionVars() + + # Init Runtime Command + self.delRuntimeCmds() + + # Get hotkeys from csv file + hotkeys = getHotkeys() + + for i in range(len(hotkeys)): + cmd = 'import gs_curvetools\ntry:\n ' + hotkeys[i][1] + '\nexcept:\n print("This function requires GS CurveTools.")' + commandName = 'GSCT_' + hotkeys[i][0].strip().replace(' ', '_') + if not mc.runTimeCommand(commandName, ex=1): + mc.runTimeCommand( + commandName, + c=cmd, + ann=hotkeys[i][2], + cat=hotkeys[i][3], + cl=hotkeys[i][4] + ) + else: + mc.runTimeCommand(commandName, e=1, delete=1) + mc.runTimeCommand( + commandName, + c=cmd, + ann=hotkeys[i][2], + cat=hotkeys[i][3], + cl=hotkeys[i][4] + ) + + # Shelf + folder = utils.getFolder.icons() + shelf = mel.eval('$gsTempShelfTopLevel = $gShelfTopLevel') + ui = 'import gs_curvetools.main as ct_main\nct_main.main()' + uiIcon = folder + 'gsCurveToolsIcon_ui.png' + reset = 'import gs_curvetools.utils.utils as ct_ut\nfrom imp import reload\nreload(ct_ut)\nct_ut.resetUI()' + resetIcon = folder + 'gsCurveToolsIcon_reset.png' + stop = 'import gs_curvetools.utils.utils as ct_ut\nfrom imp import reload\nreload(ct_ut)\nct_ut.stopUI()' + stopIcon = folder + 'gsCurveToolsIcon_stop.png' + if MAYA_VER <= 2018: + uiIcon = folder + 'gsCurveToolsIcon_ui_legacy.png' + resetIcon = folder + 'gsCurveToolsIcon_reset_legacy.png' + stopIcon = folder + 'gsCurveToolsIcon_stop_legacy.png' + if mc.tabLayout(shelf, ex=1): + allTabs = mc.tabLayout(shelf, q=1, tl=1) + ex = False + for ele in allTabs: + if ele == 'GS': + ex = True + break + if not ex: + mel.eval('addNewShelfTab ("GS");') + mc.tabLayout(shelf, e=1, st='GS') + buttons = mc.shelfLayout(shelf + '|GS', q=1, ca=1) + if buttons: + for button in buttons: + if mc.shelfButton(button, q=1, l=1) == 'GS_CurveTools'\ + or mc.shelfButton(button, q=1, l=1) == 'GS_CurveTools_Reset'\ + or mc.shelfButton(button, q=1, l=1) == 'GS_CurveTools_Stop': + mc.deleteUI(button, ctl=1) + mel.eval('shelfTabRefresh;') + currentShelf = mc.tabLayout(shelf, q=1, st=1) + mc.setParent(currentShelf) + mc.shelfButton(style=mc.shelfLayout(currentShelf, query=1, style=1), + sourceType="python", + label="GS_CurveTools", + width=mc.shelfLayout(currentShelf, query=1, cellWidth=1), + command=ui, + image1=uiIcon, + height=mc.shelfLayout(currentShelf, query=1, cellHeight=1), + annotation="Toggle GS CurveTools Window") + mc.shelfButton(style=mc.shelfLayout(currentShelf, query=1, style=1), + sourceType="python", + label="GS_CurveTools_Reset", + width=mc.shelfLayout(currentShelf, query=1, cellWidth=1), + command=reset, + image1=resetIcon, + height=mc.shelfLayout(currentShelf, query=1, cellHeight=1), + annotation="GS CurveTools Reset to Defaults") + mc.shelfButton(style=mc.shelfLayout(currentShelf, query=1, style=1), + sourceType="python", + label="GS_CurveTools_Stop", + width=mc.shelfLayout(currentShelf, query=1, cellWidth=1), + command=stop, + image1=stopIcon, + height=mc.shelfLayout(currentShelf, query=1, cellHeight=1), + annotation="GS CurveTools Stop Scripts") + msg = '

\ +
- Launches the menu.\n\ + - Resets the menu to default values.\n\ + - Closes the menu and stops all background scripts.\n
\ +
To close this message simply click on it.\n\ + Have fun! :)
\ +
\n
\ +
\n
\ +
\n
\ +
\n
\ +
\n
' % (uiIcon, resetIcon, stopIcon) + mc.inViewMessage(msg=msg, a=1, pos='topCenter', fade=True, ck=1, fst=60000) + + def __repr__(self): + return 'GS CurveTools Initialized' + + def delRuntimeCmds(self): + commands = mc.runTimeCommand(q=1, ca=1) + for command in commands: + if 'GSCT_' in command: + mc.runTimeCommand(command, e=1, delete=1) diff --git a/Scripts/Modeling/Edit/gs_curvetools/log.log b/Scripts/Modeling/Edit/gs_curvetools/log.log new file mode 100644 index 0000000..ef0b1ca --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/log.log @@ -0,0 +1,132 @@ +[15:47:11,248 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[15:47:11,249 | DEBUG | main | 54]: () : 25/10/2024, 15:47:11 +[15:47:19,392 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CurveTools +[15:47:19,393 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CustomLayerColorsWindow +[15:47:19,393 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CardToCurvePopOut +[15:52:10,648 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[15:52:10,648 | DEBUG | main | 54]: () : 25/10/2024, 15:52:10 +[15:55:45,945 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[15:55:45,946 | DEBUG | main | 54]: () : 25/10/2024, 15:55:45 +[15:55:49,786 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[15:55:52,238 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CurveTools +[15:55:52,994 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[15:55:56,505 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[15:56:50,105 | ERROR | utils | 109]: warningInView() : Wrong Selection: extractedGeo_1 is skipped +[15:56:50,109 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5659, in curveAddToLayer +[15:56:54,291 | ERROR | utils | 109]: warningInView() : Select at least one Curve +[15:56:54,291 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5341, in extractSelectedCurves +[15:56:56,160 | ERROR | utils | 109]: warningInView() : Wrong Selection: extractedGeo_2 is skipped +[15:56:56,161 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5659, in curveAddToLayer +[15:57:13,014 | INFO | core | 4036]: saveOptions() : Saving options +[15:57:13,979 | INFO | core | 4036]: saveOptions() : Saving options +[15:59:57,188 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CurveTools +[15:59:57,964 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[16:00:00,991 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[16:02:50,064 | ERROR | utils | 109]: warningInView() : Layer is Empty +[16:02:50,067 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5599, in curveGeometryEditToggle +[16:03:31,403 | ERROR | utils | 109]: warningInView() : Select at least two Curves +[16:03:31,405 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 358, in transferUV +[16:06:58,025 | ERROR | utils | 109]: warningInView() : Layer is Empty +[16:06:58,027 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5582, in curveLayerSelectObj +[16:07:00,139 | ERROR | utils | 109]: warningInView() : Layer is Empty +[16:07:00,142 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5582, in curveLayerSelectObj +[16:07:08,822 | ERROR | utils | 109]: warningInView() : Layer is Empty +[16:07:08,824 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5582, in curveLayerSelectObj +[16:07:23,048 | ERROR | utils | 109]: warningInView() : Layer is Empty +[16:07:23,050 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5582, in curveLayerSelectObj +[16:28:53,631 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[16:28:53,631 | DEBUG | main | 54]: () : 25/10/2024, 16:28:53 +[16:30:21,253 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[16:30:22,539 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CurveTools +[16:30:23,312 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[16:30:25,548 | INFO | utils | 447]: stopUI() : Deleting window state for GSCT_CurveTools +[16:30:26,317 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[16:31:38,714 | INFO | core | 4036]: saveOptions() : Saving options +[16:35:38,164 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[16:35:38,165 | DEBUG | main | 54]: () : 25/10/2024, 16:35:38 +[16:35:57,855 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[16:37:07,264 | ERROR | utils | 109]: warningInView() : Select at least two curves +[16:37:07,268 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 774, in fill +[16:37:09,026 | ERROR | utils | 109]: warningInView() : Select one curve and one geometry or Select one target curve and any number of curveCards/Tubes +[16:37:09,027 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 1356, in bind +[16:37:10,565 | ERROR | utils | 109]: warningInView() : Select at least one compatible curve (Bound/Warp curves or geometry) +[16:37:10,565 | DEBUG | utils | 111]: warningInView() : File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 1614, in unbind +[16:37:11,893 | ERROR | utils | 217]: wrapper() : 'NoneType' object is not iterable +Traceback (most recent call last): + File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\utils\utils.py", line 208, in wrapper + rv = func(*args, **kwargs) + File "D:\P4/Art/Tools/MetaBox\Scripts\Modeling\Edit\gs_curvetools\core.py", line 5454, in extractAllCurves + parentGrps = set(mc.listRelatives(allGeo, p=1)) +TypeError: 'NoneType' object is not iterable +[16:37:22,299 | INFO | utils | 117]: printInView() : Regrouped 0 curve(s) +[16:37:22,497 | INFO | utils | 117]: printInView() : Regrouped 0 curve(s) +[16:55:35,765 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[16:55:35,766 | DEBUG | main | 54]: () : 25/10/2024, 16:55:35 +[16:59:15,547 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[16:59:15,548 | DEBUG | main | 54]: () : 25/10/2024, 16:59:15 +[17:02:46,653 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:02:46,653 | DEBUG | main | 54]: () : 25/10/2024, 17:02:46 +[17:03:34,706 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:03:34,707 | DEBUG | main | 54]: () : 25/10/2024, 17:03:34 +[17:05:26,126 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:05:26,126 | DEBUG | main | 54]: () : 25/10/2024, 17:05:26 +[17:08:00,366 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:08:00,366 | DEBUG | main | 54]: () : 25/10/2024, 17:08:00 +[17:11:21,232 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:11:21,232 | DEBUG | main | 54]: () : 25/10/2024, 17:11:21 +[17:12:35,318 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:12:35,318 | DEBUG | main | 54]: () : 25/10/2024, 17:12:35 +[17:27:17,603 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[17:27:17,603 | DEBUG | main | 54]: () : 25/10/2024, 17:27:17 +[12:28:49,045 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[12:28:49,046 | DEBUG | main | 54]: () : 29/10/2024, 12:28:49 +[12:31:52,237 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[12:31:52,237 | DEBUG | main | 54]: () : 29/10/2024, 12:31:52 +[12:32:55,923 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[12:32:55,924 | DEBUG | main | 54]: () : 29/10/2024, 12:32:55 +[12:43:17,785 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[12:43:17,786 | DEBUG | main | 54]: () : 29/10/2024, 12:43:17 +[12:56:52,636 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[12:56:52,637 | DEBUG | main | 54]: () : 29/10/2024, 12:56:52 +[13:12:57,427 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:12:57,427 | DEBUG | main | 54]: () : 29/10/2024, 13:12:57 +[13:26:02,083 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:26:02,083 | DEBUG | main | 54]: () : 29/10/2024, 13:26:02 +[13:31:31,387 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:31:31,387 | DEBUG | main | 54]: () : 29/10/2024, 13:31:31 +[13:33:05,154 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:33:05,155 | DEBUG | main | 54]: () : 29/10/2024, 13:33:05 +[13:37:03,867 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:37:03,868 | DEBUG | main | 54]: () : 29/10/2024, 13:37:03 +[13:38:50,115 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:38:50,116 | DEBUG | main | 54]: () : 29/10/2024, 13:38:50 +[13:41:58,626 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:41:58,626 | DEBUG | main | 54]: () : 29/10/2024, 13:41:58 +[13:52:12,721 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:52:12,721 | DEBUG | main | 54]: () : 29/10/2024, 13:52:12 +[13:56:05,229 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:56:05,230 | DEBUG | main | 54]: () : 29/10/2024, 13:56:05 +[13:58:23,170 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[13:58:23,170 | DEBUG | main | 54]: () : 29/10/2024, 13:58:23 +[20:53:31,681 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[20:53:31,681 | DEBUG | main | 54]: () : 02/01/2025, 20:53:31 +[20:55:41,532 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[20:55:41,532 | DEBUG | main | 54]: () : 02/01/2025, 20:55:41 +[20:57:14,643 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[20:57:14,643 | DEBUG | main | 54]: () : 02/01/2025, 20:57:14 +[21:02:25,417 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[21:02:25,417 | DEBUG | main | 54]: () : 02/01/2025, 21:02:25 +[21:03:03,240 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[09:58:36,078 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[09:58:36,078 | DEBUG | main | 54]: () : 10/01/2025, 09:58:36 +[09:59:04,960 | INFO | main | 78]: checkScriptJobs() : scriptJobs created for "main" UI! +[10:29:44,404 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[10:29:44,404 | DEBUG | main | 54]: () : 10/01/2025, 10:29:44 +[10:31:50,217 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[10:31:50,218 | DEBUG | main | 54]: () : 10/01/2025, 10:31:50 +[21:31:51,516 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[21:31:51,516 | DEBUG | main | 54]: () : 10/01/2025, 21:31:51 +[18:19:43,116 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[18:19:43,116 | DEBUG | main | 54]: () : 11/01/2025, 18:19:43 +[01:08:10,709 | DEBUG | main | 53]: () : -----------------Starting Log Session----------------- +[01:08:10,728 | DEBUG | main | 54]: () : 14/01/2025, 01:08:10 diff --git a/Scripts/Modeling/Edit/gs_curvetools/main.py b/Scripts/Modeling/Edit/gs_curvetools/main.py new file mode 100644 index 0000000..254999c --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/main.py @@ -0,0 +1,878 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import os +import sys +from datetime import datetime +from functools import partial as pa +from imp import reload + +import maya.cmds as mc +from PySide2 import QtCore, QtGui, QtWidgets + +from . import core, ui +from . import constants +from .constants import * +from .utils import style, tooltips, utils, wrap +from .utils.utils import deferred, deferredLp, noUndo, undo +from .utils.wrap import WIDGETS + +reload(core) +reload(utils) +reload(style) +reload(wrap) +reload(ui) +reload(tooltips) +reload(constants) + +# Loggers +MESSAGE = utils.logger +LOGGER = utils.logger.logger + +LOGGER.debug('-----------------Starting Log Session-----------------') +LOGGER.debug(' {} '.format(datetime.now().strftime("%d/%m/%Y, %H:%M:%S"))) + +### Interface Script Jobs ### + + +def checkScriptJobs(controlName): + """ Checks if script jobs exist and adds them if necessary """ + jobNumbers = list() + workspaceExists = False + UI = 'control' + if mc.workspaceControl(controlName, exists=1): + workspaceExists = True + if controlName == MAIN_WINDOW_NAME: + UI = 'main' + elif controlName == UV_EDITOR_NAME: + UI = 'uv_editor' + elif controlName == SCALE_FACTOR_UI: + UI = 'scale_factor_ui' + jobList = mc.scriptJob(lj=1) + for job in jobList: + if controlName in job: + jobNumbers.append(job.split()[0][0:-1]) + if len(jobNumbers) == 0 and workspaceExists: + scriptJobsInit(UI) + LOGGER.info('scriptJobs created for "%s" UI!' % UI) + elif len(jobNumbers) > 0 and not workspaceExists: + LOGGER.info('scriptJobs deleted!') + for number in jobNumbers: + mc.scriptJob(k=number) + + +def scriptJobsInit(uiName): + if uiName == 'main': + mc.scriptJob(per=1, p=MAIN_WINDOW_NAME, event=['SceneOpened', pa(checkScriptJobs, MAIN_WINDOW_NAME)]) + mc.scriptJob(per=1, p=MAIN_WINDOW_NAME, event=['SceneOpened', pa(deferred(core.updateMainUI), True)]) + mc.scriptJob(per=1, p=MAIN_WINDOW_NAME, event=['SceneOpened', deferredLp(core.onSceneOpenedUpdateLayerCount)]) + mc.scriptJob(per=1, p=MAIN_WINDOW_NAME, event=['SelectionChanged', core.updateMainUI]) + mc.scriptJob(per=1, p=MAIN_WINDOW_NAME, event=['Undo', core.updateMainUI]) + elif uiName == 'control': + mc.scriptJob(per=1, p=CURVE_CONTROL_NAME, event=['SelectionChanged', core.curveControlUI.updateUI]) + mc.scriptJob(per=1, p=CURVE_CONTROL_NAME, event=['Undo', deferred(core.curveControlUI.updateUI)]) + elif uiName == 'uv_editor': + def updateUVs(): + if (mc.workspaceControl(UV_EDITOR_NAME, q=1, ex=1) and + mc.workspaceControl(UV_EDITOR_NAME, q=1, r=1)): + ui.uveditor.updateEditor() + mc.scriptJob(per=1, p=UV_EDITOR_NAME, event=['SelectionChanged', updateUVs]) + mc.scriptJob(per=1, p=UV_EDITOR_NAME, event=['Undo', updateUVs]) + + +def main(): + utils.checkNativePlugins(['curveWarp'], MAIN_WINDOW_NAME) + if mc.workspaceControl(MAIN_WINDOW_NAME, q=1, ex=1): + if MAYA_VER >= 2018: + if not mc.workspaceControl(MAIN_WINDOW_NAME, q=1, vis=1): + mc.workspaceControl(MAIN_WINDOW_NAME, e=1, rs=1) + core.updateMainUI() + else: + mc.workspaceControl(MAIN_WINDOW_NAME, e=1, vis=0) + else: + mc.workspaceControl(MAIN_WINDOW_NAME, e=1, fl=1) + mc.deleteUI(MAIN_WINDOW_NAME) + else: + CurveToolsUI() + mc.workspaceControl(MAIN_WINDOW_NAME, e=1, ui=UI_SCRIPT) + try: + core.toggleColor.checkColorStorageNode() + checkScriptJobs(MAIN_WINDOW_NAME) + utils.deferred(core.onSceneOpenedUpdateLayerCount)() # Also updates the UI + except Exception as e: + LOGGER.exception(e) + +# Main UI + + +class CurveToolsUI(QtWidgets.QWidget): + + def __init__(self): + WIDGETS.clear() + # Maya Native Workspace + parent = ui.mayaWorkspaceControl(name=MAIN_WINDOW_NAME, label=MAIN_WINDOW_LABEL) + + # Resolve Fonts + fontDatabase = QtGui.QFontDatabase() + fontDatabase.removeAllApplicationFonts() + fonts = os.listdir(utils.getFolder.fonts()) + for font in fonts: + fontDatabase.addApplicationFont(utils.getFolder.fonts() + font) + + # Dockable Workspace Connection + super(CurveToolsUI, self).__init__(parent) + self.ui() + parent.layout().addWidget(self) + self.scrollWidget.setFocus() + + checkScriptJobs(MAIN_WINDOW_NAME) + + def ui(self): + # Layout + mainLayout = QtWidgets.QVBoxLayout(self) + mainLayout.setContentsMargins(*style.scale([2, 0, 2, 0])) + + self.scrollWidget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(self.scrollWidget) + scrollArea = QtWidgets.QScrollArea() + scrollArea.setWidget(self.scrollWidget) + mainLayout.addWidget(scrollArea) + + # Layout Settings + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(style.scale(2)) + layout.setMargin(0) + layout.setAlignment(QtCore.Qt.AlignTop) + + scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + scrollArea.setWidgetResizable(True) + + # Menu buttons + menuBarWidget = QtWidgets.QWidget() + menuBarLayout = QtWidgets.QHBoxLayout(menuBarWidget) + menuBarLayout.setContentsMargins(0, 0, 0, 0) + menuBarLayout.setAlignment(QtCore.Qt.AlignCenter) + + menuBar = QtWidgets.QMenuBar() + menuBar.setSizePolicy(PREFERRED_POLICY) + menuBarLayout.addWidget(menuBar) + layout.addWidget(menuBarWidget) + + # Options Menu + with wrap.Menu('Options', menuBar) as menu: + menu.triggered.connect(core.saveOptions) + + menu.addSection('Import / Export') + importCurves = wrap.MenuItem('importCurves', 'Import Curves', menu) + importCurves.triggered.connect(noUndo(core.importExport.importCurves)) + exportCurves = wrap.MenuItem('exportCurves', 'Export Curves', menu) + exportCurves.triggered.connect(core.importExport.exportCurves) + + menu.addSection('Global Modifiers') + changeScaleFactor = wrap.MenuItem('changeScaleFactor', 'Change Scale Factor', menu) + changeScaleFactor.triggered.connect(ui.scaleFactorWindow) + globalCurveThickness = wrap.MenuItem('globalCurveThickness', 'Global Curve Thickness', menu) + globalCurveThickness.triggered.connect(ui.curveThicknessWindow) + + menu.addSection('Viewport Commands') + setAOSettings = wrap.MenuItem('setAOSettings', 'Set AO Settings', menu) + setAOSettings.triggered.connect(core.setAOSettings) + + with wrap.Menu('Transparency Settings', menu) as transparencySettingsMenu: + simpleTransparency = wrap.MenuItem( + 'setSimpleTransparency', + 'Simple Transparency (Fast, Inncaurate)', + transparencySettingsMenu + ) + simpleTransparency.triggered.connect(noUndo(pa(core.setTransparencySettings, 0))) + objectSortingTransparency = wrap.MenuItem( + 'setObjectSortingTransparency', + 'Object Sorting Transparency (Average)', + transparencySettingsMenu + ) + objectSortingTransparency.triggered.connect(noUndo(pa(core.setTransparencySettings, 1))) + setDepthTransparency = wrap.MenuItem( + 'setDepthTransparency', + 'Depth Transparency (Accurate, Slow)', + transparencySettingsMenu + ) + setDepthTransparency.triggered.connect(noUndo(pa(core.setTransparencySettings, 2))) + + menu.addSection('Convert Curves') + with wrap.Menu('Convert Curves', menu) as convertCurvesSubmenu: + convertToWarpCard = wrap.MenuItem('convertToWarpCard', "Convert to Warp Card", convertCurvesSubmenu) + convertToWarpCard.triggered.connect(pa(undo(core.convertSelectionTo), 0)) + convertToWarpTube = wrap.MenuItem('convertToWarpTube', "Convert to Warp Tube", convertCurvesSubmenu) + convertToWarpTube.triggered.connect(pa(undo(core.convertSelectionTo), 1)) + convertToExtrudeCard = wrap.MenuItem('convertToExtrudeCard', "Convert to Extrude Card", convertCurvesSubmenu) + convertToExtrudeCard.triggered.connect(pa(undo(core.convertSelectionTo), 2)) + convertToExtrudeTube = wrap.MenuItem('convertToExtrudeTube', "Convert to Extrude Tube", convertCurvesSubmenu) + convertToExtrudeTube.triggered.connect(pa(undo(core.convertSelectionTo), 3)) + + menu.addSection('General Options') + wrap.MenuItem('keepCurveAttributes', 'Keep Curve Attributes', menu, True, core.getOption('keepCurveAttributes')) + wrap.MenuItem('populateBlendAttributes', 'Add Cards/Tubes Blend Attributes', + menu, True, core.getOption('populateBlendAttributes')) + wrap.MenuItem('convertInstances', 'Auto Convert Instances', menu, True, core.getOption('convertInstances')) + + wrap.MenuItem('useAutoRefineOnNewCurves', 'Use Auto-Refine on New Curves', + menu, True, core.getOption('useAutoRefineOnNewCurves')) + wrap.MenuItem('flipUVsAfterMirror', 'Flip UVs After Mirror', + menu, True, core.getOption('flipUVsAfterMirror')) + enableTooltipsMenu = wrap.MenuItem('enableTooltips', 'Enable Tooltips', menu, True, core.getOption('enableTooltips')) + enableTooltipsMenu.triggered.connect(self.toggleTooltips) + + menu.addSection('Color Options') + syncColor = wrap.MenuItem('syncCurveColor', 'Sync Curve Color to Layer Color', menu, True, core.getOption('syncCurveColor')) + syncColor.triggered.connect(core.toggleColor.syncCurveColors) + wrap.MenuItem('colorizedRegroup', 'Colorize Regrouped Layers', menu, True, core.getOption('colorizedRegroup')) + colorOnlyDiffuse = wrap.MenuItem('colorOnlyDiffuse', 'Color Only Affects Diffuse', + menu, True, core.getOption('colorOnlyDiffuse')) + colorOnlyDiffuse.triggered.connect(core.toggleColor.updateColorOptions) + checkerPattern = wrap.MenuItem('checkerPattern', 'Checker Pattern for Color Mode', menu, True, core.getOption('checkerPattern')) + checkerPattern.triggered.connect(core.toggleColor.updateColorOptions) + + menu.addSection('Bind Options') + wrap.MenuItem('boundCurvesFollowParent', 'Bound Curves Follow Parent', menu, True, core.getOption('boundCurvesFollowParent')) + wrap.MenuItem('massBindOption', 'Bind to All Available Empty Curves', menu, True, core.getOption('massBindOption')) + wrap.MenuItem('bindDuplicatesCurves', 'Duplicate Curves Before Bind', menu, True, core.getOption('bindDuplicatesCurves')) + wrap.MenuItem('bindFlipUVs', 'Flip UVs before Bind', menu, True, core.getOption('bindFlipUVs')) + + menu.addSection('Layer Options') + wrap.MenuItem('ignoreLastLayer', 'Ignore Last Layer', menu, True, core.getOption('ignoreLastLayer')) + wrap.MenuItem('syncOutlinerLayerVis', 'Sync Outliner/Layer Visibility', menu, True, core.getOption('syncOutlinerLayerVis')) + wrap.MenuItem('replacingCurveLayerSelection', 'Replacing Curve Layer Selection', menu, True, + core.getOption('replacingCurveLayerSelection')) + onlyNumbersInLayers = wrap.MenuItem('layerNumbersOnly', 'Use Only Numbers in Layers', menu, True, + core.getOption('layerNumbersOnly')) + onlyNumbersInLayers.triggered.connect(core.changeLayersToNumbers) + onlyNumbersInLayers.triggered.connect(self.updateLayerList) + with wrap.Menu('Number of Active Layers', menu) as layerNumberMenu: + with wrap.ActionGroup('layerRowsActionGroup', layerNumberMenu) as actionGroup: + wrap.MenuItem('2layerRows', '20 Layers', layerNumberMenu, True, + core.getOption('2layerRows'), collection=actionGroup) + wrap.MenuItem('3layerRows', '30 Layers', layerNumberMenu, True, + core.getOption('3layerRows'), collection=actionGroup) + wrap.MenuItem('4layerRows', '40 Layers', layerNumberMenu, True, + core.getOption('4layerRows'), collection=actionGroup) + wrap.MenuItem('6layerRows', '60 Layers', layerNumberMenu, True, + core.getOption('6layerRows'), collection=actionGroup) + wrap.MenuItem('8layerRows', '80 Layers', layerNumberMenu, True, + core.getOption('8layerRows'), collection=actionGroup) + actionGroup.triggered.connect(core.updateMainUI) + + menu.addSection('Layer Collection Options') + wrap.MenuItem('ignoreTemplateCollections', 'Ignore "Template" Collection Names', menu, True, core.getOption('ignoreTemplateCollections')) + wrap.MenuItem('groupTemplateCollections', 'Group "Template" Collections Together', menu, True, core.getOption('groupTemplateCollections')) + layerCollectionsToggle = wrap.MenuItem('showLayerCollectionsMenu', 'Show Layer Collections Menu', + menu, True, core.getOption('showLayerCollectionsMenu')) + layerCollectionsToggle.triggered.connect(core.layerCollections.toggleLayerCollectionsWidget) + wrap.MenuItem('importIntoANewCollection', 'Import Into a New Collection', + menu, True, core.getOption('importIntoANewCollection')) + + menu.addSection('Other Options') + with wrap.Menu('Other Options', menu) as otherOptionsMenu: + otherOptionsMenu.addSection('Utility Commands') + convertToNewLayerSystem = wrap.MenuItem('convertToNewLayerSystem', 'Convert to New Layer System', otherOptionsMenu) + convertToNewLayerSystem.triggered.connect(utils.convertToNewLayerSystem) + updateLayers = wrap.MenuItem('updateLayers', 'Update Layers', otherOptionsMenu) + updateLayers.triggered.connect(undo(core.deleteUnusedLayers)) # TODO: Check if undo breaks anything + resetToDefaults = wrap.MenuItem('resetToDefaults', 'Reset to Defaults', otherOptionsMenu) + resetToDefaults.triggered.connect(utils.resetUI) + + otherOptionsMenu.addSection('Fixes') + maya2020UVFix = wrap.MenuItem('maya2020UVFix', 'Fix Maya 2020-2022 UV Bug', otherOptionsMenu) + maya2020UVFix.triggered.connect(undo(utils.fixMaya2020UVs)) + fixBrokenGraphs = wrap.MenuItem('mayaFixBrokenGraphs', 'Fix Broken Graphs', otherOptionsMenu) + fixBrokenGraphs.triggered.connect(undo(utils.fixBrokenGraphs)) + convertBezierToNurbs = wrap.MenuItem('convertBezierToNurbs', 'Convert Selected Bezier to NURBS', otherOptionsMenu) + convertBezierToNurbs.triggered.connect(undo(utils.convertBezierToNurbs)) + maya2020TwistFix = wrap.MenuItem('maya2020TwistAttribute', 'Fix Maya 2020.4 Twist Attribute', otherOptionsMenu) + maya2020TwistFix.triggered.connect(undo(utils.fixMaya2020Twist)) + maya2020UnbindFix = wrap.MenuItem('maya2020UnbindFix', 'Fix Maya 2020.4 Unbind Function', otherOptionsMenu) + maya2020UnbindFix.triggered.connect(undo(utils.fixMaya2020Unbind)) + deleteAllAnimationKeys = wrap.MenuItem('deleteAllAnimationKeys', 'Delete All Animation Keys', otherOptionsMenu) + deleteAllAnimationKeys.triggered.connect(undo(utils.deleteKeysOnAllObjects)) + + # Help Menu + with wrap.Menu('Help', menuBar) as menu: + openLogFile = wrap.MenuItem('openLogFile', 'Open Log File', menu) + openLogFile.triggered.connect(utils.logger.openLogFile) + openOnlineDocumentation = wrap.MenuItem('openOnlineDocumentation', 'Open Online Documentation', menu) + openOnlineDocumentation.triggered.connect(utils.openDocs) + usefulLinks = wrap.MenuItem('usefulLinks', 'Useful Links and Contacts', menu) + usefulLinks.triggered.connect(ui.about.socialWindow) + + # About Menu + with wrap.Menu('About', menuBar) as menu: + aboutAction = wrap.MenuItem('gsAbout', 'About', menu) + aboutAction.triggered.connect(ui.about.aboutWindow) + menu.addSeparator() + menu.addAction('Made by George Sladkovsky (%s)' % YEAR).setEnabled(False) + + # MAIN UI + + layout.addWidget(wrap.separator()) + + # Extrude Warp Switch + with wrap.Row(layout) as row: + # Switch Group + extrudeWarpSwitchGroup = QtWidgets.QButtonGroup(layout) + WIDGETS['gsExtrudeWarpSwitchGroup'] = extrudeWarpSwitchGroup + extrudeWarpSwitchGroup.buttonToggled.connect(self.extrudeWarpToggle) + extrudeWarpSwitchGroup.buttonClicked.connect(core.saveOptions) + + warpSwitch = wrap.Button(row.layout(), 'warpSwitch') + warpSwitch.setLabel('Warp', lineHeight=100) + warpSwitch.setButtonStyle('small') + warpSwitch.setCheckable(True) + + extrudeSwitch = wrap.Button(row.layout(), 'extrudeSwitch') + extrudeSwitch.setLabel('Extrude', lineHeight=100) + extrudeSwitch.setButtonStyle('small') + extrudeSwitch.setCheckable(True) + + extrudeWarpSwitchGroup.addButton(warpSwitch, 0) + extrudeWarpSwitchGroup.addButton(extrudeSwitch, 1) + + # New Card and New Tube + with wrap.Row(layout) as row: + newCard = wrap.Button(row.layout(), 'newCard') + newCard.setLabel('New Card') + newCard.clicked.connect(pa(undo(core.create.new), 0)) + + newTube = wrap.Button(row.layout(), 'newTube') + newTube.setLabel('New Tube') + newTube.clicked.connect(pa(undo(core.create.new), 1)) + + # Convert Curve to Card and Convert Curve to Tube + with wrap.Row(layout) as row: + curveCard = wrap.Button(row.layout(), 'curveCard') + curveCard.setLabel('Curve Card') + curveCard.clicked.connect(pa(undo(core.create.multiple), 0)) + + curveTube = wrap.Button(row.layout(), 'curveTube') + curveTube.setLabel('Curve Tube') + curveTube.clicked.connect(pa(undo(core.create.multiple), 1)) + + # Bind and Unbind + with wrap.Row(layout) as row: + bind = wrap.Button(row.layout(), 'gsBind') + bind.setLabel('Bind') + bind.setIcon('mod-top') + bind.clicked.connect(undo(core.create.bind)) + + unbind = wrap.Button(row.layout(), 'gsUnbind') + unbind.setLabel('Unbind') + unbind.clicked.connect(undo(core.create.unbind)) + + layout.addWidget(wrap.separator()) + + # Add Curve and Add Tube + with wrap.Row(layout) as row: + addCards = wrap.Button(row.layout(), 'addCards') + addCards.setLabel('Add Card') + addCards.setIcon('mod-top') + addCards.clicked.connect(pa(undo(core.create.populate), 0)) + + addTubes = wrap.Button(row.layout(), 'addTubes') + addTubes.setLabel('Add Tube') + addTubes.setIcon('mod-top') + addTubes.clicked.connect(pa(undo(core.create.populate), 1)) + + # Fill and Multiply + with wrap.Row(layout) as row: + fill = wrap.Button(row.layout(), 'gsFill') + fill.setLabel('Fill') + fill.setIcon('mod-top') + fill.clicked.connect(pa(undo(core.create.fill))) + + subdivide = wrap.Button(row.layout(), 'gsSubdivide') + subdivide.setLabel('Subdivide') + subdivide.setIcon('mod-top') + subdivide.clicked.connect(pa(undo(core.subdivideCurve))) + + # Add Slider + with wrap.Row(layout) as row: + m_addCardsSlider = mc.intSliderGrp('gsCurvesSlider', l='Add', f=1, adj=3, + cw=[(1, 20), (2, 25), (3, 1)], + min=1, max=10, v=3) + addCardsSlider = wrap.mayaSlider(m_addCardsSlider, layout=row.layout()) + WIDGETS['gsCurvesSlider'] = addCardsSlider + addCardsSlider.setContentsMargins(0, 0, 0, 0) + addCardsSlider.children()[2].setFocusPolicy(QtCore.Qt.NoFocus) + + layout.addWidget(wrap.separator()) + + # Edge to Curve + with wrap.Row(layout) as row: + edgeToCurve = wrap.Button(row.layout(), 'gsEdgeToCurve') + edgeToCurve.setLabel('Edge To Curve') + edgeToCurve.clicked.connect(pa(undo(core.edgeToCurve))) + + cardToCurve = wrap.Button(row.layout(), 'gsCardToCurve') + cardToCurve.setLabel('Card To Curve') + cardToCurve.clicked.connect(ui.cardToCurveWindow.openUI) + + layout.addWidget(wrap.separator()) + + # Layer Collections Menu + with wrap.Row(layout, objName='LayerCollectionsLayout', spacing=1) as layerCollectionsLayout: + layerComboBox = wrap.LayerCollectionWidget('layerCollectionsComboBox') + layerComboBox.setStyleSheet(style.smallComboBox) + + layerComboBox.currentIndexChanged.connect(lambda *_: deferred(core.updateMainUI)()) + layerComboBox.setSizeAdjustPolicy(layerComboBox.AdjustToMinimumContentsLength) + layerComboBox.setDuplicatesEnabled(False) + layerComboBox.setMinimumWidth(0) + layerComboBox.setFixedHeight(style.scale(16)) + + mc.popupMenu(mm=1, p=layerComboBox.objectName()) + mc.menuItem(rp='N', l='Clear', c=lambda *_: undo(core.layerCollections.clear)()) + mc.menuItem(rp='E', l='Copy', c=lambda *_: noUndo(core.layerCollections.copy)()) + mc.menuItem(rp='NE', l='Move Up', c=lambda *_: noUndo(core.layerCollections.moveUp)()) + mc.menuItem(rp='NW', l='Merge Up', c=lambda *_: noUndo(core.layerCollections.mergeUp)()) + mc.menuItem(rp='W', l='Paste', c=lambda *_: undo(core.layerCollections.paste)()) + mc.menuItem(rp='SW', l='Merge Down', c=lambda *_: noUndo(core.layerCollections.mergeDown)()) + mc.menuItem(rp='SE', l='Move Down', c=lambda *_: noUndo(core.layerCollections.moveDown)()) + mc.menuItem(rp='S', l='Rename', c=lambda *_: noUndo(core.layerCollections.rename)()) + + with wrap.Row(layerCollectionsLayout.layout(), margins=style.scale([1, 0, 0, 0])) as plusMinusButtonsLayout: + layerComboPlus = wrap.Button(objName="layerCollectionsPlus", layout=plusMinusButtonsLayout.layout()) + layerComboPlus.setFixedHeight(style.scale(16)) + layerComboPlus.setLabel("+", lineHeight=100) + layerComboPlus.setMinimumWidth(1) + layerComboPlus.setButtonStyle("small-filled") + layerComboPlus.clicked.connect(core.layerCollections.createLayerCollection) + + layerComboMinus = wrap.Button(objName="layerCollectionsMinus", layout=plusMinusButtonsLayout.layout()) + layerComboMinus.setEnabled(False) + layerComboMinus.setFixedHeight(style.scale(16)) + layerComboMinus.setMinimumWidth(1) + layerComboMinus.setLabel("-", lineHeight=100) + layerComboMinus.setButtonStyle("small-filled") + layerComboMinus.clicked.connect(undo(core.layerCollections.deleteLayerCollection)) + + layerCollectionsLayout.layout().addWidget(layerComboBox, 3) + layerCollectionsLayout.layout().addWidget(plusMinusButtonsLayout, 1) + + layerComboBox.insertItem(0, "Main") + + # Layer Filters + with wrap.Row(layout) as row: + allFilter = wrap.Button(row.layout(), 'gsAllFilter') + allFilter.setButtonStyle('small-filled') + allFilter.setLabel('All', lineHeight=100) + allFilter.setIcon('mod-top') + allFilter.clicked.connect(pa(undo(core.layersFilterToggle), True, True)) + + curveFilter = wrap.Button(row.layout(), 'gsCurveFilter') + curveFilter.setButtonStyle('small-filled') + curveFilter.setLabel('Curve', lineHeight=100) + curveFilter.clicked.connect(pa(undo(core.layersFilterToggle), True, False, ignore=["Shift+Ctrl"])) + + mc.popupMenu(mm=1, p=curveFilter.objectName()) + mc.menuItem(rp='N', l='Toggle Always on Top', c=lambda *_: undo(core.alwaysOnTopToggle)()) + mc.menuItem(rp='S', l='Auto-Hide Curves on Inactive Collections', + cb=mc.optionVar(q='GSCT_AutoHideCurvesOnInactiveCollections'), + c=lambda cb: undo(core.collectionVisibilityToggle)(cb)) + + geoFilter = wrap.Button(row.layout(), 'gsGeoFilter') + geoFilter.setButtonStyle('small-filled') + geoFilter.setLabel('Geo', lineHeight=100) + geoFilter.clicked.connect(pa(undo(core.layersFilterToggle), False, True, ignore=["Shift+Ctrl"])) + + colorMode = wrap.Button(row.layout(), 'colorMode') + colorMode.setButtonStyle('small-filled') + colorMode.setCheckable(True) + colorMode.setChecked(False) + colorMode.setLabel('Color', lineHeight=100) + colorMode.clicked.connect(pa(undo(core.toggleColor.toggleColorVis))) + + mc.popupMenu(mm=1, p=colorMode.objectName()) + mc.menuItem(rp='N', l='Randomize Colors', c=lambda *_: undo(core.toggleColor.randomizeColors)()) + mc.menuItem(rp='E', l='Reset Curve Colors', c=lambda *_: core.toggleColor.resetCurveColors()) + mc.menuItem(rp='W', l='Apply Curve Colors', c=lambda *_: core.toggleColor.syncCurveColors(True)) + mc.menuItem(rp='S', l='Custom Colors Window', c=ui.customLayerColors.window) + + # Layers + with wrap.Layout(layout, objName='LayerLayout') as layerLayout: + layerButtonGrp = QtWidgets.QButtonGroup(layerLayout.layout()) + layerButtonGrp.setObjectName(wrap.getUniqueName('LayerGroup')) + layerButtonGrp.setExclusive(True) + WIDGETS['LayerGroup'] = layerButtonGrp + + # First Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow0', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i, row.layout(), i)) + + # Second Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow1', spacing=0) as row: + letter = ord('A') + letters = [chr(i) for i in range(letter, letter + 10)] + if WIDGETS['layerNumbersOnly'].isChecked(): + letters = [i for i in range(10, 20)] + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 10, row.layout(), letters[i])) + + # Third Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow2', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 20, row.layout(), i + 20)) + WIDGETS['layerRow2'].setHidden(True) + + # Fourth Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow3', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 30, row.layout(), i + 30)) + WIDGETS['layerRow3'].setHidden(True) + + # Fifth Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow4', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 40, row.layout(), i + 40)) + WIDGETS['layerRow4'].setHidden(True) + + # Sixth Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow5', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 50, row.layout(), i + 50)) + WIDGETS['layerRow5'].setHidden(True) + + # Seventh Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow6', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 60, row.layout(), i + 60)) + WIDGETS['layerRow6'].setHidden(True) + + # Eighth Layer Row + with wrap.Row(layerLayout.layout(), 'layerRow7', spacing=0) as row: + for i in range(10): + layerButtonGrp.addButton(self.selectionSets(i + 70, row.layout(), i + 70)) + WIDGETS['layerRow7'].setHidden(True) + + WIDGETS['curveGrp0'].setChecked(True) + + # Extract Selected and Extract All + with wrap.Row(layout) as row: + extractSelected = wrap.Button(row.layout(), 'gsExtractSelected') + extractSelected.setLabel('Extract
Selected') + extractSelected.setIcon('mod-bottom') + extractSelected.clicked.connect(pa(undo(core.extractSelectedCurves))) + + extractAll = wrap.Button(row.layout(), 'gsExtractAll') + extractAll.setLabel('Extract
All') + extractAll.setIcon('mod-bottom') + extractAll.clicked.connect(undo(core.extractAllCurves)) + + layout.addWidget(wrap.separator()) + + # Select Curve, Geo and Group + with wrap.Row(layout) as row: + selectCurve = wrap.Button(row.layout(), 'gsSelectCurve') + selectCurve.setLabel('Select
Curve') + selectCurve.clicked.connect(pa(undo(core.selectPart), 1)) + + selectGeo = wrap.Button(row.layout(), 'gsSelectGeo') + selectGeo.setLabel('Select
Geo') + selectGeo.clicked.connect(pa(undo(core.selectPart), 2)) + + selectGroup = wrap.Button(row.layout(), 'gsSelectGroup') + selectGroup.setLabel('Select
Group') + selectGroup.clicked.connect(pa(undo(core.selectPart), 0)) + + # Group Curves Button + with wrap.Row(layout) as row: + groupCurves = wrap.Button(row.layout(), 'gsGroupCurves') + groupCurves.setLabel('Group
Curves') + groupCurves.clicked.connect(undo(core.groupCurves)) + + regroupByLayer = wrap.Button(row.layout(), 'gsRegroupByLayer') + regroupByLayer.setLabel('Regroup
by Layer') + regroupByLayer.clicked.connect(undo(core.regroupByLayer)) + + # Group Name Input Field + groupName = wrap.LineEdit('gsGroupNameTextField', layout) + groupName.setAlignment(QtCore.Qt.AlignCenter) + groupName.setAutoFormat(True) + groupName.setClearButtonEnabled(True) + groupName.setPlaceholderText("Group Name") + + # Custom Layer Names Window + customLayerNamesColors = wrap.Button(layout, 'gsCustomLayerNamesAndColors') + customLayerNamesColors.setButtonStyle('small-filled') + customLayerNamesColors.setLabel('Layer Names & Colors') + customLayerNamesColors.clicked.connect(ui.customLayerColors.window) + + layout.addWidget(wrap.separator()) + + # Select CVs Label + layout.addWidget(wrap.wrapControl(mc.text(l='Select CVs'))) + + # Select CVs Slider + sliderWidget = wrap.wrapControl( + mc.floatSliderGrp( + 'gsSelectCVSlider', + w=1, min=0, max=1, step=0.05, + dc=core.sliders.selectCVSlider, + cc=core.sliders.release + ) + ) + WIDGETS['gsSelectCVSlider'] = sliderWidget + layout.addWidget(sliderWidget) + + # Select Curve, Geo and Group + with wrap.Row(layout) as row: + transferAttributes = wrap.Button(row.layout(), 'gsTransferAttributes') + transferAttributes.setLabel('Transfer
Attr.') + transferAttributes.setIcon('mod-bottom') + transferAttributes.clicked.connect(undo(core.attributes.transferAttr)) + mc.popupMenu(mm=1, p=transferAttributes.objectName()) + mc.menuItem('gsCopyAttributes', rp='N', l='Copy Attrs', aob=1, c=lambda _: core.attributes.copyAttributes()) + mc.menuItem(ob=1, c=lambda _: ui.attributesFilter.openUI()) + mc.menuItem('gsPasteAttributes', rp='S', l='Paste Attrs', aob=1, c=lambda _: core.attributes.pasteAttributes()) + mc.menuItem(ob=1, c=lambda _: ui.attributesFilter.openUI()) + + transferUVs = wrap.Button(row.layout(), 'gsTransferUVs') + transferUVs.setLabel('Transfer
UVs') + transferUVs.setIcon('mod-bottom') + transferUVs.clicked.connect(undo(core.attributes.transferUV)) + mc.popupMenu(mm=1, p=transferUVs.objectName()) + mc.menuItem('gsCopyUVs', aob=1, rp='N', l='Copy UVs', c=lambda _: core.attributes.copyUVs()) + mc.menuItem(ob=1, c=lambda _: ui.attributesFilter.openUI()) + mc.menuItem('gsPasteUVs', aob=1, rp='S', l='Paste UVs', c=lambda _: core.attributes.pasteUVs()) + mc.menuItem(ob=1, c=lambda _: ui.attributesFilter.openUI()) + + resetPivot = wrap.Button(row.layout(), 'gsResetPivot') + resetPivot.setLabel('Reset
Pivot') + resetPivot.clicked.connect(undo(core.resetCurvePivotPoint)) + resetPivot.setIcon('mod-bottom') + mc.popupMenu(mm=1, p=resetPivot.objectName()) + mc.menuItem('gsResetPivotToRoot', rp='N', l='Reset to Root', c=lambda _: core.resetCurvePivotPoint()) + mc.menuItem('gsResetPivotToTip', rp='S', l='Reset to Tip', c=lambda _: core.resetCurvePivotPoint(2)) + + layout.addWidget(wrap.separator()) + + # Rebuild Curve + def rebuildSliderRelease(_): + core.sliders.rebuildSliderRelease() + core.sliders.release() + + def rebuildButtonClicked(): + core.sliders.rebuildSliderDrag() + rebuildSliderRelease(None) + + with wrap.Row(layout, margins=style.scale([1.5, 0, 1.5, 0])) as rebuildResetRow: + rebuildButton = wrap.Button(objName='gsRebuildWithCurrentValue') + rebuildButton.setButtonStyle('small-filled') + rebuildButton.setLabel("R", lineHeight=100) + rebuildButton.setMinimumWidth(1) + rebuildButton.setMaximumSize(*style.scale([16, 16])) + rebuildButton.clicked.connect(rebuildButtonClicked) + + resetButton = wrap.Button(objName='gsResetRebuildSliderRange') + resetButton.setButtonStyle('small-filled') + resetButton.setIcon('reset') + resetButton.setMinimumWidth(1) + resetButton.setMaximumSize(*style.scale([16, 16])) + resetButton.clicked.connect(lambda: mc.intSliderGrp('gsRebuildSlider', e=1, min=1, max=50)) + + rebuildResetRow.layout().addWidget(rebuildButton, 1) + rebuildResetRow.layout().addWidget(wrap.wrapControl(mc.text(l='Rebuild Curve')), 3) + rebuildResetRow.layout().addWidget(resetButton, 1) + + rebuildCurveSlider = wrap.wrapControl(mc.intSliderGrp( + 'gsRebuildSlider', f=1, cw=[(1, 32), (2, 28)], min=1, max=50, fmx=999, v=1, + dc=core.sliders.rebuildSliderDrag, + cc=rebuildSliderRelease)) + WIDGETS['gsRebuildSlider'] = rebuildCurveSlider + layout.addWidget(rebuildCurveSlider) + + # Duplicate and Randomize + with wrap.Row(layout) as row: + duplicateCurve = wrap.Button(row.layout(), 'gsDuplicateCurve') + duplicateCurve.setLabel('Duplicate') + duplicateCurve.clicked.connect(undo(core.duplicateCurve)) + + randomizeCurve = wrap.Button(row.layout(), 'gsRandomizeCurve') + randomizeCurve.setLabel('Randomize') + randomizeCurve.clicked.connect(ui.randomizeCurveWindow) + + # Extend and Reduce + with wrap.Row(layout) as row: + extendCurve = wrap.Button(row.layout(), 'gsExtendCurve') + extendCurve.setLabel('Extend') + extendCurve.clicked.connect(undo(core.extendCurve)) + + reduceCurve = wrap.Button(row.layout(), 'gsReduceCurve') + reduceCurve.setLabel('Reduce') + reduceCurve.clicked.connect(undo(core.reduceCurve)) + + # Smooth + with wrap.Row(layout) as row: + smooth = wrap.Button(row.layout(), 'gsSmooth') + smooth.setLabel('Smooth') + smooth.clicked.connect(undo(core.smoothCurve)) + smooth.setIcon('marking-top') + mc.popupMenu(mm=1, p=smooth.objectName()) + mc.radioMenuItemCollection() + mc.menuItem('gsSmoothMult1', rp='N', rb=1, l='x1') + mc.menuItem('gsSmoothMult3', rp='E', rb=0, l='x3') + mc.menuItem('gsSmoothMult5', rp='S', rb=0, l='x5') + mc.menuItem('gsSmoothMult10', rp='W', rb=0, l='x10') + + # Smooth Slider + factorSlider = wrap.wrapControl(mc.floatSliderGrp( + 'gsFactorSlider', l='Factor', adj=3, w=1, cw=[(1, 32), (2, 28)], min=1, max=100)) + WIDGETS['gsFactorSlider'] = factorSlider + layout.addWidget(factorSlider) + + layout.addWidget(wrap.separator()) + + # Mirroring + with wrap.Frame(layout, objName='MirrorFrame', label='Mirroring', margins=[1, 1, 1, 1]) as frame: + with wrap.Row(frame.getFrameLayout()) as row: + mirrorX = wrap.Button(row.layout(), 'mirrorX') + mirrorX.setLabel('X') + mirrorX.setFontSize(16) + mirrorX.clicked.connect(pa(undo(core.mirrorHair), 0)) + mirrorY = wrap.Button(row.layout(), 'mirrorY') + mirrorY.setLabel('Y') + mirrorY.setFontSize(16) + mirrorY.clicked.connect(pa(undo(core.mirrorHair), 1)) + mirrorZ = wrap.Button(row.layout(), 'mirrorZ') + mirrorZ.setLabel('Z') + mirrorZ.setFontSize(16) + mirrorZ.clicked.connect(pa(undo(core.mirrorHair), 2)) + + with wrap.Row(frame.getFrameLayout()) as row: + mirrorFlipGrp = QtWidgets.QButtonGroup(row.layout()) + mirror = wrap.Button(row.layout(), 'mirrorRadio') + mirror.setLabel('Mirror') + mirror.setButtonStyle('small') + mirror.setCheckable(True) + mirror.setChecked(True) + flip = wrap.Button(row.layout(), 'flipRadio') + flip.setLabel('Flip') + flip.setButtonStyle('small') + flip.setCheckable(True) + + mirrorFlipGrp.addButton(mirror) + mirrorFlipGrp.addButton(flip) + + layout.addWidget(wrap.separator()) + + # Control Curve and Apply + with wrap.Row(layout) as row: + controlCurve = wrap.Button(row.layout(), 'gsControlCurve') + controlCurve.setLabel('Control Curve') + controlCurve.clicked.connect(undo(core.controlCurveCreate)) + + applyControlCurve = wrap.Button(row.layout(), 'gsApplyControlCurve') + applyControlCurve.setLabel('Apply') + applyControlCurve.setFixedWidth(style.scale(48)) + applyControlCurve.clicked.connect(undo(core.controlCurveApply)) + + layout.addWidget(wrap.separator()) + + # Curve Control Window + with wrap.Row(layout) as row: + curveControlWindow = wrap.Button(row.layout(), 'gsCurveControlWindow') + curveControlWindow.setLabel('Curve Control Window') + curveControlWindow.pressed.connect(ui.curveControlWorkspace) + + layout.addWidget(wrap.separator()) + + # UV Editor Window + with wrap.Row(layout) as row: + uvEditor = wrap.Button(row.layout(), 'gsUVEditorMain') + uvEditor.setLabel('UV Editor Window') + uvEditor.pressed.connect(ui.uvEditorWorkspace) + + layout.addWidget(wrap.separator()) + + # Version + layout.addWidget(wrap.wrapControl(mc.text(l=core.VERSION))) + + # Toggling the correct switch + warpSwitch.setChecked(core.getOption('warpSwitch')) + extrudeSwitch.setChecked(not core.getOption('warpSwitch')) + + # Toggling layer collections widget + core.layerCollections.toggleLayerCollectionsWidget() + + # Setting the custom tooltips + tooltips.toggleCustomTooltipsMain(core.getOption('enableTooltips')) + + def selectionSets(self, i, layout, label): # Creates layer button + def toggleGeometryEdit(*_): + core.curveGeometryEditToggle(i) + core.updateMainUI() + + def toggleCurveVisibility(*_): + core.toggleObjVisibility(i, 0) + core.updateMainUI() + + def toggleGeoVisibility(*_): + core.toggleObjVisibility(i, 1) + core.updateMainUI() + + def toggleLayerVisibility(*_): + core.toggleLayerVisibility(i) + core.updateMainUI() + selSet = wrap.Layer(layout=layout, objName='curveGrp%s' % i) + selSet.setStyleSheet(style.layer()) + selSet.setLabel(str(label)) + mc.popupMenu(mm=1, p=selSet.objectName()) + mc.menuItem(rp='N', l='Add Selection to Layer', c=lambda _: core.curveAddToLayer(i)) + mc.menuItem(rp='NW', l='Extract Geometry', c=lambda _: core.extractCurveGeo(i)) + mc.menuItem(rp='NE', l='Toggle Geometry Edit', c=toggleGeometryEdit) + mc.menuItem(rp='W', l='Select Curves', c=lambda _: core.curveLayerSelectObj(i, 0)) + mc.menuItem(rp='E', l='Select Geometry', c=lambda _: core.curveLayerSelectObj(i, 1)) + mc.menuItem(rp='SW', l='Toggle Curve Visibility', c=toggleCurveVisibility) + mc.menuItem(rp='SE', l='Toggle Geo Visibility', c=toggleGeoVisibility) + mc.menuItem(rp='S', l='Toggle Layer Visibility', c=toggleLayerVisibility) + selSet.clicked.connect(pa(undo(core.layerClicked), i)) + return selSet + + def extrudeWarpToggle(self): + buttons = ['newCard', 'newTube', 'curveCard', 'curveTube', 'addCards', 'addTubes'] + buttonStyle = style.buttonNormal + if WIDGETS['extrudeSwitch'].isChecked(): + buttonStyle = style.buttonNormalBlueBorder + for button in buttons: + WIDGETS[button].setStyleSheet(buttonStyle) + + def updateLayerList(self): + if 'gsLayerSelector' in WIDGETS: + WIDGETS['gsLayerSelector'].updateLayerList() + core.curveControlUI.updateUI() + + def toggleTooltips(self): + for widget in WIDGETS: + if hasattr(WIDGETS[widget], "enableTooltip") and callable(getattr(WIDGETS[widget], "enableTooltip")): + WIDGETS[widget].enableTooltip(core.getOption('enableTooltips')) + tooltips.toggleCustomTooltipsMain(core.getOption('enableTooltips')) + tooltips.toggleCustomTooltipsCurveControl(core.getOption('enableTooltips')) diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/2018/cv_manip.mll b/Scripts/Modeling/Edit/gs_curvetools/plugins/2018/cv_manip.mll new file mode 100644 index 0000000000000000000000000000000000000000..00782194a6c4eb26520b6ee58257183c6100d2f9 GIT binary patch literal 90112 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%0D?{@T&P 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%0D%UQys++!2W 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 z z2K)m(1HA(~2l@u~4ul8#2L=bS59A#vK43XecEEO^=79Y`(}9))JqLOZ>^xvU*mSVv zV8=oK!JdP?2X`LqJGl2?_+bCR!Gnhm4jnvlaQNWSgChq=4~`u?c~Fc%hmoY=^l)}K zFKiAMhb`f zC&OZ2`o8RadHc-!iuYOemF=_btJ!DY*R-!?U&lWGzMg%(`}Xc1+<$1l?Y^e_TJGz( n&wpRfeNeC%zi2Gp3yvqlX2kIC8{9Xt&$7Q|KMD2!*YE!VU|bA4 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/2019/cv_manip.mll b/Scripts/Modeling/Edit/gs_curvetools/plugins/2019/cv_manip.mll new file mode 100644 index 0000000000000000000000000000000000000000..93736ce4d6bb0af1a32f313ce56cabd779de043e GIT binary patch literal 90624 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

How To Fix Maya 2020-2022 UV Bug:

') + + linkButton = wrap.Button(row.layout()) + linkButton.setLabel('Open Fix') + linkButton.setButtonStyle('small-filled') + linkButton.clicked.connect(lambda: utils.openLink( + 'https://gs-curvetools.readthedocs.io/en/latest/faq.html#maya-2020-2022-and-broken-uvs')) + + dismissMessage = wrap.Button(row.layout()) + dismissMessage.setLabel('Dismiss') + dismissMessage.setButtonStyle('small-filled') + dismissMessage.clicked.connect(lambda: row.setHidden(True)) + dismissMessage.clicked.connect(lambda: mc.optionVar(iv=['GSCT_UVBugMessageDismissed', 1])) + + # Advanced Visibility Frame + with wrap.Frame(layout, 'advancedVisibilityFrame', label='Advanced Visibility', + margins=style.scale([3, 2, 3, 2]), spacing=style.scale(3)) as visibilityFrame: + with wrap.Row(visibilityFrame.getFrameLayout()) as row: + + geometryHighlight = wrap.Button(row.layout(), 'geometryHighlight') + geometryHighlight.setLabel('Geometry Highlight', lineHeight=100) + geometryHighlight.setCheckable(True) + geometryHighlight.setChecked(bool(mc.optionVar(q="GSCT_GeometryHighlightEnabled"))) + geometryHighlight.setButtonStyle('small') + + geometryHighlight.clicked.connect(noUndo(core.advancedVisibility.geometryHighlightCommand)) + + curveHighlight = wrap.Button(row.layout(), 'curveHighlight') + curveHighlight.setLabel('Curve Highlight', lineHeight=100) + curveHighlight.setCheckable(True) + curveHighlight.setButtonStyle('small') + curveHighlight.setChecked(False) + curveHighlight.clicked.connect(undo(core.advancedVisibility.toggleCurveHighlightFromUI)) + + visibilityFrame.getFrameLayout().addWidget(wrap.separator()) + + def sliderChangeCommand(*_): + core.advancedVisibility.applySettingsToNode() + core.advancedVisibility.saveSettingsFromUI() + + with wrap.Column(visibilityFrame.getFrameLayout(), objName='gsCurveHighlightFrame', spacing=style.scale(4)) as column: + column.setEnabled(False) # Disable immediately. We are going to check its state later. + + # CV Size + with wrap.Row(column.layout(), spacing=4) as row: + CVsizeLabel = wrap.Label() + CVsizeLabel.setLabel("CV Size:") + CVsizeLabel.setFixedWidth(style.scale(100)) + CVsizeLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + pointSizeSlider = wrap.mayaSlider( + mc.floatSliderGrp( + 'gsPointSizeSlider', + f=1, adj=2, w=1, cw=[1, 45], min=1, max=100, v=10, + dc=lambda _: noUndo(core.advancedVisibility.applySettingsToNode)(), + cc=noUndo(sliderChangeCommand), + ) + ) + WIDGETS['gsPointSizeSlider'] = pointSizeSlider + + row.layout().addWidget(CVsizeLabel, 0) + row.layout().addWidget(pointSizeSlider, 1) + + # Selected Color + with wrap.Row(column.layout(), spacing=style.scale(4)) as row: + selectedColorLabel = wrap.Label() + selectedColorLabel.setLabel("Selected Color:") + selectedColorLabel.setFixedWidth(style.scale(100)) + selectedColorLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + selectedColorPicker = wrap.ColorPicker("gsSelectedCVColor") + selectedColorPicker.connectDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + selectedColorPicker.connectCommand(noUndo(sliderChangeCommand)) + + selectedAlphaValue = wrap.FloatField("gsSelectedCVAlpha") + selectedAlphaValue.setFixedWidth(style.scale(45)) + selectedAlphaValue.setValue(1.0) + selectedAlphaValue.setRange(0, 1) + selectedAlphaValue.setStep(0.01) + selectedAlphaValue.setPrecision(2) + selectedAlphaValue.setDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + selectedAlphaValue.setReleaseCommand(noUndo(sliderChangeCommand)) + + row.layout().addWidget(selectedColorLabel, 0) + row.layout().addWidget(selectedColorPicker, 1) + row.layout().addWidget(selectedAlphaValue, 0) + + # Deselected Color + with wrap.Row(column.layout(), spacing=style.scale(4)) as row: + deselectedColorLabel = wrap.Label() + deselectedColorLabel.setLabel("Deselected Color:") + deselectedColorLabel.setFixedWidth(style.scale(100)) + deselectedColorLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + deselectedColorPicker = wrap.ColorPicker("gsDeselectedCVColor") + deselectedColorPicker.connectDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + deselectedColorPicker.connectCommand(noUndo(sliderChangeCommand)) + + deselectedAlphaValue = wrap.FloatField("gsDeselectedCVAlpha") + deselectedAlphaValue.setFixedWidth(style.scale(45)) + deselectedAlphaValue.setValue(1) + deselectedAlphaValue.setRange(0, 1) + deselectedAlphaValue.setStep(0.01) + deselectedAlphaValue.setPrecision(2) + deselectedAlphaValue.setDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + deselectedAlphaValue.setReleaseCommand(noUndo(sliderChangeCommand)) + + row.layout().addWidget(deselectedColorLabel, 0) + row.layout().addWidget(deselectedColorPicker, 1) + row.layout().addWidget(deselectedAlphaValue, 1) + + column.layout().addWidget(wrap.separator()) + + # Curve Visibility + curveVisibility = wrap.Button(column.layout(), 'curveVisibility') + curveVisibility.setLabel('Curve Visibility', lineHeight=100) + curveVisibility.setCheckable(True) + curveVisibility.setButtonStyle('small') + curveVisibility.setChecked(True) + curveVisibility.clicked.connect(noUndo(sliderChangeCommand)) + + with wrap.Row(column.layout(), spacing=4) as row: + curveWidthLabel = wrap.Label() + curveWidthLabel.setLabel("Curve Width:") + curveWidthLabel.setFixedWidth(style.scale(100)) + curveWidthLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + curveWidthSlider = wrap.mayaSlider( + mc.floatSliderGrp( + 'gsCurveWidthSlider', + f=1, adj=2, w=1, cw=[1, 45], min=1, max=100, v=4, + dc=lambda _: noUndo(core.advancedVisibility.applySettingsToNode)(), + cc=noUndo(sliderChangeCommand), + ), + ) + WIDGETS['gsCurveWidthSlider'] = curveWidthSlider + + row.layout().addWidget(curveWidthLabel, 0) + row.layout().addWidget(curveWidthSlider, 1) + + with wrap.Row(column.layout(), spacing=style.scale(4)) as row: + curveHighlightColorLabel = wrap.Label() + curveHighlightColorLabel.setLabel("Curve Color:") + curveHighlightColorLabel.setFixedWidth(style.scale(100)) + curveHighlightColorLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + curveHighlightColor = wrap.ColorPicker("gsCurveHighlightColor") + curveHighlightColor.connectDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + curveHighlightColor.connectCommand(noUndo(sliderChangeCommand)) + + curveHighlightAlpha = wrap.FloatField("gsCurveHighlightAlpha") + curveHighlightAlpha.setFixedWidth(style.scale(45)) + curveHighlightAlpha.setValue(1.0) + curveHighlightAlpha.setRange(0, 1) + curveHighlightAlpha.setStep(0.01) + curveHighlightAlpha.setPrecision(2) + curveHighlightAlpha.setDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + curveHighlightAlpha.setReleaseCommand(noUndo(sliderChangeCommand)) + + row.layout().addWidget(curveHighlightColorLabel, 0) + row.layout().addWidget(curveHighlightColor, 1) + row.layout().addWidget(curveHighlightAlpha, 1) + + column.layout().addWidget(wrap.separator()) + + # Hull Visibility + hullVisibility = wrap.Button(column.layout(), 'hullVisibility') + hullVisibility.setLabel('Hull Visibility', lineHeight=100) + hullVisibility.setCheckable(True) + hullVisibility.setButtonStyle('small') + hullVisibility.clicked.connect(noUndo(sliderChangeCommand)) + + with wrap.Row(column.layout(), spacing=4) as row: + hullWidthLabel = wrap.Label() + hullWidthLabel.setLabel("Hull Width:") + hullWidthLabel.setFixedWidth(style.scale(100)) + hullWidthLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + hullWidthSlider = wrap.mayaSlider( + mc.floatSliderGrp( + 'gsHullWidthSlider', + f=1, adj=2, w=1, cw=[1, 45], min=1, max=100, v=3, + dc=lambda _: noUndo(core.advancedVisibility.applySettingsToNode)(), + cc=noUndo(sliderChangeCommand), + ) + ) + WIDGETS['gsHullWidthSlider'] = hullWidthSlider + + row.layout().addWidget(hullWidthLabel, 0) + row.layout().addWidget(hullWidthSlider, 1) + + with wrap.Row(column.layout(), spacing=style.scale(4)) as row: + hullColorLabel = wrap.Label() + hullColorLabel.setLabel("Hull Color:") + hullColorLabel.setFixedWidth(style.scale(100)) + hullColorLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + hullColor = wrap.ColorPicker("gsHullHighlightColor") + hullColor.connectDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + hullColor.connectCommand(noUndo(sliderChangeCommand)) + + hullAlpha = wrap.FloatField("gsHullHighlightAlpha") + hullAlpha.setFixedWidth(style.scale(45)) + hullAlpha.setValue(1.0) + hullAlpha.setRange(0, 1) + hullAlpha.setStep(0.01) + hullAlpha.setPrecision(2) + hullAlpha.setDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + hullAlpha.setReleaseCommand(noUndo(sliderChangeCommand)) + + row.layout().addWidget(hullColorLabel, 0) + row.layout().addWidget(hullColor, 1) + row.layout().addWidget(hullAlpha, 1) + + column.layout().addWidget(wrap.separator()) + + with wrap.Frame(column.layout(), 'advancedVisibilityFrame', label='Other Options:', + margins=style.scale([3, 2, 3, 2]), spacing=style.scale(3)) as optionsFrame: + with wrap.Row(optionsFrame.getFrameLayout()) as row: + lazyUpdate = wrap.Button(row.layout(), 'lazyUpdate') + lazyUpdate.setLabel("Lazy Update", lineHeight=100) + lazyUpdate.setCheckable(True) + lazyUpdate.setButtonStyle('small') + lazyUpdate.clicked.connect(noUndo(sliderChangeCommand)) + + alwaysOnTop = wrap.Button(row.layout(), 'alwaysOnTop') + alwaysOnTop.setLabel("Always On Top", lineHeight=100) + alwaysOnTop.setCheckable(True) + alwaysOnTop.setButtonStyle('small') + alwaysOnTop.setChecked(True) + alwaysOnTop.clicked.connect(noUndo(sliderChangeCommand)) + + optionsFrame.getFrameLayout().addWidget(wrap.separator()) + + experimentalLabel = wrap.Label(optionsFrame.getFrameLayout()) + experimentalLabel.setLabel("Distance Colors:") + + with wrap.Row(optionsFrame.getFrameLayout()) as row: + curveDistanceColor = wrap.Button(row.layout(), 'curveDistanceColor') + curveDistanceColor.setLabel("Curve", lineHeight=100) + curveDistanceColor.setCheckable(True) + curveDistanceColor.setButtonStyle('small') + curveDistanceColor.setChecked(True) + curveDistanceColor.clicked.connect(noUndo(sliderChangeCommand)) + + cvDistanceColor = wrap.Button(row.layout(), 'cvDistanceColor') + cvDistanceColor.setLabel("CV", lineHeight=100) + cvDistanceColor.setCheckable(True) + cvDistanceColor.setButtonStyle('small') + cvDistanceColor.setChecked(True) + cvDistanceColor.clicked.connect(noUndo(sliderChangeCommand)) + + hullDistanceColor = wrap.Button(row.layout(), 'hullDistanceColor') + hullDistanceColor.setLabel("Hull", lineHeight=100) + hullDistanceColor.setCheckable(True) + hullDistanceColor.setButtonStyle('small') + hullDistanceColor.setChecked(True) + hullDistanceColor.clicked.connect(noUndo(sliderChangeCommand)) + + with wrap.Row(optionsFrame.getFrameLayout()) as row: + distanceColorMinLabel = wrap.Label() + distanceColorMinLabel.setLabel("Color Min:") + distanceColorMinLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + distanceColorMinInput = wrap.FloatField("gsDistanceColorMinValue") + distanceColorMinInput.setValue(0.25) + distanceColorMinInput.setRange(0.01, 1) + distanceColorMinInput.setStep(0.01) + distanceColorMinInput.setPrecision(2) + distanceColorMinInput.setDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + distanceColorMinInput.setReleaseCommand(noUndo(sliderChangeCommand)) + + distanceColorMaxLabel = wrap.Label() + distanceColorMaxLabel.setLabel("Color Max:") + distanceColorMaxLabel.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) + + distanceColorMaxInput = wrap.FloatField("gsDistanceColorMaxValue") + distanceColorMaxInput.setValue(1.0) + distanceColorMaxInput.setRange(0.01, 1) + distanceColorMaxInput.setStep(0.01) + distanceColorMaxInput.setPrecision(2) + distanceColorMaxInput.setDragCommand(lambda *_: noUndo(core.advancedVisibility.applySettingsToNode)()) + distanceColorMaxInput.setReleaseCommand(noUndo(sliderChangeCommand)) + + row.layout().addWidget(distanceColorMinLabel, 1) + row.layout().addWidget(distanceColorMinInput, 1) + row.layout().addWidget(distanceColorMaxLabel, 1) + row.layout().addWidget(distanceColorMaxInput, 1) + + optionsFrame.getFrameLayout().addWidget(wrap.separator()) + + experimentalLabel = wrap.Label(optionsFrame.getFrameLayout()) + experimentalLabel.setLabel("Experimental (possibly slow):") + + CVocclusion = wrap.Button(optionsFrame.getFrameLayout(), 'CVocclusion') + CVocclusion.setLabel("Enable CV Occlusion", lineHeight=100) + CVocclusion.setCheckable(True) + CVocclusion.setButtonStyle('small') + CVocclusion.clicked.connect(noUndo(sliderChangeCommand)) + + with wrap.Row(optionsFrame.getFrameLayout(), spacing=style.scale(4)) as row: + selectOccluderButton = wrap.Button(row.layout(), "gsSelectOccluderButton") + selectOccluderButton.setFixedWidth(style.scale(100)) + selectOccluderButton.setLabel('Select Occluder') + selectOccluderButton.clicked.connect(noUndo(core.advancedVisibility.selectOccluderFromScene)) + + occluderMeshName = wrap.LineEdit('gsOccluderMeshName', row.layout()) + occluderMeshName.setPlaceholderText('Select or Type Occluder Mesh Name') + occluderMeshName.setClearButtonEnabled(True) + occluderMeshName.editingFinished.connect(noUndo(sliderChangeCommand)) + core.advancedVisibility.loadSettingsFromOptionVar() + tooltips.toggleCustomTooltipsCurveControl(core.getOption('enableTooltips')) + + layout.addWidget(wrap.separator()) + + resetButton = wrap.Button(layout, 'resetControlSliders') + resetButton.setLabel('Reset Sliders Range') + resetButton.clicked.connect(core.resetControlSliders) + + def twistGraphPopOut(self): + name = "GSCT_TwistGraphPopOut" + if mc.workspaceControl(name, q=1, ex=1): + mc.deleteUI(name) + popOut = CreatePopOut(name, "Twist Graph Large", 512, 400) + graph = wrap.FallOffCurve(popOut.widgetLayout, 'twistCurve_large') + + def twistGraphCommand(_): + core.attributes.propagateGraphs(graph) + core.attributes.storeGraphs(graph) + graph.changeCommand(twistGraphCommand) + with wrap.Row(popOut.widgetLayout, margins=style.scale([5, 0, 5, 2])) as row: + row.setFixedHeight(style.BUTTON_HEIGHT) + + magnitude = wrap.FloatField('Magnitude_large', row.layout(), attrName='Magnitude') + magnitude.setFixedWidth(style.scale(45)) + magnitude.setRange(-99, 99) + magnitude.setStep(0.01) + magnitude.setPrecision(2) + magnitude.setDragCommand(pa(core.sliders.curveControlSliderDrag, magnitude)) + magnitude.setReleaseCommand(core.sliders.release) + + resetButton = wrap.Button(row.layout()) + resetButton.setLabel('Reset Curve') + + def resetTwist(): + utils.resetSingleGraph('twist') + core.attributes.storeGraphs(graph) + resetButton.clicked.connect(undo(resetTwist)) + + core.curveControlUI.updateUI() + + def widthGraphPopOut(self): + name = "GSCT_WidthGraphPopOut" + if mc.workspaceControl(name, q=1, ex=1): + mc.deleteUI(name) + popOut = CreatePopOut(name, "Width Graph Large", 512, 400) + graph = wrap.FallOffCurve(popOut.widgetLayout, 'scaleCurve_large') + + def widthGraphCommand(_): + core.attributes.propagateGraphs(graph) + core.attributes.storeGraphs(graph) + graph.changeCommand(widthGraphCommand) + with wrap.Row(popOut.widgetLayout, margins=style.scale([5, 0, 5, 2])) as row: + row.setFixedHeight(style.BUTTON_HEIGHT) + resetButton = wrap.Button(row.layout()) + resetButton.setLabel('Reset Curve') + + def resetWidthCmd(): + utils.resetSingleGraph('width') + core.attributes.storeGraphs(graph) + resetButton.clicked.connect(undo(resetWidthCmd)) + core.curveControlUI.updateUI() + + def profileGraphPopOut(self): + name = "GSCT_ProfileGraphPopOut" + if mc.workspaceControl(name, q=1, ex=1): + mc.deleteUI(name) + popOut = CreatePopOut(name, "Profile Graph Large", 512, 400) + graph = wrap.FallOffCurve(popOut.widgetLayout, 'profileCurve_large', attr=False) + + def changeCommand(value): + core.updateLattice(value) + core.equalizeProfileCurve() + graph.changeCommand(changeCommand) + with wrap.Row(popOut.widgetLayout, margins=style.scale([5, 0, 5, 2])) as row: + row.setFixedHeight(style.BUTTON_HEIGHT) + resetButton = wrap.Button(row.layout()) + resetButton.setLabel('Reset Curve') + resetButton.clicked.connect(undo(core.resetProfileCurve)) + core.curveControlUI.updateUI() + + +# UV Editor Window +def uvEditorWorkspace(): + global uveditor + if mc.workspaceControl(UV_EDITOR_NAME, q=1, ex=1): + if MAYA_VER >= 2018: + if not mc.workspaceControl(UV_EDITOR_NAME, q=1, vis=1): + mc.workspaceControl(UV_EDITOR_NAME, e=1, rs=1) + else: + mc.workspaceControl(UV_EDITOR_NAME, e=1, vis=0) + try: + uveditor.updateEditor() # pylint: disable=used-before-assignment + except Exception as e: + LOGGER.exception(e) + else: + mc.workspaceControl(UV_EDITOR_NAME, e=1, fl=1) + mc.deleteUI(UV_EDITOR_NAME) + else: + uveditor = UVEditor('uveditor') + uveditor.init() + uveditor.updateEditor() + from . import main + main.checkScriptJobs(UV_EDITOR_NAME) + + +class UVEditor(QtWidgets.QWidget): + + def __init__(self, name): + super(UVEditor, self).__init__() + self.name = name + self.timer = utils.Timer() + self.mouseToggle = False + self.previousSelection = [] + self.uvUpdateCheck = 0 + self.isolateMode = False + self.currentSelection = [] + + def init(self): + if mc.workspaceControl(UV_EDITOR_NAME, q=1, ex=1): + mc.deleteUI(UV_EDITOR_NAME) + parent = mayaWorkspaceControl(name=UV_EDITOR_NAME, + label=UV_EDITOR_LABEL, + retain=False, iw=705, ih=575, + widthProperty="free") + self.setParent(parent) + + parent.layout().addWidget(self) + self.window() + + self.editor.signal_keyPress.connect(self.updateButtons) + self.editor.signal_mouseMove.connect(self._updateCurves) + self.editor.signal_mouseRelease.connect(self._stopCurvesUpdate) + self.editor.signal_uvDrawEnd.connect(self.manualCurveUpdate) + self.editor.signal_functionKeyPress.connect(undo(self.functionSwitch)) + self.uvList.signal_keyPressed.connect(self.updateVisibility) + + def window(self): + # Layout + mainLayout = QtWidgets.QVBoxLayout(self) + mainLayout.setContentsMargins(*style.scale([2, 2, 2, 2])) + + self.scrollWidget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(self.scrollWidget) + scrollArea = QtWidgets.QScrollArea() + scrollArea.setWidget(self.scrollWidget) + mainLayout.addWidget(scrollArea) + + # Layout Settings + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(style.scale(2)) + layout.setMargin(0) + layout.setAlignment(QtCore.Qt.AlignTop) + + scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + scrollArea.setWidgetResizable(True) + + # Editor Init + self.editor = uv_editor.Editor() + self.editor.init() + self.updateColors() + + # Main Widgets + with wrap.Row(layout) as mainRow: + with wrap.Column(mainRow.layout()) as mainColumn: + mainColumn.layout().setAlignment(QtCore.Qt.AlignTop) + mainColumn.setFixedWidth(style.scale(130)) + + # Controller switches + mainColumn.layout().addWidget(wrap.separator()) + controllerLabel = wrap.Label(mainColumn.layout()) + controllerLabel.setLabel('Controller Mode') + + self.controllerGroup = QtWidgets.QButtonGroup(mainColumn.layout()) + self.controllerGroup.buttonReleased.connect(self.updateControllerMode) + + with wrap.Row(mainColumn.layout()) as selectMove: + select = wrap.Button(selectMove.layout(), 'gsUVSelect') + select.setCheckable(True) + select.setChecked(True) + select.setLabel("Select (Q)") + move = wrap.Button(selectMove.layout(), 'gsUVMove') + move.setCheckable(True) + move.setLabel("Move (W)") + with wrap.Row(mainColumn.layout()) as rotateScale: + rotate = wrap.Button(rotateScale.layout(), 'gsUVRotate') + rotate.setCheckable(True) + rotate.setLabel("Rotate (E)") + scale = wrap.Button(rotateScale.layout(), 'gsUVScale') + scale.setCheckable(True) + scale.setLabel("Scale (R)") + + with wrap.Row(mainColumn.layout()) as scaleRow: + self.directionSwitch = QtWidgets.QButtonGroup(scaleRow.layout()) + self.directionSwitch.buttonReleased.connect(self.updateControllerMode) + + horizontal = wrap.Button(scaleRow.layout(), 'gsUVHorizontalScale') + horizontal.setLabel('H') + horizontal.setButtonStyle('small') + horizontal.setCheckable(True) + horizontal.setChecked(True) + vertical = wrap.Button(scaleRow.layout(), 'gsUVVerticalScale') + vertical.setButtonStyle('small') + vertical.setCheckable(True) + vertical.setLabel('V') + + self.directionSwitch.addButton(horizontal, 0) + self.directionSwitch.addButton(vertical, 1) + + drawUV = wrap.Button(mainColumn.layout(), 'gsDrawUVs') + drawUV.setCheckable(True) + drawUV.setLabel('Draw (D)') + + self.controllerGroup.addButton(select, 0) + self.controllerGroup.addButton(move, 1) + self.controllerGroup.addButton(rotate, 2) + self.controllerGroup.addButton(scale, 3) + self.controllerGroup.addButton(drawUV, 4) + + mainColumn.layout().addWidget(wrap.separator()) + + # Utility Functions + utilityLabel = wrap.Label(mainColumn.layout()) + utilityLabel.setLabel('Utility Functions') + + with wrap.Row(mainColumn.layout()) as HVFlip: + hFlipUV = wrap.Button(HVFlip.layout(), 'gsHorizontalFlipUV') + hFlipUV.setLabel('H-Flip (H)') + hFlipUV.clicked.connect(undo(self.horizontalFlipUV)) + + vFlipUV = wrap.Button(HVFlip.layout(), 'gsVerticalFlipUV') + vFlipUV.setLabel('V-Flip (V)') + vFlipUV.clicked.connect(undo(self.verticalFlipUV)) + + resetUV = wrap.Button(mainColumn.layout(), 'gsResetUVs') + resetUV.setLabel('Reset UV (X)') + resetUV.clicked.connect(undo(self.resetUVs)) + + syncSelection = wrap.Button(mainColumn.layout(), 'gsSyncSelectionUVs') + syncSelection.setLabel('Sync Selection (S)') + syncSelection.clicked.connect(undo(self.syncSelection)) + + randomizeUVs = wrap.Button(mainColumn.layout(), 'gsRandomizeUVs') + randomizeUVs.setLabel('Randomize') + randomizeUVs.setIcon('mod-bottom') + randomizeUVs.clicked.connect(undo(self.randomizeUVs)) + + mainColumn.layout().addWidget(wrap.separator()) + + # UV List + uvListLabel = wrap.Label(mainColumn.layout()) + uvListLabel.setLabel('UV List') + + self.uvList = wrap.UVItemList(mainColumn.layout()) + self.uvList.setLayout(mainColumn.layout()) + self.uvList.signal_mouseReleased.connect(self.updateVisibility) + + focusView = wrap.Button(mainColumn.layout(), 'gsFocusUVs') + focusView.setLabel('Focus View (F)') + focusView.clicked.connect(self.editor.focusView) + + with wrap.Row(mainColumn.layout()) as hideIsolate: + isolateSelect = wrap.Button(hideIsolate.layout(), 'gsUVIsolateSelect') + isolateSelect.setLabel('Isolate (I)') + isolateSelect.clicked.connect(self.isolateSelect) + + hide = wrap.Button(hideIsolate.layout(), 'gsUVHide') + hide.setLabel('Hide (O)') + hide.clicked.connect(self.hide) + + showAllButton = wrap.Button(mainColumn.layout(), 'gsUVShowAll') + showAllButton.setLabel('Show All (A)') + showAllButton.clicked.connect(self.showAll) + + mainColumn.layout().addWidget(wrap.separator()) + + with wrap.Frame(mainColumn.layout(), label="Options") as optionsFrame: + # Texture Controls + textureFunctionsLabel = wrap.Label(optionsFrame.getFrameLayout()) + textureFunctionsLabel.setLabel('Texture Controls:') + + transparency = wrap.Button(optionsFrame.getFrameLayout(), 'UVEditorTransparencyToggle') + transparency.setButtonStyle('small') + transparency.setCheckable(True) + transparency.setChecked(mc.optionVar(q='GSCT_UVEditorTransparencyToggle')) + transparency.setLabel("Transparency") + + alphaOnly = wrap.Button(optionsFrame.getFrameLayout(), 'UVEditorAlphaOnlyToggle') + alphaOnly.setButtonStyle('small') + alphaOnly.setCheckable(True) + alphaOnly.setChecked(mc.optionVar(q='GSCT_UVEditorAlphaOnly')) + alphaOnly.setLabel("Alpha Only") + + useTransforms = wrap.Button(optionsFrame.getFrameLayout(), 'UVEditorUseTransforms') + useTransforms.setButtonStyle('small') + useTransforms.setCheckable(True) + useTransforms.setChecked(True) + useTransforms.setLabel("Use Transforms") + useTransforms.clicked.connect(self.updateTexture) + + def toggleAlphaCommand(): + if transparency.isChecked(): + self.editor.toggleAlpha(True) + else: + self.editor.toggleAlpha(False) + alphaOnly.setChecked(False) + self.updateTexture() + mc.optionVar(iv=['GSCT_UVEditorTransparencyToggle', transparency.isChecked()]) + transparency.clicked.connect(toggleAlphaCommand) + + def toggleAlphaOnlyCommand(): # BUG: Find out why switching textures does not update alpha + if alphaOnly.isChecked(): + transparency.setChecked(True) + else: + transparency.setChecked(False) + toggleAlphaCommand() + mc.optionVar(iv=['GSCT_UVEditorAlphaOnly', alphaOnly.isChecked()]) + alphaOnly.clicked.connect(toggleAlphaOnlyCommand) + + optionsFrame.getFrameLayout().addWidget(wrap.separator()) + + # Editor Colors + textureFunctionsLabel = wrap.Label(optionsFrame.getFrameLayout()) + textureFunctionsLabel.setLabel('Editor Colors:') + + with wrap.Row(optionsFrame.getFrameLayout(), margins=style.scale([5, 0, 5, 5])) as colorRow: + backgroundColorPicker = wrap.ColorPicker('UVEditorBGColorPicker', colorRow.layout()) + backgroundColorPicker.setRGBColors( + utils.colorFrom255to1(eval(mc.optionVar(q="GSCT_UVEditorBGColor"))) + ) + backgroundColorPicker.connectCommand(self.updateColors) + + gridColorPicker = wrap.ColorPicker('UVEditorGridColorPicker', colorRow.layout()) + gridColorPicker.setRGBColors( + utils.colorFrom255to1(eval(mc.optionVar(q="GSCT_UVEditorGridColor"))) + ) + gridColorPicker.connectCommand(self.updateColors) + + frameColorPicker = wrap.ColorPicker('UVEditorFrameColorPicker', colorRow.layout()) + frameColorPicker.setRGBColors( + utils.colorFrom255to1(eval(mc.optionVar(q="GSCT_UVEditorFrameColor"))) + ) + frameColorPicker.connectCommand(self.updateColors) + + # UV Card Colors + cardColors = wrap.Label(optionsFrame.getFrameLayout()) + cardColors.setLabel('UV Colors:') + + with wrap.Row(optionsFrame.getFrameLayout(), margins=style.scale([5, 0, 5, 5])) as colorRow2: + selectedCardFrameColor = wrap.ColorPicker('UVEditorUVFrameSelectedColorPicker', colorRow2.layout()) + selectedCardFrameColor.setRGBColors( + utils.colorFrom255to1(eval(mc.optionVar(q="GSCT_UVEditorUVFrameSelectedColor"))) + ) + selectedCardFrameColor.connectCommand(self.updateColors) + + deselectedCardFrameColor = wrap.ColorPicker('UVEditorUVFrameDeselectedColorPicker', colorRow2.layout()) + deselectedCardFrameColor.setRGBColors( + utils.colorFrom255to1(eval(mc.optionVar(q="GSCT_UVEditorUVFrameDeselectedColor"))) + ) + deselectedCardFrameColor.connectCommand(self.updateColors) + + cardFillColor = wrap.ColorPicker('UVEditorUVCardFillColorPicker', colorRow2.layout()) + cardFillColor.setRGBColors( + utils.colorFrom255to1(eval(mc.optionVar(q="GSCT_UVEditorUVCardFillColor"))) + ) + cardFillColor.connectCommand(self.updateColors) + + mainColumn.layout().addWidget(wrap.separator()) + + mainRow.layout().addWidget(self.editor) + + if MAYA_VER in [2020, 2022] and not mc.optionVar(q='GSCT_UVBugMessageDismissed'): + with wrap.Row(mainLayout) as row: + label = wrap.Label(row.layout()) + label.setLabel('

How To Fix Maya 2020-2022 UV Bug:

') + + linkButton = wrap.Button(row.layout()) + linkButton.setLabel('Open Fix') + linkButton.setButtonStyle('small-filled') + linkButton.clicked.connect(lambda: utils.openLink( + 'https://gs-curvetools.readthedocs.io/en/latest/faq.html#maya-2020-2022-and-broken-uvs') + ) + + dismissMessage = wrap.Button(row.layout()) + dismissMessage.setLabel('Dismiss Message') + dismissMessage.setButtonStyle('small-filled') + dismissMessage.clicked.connect(lambda: row.setHidden(True)) + dismissMessage.clicked.connect(lambda: mc.optionVar(iv=['GSCT_UVBugMessageDismissed', 1])) + + def updateColors(self, *_): + if self.editor: + core.saveOptions() + try: + self.editor.changeColor( + eval(mc.optionVar(q="GSCT_UVEditorBGColor")), + eval(mc.optionVar(q="GSCT_UVEditorGridColor")), + eval(mc.optionVar(q="GSCT_UVEditorFrameColor")), + eval(mc.optionVar(q="GSCT_UVEditorUVCardFillColor")), + eval(mc.optionVar(q="GSCT_UVEditorUVFrameSelectedColor")), + eval(mc.optionVar(q="GSCT_UVEditorUVFrameDeselectedColor")) + ) + except Exception as e: + self.editor.changeColor() + LOGGER.exception(e) + self.editor.update() + + @noUndo + def updateEditor(self): + uveditor.updateItemList() + uveditor.updateUVs() + uveditor.updateTexture() + + def showAll(self): + self.uvList.expandAll() + self.uvList.selectAll() + self.updateVisibility() + self.isolateMode = False + + @noUndo + def updateUVs(self, keepSelection=False): + sel = mc.filterExpand(mc.ls(sl=1, fl=1, l=1), sm=9) + oldUVs = self.editor.UVDict + self.editor.purgeUVs() + if not sel: + return + sel = self.checkForLegacyUVs(sel) + for item in sel: + if (mc.attributeQuery('Orientation', n=item, ex=1) and + mc.connectionInfo(item + '.Orientation', isSource=1)): + if (mc.attributeQuery('gsmessage', n=item, ex=1) and + mc.listConnections(item + '.gsmessage')): + curves = core.getAllConnectedCurves(item) + else: + curves = [item] + for curve in curves: + attrs = core.attributes.getUVs(curve) + cb = core.attributes.getCheckboxes(curve) + if attrs: + uv = self.editor.createUV(curve) + if 'flipUV' in cb: + uv.flip = not cb['flipUV'] + uv.moveUV( + x=attrs['moveU'], + y=attrs['moveV'], + rot=attrs['rotateUV'] * -1, + sx=attrs['scaleU'], + sy=attrs['scaleV'], + ) + if keepSelection: + newUVs = self.editor.UVDict + for uv in newUVs: + if uv in oldUVs: + newUVs[uv].setSelected(True) + + def checkForLegacyUVs(self, curves): + legacyCurves = [] + for curve in curves: + root = mc.attributeQuery('rotateRootUV', n=curve, ex=1) + tip = mc.attributeQuery('rotateTipUV', n=curve, ex=1) + if root or tip: + rootValue = mc.getAttr(curve + '.rotateRootUV') + tipValue = mc.getAttr(curve + '.rotateTipUV') + if rootValue or tipValue: + legacyCurves.append(curve) + if not legacyCurves: + return curves + else: + MESSAGE.warningInView('Non Zero Legacy UV Attributes Detected') + dialog = mc.confirmDialog( + title='Legacy UVs', + message='Non Zero Legacy UVs Detected.\nUV Editor is not compatible with them.\nZero them out to proceed?', + icon='warning', + button=['Yes', 'Cancel'], + cancelButton='Cancel', + dismissString='Cancel' + ) + if dialog == 'Yes': + for curve in legacyCurves: + mc.setAttr(curve + '.rotateRootUV', 0) + mc.setAttr(curve + '.rotateTipUV', 0) + return curves + elif dialog == 'Cancel': + dialog = mc.confirmDialog( + title='Legacy UVs', + message='UV Editor is not compatible with the old UVs.\nSome of the cards are ignored.', + icon='information', + button='OK', + cancelButton='OK', + dismissString='OK' + ) + return list(set(curves) - set(legacyCurves)) + + @noUndo + def updateTexture(self): + sel = core.selectPart(2, justReturn=True) + if not sel: + self.editor.removeTexture() + self.editor.diffusePath = '' + return + geo = mc.filterExpand(sel, sm=12) + if not geo: + return + dag = mc.ls(geo[-1], dag=1, s=1) + shader = mc.listConnections(dag, d=1, s=1, t='shadingEngine') + if not shader: + return + network = mc.hyperShade(lun=shader[0]) + fileNode = None + alphaNode = None + if network: + for node in network: + if mc.nodeType(node) == 'file' and mc.connectionInfo(node + '.outColor', isSource=1): + fileNode = node + break + for node in network: + if mc.nodeType(node) == 'file' and mc.connectionInfo(node + '.outTransparency', isSource=1): + alphaNode = node + break + if not fileNode: + return + texturePath = mc.getAttr(fileNode + '.fileTextureName') + alphaPath = mc.getAttr(alphaNode + '.fileTextureName') if alphaNode else None + + # Find place2dTexture node + place2dTexture = None + if mc.attributeQuery('coverage', n=fileNode, ex=1): + info = mc.connectionInfo(fileNode + '.coverage', sfd=1) + if info: + place2dTexture = info.split('.') + if place2dTexture: + place2dTexture = place2dTexture[0] + + # Get coverage and transformFrame from place2dTexture + cov, trans = None, None + if place2dTexture and mc.objExists(place2dTexture) and WIDGETS['UVEditorUseTransforms'].isChecked(): + if (mc.attributeQuery('coverage', ex=1, n=place2dTexture) and + mc.attributeQuery('translateFrame', ex=1, n=place2dTexture)): + try: + cov = mc.getAttr(place2dTexture + '.coverage')[0] + trans = mc.getAttr(place2dTexture + '.translateFrame')[0] + except Exception as e: + LOGGER.exception(e) + coverage = cov if cov else (1.0, 1.0) + translation = (trans[0], trans[1]) if trans and trans else (0, 0) + + if not WIDGETS['UVEditorTransparencyToggle'].isChecked() and not WIDGETS['UVEditorAlphaOnlyToggle'].isChecked(): + alphaPath = None + if texturePath: + _, ext = os.path.splitext(texturePath) + try: # Python 3 + supportedFormats = list([str(x, encoding='ASCII').lower() for x in QtGui.QImageReader.supportedImageFormats()]) + except BaseException: # Python 2 fallback + supportedFormats = list([str(x).decode('ASCII').lower() for x in QtGui.QImageReader.supportedImageFormats()]) + if ext and (ext[1:].lower() not in supportedFormats): + MESSAGE.warning( + "{} format is not supported. Use JPG/JPEG, PNG, TIF/TIFF (LZW or No Compression), TGA (24bit, no RLE)".format(ext[1:].upper())) + return + err = self.editor.setTexture('%s' % texturePath, alphaPath, coverage, translation) + if err == 'SamePath': + return + elif err == 'NoTexture': + MESSAGE.warning("Texture file could not be loaded.") + return + elif err == 'ZeroTexture': + MESSAGE.warning("Invalid Path or Zero Texture") + return + + def manualCurveUpdate(self): + try: + self._updateCurves() + except Exception as e: + LOGGER.exception(e) + finally: + self._stopCurvesUpdate() + + def _updateCurves(self): + if self.uvUpdateCheck == 0: + mc.undoInfo(ock=1, cn='gsUVUpdate') + self.uvUpdateCheck = 1 + + sel = mc.filterExpand(mc.ls(sl=1), sm=9) + uvs = self.editor.getUVs() + self.currentSelection *= 0 + + if not sel: + return + + for curve in sel: + if curve in uvs: + self.currentSelection.append(curve) + else: + if (mc.attributeQuery('gsmessage', n=curve, ex=1) and + mc.listConnections(curve + '.gsmessage')): + boundCurves = core.getAllConnectedCurves(curve) + if boundCurves: + self.currentSelection += boundCurves + + if not self.timer.increment(1.0 / 60.0): + return + if not self.currentSelection: + return + + uvs = self.editor.getUVs() + for curve in self.currentSelection: + if curve in uvs: + core.attributes.setAttr(curve, uvs[curve]) + + def _stopCurvesUpdate(self): + if self.uvUpdateCheck == 1: + mc.undoInfo(cck=1) + self.uvUpdateCheck = 0 + self.currentSelection *= 0 + core.curveControlUI.updateUI() + + def updateButtons(self, controllerMode, scaleMode): + buttons = self.controllerGroup.buttons() + direction = self.directionSwitch.buttons() + if controllerMode == 'SELECT': + buttons[0].setChecked(True) + elif controllerMode == 'MOVE': + buttons[1].setChecked(True) + elif controllerMode == 'ROTATE': + buttons[2].setChecked(True) + elif controllerMode == 'SCALE': + buttons[3].setChecked(True) + if scaleMode == 'H': + direction[0].setChecked(True) + else: + direction[1].setChecked(True) + elif controllerMode == 'DRAW': + buttons[4].setChecked(True) + + def updateControllerMode(self): + buttonID = self.controllerGroup.checkedId() + scaleID = self.directionSwitch.checkedId() + scale = 'H' + if buttonID == 0: + mode = 'SELECT' + elif buttonID == 1: + mode = 'MOVE' + elif buttonID == 2: + mode = 'ROTATE' + elif buttonID == 3: + mode = 'SCALE' + if scaleID == 0: + scale = 'H' + else: + scale = 'V' + else: + mode = 'DRAW' + self.editor.controllerModeChange(mode, scale) + + def updateItemList(self): + sel = mc.filterExpand(mc.ls(sl=1), sm=9) + if not sel: + self.uvList.clearItemList() + return + + finalDict = {} + for curve in sel: + if (mc.attributeQuery('Orientation', n=curve, ex=1) and + mc.connectionInfo(curve + '.Orientation', isSource=1)): + if (mc.attributeQuery('gsmessage', n=curve, ex=1) and + mc.listConnections(curve + '.gsmessage')): + boundCurves = core.getAllConnectedCurves(curve) + finalDict[curve] = boundCurves + else: + finalDict[curve] = [] + self.uvList.updateItemList(finalDict) + + def updateVisibility(self): + itemList = self.uvList.getSelection() + items = self.editor.getAllUVs() + for item in items: + if item.name in itemList: + item.setVisible(True) + else: + item.setVisible(False) + self.editor.update() + + def hide(self): + """Hides selected UV items""" + selUVs = self.editor.getAllUVs(selected=True) + selUVsList = [i.name for i in selUVs] + for uv in selUVs: + uv.setVisible(False) + itemList = self.uvList.getItemList() + selectList = [] + for item in itemList: + if item.curveName in selUVsList: + selectList.append(item) + self.uvList.selectItems(selectList) + + def isolateSelect(self): + allUVs = self.editor.getAllUVs() + selUVs = self.editor.getAllUVs(selected=True) + if not selUVs: + return + if self.isolateMode: + self.showAll() + self.isolateMode = False + return + self.isolateMode = True + selUVsList = [i.name for i in selUVs] + for uv in allUVs: + if uv.name not in selUVsList: + uv.setVisible(False) + itemList = self.uvList.getItemList() + deselectList = [] + for item in itemList: + if item.curveName not in selUVsList: + deselectList.append(item) + self.uvList.selectItems(deselectList) + + def horizontalFlipUV(self): + items = self.editor.getAllUVs(selected=True) + for item in items: + if item.name and mc.attributeQuery('flipUV', n=item.name, ex=1): + flip = mc.getAttr(item.name + '.flipUV') + mc.setAttr(item.name + '.flipUV', not flip) + item.flip = flip + item.update() + if items: + self.editor.update() + + def verticalFlipUV(self): + self.editor.verticalFlipUV() + self.manualCurveUpdate() + + def functionSwitch(self, key): + if key == 'H': + self.horizontalFlipUV() + elif key == 'V': + self.verticalFlipUV() + elif key == 'X': + self.resetUVs() + elif key == 'I': + self.isolateSelect() + elif key == 'O': + self.hide() + elif key == 'A': + self.showAll() + elif key == 'S': + self.syncSelection() + + def resetUVs(self): + self.editor.resetUV() + self.manualCurveUpdate() + + def randomizeUVs(self): + if utils.getMod() == 'Shift': + self.editor.randomizeUVs(True) + else: + self.editor.randomizeUVs(False) + self.manualCurveUpdate() + + def syncSelection(self): + """Selects curves in Maya Viewport based on UV Editor selection""" + sel = mc.filterExpand(mc.ls(sl=1), sm=9) + if not sel: + return + selUVs = [x.name for x in self.editor.getAllUVs(selected=True)] + newSel = [x for x in sel if x in selUVs] + if newSel: + mc.select(newSel, r=1) + + @utils.deferredLp + def x(): + uvs = self.editor.getAllUVs() + for uv in uvs: + uv.setSelected(True) + self.editor.scene().update() + x() + + +uveditor = UVEditor('uveditor') + + +# Colors Window + +class CustomLayerColors: + + def window(self, *_): + self.windowName = 'GSCT_CustomLayerColorsWindow' + if mc.workspaceControl(self.windowName, q=1, ex=1): + mc.deleteUI(self.windowName) + popOut = CreatePopOut(self.windowName, "Layers Customization", 280, 586) + + layout = popOut.widgetLayout + layout.setAlignment(QtCore.Qt.AlignTop) + + letters = [chr(l) for l in range(ord('A'), ord('A') + 10)] + + wrap.Label(layout).setLabel("Color Controls:") + + with wrap.Frame(layout, label='Gradient', margins=[2, 2, 2, 2]) as generateFrame: + + self.gradientRowsNumber = wrap.ControlSlider(generateFrame.getFrameLayout()) + self.gradientRowsNumber.setLabel('Rows') + self.gradientRowsNumber.setMinMax(1, 80) + self.gradientRowsNumber.setValue(20) + + with wrap.Row(generateFrame.getFrameLayout()) as gradientColorsLayout: + self.minGradientSwatch = wrap.ColorPicker('gradientSwatchMin', gradientColorsLayout.layout()) + self.minGradientSwatch.setHSVColors([0.1, 1, 1]) + self.maxGradientSwatch = wrap.ColorPicker('gradientSwatchMax', gradientColorsLayout.layout()) + self.maxGradientSwatch.setHSVColors([300, 1, 1]) + + generateGradient = wrap.Button(generateFrame.getFrameLayout(), 'gsGenerateLayerColorGradient') + generateGradient.setLabel('Generate Gradient') + generateGradient.clicked.connect(self.generateColorGradient) + + with wrap.Frame(layout, label='Randomize', margins=[2, 2, 2, 2]) as randomizeFrame: + + self.saturationMin = wrap.ControlSlider(randomizeFrame.getFrameLayout(), typ='float') + self.saturationMin.setMinMax(0, 1) + self.saturationMin.setValue(0.8) + self.saturationMin.setStep(0.01) + self.saturationMin.setLabel('SatMin') + + self.saturationMax = wrap.ControlSlider(randomizeFrame.getFrameLayout(), typ='float') + self.saturationMax.setMinMax(0, 1) + self.saturationMax.setValue(1.0) + self.saturationMax.setStep(0.01) + self.saturationMax.setLabel('SatMax') + + randomizeButton = wrap.Button(randomizeFrame.getFrameLayout(), 'gsRandomizeLayerColors') + randomizeButton.setLabel('Randomize All') + randomizeButton.clicked.connect(self.randomizeAllColors) + + layout.addWidget(wrap.separator()) + + wrap.Label(layout).setLabel("Layers:") + + def colorRow(parent, i): + with wrap.Row(parent) as singleColor: + singleColor.setFixedHeight(style.scale(16)) + + label = wrap.Label() + if 10 <= i < 20: + label.setLabel('%s (%s)' % (i, letters[i - 10])) + else: + label.setLabel(str(i)) + label.setFixedWidth(style.scale(int(35))) + + layerName = wrap.LineEdit('layerCustomName_%s' % i) + newFont = QtGui.QFont('Roboto') + newFont.setPointSizeF(style.scale(5)) + layerName.setFixedHeight(style.scale(18)) + layerName.setFrame(False) + layerName.setFont(newFont) + layerName.setAutoFormat(True) + layerName.setClearButtonEnabled(True) + + swatch = wrap.ColorPicker('layerColorPicker_%s' % i) + + randButton = wrap.Button() + randButton.setButtonStyle('small-filled') + randButton.setLabel('Rand', lineHeight=100) + randButton.setLabelStyle('small') + randButton.setWidthHeight(h=style.scale(10)) + randButton.clicked.connect(pa(self.randomizeColor, swatch)) + + resetButton = wrap.Button() + resetButton.setButtonStyle('small-filled') + resetButton.setLabel('Reset', lineHeight=100) + resetButton.setLabelStyle('small') + resetButton.setWidthHeight(h=style.scale(10)) + resetButton.clicked.connect(pa(self.resetColor, swatch)) + resetButton.clicked.connect(pa(self.resetName, layerName)) + + singleColor.layout().addWidget(label) + singleColor.layout().addWidget(layerName, 3) + singleColor.layout().addWidget(swatch, 1) + singleColor.layout().addWidget(randButton, 1) + singleColor.layout().addWidget(resetButton, 1) + return singleColor + + with wrap.Frame(layout, label='0-19') as frame1: + for i in range(20): + colorRow(frame1.getFrameLayout(), i) + frame1.setCollapsed(False) + with wrap.Frame(layout, label='20-39') as frame2: + for i in range(20, 40): + colorRow(frame2.getFrameLayout(), i) + frame2.setCollapsed(True) + with wrap.Frame(layout, label='40-79') as frame3: + for i in range(40, 80): + colorRow(frame3.getFrameLayout(), i) + frame3.setCollapsed(True) + + layout.addWidget(wrap.separator()) + + wrap.Label(layout).setLabel("Commands:") + + resetButton = wrap.Button(layout, 'gsResetAllLayerColors') + resetButton.setLabel('Reset All') + resetButton.clicked.connect(self.resetAll) + + with wrap.Row(layout) as controlButtons: + getCurrent = wrap.Button(controlButtons.layout(), 'gsGetCurrentSceneLayers') + getCurrent.setLabel('Get From Scene') + getCurrent.clicked.connect(self.getFromLayers) + + setAsCurrent = wrap.Button(controlButtons.layout(), 'gsSetAsCurrentSceneLayers') + setAsCurrent.setLabel('Set To Scene') + setAsCurrent.clicked.connect(self.setToLayers) + + with wrap.Row(layout) as buttonsFrame: + + loadSaved = wrap.Button(buttonsFrame.layout(), 'gsLoadGlobalLayerPreset') + loadSaved.setLabel('Load Preset') + loadSaved.clicked.connect(self.loadPreset) + + saveButton = wrap.Button(buttonsFrame.layout(), 'gsSaveGlobalLayerPreset') + saveButton.setLabel('Save As Preset') + saveButton.clicked.connect(self.savePreset) + + self.getFromLayers() + + def randomizeColor(self, swatch): + satMin = self.saturationMin.getValue() + satMax = self.saturationMax.getValue() + swatch.setRGBColors(core.toggleColor.generateBrightColor(satMin, satMax)) + + def randomizeAllColors(self): + for i in range(80): + self.randomizeColor(WIDGETS['layerColorPicker_%s' % i]) + + def resetColor(self, swatch): + swatch.setRGBColors([0, 0, 0]) + + def resetName(self, field): + field.clear() + + def resetAll(self): + for i in range(80): + WIDGETS['layerColorPicker_%s' % i].setRGBColors([0, 0, 0]) + WIDGETS['layerCustomName_%s' % i].clear() + + def generateColorGradient(self): + rows = self.gradientRowsNumber.getValue() + colorMin = self.minGradientSwatch.getHSVColors() + colorMax = self.maxGradientSwatch.getHSVColors() + for i in range(rows): + fraction = i / float(rows) + h = mt.lerp(fraction, colorMin[0], colorMax[0]) + s = mt.lerp(fraction, colorMin[1], colorMax[1]) + v = mt.lerp(fraction, colorMin[2], colorMax[2]) + WIDGETS['layerColorPicker_%s' % i].setHSVColors([h, s, v]) + + def getFromLayers(self): + # Getting colors + colorDict = core.toggleColor.readColorDict() + for key in colorDict: + WIDGETS['layerColorPicker_%s' % key].setRGBColors(colorDict[key]) + # Getting names + core.toggleColor.checkColorStorageNode() + nameDict = eval(mc.getAttr(core.toggleColor.STORAGE_NODE + '.layerName')) + for key in nameDict: + if nameDict[key]: + WIDGETS['layerCustomName_%s' % key].setText(nameDict[key]) + + def getCurrentSwatches(self): + colorDict = {} + for i in range(80): + colorDict[i] = WIDGETS['layerColorPicker_%s' % i].getRGBColors() + return colorDict + + def getCurrentNames(self): + core.toggleColor.checkColorStorageNode() + nameDict = {} + for i in range(80): + nameDict[i] = WIDGETS['layerCustomName_%s' % i].text() + return nameDict + + def setToLayers(self): + colorDict = self.getCurrentSwatches() + core.toggleColor.writeColorDict(colorDict) + if WIDGETS['colorMode'].isChecked(): + core.toggleColor.updateColors() + if WIDGETS['syncCurveColor'].isChecked(): + core.toggleColor.syncCurveColors() + core.updateMainUI() + + # Setting names + nameDict = self.getCurrentNames() + core.toggleColor.checkColorStorageNode() + mc.setAttr(core.toggleColor.STORAGE_NODE + '.layerName', str(nameDict), typ='string') + + def setCurrentSwathes(self, colorDict): + for key in colorDict: + WIDGETS['layerColorPicker_%s' % key].setRGBColors(colorDict[key]) + + def setCurrentNames(self, nameDict): + for key in nameDict: + WIDGETS['layerCustomName_%s' % key].setText(nameDict[key]) + + def savePreset(self): + mc.optionVar(sv=['gsCurveToolsCustomColors', str(self.getCurrentSwatches())]) + mc.optionVar(sv=['gsCurveToolsCustomLayerNames', str(self.getCurrentNames())]) + + def loadPreset(self): + colorString = mc.optionVar(q='gsCurveToolsCustomColors') + colorDict = eval(colorString) + self.setCurrentSwathes(colorDict) + nameString = mc.optionVar(q='gsCurveToolsCustomLayerNames') + nameDict = eval(nameString) + self.setCurrentNames(nameDict) + + +customLayerColors = CustomLayerColors() + + +class CreatePopOut(QtWidgets.QWidget): + + def __init__(self, name, label, width, height): + self.name = name + + parent = mayaWorkspaceControl(name=name, + label=label, + retain=False, iw=width, ih=height, widthProperty="free") + + super(CreatePopOut, self).__init__(parent) + + self.ui() + + parent.layout().addWidget(self) + + def ui(self): + self.mainLayout = QtWidgets.QVBoxLayout(self) + self.mainLayout.setContentsMargins(*style.scale([5, 5, 5, 5])) + + self.scrollWidget = QtWidgets.QWidget() + self.widgetLayout = QtWidgets.QVBoxLayout(self.scrollWidget) + scrollArea = QtWidgets.QScrollArea() + scrollArea.setWidget(self.scrollWidget) + self.mainLayout.addWidget(scrollArea) + + # Layout Settings + self.widgetLayout.setContentsMargins(0, 0, 0, 0) + self.widgetLayout.setSpacing(style.scale(2)) + self.widgetLayout.setMargin(0) + + scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + scrollArea.setWidgetResizable(True) + + +class About: # Creates "About" and "Contacts" windows + + def social(self): + layout = mc.columnLayout() + mc.textFieldButtonGrp( + bl='>', + bc=pa( + utils.openLink, + 'https://discord.gg/f4DH6HQ'), + l='Discord Server', + tx='https://discord.gg/f4DH6HQ') + mc.textFieldButtonGrp(bl='>', bc=pa(utils.openLink, 'https://sladkovsky3d.artstation.com/store'), + l='Online Store', tx='https://sladkovsky3d.artstation.com/store') + mc.textFieldButtonGrp(bl='>', bc=pa(utils.openLink, 'http://gs-curvetools.readthedocs.io/'), + l='Online Documentation', tx='http://gs-curvetools.readthedocs.io/') + mc.textFieldButtonGrp(bl='>', bc=pa(utils.openLink, 'https://www.twitch.tv/videonomad'), + l='Twitch Channel', tx='https://www.twitch.tv/videonomad') + mc.textFieldButtonGrp(bl='>', bc=pa(utils.openLink, 'https://www.youtube.com/GeorgeSladkovsky'), + l='YouTube Channel', tx='https://www.youtube.com/GeorgeSladkovsky') + mc.textFieldButtonGrp(bl='>', bc=pa(utils.openLink, 'https://www.artstation.com/sladkovsky3d'), + l='ArtStation Portfolio', tx='https://www.artstation.com/sladkovsky3d') + mc.textFieldButtonGrp(bl='>', bc=pa(utils.openLink, 'mailto:george.sladkovsky@gmail.com'), + l='Contact Email', tx='george.sladkovsky@gmail.com') + return layout + + def socialWindow(self): + if (mc.window('gsSocialMedia', ex=1)): + mc.deleteUI('gsSocialMedia') + mc.window('gsSocialMedia', t='Useful Links:', tlb=1) + self.social() + mc.showWindow() + + def aboutWindow(self): + if mc.window('gsAboutWindow', ex=1): + mc.deleteUI('gsAboutWindow') + mc.window('gsAboutWindow', t='About', tlb=1) + mc.rowColumnLayout(h=400, co=[1, 'left', 10], cal=([1, 'left'], [2, 'right']), nc=2, cw=[1, 200]) + mc.text(al='left', l='Version: \n%s' % core.VERSION) + mc.iconTextButton(al='right', w=90, h=80, i=utils.getFolder.icons() + 'gsCurveToolsIcon_logo.png') + mc.setParent('..') + mc.columnLayout(w=460, h=400) + mc.text(w=400, ww=1, al='left', + l='License:\nThis collection of code named GS CurveTools is a property of George Sladkovsky\ + (Yehor Sladkovskyi) and can not be copied or distributed without his written permission.\ + \n\n%s\nGeorge Sladkovsky (Yehor Sladkovskyi)\nAll Rights Reserved\ + \n\nAutodesk Maya is a property of Autodesk, Inc.\n' % core.VERSION) + mc.text(l='Social Media and Contact Links:\n') + self.social() + mc.showWindow('gsAboutWindow') + + +about = About() + + +class AttributesFilter: + + def __init__(self): + self.uiName = "GSCT_AttributesFilterPopOut" + self.attrs = [ + ('Length Divisions', 'lengthDivisions'), + ('Dynamic Divisions', 'dynamicDivisions'), + ('Width Divisions', 'widthDivisions'), + ('Orientation', 'Orientation'), + ('Twist', 'Twist'), + ('Inverted Twist', 'invTwist'), + ('Width', 'Width'), + ('WidthX', 'WidthX'), + ('WidthZ', 'WidthZ'), + ('Taper', 'Taper'), + ('Length Lock', 'LengthLock'), + ('Length', 'Length'), + ('Offset', 'Offset'), + ('Profile', 'Profile'), + ('Refine', 'curveRefine'), + ('Auto Refine', 'autoRefine'), + ('Smooth', 'curveSmooth'), + ('Normals', 'surfaceNormals'), + ('Reverse Normals', 'reverseNormals'), + ('Axis Flip', 'AxisFlip'), + ('Line Width', 'lineWidth'), + ('Sampling', 'samplingAccuracy'), + ] + + self.solidify = [ + ('Solidify', 'solidify'), + ('Solidify Thickness', 'solidifyThickness'), + ('Solidify Divisions', 'solidifyDivisions'), + ('Solidify Scale X', 'solidifyScaleX'), + ('Solidify Scale Y', 'solidifyScaleY'), + ('Solidify Offset', 'solidifyOffset'), + ('Solidify Normals', 'solidifyNormals'), + ] + + self.uvs = [ + ('Move U', 'moveU'), + ('Move V', 'moveV'), + ('Rotate UV', 'rotateUV'), + ('Scale U', 'scaleU'), + ('Scale V', 'scaleV'), + ('Flip UV', 'flipUV'), + ('Rotate Root UV', 'rotateRootUV'), + ('Rotate Tip UV', 'rotateTipUV'), + ] + + self.graphs = [ + ('Twist Graph', 'twistCurve'), + ('Scale Graph', 'scaleCurve'), + ('Profile Graph', 'profileCurve'), + ('Twist Magnitude', 'Magnitude'), + ('Profile Smoothing', 'profileSmoothing'), + ('Profile Magnitude', 'profileMagnitude'), + ] + + def openUI(self): + if mc.workspaceControl(self.uiName, q=1, ex=1): + if mc.workspaceControl(self.uiName, q=1, vis=1): + mc.deleteUI(self.uiName) + return + else: + mc.deleteUI(self.uiName) + popOut = CreatePopOut(self.uiName, "Attributes Filter", 260, 456.0) + + layout = popOut.widgetLayout + layout.setAlignment(QtCore.Qt.AlignTop) + with wrap.Row(layout, margins=style.scale([6, 0, 6, 0]), spacing=style.scale(10)) as mainRow: + mainRow.layout().setAlignment(QtCore.Qt.AlignTop) + with wrap.Column(mainRow.layout()) as leftColumn: + with wrap.Frame(leftColumn.layout(), label='Attributes', margins=[2, 0, 2, 0]) as leftFrame: + leftFrame.setCollapsible(False) + leftFrame.getFrameLayout().setAlignment(QtCore.Qt.AlignTop) + for i in range(len(self.attrs)): + btn = wrap.Button(leftFrame.getFrameLayout(), 'gsFilter_%s' % self.attrs[i][1]) + btn.setLabel(self.attrs[i][0]) + btn.setButtonStyle('small') + btn.setCheckable(True) + btn.setChecked(False if self.attrs[i][0] == 'Orientation' else True) + with wrap.Column(mainRow.layout()) as rightColumn: + rightColumn.layout().setAlignment(QtCore.Qt.AlignTop) + with wrap.Frame(rightColumn.layout(), label='Solidify', margins=[2, 0, 2, 0]) as rightFrame1: + rightFrame1.setCollapsible(False) + rightFrame1.getFrameLayout().setAlignment(QtCore.Qt.AlignTop) + for i in range(len(self.solidify)): + btn = wrap.Button(rightFrame1.getFrameLayout(), 'gsFilter_%s' % self.solidify[i][1]) + btn.setLabel(self.solidify[i][0]) + btn.setButtonStyle('small') + btn.setCheckable(True) + btn.setChecked(True) + with wrap.Frame(rightColumn.layout(), label='Graphs', margins=[2, 0, 2, 0]) as rightFrame2: + rightFrame2.setCollapsible(False) + rightFrame2.getFrameLayout().setAlignment(QtCore.Qt.AlignTop) + for i in range(len(self.graphs)): + btn = wrap.Button(rightFrame2.getFrameLayout(), 'gsFilter_%s' % self.graphs[i][1]) + btn.setLabel(list(self.graphs)[i][0]) + btn.setButtonStyle('small') + btn.setCheckable(True) + btn.setChecked(True) + with wrap.Frame(rightColumn.layout(), label='UVs', margins=[2, 0, 2, 0]) as rightFrame3: + rightFrame3.setCollapsible(False) + rightFrame3.getFrameLayout().setAlignment(QtCore.Qt.AlignTop) + for i in range(len(self.uvs)): + btn = wrap.Button(rightFrame3.getFrameLayout(), 'gsFilter_%s' % self.uvs[i][1]) + btn.setLabel(list(self.uvs)[i][0]) + btn.setButtonStyle('small') + btn.setCheckable(True) + btn.setChecked(True) + + layout.addWidget(wrap.separator()) + + with wrap.Row(layout) as confirmCancelLayout: + btn = wrap.Button(confirmCancelLayout.layout()) + btn.setLabel('Save') + btn.clicked.connect(self.saveToOptionVar) + btn.clicked.connect(lambda: MESSAGE.printInView("Filters Saved")) + btn = wrap.Button(confirmCancelLayout.layout()) + btn.setLabel('Close') + btn.clicked.connect(self.closeUI) + + self.updateUI() + self.saveToOptionVar() + + def closeUI(self): + if mc.workspaceControl(self.uiName, q=1, ex=1): + mc.deleteUI(self.uiName) + + def updateUI(self): + if mc.workspaceControl(self.uiName, q=1, ex=1): + controlsDict = self.getFromOptionVar() + if controlsDict and isinstance(controlsDict, dict): + for key in controlsDict: + buttonName = 'gsFilter_%s' % key + WIDGETS[buttonName].setChecked(controlsDict[key]) + + def saveToOptionVar(self): + if mc.workspaceControl(self.uiName, q=1, ex=1): + allControls = self.attrs + self.graphs + self.uvs + self.solidify + controlsDict = {} + for control in allControls: + buttonName = 'gsFilter_%s' % control[1] + button = WIDGETS[buttonName] if buttonName in WIDGETS else None + if button: + controlsDict.update({control[1]: button.isChecked()}) + mc.optionVar(sv=('GSCT_AttributesFilter', str(controlsDict))) + + def getFromOptionVar(self): + if mc.optionVar(ex='GSCT_AttributesFilter'): + dictString = mc.optionVar(q='GSCT_AttributesFilter') + if dictString: + return dict(eval(dictString)) + + +attributesFilter = AttributesFilter() + + +class CardToCurveWindow: + + def __init__(self): + self.uiName = "GSCT_CardToCurvePopOut" + self.buttonsDict = {} + + def openUI(self): + if mc.workspaceControl(self.uiName, q=1, ex=1): + if mc.workspaceControl(self.uiName, q=1, vis=1): + mc.deleteUI(self.uiName) + return + else: + mc.deleteUI(self.uiName) + + self.buttonsDict = self.loadClickedButtons() + self.getBtn = lambda name: self.buttonsDict[name] if name in self.buttonsDict else True + + popOut = CreatePopOut(self.uiName, "Card to Curve", 245, 271) + layout = popOut.widgetLayout + layout.setAlignment(QtCore.Qt.AlignTop) + self.mainButtonGroup = QtWidgets.QButtonGroup() + self.mainButtonGroup.setExclusive(False) + self.mainButtonGroup.buttonClicked.connect(self.saveButtonsState) + with wrap.Column(layout) as mainColumn: + mainColumn.layout().setAlignment(QtCore.Qt.AlignTop) + + with wrap.Frame(mainColumn.layout(), label='Output Type:', objName='gsCardToCurve_outputTypeSwitch') as outputTypeFrame: + outputTypeFrame.setCollapsible(False) + outputTypeFrame.setCollapsed(False) + + with wrap.Row(outputTypeFrame.getFrameLayout()) as outputTypeSwitch: + self.outputTypeGroup = QtWidgets.QButtonGroup() + WIDGETS['gsCardToCurve_cardTypeGroup'] = self.outputTypeGroup + + generateCards = wrap.Button(outputTypeSwitch.layout(), 'gsCardToCurve_generateCards') + generateCards.setButtonStyle('small') + generateCards.setLabel('Cards') + generateCards.setCheckable(True) + + generateCurves = wrap.Button(outputTypeSwitch.layout(), 'gsCardToCurve_generateCurves') + generateCurves.setButtonStyle('small') + generateCurves.setLabel('Curves Only') + generateCurves.setCheckable(True) + + self.outputTypeGroup.addButton(generateCards, 0) + self.outputTypeGroup.addButton(generateCurves, 1) + generateCards.setChecked(not mc.optionVar(q='GSCT_CardToCurveOutputType')) + generateCurves.setChecked(mc.optionVar(q='GSCT_CardToCurveOutputType')) + self.outputTypeGroup.buttonClicked.connect(self.updateActiveButtons) + + with wrap.Frame(mainColumn.layout(), label='Card Type:', objName="gsCardToCurve_cardType") as cardTypeFrame: + self.cardTypeFrame = cardTypeFrame + cardTypeFrame.setCollapsible(False) + cardTypeFrame.setCollapsed(False) + + with wrap.Row(cardTypeFrame.getFrameLayout()) as outputTypeSwitch: + self.cardTypeGroup = QtWidgets.QButtonGroup() + WIDGETS['gsCardToCurve_cardTypeGroup'] = self.cardTypeGroup + + self.warpCards = wrap.Button(outputTypeSwitch.layout(), 'gsCardToCurve_warp') + self.warpCards.setButtonStyle('small') + self.warpCards.setLabel('Warp') + self.warpCards.setCheckable(True) + + extrudeCards = wrap.Button(outputTypeSwitch.layout(), 'gsCardToCurve_extrude') + extrudeCards.setButtonStyle('small') + extrudeCards.setLabel('Extrude') + extrudeCards.setCheckable(True) + + self.cardTypeGroup.addButton(self.warpCards, 0) + self.cardTypeGroup.addButton(extrudeCards, 1) + self.warpCards.setChecked(not mc.optionVar(q='GSCT_CardToCurveCardType')) + extrudeCards.setChecked(mc.optionVar(q='GSCT_CardToCurveCardType')) + self.cardTypeGroup.buttonClicked.connect(self.updateActiveButtons) + + with wrap.Frame(mainColumn.layout(), label='Match Attributes:', objName="gsCardToCurve_matchAttributes") as matchAttributeFrame: + self.matchAttributeFrame = matchAttributeFrame + matchAttributeFrame.setCollapsible(False) + matchAttributeFrame.setCollapsed(False) + + with wrap.Row(matchAttributeFrame.getFrameLayout()) as row: + orientation = wrap.Button(row.layout(), 'gsCardToCurve_orientation') + orientation.setButtonStyle('small') + orientation.setLabel('Orientation') + orientation.setCheckable(True) + orientation.setChecked(self.getBtn('gsCardToCurve_orientation')) + + width = wrap.Button(row.layout(), 'gsCardToCurve_width') + width.setButtonStyle('small') + width.setLabel('Width') + width.setCheckable(True) + width.setChecked(self.getBtn('gsCardToCurve_width')) + + self.mainButtonGroup.addButton(orientation) + self.mainButtonGroup.addButton(width) + + with wrap.Row(matchAttributeFrame.getFrameLayout()) as row: + taper = wrap.Button(row.layout(), 'gsCardToCurve_taper') + taper.setButtonStyle('small') + taper.setLabel('Taper') + taper.setCheckable(True) + taper.setChecked(self.getBtn('gsCardToCurve_taper')) + + twist = wrap.Button(row.layout(), 'gsCardToCurve_twist') + twist.setButtonStyle('small') + twist.setLabel('Twist') + twist.setCheckable(True) + twist.setChecked(self.getBtn('gsCardToCurve_twist')) + + self.mainButtonGroup.addButton(taper) + self.mainButtonGroup.addButton(twist) + + with wrap.Row(matchAttributeFrame.getFrameLayout()) as row: + profile = wrap.Button(row.layout(), 'gsCardToCurve_profile') + profile.setButtonStyle('small') + profile.setLabel('Profile') + profile.setCheckable(True) + profile.setChecked(self.getBtn('gsCardToCurve_profile')) + + material = wrap.Button(row.layout(), 'gsCardToCurve_material') + material.setButtonStyle('small') + material.setLabel('Material') + material.setCheckable(True) + material.setChecked(self.getBtn('gsCardToCurve_material')) + + self.mainButtonGroup.addButton(profile) + self.mainButtonGroup.addButton(material) + + UVs = wrap.Button(matchAttributeFrame.getFrameLayout(), 'gsCardToCurve_UVs') + UVs.setButtonStyle('small') + UVs.setLabel('UVs') + UVs.setCheckable(True) + UVs.setChecked(self.getBtn('gsCardToCurve_UVs')) + UVs.clicked.connect(self.updateActiveButtons) + self.mainButtonGroup.addButton(UVs) + + with wrap.Frame(mainColumn.layout(), label='UV Match Options:', objName="gsCardToCurve_UVMatchOptions") as uvMatchOptionsFrame: + self.uvMatchOptionsFrame = uvMatchOptionsFrame + uvMatchOptionsFrame.setCollapsible(False) + uvMatchOptionsFrame.setCollapsed(False) + + with wrap.Row(uvMatchOptionsFrame.getFrameLayout()) as row: + verticalFlip = wrap.Button(row.layout(), 'gsCardToCurve_verticalFlip') + verticalFlip.setButtonStyle('small') + verticalFlip.setLabel('Vertical Flip') + verticalFlip.setCheckable(True) + verticalFlip.setChecked(self.getBtn('gsCardToCurve_verticalFlip')) + + horizontalFlip = wrap.Button(row.layout(), 'gsCardToCurve_horizontalFlip') + horizontalFlip.setButtonStyle('small') + horizontalFlip.setLabel('Horizontal Flip') + horizontalFlip.setCheckable(True) + horizontalFlip.setChecked(self.getBtn('gsCardToCurve_horizontalFlip')) + + self.mainButtonGroup.addButton(verticalFlip) + self.mainButtonGroup.addButton(horizontalFlip) + + with wrap.Frame(mainColumn.layout(), label='Other:') as otherOptionsFrame: + otherOptionsFrame.setCollapsible(False) + otherOptionsFrame.setCollapsed(False) + reverseCurve = wrap.Button(otherOptionsFrame.getFrameLayout(), 'gsCardToCurve_reverseCurve') + reverseCurve.setButtonStyle('small') + reverseCurve.setLabel('Reverse Curve') + reverseCurve.setCheckable(True) + reverseCurve.setChecked(self.getBtn('gsCardToCurve_reverseCurve')) + + self.mainButtonGroup.addButton(reverseCurve) + + mainColumn.layout().addWidget(wrap.separator()) + with wrap.Row(mainColumn.layout(), margins=style.scale([0, 3, 0, 3])) as _: + pass + mainColumn.layout().addWidget(wrap.separator()) + with wrap.Row(mainColumn.layout()) as row: + convertSelected = wrap.Button(row.layout(), objName="gsCardToCurve_convertSelected") + convertSelected.setLabel('Convert Selected') + convertSelected.clicked.connect(undo(core.cardToCurve)) + + cancel = wrap.Button(row.layout()) + cancel.setLabel('Cancel') + cancel.clicked.connect(self.closeUI) + + self.updateActiveButtons() + self.saveButtonsState() + + def closeUI(self): + self.saveButtonsState() + if mc.workspaceControl(self.uiName, q=1, ex=1): + if mc.workspaceControl(self.uiName, q=1, vis=1): + mc.deleteUI(self.uiName) + else: + mc.deleteUI(self.uiName) + + def updateActiveButtons(self): + if self.outputTypeGroup.checkedId() == 1: + self.matchAttributeFrame.setEnabled(False) + self.uvMatchOptionsFrame.setEnabled(False) + self.cardTypeFrame.setEnabled(False) + mc.optionVar(iv=['GSCT_CardToCurveOutputType', 1]) + else: + mc.optionVar(iv=['GSCT_CardToCurveOutputType', 0]) + mc.optionVar(iv=['GSCT_CardToCurveCardType', int(not self.warpCards.isChecked())]) + self.matchAttributeFrame.setEnabled(True) + self.cardTypeFrame.setEnabled(True) + if WIDGETS['gsCardToCurve_UVs'].isChecked(): + self.uvMatchOptionsFrame.setEnabled(True) + else: + self.uvMatchOptionsFrame.setEnabled(False) + + def saveButtonsState(self): + buttons = self.mainButtonGroup.buttons() + buttonsDict = {} + for b in buttons: + buttonsDict.update({b.objName: b.isChecked()}) + mc.optionVar(sv=['GSCT_CardToCurveOptions', str(buttonsDict)]) + + def loadClickedButtons(self): + return eval(mc.optionVar(q='GSCT_CardToCurveOptions')) + + +cardToCurveWindow = CardToCurveWindow() + + +def scaleFactorWindow(): + windowName = SCALE_FACTOR_UI + if mc.workspaceControl(windowName, q=1, ex=1): + mc.deleteUI(windowName) + popOut = CreatePopOut(windowName, "Scale Factor", 300, 95) + + layout = popOut.widgetLayout + + scaleFactor = core.getScaleFactor() + m_slider = mc.floatSliderGrp('GSCT_scaleFactorSlider', + pre=3, ss=0.01, min=0.01, max=10, fmn=0.001, fmx=1000, f=1, + v=scaleFactor + ) + + wrap.MayaSlider(m_slider, layout=layout) + + with wrap.Row(layout) as row: + saveButton = wrap.Button(row.layout()) + saveButton.setLabel('Save') + saveButton.clicked.connect(pa(core.saveScaleFactor, windowName, False)) + saveButton.clicked.connect(updateScaleFactorWindow) + saveAndCloseButton = wrap.Button(row.layout()) + saveAndCloseButton.setLabel('Save & Close') + saveAndCloseButton.clicked.connect(pa(core.saveScaleFactor, windowName)) + cancelButton = wrap.Button(row.layout()) + cancelButton.setLabel('Cancel') + cancelButton.clicked.connect(lambda: mc.deleteUI(windowName)) + + layout.addWidget(wrap.separator()) + + with wrap.Row(layout) as row: + with wrap.Row(row.layout(), margins=style.scale([2, 0, 2, 0])) as globalRow: + label = wrap.Label(globalRow.layout()) + label.setLabel("Global:") + globalValue = mc.optionVar(q=('GSCT_globalScaleFactor')) + field = wrap.LineEdit("scaleFactorGlobalValue", globalRow.layout()) + field.setText(str(globalValue)) + field.setEnabled(False) + with wrap.Row(row.layout(), margins=style.scale([2, 0, 2, 0])) as sceneRow: + label = wrap.Label(sceneRow.layout()) + label.setLabel("Scene:") + if mc.objExists('gsScaleFactorStorageNode') and mc.attributeQuery('scaleFactor', n='gsScaleFactorStorageNode', ex=1): + sceneValue = mc.getAttr('gsScaleFactorStorageNode.scaleFactor') + else: + sceneValue = "####" + field = wrap.LineEdit("scaleFactorSceneValue", sceneRow.layout()) + field.setText(str(sceneValue)) + field.setEnabled(False) + + with wrap.Row(row.layout(), margins=style.scale([2, 0, 2, 0])) as sceneRow: + label = wrap.Label(sceneRow.layout()) + label.setLabel("Selected:") + if mc.objExists('gsScaleFactorStorageNode') and mc.attributeQuery('scaleFactor', n='gsScaleFactorStorageNode', ex=1): + sceneValue = mc.getAttr('gsScaleFactorStorageNode.scaleFactor') + else: + sceneValue = "####" + field = wrap.LineEdit("scaleFactorSelectedValue", sceneRow.layout()) + field.setText(str(sceneValue)) + field.setEnabled(False) + + from . import main + main.checkScriptJobs(SCALE_FACTOR_UI) + + +def updateScaleFactorWindow(): + WIDGETS["scaleFactorGlobalValue"].setText(str(mc.optionVar(q=('GSCT_globalScaleFactor')))) + if mc.objExists('gsScaleFactorStorageNode') and mc.attributeQuery('scaleFactor', n='gsScaleFactorStorageNode', ex=1): + sceneValue = mc.getAttr('gsScaleFactorStorageNode.scaleFactor') + else: + sceneValue = "####" + WIDGETS["scaleFactorSceneValue"].setText(str(sceneValue)) + + +def scaleFactorConversionDialog(): + MESSAGE.warningInView('Curves Without Scale Factor Detected') + dialog = mc.confirmDialog( + title='No Scale Factor', + message='Curves without scale factor detected\nThose Curves were created before v1.2.7 and might not convert correctly', + icon='warning', + button=['Skip Old Curves', 'Convert All', 'Cancel'], + cancelButton='Cancel', + dismissString='Cancel' + ) + if dialog == 'Cancel': + return None + if dialog == 'Skip Old Curves': + return 'Skip' + if 'Convert All': + return 'All' + + +def curveThicknessWindow(): + name = 'GSCT_CurveThicknessWindow' + if mc.workspaceControl(name, q=1, ex=1): + mc.deleteUI(name) + popOut = CreatePopOut(name, "Curve Thickness", 300, 65) + + layout = popOut.widgetLayout + + layout.setAlignment(QtCore.Qt.AlignTop) + + m_slider = mc.floatSliderGrp('GSCT_curveThicknessSlider', + pre=3, ss=0.01, min=-1, max=10, fmn=0.001, fmx=1000, f=1, + v=mc.optionVar(q='GSCT_globalCurveThickness') + ) + + wrap.MayaSlider(m_slider, layout=layout) + + with wrap.Row(layout, margins=style.scale([0, 5, 0, 0])) as row: + def save(): + mc.optionVar(fv=('GSCT_globalCurveThickness', mc.floatSliderGrp('GSCT_curveThicknessSlider', q=1, v=1))) + mc.deleteUI(name) + + def update(): + mc.optionVar(fv=('GSCT_globalCurveThickness', mc.floatSliderGrp('GSCT_curveThicknessSlider', q=1, v=1))) + core.updateLayerThickness() + saveButton = wrap.Button(row.layout()) + saveButton.setLabel('Save') + saveButton.clicked.connect(save) + updateCurves = wrap.Button(row.layout()) + updateCurves.setLabel('Update Curves') + updateCurves.clicked.connect(update) + cancelButton = wrap.Button(row.layout()) + cancelButton.setLabel('Cancel') + cancelButton.clicked.connect(lambda: mc.deleteUI(name)) + + +def randomizeCurveWindow(): + name = "GSCT_RandomizeCurvePopOut" + if mc.workspaceControl(name, q=1, ex=1): + if mc.workspaceControl(name, q=1, vis=1): + mc.deleteUI(name) + return + else: + mc.deleteUI(name) + popOut = CreatePopOut(name, "Randomize Curve", 270, 565) + + layout = popOut.widgetLayout + layout.setAlignment(QtCore.Qt.AlignTop) + + def release(slider, *_): + core.sliders.randSliderDrag(slider) + core.sliders.randSliderRelease(slider) + core.sliders.release() + # Control Points + with wrap.Frame(layout, label='Control Points') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider0') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveCVsRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + with wrap.Row(frame.getFrameLayout()) as row: + lockFirst = wrap.Button(row.layout(), 'gsLockFirstCV') + lockFirst.setLabel('Lock First CV', lineHeight=100) + lockFirst.setButtonStyle('small') + lockFirst.setCheckable(True) + + lockLast = wrap.Button(row.layout(), 'gsLockLastCV') + lockLast.setLabel('Lock Last CV', lineHeight=100) + lockLast.setButtonStyle('small') + lockLast.setCheckable(True) + + with wrap.Row(frame.getFrameLayout()) as row: + axisX = wrap.Button(row.layout(), 'gsRandAxisX') + axisX.setLabel('X', lineHeight=100) + axisX.setButtonStyle('small') + axisX.setCheckable(True) + + axisY = wrap.Button(row.layout(), 'gsRandAxisY') + axisY.setLabel('Y', lineHeight=100) + axisY.setButtonStyle('small') + axisY.setCheckable(True) + + axisZ = wrap.Button(row.layout(), 'gsRandAxisZ') + axisZ.setLabel('Z', lineHeight=100) + axisZ.setButtonStyle('small') + axisZ.setCheckable(True) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveCVsRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 0), + cc=pa(release, 0), + ), + layout=frame.getFrameLayout() + ) + + # Rotation + with wrap.Frame(layout, label='Rotation') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider1') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveRotationRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + with wrap.Row(frame.getFrameLayout()) as row: + axisX = wrap.Button(row.layout(), 'gsRandRotateAxisX') + axisX.setLabel('X', lineHeight=100) + axisX.setButtonStyle('small') + axisX.setCheckable(True) + + axisY = wrap.Button(row.layout(), 'gsRandRotateAxisY') + axisY.setLabel('Y', lineHeight=100) + axisY.setButtonStyle('small') + axisY.setCheckable(True) + + axisZ = wrap.Button(row.layout(), 'gsRandRotateAxisZ') + axisZ.setLabel('Z', lineHeight=100) + axisZ.setButtonStyle('small') + axisZ.setCheckable(True) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveRotationRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 1), + cc=pa(release, 1), + ), + layout=frame.getFrameLayout() + ) + + # Orientation + with wrap.Frame(layout, label='Orientation') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider2') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveOrientationRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveOrientationRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 2), + cc=pa(release, 2), + ), + layout=frame.getFrameLayout() + ) + + # Twist + with wrap.Frame(layout, label='Twist') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider3') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveTwistRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveTwistRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 3), + cc=pa(release, 3), + ), + layout=frame.getFrameLayout() + ) + + # Width + with wrap.Frame(layout, label='Width') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider4') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveWidthRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + uniform = wrap.Button(frame.getFrameLayout(), 'gsWidthCheckBoxUniform') + uniform.setButtonStyle('small') + uniform.setLabel('Uniform', lineHeight=100) + uniform.setCheckable(True) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveWidthRand', min=0.001, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 4), + cc=pa(release, 4), + ), + layout=frame.getFrameLayout() + ) + + # Taper + with wrap.Frame(layout, label='Taper') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider5') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveTaperRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveTaperRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 5), + cc=pa(release, 5), + ), + layout=frame.getFrameLayout() + ) + + # Profile + with wrap.Frame(layout, label='Profile') as frame: + frame.setCollapsible(False) + with wrap.Row(frame.getFrameLayout()) as row: + enable = wrap.Button(row.layout(), 'curveRandomizeSlider6') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + slider = wrap.mayaSlider(mc.floatSliderGrp('gsCurveProfileRandMulti', l='Mult:', + min=1, max=50, pre=1, step=0.5, cw=[(1, 30), (2, 1)])) + row.layout().addWidget(slider) + + uniform = wrap.Button(frame.getFrameLayout(), 'gsProfileCheckBoxNegative') + uniform.setButtonStyle('small') + uniform.setLabel('Allow Negative Values', lineHeight=100) + uniform.setCheckable(True) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveProfileRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 6), + cc=pa(release, 6), + ), + layout=frame.getFrameLayout() + ) + + # Selection + with wrap.Frame(layout, label='Selection') as frame: + frame.setCollapsible(False) + + enable = wrap.Button(frame.getFrameLayout(), 'curveRandomizeSlider7') + enable.setLabel('Enabled', lineHeight=100) + enable.setButtonStyle('small') + enable.setCheckable(True) + + wrap.MayaSlider( + mc.floatSliderGrp( + 'gsCurveSelectRand', min=0, max=1, step=0.05, + dc=pa(core.sliders.randSliderDrag, 7), + cc=pa(release, 7), + ), + layout=frame.getFrameLayout() + ) + + layout.addWidget(wrap.separator()) + + with wrap.Row(layout) as row: + def randomizeClick(): + core.sliders.randSliderDrag(-1) + core.sliders.randSliderRelease(-1) + core.sliders.release() + randomize = wrap.Button(row.layout()) + randomize.setLabel("Randomize") + randomize.clicked.connect(randomizeClick) + + close = wrap.Button(row.layout()) + close.setLabel("Close") + close.clicked.connect(lambda: mc.deleteUI(name)) diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/__init__.py b/Scripts/Modeling/Edit/gs_curvetools/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/gs_math.py b/Scripts/Modeling/Edit/gs_curvetools/utils/gs_math.py new file mode 100644 index 0000000..691b0f1 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/gs_math.py @@ -0,0 +1,62 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +from PySide2 import QtGui + + +def lerp(x, y0, y1, x0=0, x1=1): + """ Returns the value between y0 and y1 based on x in range of x0 and x1 + + https://en.wikipedia.org/wiki/Linear_interpolation""" + return float((y0 * (x1 - x) + y1 * (x - x0)) / (x1 - x0)) + + +def quad(y1, y2, y3, fPoint, x1=0, x2=0.5, x3=1): + """ Takes three points (y1, y2, y3) and returns a point on computed curve with fPoint """ + A = (x2 * (y1 - y3) + x3 * (y2 - y1)) / ((x1 - x2) * (x1 - x3) * (x2 - x3)) + B = (y2 - y1) / (x2 - x1) - A * (x1 + x2) + C = y1 + return (A * (fPoint**2)) + (B * fPoint) + C + + +def dot(v1, v2): + """ Dot product of two QVector2D """ + return QtGui.QVector2D.dotProduct(v1, v2) + + +def projectPoint(A, B, P): + """ Project a point P onto a line made of two points A and B """ + A = QtGui.QVector2D(A) + B = QtGui.QVector2D(B) + P = QtGui.QVector2D(P) + AP = A - P + AB = A - B + return (A - dot(AP, AB) / dot(AB, AB) * AB).toPointF() + + +def angleDiff(a1, a2): + """ Find an absolute angle difference within 360 degrees""" + a1 = a1 % 360 + a2 = a2 % 360 + return abs(a1) - abs(a2) diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/hotkeys.csv b/Scripts/Modeling/Edit/gs_curvetools/utils/hotkeys.csv new file mode 100644 index 0000000..6d7a1ce --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/hotkeys.csv @@ -0,0 +1,79 @@ +Show Hide UI|import gs_curvetools.main as ct_main;ct_main.main()|Show/Hide UI|GS.GS_CurveTools.UI|python +Reset to Defaults|import gs_curvetools.utils.utils as ct_utils;from imp import reload;reload(ct_utils);ct_utils.resetUI()|Reset the UI to Defaults|GS.GS_CurveTools.UI|python +Stop UI|import gs_curvetools.utils.utils as ct_utils;from imp import reload;reload(ct_utils);ct_utils.stopUI()|Close the UI and Stop Scripts|GS.GS_CurveTools.UI|python +New Card|gs_curvetools.core.create.new(0)|Create New Card|GS.GS_CurveTools.Create|python +New Tube|gs_curvetools.core.create.new(1)|Create New Tube|GS.GS_CurveTools.Create|python +Curve Card|gs_curvetools.core.create.multiple(0)|Convert to Card|GS.GS_CurveTools.Create|python +Curve Tube|gs_curvetools.core.create.multiple(1)|Convert to Tube|GS.GS_CurveTools.Create|python +Bind|gs_curvetools.core.create.bind(1)|Bind Curves or Geo|GS.GS_CurveTools.Create|python +Bind Duplicate|gs_curvetools.core.create.bind(2)|Bind Curves or Geo with duplication|GS.GS_CurveTools.Create|python +Unbind|gs_curvetools.core.create.unbind()|Unbind Curves|GS.GS_CurveTools.Create|python +Add Card|gs_curvetools.core.create.populate(0,1)|Add Cards between selection|GS.GS_CurveTools.Create|python +Add Card No Blend|gs_curvetools.core.create.populate(0,2)|Add Cards between selection without attribute blend|GS.GS_CurveTools.Create|python +Add Tube|gs_curvetools.core.create.populate(1,1)|Add Tubes between selection|GS.GS_CurveTools.Create|python +Add Tube No Blend|gs_curvetools.core.create.populate(1,2)|Add Tubes between selection without attribute blend|GS.GS_CurveTools.Create|python +Fill|gs_curvetools.core.create.fill(1)|Duplicate Fill between selection|GS.GS_CurveTools.Create|python +Fill No Blend|gs_curvetools.core.create.fill(2)|Duplicate Fill between selection without attribute blend|GS.GS_CurveTools.Create|python +Subdivide|gs_curvetools.core.subdivideCurve(1)|Subdivide Selected Card/Tube|GS.GS_CurveTools.Create|python +Subdivide Duplicate|gs_curvetools.core.subdivideCurve(2)|Subdivide Selected Card/Tube and Duplicate|GS.GS_CurveTools.Create|python +Edge to Curve|gs_curvetools.core.edgeToCurve()|Convert Edges to Curves|GS.GS_CurveTools.Create|python +Card to Curve|gs_curvetools.core.cardToCurve()|Convert Cards to Curves|GS.GS_CurveTools.Create|python +Add Layer Collection|gs_curvetools.core.layerCollections.createLayerCollection()|Adds a New Layer Collection With the Specified Name|GS.GS_CurveTools.LayerCollections|python +Delete Layer Collection|gs_curvetools.core.layerCollections.deleteLayerCollection()|Deletes Current Layer Collection|GS.GS_CurveTools.LayerCollections|python +Clear Layer Collection|gs_curvetools.core.layerCollections.clear()|Deletes All The Curves from the Current Layer Collection|GS.GS_CurveTools.LayerCollections|python +Rename Layer Collection|gs_curvetools.core.layerCollections.rename()|Renames the Current Layer Collection|GS.GS_CurveTools.LayerCollections|python +Merge Up Layer Collection|gs_curvetools.core.layerCollections.mergeUp()|Merges the Current Layer Collection With One Above It|GS.GS_CurveTools.LayerCollections|python +Merge Down Layer Collection|gs_curvetools.core.layerCollections.mergeDown()|Merges the Current Layer Collection With One Below It|GS.GS_CurveTools.LayerCollections|python +Move Up Layer Collection|gs_curvetools.core.layerCollections.moveUp()|Moves the Current Layer Collection Up One Index|GS.GS_CurveTools.LayerCollections|python +Move Down Layer Collection|gs_curvetools.core.layerCollections.moveDown()|Moves the Current Layer Collection Down One Index|GS.GS_CurveTools.LayerCollections|python +Copy Layer Collection|gs_curvetools.core.layerCollections.copy()|Copies Curves from Current Layer Collection|GS.GS_CurveTools.LayerCollections|python +Paste Layer Collection|gs_curvetools.core.layerCollections.paste()|Pastes Copied Curves to Current Layer Collection|GS.GS_CurveTools.LayerCollections|python +Filter All Show|gs_curvetools.core.layersFilterToggle(True, True, hotkey=True)|Show All Layers|GS.GS_CurveTools.Layers|python +Filter All Show All Collections|"gs_curvetools.core.layersFilterToggle(True, True, hotkey=True, mod=""Ctrl"")"|Show All Layers in All Collections|GS.GS_CurveTools.Layers|python +Filter All Hide|gs_curvetools.core.layersFilterToggle(False, False, hotkey=True)|Hide All Layers|GS.GS_CurveTools.Layers|python +Filter All Hide All Collections|"gs_curvetools.core.layersFilterToggle(True, True, hotkey=True, mod=""Shift+Ctrl"")"|Hide All Layers in All Collections|GS.GS_CurveTools.Layers|python +Filter Curve|gs_curvetools.core.layersFilterToggle(True, False, hotkey=True)|Show Only Curve Component|GS.GS_CurveTools.Layers|python +Filter Curve in All Collections|"gs_curvetools.core.layersFilterToggle(True, False, hotkey=True, mod=""Ctrl"")"|Show Only Curve Component in All Collections|GS.GS_CurveTools.Layers|python +Toggle Always on Top|gs_curvetools.core.alwaysOnTopToggle()|Toggle Always on Top Mode for Curves|GS.GS_CurveTools.Layers|python +Filter Geo|gs_curvetools.core.layersFilterToggle(False, True, hotkey=True)|Show Only Geo Component|GS.GS_CurveTools.Layers|python +Filter Geo in All Collections|"gs_curvetools.core.layersFilterToggle(False, True, hotkey=True, mod=""Ctrl"")"|Show Only Geo Component in All Collections|GS.GS_CurveTools.Layers|python +Toggle Color|gs_curvetools.core.toggleColor.toggleColorVis()|Toggle Color Mode|GS.GS_CurveTools.Layers|python +Extract Selected|"gs_curvetools.core.extractSelectedCurves(""Shift"", hotkey=True)"|Extract Selected Geo|GS.GS_CurveTools.Layers|python +Extract Selected Medged|gs_curvetools.core.extractSelectedCurves(hotkey=True)|Extract Selected Geo Merged|GS.GS_CurveTools.Layers|python +Extract All|"gs_curvetools.core.extractAllCurves(""Shift"", hotkey=True)"|Extract All Geo|GS.GS_CurveTools.Layers|python +Extract All Medged|gs_curvetools.core.extractAllCurves(hotkey=True)|Extract All Geo Merged|GS.GS_CurveTools.Layers|python +Select Curve|gs_curvetools.core.selectPart(1)|Select Curve Component|GS.GS_CurveTools.Selection|python +Select Geo|gs_curvetools.core.selectPart(2)|Select Geo Component|GS.GS_CurveTools.Selection|python +Select Group|gs_curvetools.core.selectPart(0)|Select Group|GS.GS_CurveTools.Selection|python +Group Curves|gs_curvetools.core.groupCurves()|Group Selected Curves|GS.GS_CurveTools.Selection|python +Regroup by Layer|gs_curvetools.core.regroupByLayer()|Regroup using Layers|GS.GS_CurveTools.Selection|python +Transfer Attr Forward|gs_curvetools.core.attributes.transferAttr(1)|Transfer Attributes Forward|GS.GS_CurveTools.Utilities|python +Transfer Attr Backwards|gs_curvetools.core.attributes.transferAttr(2)|Transfer Attributes Backwards|GS.GS_CurveTools.Utilities|python +Copy Attributes|gs_curvetools.core.attributes.copyAttributes()|Copy attributes from selected curves|GS.GS_CurveTools.Utilities|python +Paste Attributes|gs_curvetools.core.attributes.pasteAttributes()|Paste attributes to selected curves|GS.GS_CurveTools.Utilities|python +Transfer UVs Forward|gs_curvetools.core.attributes.transferUV(1)|Transfer UVs Forward|GS.GS_CurveTools.Utilities|python +Transfer UVs Backwards|gs_curvetools.core.attributes.transferUV(2)|Transfer UVs Backwards|GS.GS_CurveTools.Utilities|python +Copy UVs|gs_curvetools.core.attributes.copyUVs()|Copy UVs from selected curves|GS.GS_CurveTools.Utilities|python +Paste UVs|gs_curvetools.core.attributes.pasteUVs()|Paste UVs to selected curves|GS.GS_CurveTools.Utilities|python +Reset Pivot to Root|gs_curvetools.core.resetCurvePivotPoint(1)|Reset Pivot Point to Root|GS.GS_CurveTools.Utilities|python +Reset Pivot to Tip|gs_curvetools.core.resetCurvePivotPoint(2)|Reset Pivot Point to Tip|GS.GS_CurveTools.Utilities|python +Rebuild Curves|gs_curvetools.core.sliders.rebuildButtonClicked()|Rebuild Selected Curves|GS.GS_CurveTools.Utilities|python +Duplicate|gs_curvetools.core.duplicateCurve()|Duplicate Selected Curves|GS.GS_CurveTools.Utilities|python +Delete Curves|gs_curvetools.core.deleteSelectedCurves()|Delete selected curves and objects|GS.GS_CurveTools.Utilities|python +Extend|gs_curvetools.core.extendCurve()|Extend Curves|GS.GS_CurveTools.Utilities|python +Reduce|gs_curvetools.core.reduceCurve()|Reduce Curves|GS.GS_CurveTools.Utilities|python +Smooth|gs_curvetools.core.smoothCurve()|Smooth Curves|GS.GS_CurveTools.Utilities|python +Orient to Normal|gs_curvetools.core.orientToFaceNormals()|Orients Selected Curve to Scalp Normals|GS.GS_CurveTools.Utilities|python +Mirror X|gs_curvetools.core.mirrorHair(0)|Mirror on X Axis|GS.GS_CurveTools.Mirror|python +Mirror Y|gs_curvetools.core.mirrorHair(1)|Mirror on Y Axis|GS.GS_CurveTools.Mirror|python +Mirror Z|gs_curvetools.core.mirrorHair(2)|Mirror on Z Axis|GS.GS_CurveTools.Mirror|python +Flip X|gs_curvetools.core.mirrorHair(0,1)|Flip on X Axis|GS.GS_CurveTools.Mirror|python +Flip Y|gs_curvetools.core.mirrorHair(1,1)|Flip on Y Axis|GS.GS_CurveTools.Mirror|python +Flip Z|gs_curvetools.core.mirrorHair(2,1)|Flip on Z Axis|GS.GS_CurveTools.Mirror|python +Add Control Curve|gs_curvetools.core.controlCurveCreate()|Add Control Curve|GS.GS_CurveTools.Control|python +Apply Control Cuve|gs_curvetools.core.controlCurveApply()|Apply Control Curve|GS.GS_CurveTools.Control|python +Curve Control Window|gs_curvetools.ui.curveControlWorkspace()|Open Curve Control Window|GS.GS_CurveTools.Control|python +UV Editor Window|gs_curvetools.ui.uvEditorWorkspace()|Open UV Editor Window|GS.GS_CurveTools.Control|python +AO Toggle|gs_curvetools.utils.utils.AOToggle()|Toggle Viewport AO|GS.GS_CurveTools.Misc|python +Advanced Visibility Toggle|gs_curvetools.core.advancedVisibility.toggleCurveHighlightFromUI(True)|Toggle Advanced Visibility|GS.GS_CurveTools.Misc|python +Geometry Highlight Toggle|gs_curvetools.core.advancedVisibility.geometryHighlightCommand()|Toggle Geometry Highlight|GS.GS_CurveTools.Misc|python \ No newline at end of file diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/style.py b/Scripts/Modeling/Edit/gs_curvetools/utils/style.py new file mode 100644 index 0000000..af200a0 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/style.py @@ -0,0 +1,786 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import math +from imp import reload + +import maya.cmds as mc + +from . import utils + +reload(utils) + +# Scaling + +MULT = 1 +if mc.about(mac=True): + MULT = 1 +else: + MULT = mc.mayaDpiSetting(q=1, rsv=1) + # mult = 1 + + +def scale(a): + # types (int\float) -> (float) + if isinstance(a, list): + return [i * MULT for i in a] + else: + return a * MULT + + +# Fonts +NORMAL_FONT = 11 * MULT +SMALL_FONT = 10 * MULT + +# Normal Button +BUTTON_HEIGHT = 24 * MULT +BORDER_RADIUS = 4 * MULT +BORDER_WIDTH = 2 * MULT +BACKGROUND_COLOR = 'rgb(93,93,93)' +PRESSED_BACKGROUND_COLOR = 'rgb(128,128,128)' +HOVER_BACKGROUND_COLOR = 'rgb(100,100,100)' + +# Active Button (Orange) +ORANGE = 'rgb(219,148,86)' +ORANGE_HOVER = 'rgb(225,155,90)' +ORANGE_PRESSED = 'rgb(235,170,105)' + +# Active Button (Blue) +BLUE = 'rgb(82,133,166)' +BLUE_HOVER = 'rgb(95,145,185)' + +# Active Button (White) +BORDER_WIDTH_WHITE = 2 * MULT + +# Small Button +SMALL_BORDER_RADIUS = 4 * MULT +SMALL_BORDER_WIDTH = 1 * MULT +SMALL_BORDER_COLOR = 'rgb(93,93,93)' +SMALL_BACKGROUND_COLOR = 'rgba(93,93,93,0)' +SMALL_PRESSED_COLOR = 'rgba(128,128,128,255)' +SMALL_HOVER_COLOR = 'rgb(100,100,100)' + +# Layer/Set Button +LAYER_BOTTOM_PADDING = 1 * MULT +LAYER_BORDER_RADIUS = 4 * MULT +LAYER_HEIGHT = 16 * MULT +LAYER_BORDER_WIDTH = 1 * MULT + +# Images/Icons +DROP_DOWN_ARROW_IMAGE = utils.getFolder.icons() + 'drop-down-arrow.png' + +### Regular Buttons ### + +buttonNormal = ''' + QPushButton {{ + background-color: {bg_clr}; + border-radius: {br}; + min-height: {h}px; + max-height: {h}px; + }} + QPushButton:pressed {{ + background-color: {pr_bg_clr}; + }} + QPushButton:checked {{ + background-color: {ac_bg_clr}; + }} + QPushButton:hover {{ + background-color: {h_bg_clr}; + }} +}} +'''.format(br=int(BORDER_RADIUS), + h=int(BUTTON_HEIGHT), + bg_clr=str(BACKGROUND_COLOR), + pr_bg_clr=str(PRESSED_BACKGROUND_COLOR), + h_bg_clr=str(HOVER_BACKGROUND_COLOR), + ac_bg_clr=BLUE) + +buttonNormalBlueBorder = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-radius: {br}; + border-color: {borderBlue}; + border-style: solid; + border-width: {bw}px; + padding: -{bw}px; + min-height: {h}px; + max-height: {h}px; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:checked +{{ + background-color: {ac_bg_clr}; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +'''.format(br=int(BORDER_RADIUS), + h=int(BUTTON_HEIGHT), + bw=int(BORDER_WIDTH), + bg_clr=str(BACKGROUND_COLOR), + pr_bg_clr=str(PRESSED_BACKGROUND_COLOR), + h_bg_clr=str(HOVER_BACKGROUND_COLOR), + borderBlue=str(BLUE), + ac_bg_clr=BLUE) + +buttonIcon = ''' + QPushButton + {{ + background-color: none; + border-radius: {br}px; + border:none; + }} + QPushButton:checked + {{ + background-color: none; + border:none; + }} + QPushButton:pressed + {{ + background-color: {pr_bg_clr}; + }} + QPushButton:checked:pressed + {{ + background-color: {pr_bg_clr}; + }} + QPushButton:checked:hover + {{ + background-color: {h_bg_clr}; + }} + QPushButton:hover + {{ + background-color: {h_bg_clr}; + }} +}} +'''.format(br=int(BORDER_RADIUS / 2), + h=int(BUTTON_HEIGHT), + bg_clr=str(BACKGROUND_COLOR), + pr_bg_clr=str(PRESSED_BACKGROUND_COLOR), + h_bg_clr=str(HOVER_BACKGROUND_COLOR), + ac_bg_clr=BLUE) + +buttonActiveOrange = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-radius: {br}px; + min-height: {h}px; + max-height: {h}px; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +'''.format(br=int(BORDER_RADIUS), + h=int(BUTTON_HEIGHT), + bg_clr=ORANGE, + pr_bg_clr=str(ORANGE_PRESSED), + h_bg_clr=str(ORANGE_HOVER), + ac_bg_clr=BLUE) + +buttonActiveBlue = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-radius: {br}px; + min-height: {h}px; + max-height: {h}px; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +'''.format(br=int(BORDER_RADIUS), + h=int(BUTTON_HEIGHT), + bg_clr=BLUE, + pr_bg_clr=str(PRESSED_BACKGROUND_COLOR), + h_bg_clr=str(HOVER_BACKGROUND_COLOR), + ac_bg_clr=BLUE) + +buttonActiveWhite = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-radius: {br}px; + border-width: {bw}px; + border-color: white; + border-style: solid; + padding: -{bw}px; + min-height: {h}px; + max-height: {h}px; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:hover +{{ + background-color: {bg_clr}; +}} +'''.format(br=int(BORDER_RADIUS), + h=int(BUTTON_HEIGHT), + bw=int(BORDER_WIDTH_WHITE), + bg_clr=ORANGE, + pr_bg_clr=str(PRESSED_BACKGROUND_COLOR)) + +frameButton = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-width: {bw}px; + border-radius: {br}px; + border-color: {br_clr}; + border-style: solid; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:checked +{{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; + border-top-left-radius: {br}px; + border-top-right-radius: {br}px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +}} +QPushButton:disabled +{{ + border-style: none; +}} +QPushButton:disabled:checked +{{ + background-color: {bg_clr}; + border-style: none; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +}} +'''.format(br=4 * MULT, + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + ac_bg_clr=BLUE) + +frameButtonNotCollapsable = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-style: solid; + border-width: {bw}px; + border-top-left-radius: {br}px; + border-top-right-radius: {br}px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +}} +'''.format(br=4 * MULT, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=BACKGROUND_COLOR) + +smallNormal = ''' + QPushButton {{ + background-color: {bg_clr}; + border-width: {bw}px; + border-radius: {br}px; + border-color: {br_clr}; + border-style: solid; + }} + QPushButton:pressed {{ + background-color: {pr_bg_clr}; + }} + QPushButton:checked {{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; + }} + QPushButton:disabled {{ + border-style: none; + }} + QPushButton:disabled:checked {{ + background-color: {bg_clr}; + border-style: none; + }} + QPushButton:hover {{ + background-color: {h_bg_clr}; + }} + QPushButton:checked:hover {{ + background-color: {h_bg_checked_clr}; + }} +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=SMALL_BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + h_bg_checked_clr=BLUE_HOVER, + ac_bg_clr=BLUE) + +smallFilled = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-width: {bw}px; + border-radius: {br}px; + border-color: {br_clr}; + border-style: solid; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:checked +{{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; +}} +QPushButton:disabled +{{ + border-style: none; +}} +QPushButton:disabled:checked +{{ + background-color: {bg_clr}; + border-style: none; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + ac_bg_clr=BLUE) + +smallFilledBlue = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-width: {bw}px; + border-radius: {br}px; + border-color: {br_clr}; + border-style: solid; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:checked +{{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; +}} +QPushButton:disabled +{{ + border-style: none; +}} +QPushButton:disabled:checked +{{ + background-color: {bg_clr}; + border-style: none; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=BLUE, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=BLUE, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=BLUE_HOVER, + ac_bg_clr=BLUE) + +smallNoBorder = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-width: 0px; + border-radius: {br}px; + border-color: {br_clr}; + border-style: solid; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:checked +{{ + color: rgb(255,0,0) + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; +}} +QPushButton:disabled +{{ + color: rgb(255,0,0); + border-style: none; +}} +QPushButton:disabled:checked +{{ + background-color: {bg_clr}; + border-style: none; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=SMALL_BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + ac_bg_clr=BLUE) + +sliderLabel = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-width: 0px; + border-radius: {br}px; + border-color: {br_clr}; + border-style: solid; +}} +QPushButton:pressed +{{ + background-color: {bg_clr}; +}} +QPushButton:checked +{{ + color: rgb(255,0,0) + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; +}} +QPushButton:disabled +{{ + color: rgb(255,0,0); + border-style: none; +}} +QPushButton:disabled:checked +{{ + background-color: {bg_clr}; + border-style: none; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=SMALL_BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + ac_bg_clr=BLUE) + +### Compound Buttons ### + +smallCompoundTopLeft = ''' + QPushButton {{ + background-color: {bg_clr}; + border-width: {bw}px; + border-top-left-radius: {br}px; + border-top-right-radius: 0px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-color: {br_clr}; + border-style: solid; + }} + QPushButton:pressed {{ + background-color: {pr_bg_clr}; + }} + QPushButton:checked {{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; + }} + QPushButton:disabled {{ + border-style: none; + }} + QPushButton:disabled:checked {{ + background-color: {bg_clr}; + border-style: none; + }} + QPushButton:hover {{ + background-color: {h_bg_clr}; + }} + QPushButton:checked:hover {{ + background-color: {h_bg_checked_clr}; + }} +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=SMALL_BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + h_bg_checked_clr=BLUE_HOVER, + ac_bg_clr=BLUE) + +smallCompoundTopRight = ''' + QPushButton {{ + background-color: {bg_clr}; + border-width: {bw}px; + border-top-left-radius: 0px; + border-top-right-radius: {br}px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + border-color: {br_clr}; + border-style: solid; + }} + QPushButton:pressed {{ + background-color: {pr_bg_clr}; + }} + QPushButton:checked {{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; + }} + QPushButton:disabled {{ + border-style: none; + }} + QPushButton:disabled:checked {{ + background-color: {bg_clr}; + border-style: none; + }} + QPushButton:hover {{ + background-color: {h_bg_clr}; + }} + QPushButton:checked:hover {{ + background-color: {h_bg_checked_clr}; + }} +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=SMALL_BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + h_bg_checked_clr=BLUE_HOVER, + ac_bg_clr=BLUE) + +smallFilledCompoundBottom = ''' +QPushButton +{{ + background-color: {bg_clr}; + border-width: {bw}px; + border-top-left-radius: 0px; + border-top-right-radius: 0px; + border-bottom-left-radius: {br}px; + border-bottom-right-radius: {br}px; + border-color: {br_clr}; + border-style: solid; +}} +QPushButton:pressed +{{ + background-color: {pr_bg_clr}; +}} +QPushButton:checked +{{ + background-color: {ac_bg_clr}; + border-color: lightgrey; + border-style: solid; + border-width: {bw}px; +}} +QPushButton:disabled +{{ + border-style: none; +}} +QPushButton:disabled:checked +{{ + background-color: {bg_clr}; + border-style: none; +}} +QPushButton:hover +{{ + background-color: {h_bg_clr}; +}} +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bw=int(SMALL_BORDER_WIDTH), + bg_clr=BACKGROUND_COLOR, + pr_bg_clr=SMALL_PRESSED_COLOR, + h_bg_clr=SMALL_HOVER_COLOR, + ac_bg_clr=BLUE) + +### Layer Button ### + + +def layer(background_color='rbga(0,0,0,0)', outline='rgb(93,93,93)'): + + styleSheet = ''' + QPushButton + {{ + background-color: {bg}; + border-width: {bw}px; + border-color: {bc}; + border-style: solid; + min-height: {h}px; + max-height: {h}px; + padding: -{bw}px; + }} + QPushButton:hover + {{ + border-width: {bw}px; + border-color: {bc_hover}; + border-style: solid; + }} + QPushButton:pressed + {{ + border-width: {bw}px; + border-color: {bc_checked}; + border-style: solid; + }} + QPushButton:checked + {{ + background-color: {bg}; + border-width: {bw}px; + border-color: {bc_checked}; + border-radius: {radius}; + border-style: solid; + min-height: {h}px; + max-height: {h}px; + padding: -{bw}px; + }} + QPushButton:checked:hover + {{ + border-width: {bw}px; + border-color: {bc_checked}; + border-style: solid; + }} + '''.format(bw=math.ceil(LAYER_BORDER_WIDTH), + h=math.ceil(LAYER_HEIGHT), + bg=background_color, + bc=outline, + bc_hover=ORANGE, + radius=BORDER_RADIUS, + bc_checked='white') + + return styleSheet + +### Labels ### + + +subDLabel = ''' +QLabel +{{ + color : {orange}; +}} +'''.format(orange=ORANGE) + +layerLabel = ''' +QLabel +{{ + font: {weight} {f_size}px; + padding-bottom: {p}px; +}} +'''.format(f_size=int(NORMAL_FONT), + weight='bold', + p=int(LAYER_BOTTOM_PADDING)) + +### Input Fields ### + +inputField = ''' +QSpinBox +{ + border-style: solid; + border-radius: 4px; +} +''' + +### Combo Box ### + +smallComboBox = ''' +QComboBox +{{ + background-color: {bg_clr}; + border-radius: {br}px; +}} +QComboBox:hover +{{ + background-color: {bc_hover}; +}} +QComboBox:on +{{ + border-top-left-radius: {br}px; + border-top-right-radius: {br}px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; +}} +QComboBox::drop-down +{{ + background-color: {bg_clr}; + border-radius: {br}px; + width: {drop_down_width}; +}} +QComboBox::down-arrow +{{ + image: url("{path}"); + width: {icon_size}px; + height: {icon_size}px; +}} +'''.format(br=int(SMALL_BORDER_RADIUS), + br_clr=SMALL_BORDER_COLOR, + bg_clr=BACKGROUND_COLOR, + bw=int(SMALL_BORDER_WIDTH), + path=DROP_DOWN_ARROW_IMAGE, + icon_size=int(scale(8)), + bc_hover=HOVER_BACKGROUND_COLOR, + drop_down_width=int(scale(16))) diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.md b/Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.md new file mode 100644 index 0000000..79f10e7 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.md @@ -0,0 +1,888 @@ + + + + + +# importCurves +Imports Curves that were Exported using the Export Button. +NOTE: Only import files that were exported using Export Curves button. +WARNING: This operation is NOT undoable! + +# exportCurves +Exports selected curves into a .curves (or .ma) file. Can then be Imported using Import Curves. + +# changeScaleFactor +Opens a window that controls the Scale Factor. +Scale factor determines the initial scale of the created curve and adjusts other parameters. +Scale factor is stored in a global preset, in the scene and on each curve. +Priority of scale factors Curve>Scene>Global. +NOTE: Using the correct scale factor can help with the control of the curves and helps in avoiding bugs due to very large or very small scenes. + +# globalCurveThickness +Opens a window that controls the thickness of the curves in the scene as well as the global curve thickness preset across the scenes. + +# setAOSettings +Manually sets the recommended AO settings for older Maya versions. +These AO settings are needed to use the "See Through" or "Toggle Always on Top" functions. +Are applied automatically by those functions. + +# setTransparencySettings +Sets recommended transparency settings for Maya viewport. +*** +Simple Transparency is fast but very inaccurate render mode. Only suitable for simple, one layer hair. +Object Sorting Transparency has average performance impact and quality. Can have issues on complex multi-layered grooms. +Depth Transparency - these are the optimal settings for the highest quality of the hair cards preview. Can have performance impact on slower systems. + +# convertToWarpCard +Converts selection to Warp Cards. +Compatible attributes are retained. + +# convertToWarpTube +Converts selection to Warp Tubes. +Compatible attributes are retained. + +# convertToExtrudeCard +Converts selection to Extrude Cards. +Compatible attributes are retained. + +# convertToExtrudeTube +Converts selection to Extrude Tubes. +Compatible attributes are retained. + +# syncCurveColor +Toggles the syncing of the curve color to the layer color. + +# colorizedRegroup +Toggles the colorization of the regrouped layers when Regroup by Layer function is used. + +# colorOnlyDiffuse +Colorize only the diffuse component of the card material. +Alpha will stay the same. + +# checkerPattern +Toggles the use of the checker pattern when the Color mode is enabled. + +# ignoreLastLayer +Toggles the filtering (All, Curve, Geo) and Extraction (Extract All) of the last layer. If enabled, the last layer is ignored. +NOTE: Last layer is typically used for templates, so it is ignored by default. + +# ignoreTemplateCollections +Ignores the filtering (All, Curve, Geo) and Extraction (Extract All) of all the collections that have "template" in their name. Case insensitive. + +# groupTemplateCollections +Collections that have "template" in their name (case insensitive) will be grouped together into "CT_Templates" group by Regroup By Layer function. + +# syncOutlinerLayerVis +Syncs the outliner and layer visibility. If enabled, hiding the layer will also hide the curve groups in the outliner. + +# keepCurveAttributes +If enabled, the attributes that are stored in the curve will be restored if the curve that was duplicated (on its own, without the geo) is used to create other cards or tubes. +If disabled, the attributes will be ignored and reset. +Example: +1. Create a card and change its twist value. +2. Ctrl+D and Shift+P the curve (not the curve group). +3. Click Curve Card and the twist value will be restored on a newly created card. + +# boundCurvesFollowParent +Will ensure that moving a parent curve in a Bound Object (Bound Group) will also move all the child curves along with it to a new layer. Recommended to keep this enabled. + +# massBindOption +Will bind selected hair clump (or geometry) to all selected "empty" curves. + +# bindDuplicatesCurves +Will automatically duplicate the curves before binding them to the curve, leaving old curves behind with no edits. + +# bindFlipUVs +Enabling this option will flip the UVs of the original cards before binding them to the curve. +It will also automatically flip the bound geo and rotate it 90 deg. +This option will also flip the UVs back when using Unbind for better workflow. +Disabling this option will result in an old Bind/Unbind behaviour. + +# populateBlendAttributes +Enables blending of the attributes when using Add Cards/Tubes or Fill functions. + +# convertInstances +Will automatically convert instanced curves to normal curves before any other function is applied. + +# replacingCurveLayerSelection +Will disable additive selection for the layers. When holding Ctrl and clicking on a new layer, old layer will be deselected automatically. + +# useAutoRefineOnNewCurves +Automatically enables auto-refine on the new curves. + +# flipUVsAfterMirror +Enabling this option will flip the UVs horizontally after mirroring the cards to achieve exact mirroring. + +# enableTooltips +Will toggle the visibility of these tooltips you are reading right now. + +# showLayerCollectionsMenu +Shows layer collections menu widget. + +# importIntoANewCollection +If enabled, all the imported curves will be placed into a new "Imported Curves" layer collection. +If disabled, all the imported curves will be placed into the "Main" layer collection + +# layerNumbersOnly +Layers will use only numbers if enabled. + +# convertToNewLayerSystem +Converts all the layers in the scene to a new layer system that is hidden from the Channel Box Display Layers window. +Layers can still be accessed from Window->Relationship Editors->Display Layer window. + +# updateLayers +Utility function that will manually update all layers. Used for if layers are correct for some reason. + +# resetToDefaults +Resets every option and the GS CurveTools to the default "factory" state. + +# maya2020UVFix +This function will fix any broken UVs when trying to open old scenes in Maya 2020 or 2022 or when opening scenes in 2020 and 2022 when using Maya Binary file type. This will have no effect on older versions of Maya (<2020). This bug is native to Maya and thus can’t be fixed in GS CurveTools plug-in. + +# mayaFixBrokenGraphs +This function will attempt to fix all the broken graphs in the scene. +Use if one of the graphs (Width, Twist or Profile) is in a broken state. + +# convertBezierToNurbs +Converts the selected Bezier curves to NURBS curves. +Bezier curves are not supported by the GS CurveTools. + +# maya2020TwistAttribute +This function will fix any broken cards created in Maya 2020.4 before v1.2.2 update. + +# maya2020UnbindFix +This function will fix any cards that are not unbinding properly and were created before v1.2.3 update in Maya 2020.4. + +# deleteAllAnimationKeys +This function will delete all the animation keys on all the curves present in the scene. +This can fix deformation issues when using duplicate or other GS CurveTools functions. +Some functions (like duplicate) will delete the keys automatically, however the keys can still cause issues. + + + +# warpSwitch +Advanced cards and tubes suitable for longer hair. +Additional options and controls. +Slower than Extrude (viewport performance). +Can have issues on very small scales. + +# extrudeSwitch +Simple cards and tubes suitable for shorter hair and brows. +Has limited controls. +Much faster than Warp (viewport performance). +Better for small scales. + +# newCard +Creates a new Card in the middle of the world. Used at the beginning of the project to create templates. + +# newTube +Creates a new Tube in the middle of the world. Used at the beginning of the project to create templates. + +# curveCard +Converts selected Maya curve to CurveTools Card. + +# curveTube +Converts selected Maya curve to CurveTools Tube. + +# gsBind +Binds selection to a single curve. Creates a Bind Group. Selection options: +1. Single "empty" curve (default Maya curve) and single combined geometry. +2. Single "empty" curve (default Maya curve) and any number of Curve Cards and Curve Tubes. +*** +Shift + Click will duplicate the original curves/geo before binding it to the empty curve. +Same option is available in the Options menu (Duplicate Curves Before Bind). + +# gsUnbind +UnBinds geometry or Cards/Tubes from selected Bound object. +Geometry and Cards/Tubes will be placed at the origin. + +# addCards +Creates Cards in-between selected Cards based on the Add slider value. +Bound objects are NOT supported. +NOTE: Selection order defines the direction of added Cards if more than 2 initial Cards are selected. + +# addTubes +Creates Tubes in-between selected Tubes based on the Add slider value. +Bound objects are NOT supported. +NOTE: Selection order defines the direction of added Tubes if more than 2 initial Tubes are selected. + +# gsFill +Creates Cards/Tubes or Bound Groups in between selected Cards/Tubes or Bound Groups based on the Add slider value. +NOTE 1: Selection order defines the direction of added curves if more than 2 initial curves are selected. +NOTE 2: The type of Card or Tube or Bound Group is defined by the previous curve in the selection chain. + +# gsSubdivide +Subdivides selected curve into multiple curves based on the Add slider value +Shift + Click subdivides selected curve but does not delete the original curve + +# gsEdgeToCurve +Converts selected geometry edges to curves. +Multiple unconnected edge groups can be selected at the same time. + +# gsCardToCurve +Opens the Card-to-Curve UI +Card to curve algorithm will attempt to generate CurveTools cards from selected geometry cards. +Selected geometry cards should be one sided meshes. +Selected geometry cards should be separate objects. + +# layerCollectionsComboBox +Layer collections drop-down menu. +Allows to separate the project into different layer collections, up to 80 layers in each collection. +Has additional functionality in markdown menu (Hold RMB): +*** +Markdown menu: +1. Clear - will delete all the cards from the current layer. Undoable operation. +2. Merge Up, Merge Down - merge all the cards from the current layer to the layer above or below it. +3. Copy, Paste - will copy all the cards from the current layer and paste them to the layer that user selects. +4. Move Up, Move Down - will rearrange the current layer collections by moving the currently selected collection up or down in the list. +5. Rename - will rename the current layer collection + +# layerCollectionsPlus +Add additional layer collection after the current one. + +# layerCollectionsMinus +Remove current layer collection. All the cards from the removed collection will be merged one layer UP. + +# gsAllFilter +Layer filter. Controls the visibility of all objects in all layers: +Normal click will show all curves and geometry in all layers. +Shift + Click will hide all curves and geometry in all layers +Ctrl + Click will show all the curves and geometry in all layers and all collections. +Ctrl + Shift + Click will hide all curves and geometry in all layers and all collections. + +# gsCurveFilter +Layer filter. Hides all geometry and shows all the curves in all layers. +Ctrl + Click will do the same thing, but for all layers and all collections. +NOTE: Holding RMB will open a marking menu with Toggle Always on Top function as well as "Auto-Hide Inactive Curves" function. +Toggle Always on Top function will toggle the Always on Top feature that will show the curve component always on top. The effect is different in different Maya versions +Auto-Hide Inactive Curves will hide all the curve components on all inactive layer collections when switching between collections. + +# gsGeoFilter +Layer filter. Hides all curves and shows only geometry. +Ctrl + Click will do the same thing, but for all layers and all collections. + +# colorMode +Color mode toggle. Enables colors for each layer and (optionally) UV checker material. +NOTE: Checker pattern can be disabled in the Options menu + +# curveGrp0 +Curve Layers that are used for organization of the cards and tubes in the scene. +Selected layer (white outline) will be used to store newly created cards. +Holding RMB will open a marking menu with all the functions of current layer. +*** +Key Combinations: +Shift + Click: additively select the contents of the layers. +Ctrl + Click: exclusively select the contents of the layer. +Alt + Click: show/hide selected layer. +Ctrl + Shift: show/hide curve component on selected layer. +Ctrl + Alt: show/hide geo component for the selected layer. +Shift + Alt + Click: isolate select the layer. +Shift + Ctrl + Alt + Click: enable Always on Top for each layer (only for Maya 2022+). +*** +Layer MMB Dragging: +MMB + Drag: move the contents of one layer to another layer. Combine if target layer has contents. +MMB + Shift + Drag: copy the contents of one layer to another layer. Copy and Add if target layer has contents. + +# gsExtractSelected +Extracts (Duplicates) the geometry component from the selected curves: +*** +Key Combinations: +Normal click will extract geometry and combine it. +Shift + Click will extract geometry as individual cards. +Ctrl + Click will extract geometry, combine it, open export menu and delete the extracted geo after export. +Shift + Ctrl click will extract geometry, open export menu and delete the extracted geo after export. + +# gsExtractAll +Extracts (Duplicates) the geometry component from all layers and collections. Original layers are HIDDEN, NOT deleted: +Last Layer in the current Collection is ignored by default. Can be changed in the options. +Collections with "template" in their name (case insensitive) will be ignored by default. Can be changed in the options. +*** +Key Combinations: +Normal click will extract geometry and combine it. +Shift + Click will extract geometry as individual cards grouped by layers. +Ctrl + Click will extract geometry, combine it, open export menu and delete the extracted geo after export. +Shift + Ctrl click will extract geometry, open export menu and delete the extracted geo after export. + +# gsSelectCurve +Selects the curve components of the selected Curve Cards/Tubes. +NOTE: Useful during the selection in the outliner. + +# gsSelectGeo +Selects the geometry component of the selected Curve Cards/Tubes. +NOTE: Useful for quick assignment of the materials. + +# gsSelectGroup +Selects the group component of the selected Curve Cards/Tubes. +NOTE: Useful when you are deleting curves from viewport selection. + +# gsGroupCurves +Groups the selected curves and assigns the name from Group Name input field (or default name if empty). + +# gsRegroupByLayer +Regroups all the curves based on their layer number, group names and collection names. +Group names can be changed in the Layer Names & Colors menu. +Groups can be colorized if the "Colorize Regrouped Layers" is enabled in the Options menu. +Collections with "template" in their name will be grouped under "CT_Templates". Can be changed in the options. + +# gsGroupNameTextField +The name used by the Group Curves function. +If empty, uses the default name. + +# gsCustomLayerNamesAndColors +Opens a menu where group names and colors can be changed and stored in a global preset. + +# gsTransferAttributes +Transfers attributes from the FIRST selected curve to ALL the other curves in the selection. +NOTE: Shift + Click transfers the attributes from the LAST selected curve to ALL others. +NOTE2: Holding RMB on this button opens a marking menu with Copy-Paste and Filter functionality + +# gsTransferUVs +Transfers UVs from the FIRST selected curve to ALL the other curves in the selection. +NOTE: Shift + Click transfers the UVs from the LAST selected curve to ALL others. +NOTE2: Holding RMB on this button opens a marking menu with Copy-Paste and Filter functionality + +# gsResetPivot +Resets the pivot on all selected curves to the default position (root CV). + +# gsRebuildWithCurrentValue +Rebuild selected curves using current rebuild slider value + +# gsResetRebuildSliderRange +Reset rebuild slider range (1 to 50) + +# gsDuplicateCurve +Duplicates all the selected curves and selects them. +NOTE: You can select either NURBS curve component, geometry component or group to duplicate. + +# gsRandomizeCurve +Opens a window where different attributes of the selected curves can be randomized: +1. Enable the sections of interest and change the parameters. +2. Dragging the sliders in each section enables a PREVIEW of the randomization. Releasing the slider will reset the curves. +3. Click Randomize if you wish to apply the current randomization. + +# gsExtendCurve +Lengthens a selected curves based on the Factor slider. + +# gsReduceCurve +Shortens the selected curves based on the Factor slider. + +# gsSmooth +Smoothes selected curves or curve CVs based on the Factor slider. +NOTE 1: At least 3 CVs should be selected for component smoothing. +NOTE 2: Holding RMB will open a marking menu where you can select a stronger smoothing algorithm. + +# mirrorX +Mirrors or Flips all the selected curves on the World X axis. + +# mirrorY +Mirrors or Flips all the selected curves on the World Y axis. + +# mirrorZ +Mirrors or Flips all the selected curves on the World Z axis. + +# gsControlCurve +Adds a Control Curve Deformer to the selected curves. Can be used to adjust groups of curves. +NOTE 1: Should NOT be used to permanently control clumps of cards. Use Bind instead. + +# gsApplyControlCurve +Applies the Control Curve Deformer. +Either the Control Curve or any controlled Curves can be selected for this to work. + +# gsCurveControlWindow +Opens a Curve Control Window. Contains all the available controls for curves. + +# gsUVEditorMain +Opens a UV editor that can be used to setup and adjust UVs on multiple cards. +NOTE 1: Lambert material with PNG, JPG/JPEG or TIF/TIFF (LZW or No Compression) texture file is recommended. TGA (24bit and no RLE) is also supported. +NOTE 2: Make sure to select the curves or the group, not the geo, to adjust the UVs. +NOTE 3: Using default Maya UV editor will break GS CurveTools Cards, Tubes and Bound Groups. +NOTE 4: Default UV editor can be used when custom geometry is used in a Bound Groups. + + + + +# gsLayerSelector +Shows the Layer of the selected curve. +Selecting different layer will change the layer of all the selected curves. + +# gsColorPicker +Layer/Card Color Picker. + +# gsCurveColorPicker +Curve Color Picker. + +# selectedObjectName +Selected object name. Editing this field will rename all the selected objects. + +# lineWidth +Controls the thickness of the selected curves. + +# gsBindAxisAuto +Automatic selection of the bind Axis (recommended). +NOTE: Change to manual X, Y, Z axis if bind operation result is not acceptable. + +# AxisFlip +Flips the direction of the bound geometry. + +# editOrigObj +Temporarily disables curve bind and shows the original objects. +Used to adjust the objects after the bind. +To add or remove from the Bound group use Unbind. + +# selectOriginalCurves +Selects the original curves that were attached to a bind curve. +Allows to edit their attributes without using Unbind or Edit Original Objects + +# twistCurveFrame +Advanced twist control graph. Allows for precise twisting of the geometry along the curve. Click to expand. + +# Magnitude +Twist multiplier. The larger the values, the more the twist. Default is 0.5. + +# gsTwistGraphResetButton +Resets the graph to the default state. + +# gsTwistGraphPopOut +Opens a larger graph in a separate window that is synced to the main graph. + +# widthLockSwitch +Links/Unlinks the X and Z width sliders. +If linked, the sliders will move as one. + +# LengthLock +Locks/Unlocks the length slider. +When Locked the geometry is stretched to the length of the curve and the slider is ignored. + +# widthCurveFrame +Advanced width control graph. Allows for precise scaling of the geometry along the curve. Click to expand. + +# gsWidthGraphResetButton +Resets the graph to the default state. + +# gsWidthGraphPopOut +Opens a larger graph in a separate window that is synced to the main graph. + +# profileCurveGraph +Advanced control over the profile of the card. Modifies the profile applied by the Profile slider. Click to expand. +Add or remove points and change them to increase or decrease the Profile value along the curve. + +# autoEqualizeSwitchOn +Locks the points horizontally to equal intervals to avoid geometry deformation. + +# autoEqualizeSwitchOff +Unlocks the points and allows for the full control. + +# equalizeCurveButton +Snaps the points to the equal horizontal intervals. + +# gsResetProfileGraphButton +Resets the curve to the default state. + +# gsProfileGraphPopOut +Opens a larger graph in a separate window that is synced to the main graph. + +# reverseNormals +Reverses the normals on the selected cards/tubes. + +# orientToNormalsFrame +Orient selected cards/tubes to the normals of the selected geo. + +# gsOrientToNormalsSelectTarget +Set selected mesh as a target for the algorithm. + +# orientRefreshViewport +Toggles the viewport update during the alignment process. +Disabling can speed up the process. + +# gsOrientToNormals +Starts the alignment process. +Will align the selected cards to the normals of the selected geometry. + +# flipUV +Flips the UVs of the card/tube horizontally. + +# resetControlSliders +Resets the range of the sliders to the default state. + +# UVFrame +Legacy controls for the UVs. Just use the new UV Editor. + +# solidifyFrame +Expands controls that control the thickness of the cards/tubes. + +# solidify +Toggles the thickness of the geometry. + + + +# lengthDivisions +Change the length divisions of the selected cards/tubes. + +# dynamicDivisions +Toggles the dynamic divisions mode. +Dynamic divisions will change the divisions of the cards/tubes based on the length of the curves. +In dynamic divisions mode, L-Div slider will control the density of the divisions, not the fixed divisions count. + +# widthDivisions +Change the width divisions of the selected cards/tubes. + +# Orientation +Change the orientation of the card/tube around the curve. + +# Twist +Smoothly twist the entire geometry card/tube. Twists the tip of the card. + +# invTwist +Smoothly twist the entire geometry card. Twists the root of the card. + +# Width +Change the width of the selected card. + +# Taper +Linearly changes the width of the card/tube along the length of the curve. + +# WidthX +Change the width of the tube along the X axis. + +# WidthZ +Change the width of the tube along the Z axis. + +# Length +Change the length of the attached geometry. Works only when Length Unlock button is checked. + +# Offset +Offset the geometry along the curve. + +# Profile +Change the profile of the card along the length of the curve uniformly. + +# profileSmoothing +Smoothing will smooth the profile transition. + +# otherFrame +Other less used options + +# curveRefine +Controls the number of "virtual" vertices on the curve. These are the vertices that are used to calculate the geometry deformation. +Zero (0) value will disable the refinement and the geometry will be attached directly to the curve. The fastest option. +Larger refine values means smoother geometry that is a closer fit to the curve. +Only increase past 20 if you need additional precision or if there are any visual glitches with the geometry. +Large refine values can cause significant performance drop, lag and other issues on smaller curve sizes. +Recommended values are: +20 for curves with less than 20 CVs. +0 (disabled) or same as the number of CVs for curves with more than 20 CVs. + +# autoRefine +Enables auto-refine for selected curves. Recommended to keep this on. +Manual refinement can be helpful if the geometry deformation is wrong or not precise enough. + +# samplingAccuracy +Increases the sampling accuracy of the deformer that attaches the geometry to a curve. +Larger values = more precise geometry fit to a curve and more lag. + +# curveSmooth +Smoothing of the geometry that is attached to a curve. + +# surfaceNormals +Changes the smoothing angle of the normals of the geometry. + +# gsIterationsSlider +Controls the number of iterations per card. + +# gsMinimumAngle +Controls the target angle difference between the normal of the mesh and the card. + +# solidifyThickness +Controls the amount of thickness on the geometry. + +# solidifyDivisions +Controls the number of divisions on the solidify extrusion. + +# solidifyScaleX +Changes the scale on the X axis. + +# solidifyScaleY +Changes the scale on the Y axis. + +# solidifyOffset +Controls the offset of the solidify extrusion. + +# solidifyNormals +Controls the smoothing angle for normals of the solidify extrusion. + +# geometryHighlight +If enabled, selecting the curve will also highlight the geometry component that is attached to that curve. +Works only on GS CurveTools curves and geo. + +# curveHighlight +If enabled, selected curves and their components will be additinally highlighted for better visibility. +The curves and components will be in X-Ray mode by default. +Colors and transparency values can be changes in the menu below. + +# gsSelectedCVColor +Selected CV highlight color + +# gsSelectedCVAlpha +Selected CV highlight transparency (alpha) + +# gsDeselectedCVColor +Deselected CV highlight color + +# gsDeselectedCVAlpha +Deselected CV highlight transparency (alpha) + +# curveVisibility +Toggle selected curves highlight + +# gsCurveHighlightColor +Selected curve highlight color + +# gsCurveHighlightAlpha +Selected curve highlight transparency (alpha) + +# hullVisibility +Toggle hull visibility. +Hull is a line that connects all the CVs on the curve. + +# gsHullHighlightColor +Hull highlight color + +# gsHullHighlightAlpha +Hull highlight transparency (alpha) + +# advancedVisibilityFrame +Better highlights for selected curves and components. + +# lazyUpdate +Enables lazy update for selected curves. +Lazy update can slightly increase the performance of the highlight, +however it has some visual drawbacks (curve highlight can fail to update when switching curves in component selection mode) + +# alwaysOnTop +Toggles X-Ray (always on top) drawing for highlighted components. +Disabling this defeats the purpose of the advanced visibility, but hey, it's your choice. + +# curveDistanceColor +Toggles the distance color effect on the curve highlight. +Distance color darkens the curve color the further it is from the camera. + +# cvDistanceColor +Toggles the distance color effect on the CV highlight. +Distance color darkens the CVs color the further it is from the camera. + +# hullDistanceColor +Toggles the distance color effect on the hull highlight. +Distance color darkens the hull color the further it is from the camera. + +# gsDistanceColorMinValue +Distance color minimum. +This value is the minimum allowed color multiplier for the Distance Color effect. +The lower this value, the darker further parts of the curve will be. +Black at 0.0 +Original color at 1.0 + +# gsDistanceColorMaxValue +Distance color maximum. +This value is the maximum allowed color multiplier for the Distance Color effect. +The higher this value, the brighter closest parts of the curve will be. +Black at 0.0 +Original color at 1.0 + +# CVocclusion +Toggles the experimental CV occlusion mode (hull is affected as well) +When the appropriate mesh name is added to Occluder Mesh input field, +this function will automatically hide CVs and hull lines that are behind this mesh (even in X-Ray mode). +Warning: enabling this mode can negatively impart viewport performance. + +# gsSelectOccluderButton +This button adds the selected mesh name to the Occluder Mesh input field. + +# gsOccluderMeshName +Type the full path for the occluder mesh here, or use the "Select Occluder" button on the left <- + + + + +# gsGenerateLayerColorGradient +Generate a color gradient for the layer colors. +Rows control the number of Rows to generate. +Left color picker sets the initial color. +Right color picker sets the final color. + +# gsRandomizeLayerColors +Generate random colors for the layers. +SatMin controls the minimum allowed saturation. +SatMax controls the maximum allowed saturation. + +# gsResetAllLayerColors +Resets all the color swatches to the default color. + +# gsGetCurrentSceneLayers +Populates the menu with the names and colors stored in the scene. + +# gsSetAsCurrentSceneLayers +Applies the names and colors from the menu to the scene. + +# gsLoadGlobalLayerPreset +Load the global names and colors preset to the menu. +NOTE: Don't forget to Set to Scene before closing the menu. + +# gsSaveGlobalLayerPreset +Saves the current names and colors from the menu to the global preset. + + + +# gsUVSelect +Enables the selection of the UVs. +Drag to draw a box selection. + +# gsUVMove +Enables the Move tool. +Move the selected UVs or move individual UVs if nothing is selected. + +# gsUVRotate +Enables the Rotation of the selected UVs. +Hold LMB and drag anywhere in the viewport to rotate the selected UVs. +Rotation pivot is the center of the individual unscaled UV. + +# gsUVScale +Enables the Scaling of the selected UVs. +Hold LMB and drag in the viewport to scale the card Horizontally of Vertically. +Repeated hotkey click will toggle between Horizontal and Vertical scaling. + +# gsUVHorizontalScale +Horizontal scaling mode selector. + +# gsUVVerticalScale +Vertical scaling mode selector. + +# gsDrawUVs +Enables the UVs Drawing Tool: +1. Select UVs using Selection Mode. +2. Enable the UV Drawing Tool. +3. Draw a UV Rectangle anywhere in the viewport to create/move the UVs there. + +# gsHorizontalFlipUV +Flips the selected UVs horizontally. +Flipped UVs have the blue circle indicator inside the root rectangle. + +# gsVerticalFlipUV +Flips the selected UVs vertically. + +# gsResetUVs +Resets the selected UVs to the default 0,1 rectangle. + +# gsSyncSelectionUVs +Syncs selection between UV editor and Maya Viewport. + +# gsRandomizeUVs +Randomize selected UV positions between already existing UV positions. +*** +Normal click will keep the overall density distribution of the UVs. +This means that if there is one card in one position and twenty cards in the other, +it will keep this distribution of 1 to 20. +*** +Shift+Click will ignore the original density distribution +and simply randomize the UVs between the original positions. + +# gsFocusUVs +Focuses on the selected UVs or on all the UVs if nothing is selected. + +# gsUVIsolateSelect +Hides all the unselected UVs and shows only the selected ones. + +# gsUVShowAll +Shows all the hidden UVs. + +# UVEditorUseTransforms +Use "Coverage" and "Translate Frame" parameters from place2dTexture node for texture. +Offset is not supported. +Diffuse and Alpha channel MUST have the same coverage and translate frame values. + +# UVEditorTransparencyToggle +Enable texture map transparency using Alpha map from Transparency plug in the material node + +# UVEditorBGColorPicker +Background Color + +# UVEditorGridColorPicker +Grid Color + +# UVEditorFrameColorPicker +Frame Color + +# UVEditorUVFrameSelectedColorPicker +Selected UV frame color + +# UVEditorUVFrameDeselectedColorPicker +Deselected UV frame color + +# UVEditorUVCardFillColorPicker +UV frame background color + + + +# gsCardToCurve_outputTypeSwitch +Controls the output of Card-to-Curve algorithm + +# gsCardToCurve_generateCards +Generate cards from selected one-sided geometry + +# gsCardToCurve_generateCurves +Generate curves from selected one-sided geometry + +# gsCardToCurve_cardType +Controls the type of generated cards + +# gsCardToCurve_warp +Generate Warp cards + +# gsCardToCurve_extrude +Generate Extrude cards + +# gsCardToCurve_matchAttributes +Controls which attributes on the new cards should be approximated from the original geometry. +NOTE: This process is not perfect and final result can be inaccurate. + +# gsCardToCurve_orientation +Match orientation attribute during the generation process + +# gsCardToCurve_width +Match orientation attribute during the generation process + +# gsCardToCurve_taper +Match taper attribute during the generation process + +# gsCardToCurve_twist +Match twist attribute during the generation process + +# gsCardToCurve_profile +Match profile attribute during the generation process + +# gsCardToCurve_material +Copy material (shader) from the original geometry + +# gsCardToCurve_UVs +Tries to approximate the UVs from the original geometry +NOTE: Matches the bounding box of the UVs. Rotated and deformed UVs are not matched precisely. + +# gsCardToCurve_UVMatchOptions +Controls UV matching behaviour + +# gsCardToCurve_verticalFlip +Vertically flip matched UVs + +# gsCardToCurve_horizontalFlip +Horizontally flip matched UVs + +# gsCardToCurve_reverseCurve +Reverse generated curve direction +Root CV should be generated near the scalp of the model. +Enable or disable if resulting card direction and taper are reversed. + +# gsCardToCurve_convertSelected +Convert selected geometry to cards or curves based on the options diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.py b/Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.py new file mode 100644 index 0000000..4ef0eaf --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/tooltips.py @@ -0,0 +1,102 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" +import io +import os + +gs_curvetools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace('\\', '/') + +def processTooltips(): + # type: () -> dict + """ Processes the tooltips markdown file and returns a dict {"name": "tooltip", ...} """ + from . import utils + filePath = os.path.join(gs_curvetools_path, 'utils', 'tooltips.md').replace('\\', '/') + finalDict = {} + with io.open(filePath, "r", encoding="utf-8") as f: + lines = f.readlines() + title = "" + tooltip = "" + commentBlock = False + for line in lines: + line = line.strip() + if not line or "": + continue + + # Exclude comments + if "" in line: + continue + if "" in line and commentBlock: + commentBlock = False + continue + if commentBlock: + continue + + # Process tooltips + if '#' in line: + if title and tooltip: + finalDict[title] = tooltip.strip() + title = line.replace("#", "").strip() + tooltip = "" + else: + tooltip += line + "\n" + # Final title/tooltip pair + if title and tooltip: + finalDict[title] = tooltip.strip() + return finalDict + + +def toggleCustomTooltipsMain(enable=True): + # Custom Tooltips that can't be automatically processed from markdown files (mostly Maya sliders) + from . import wrap + customTooltips = { + 'gsCurvesSlider': 'Selects the number of added curves.\nUsed by Add Card, Add Tube, Fill and Subdivide functions', + 'gsSelectCVSlider': 'Selects the CVs of the selected curves based on the position of the slider.\n<- Left is root of the curve and right is the tip ->\nModes:\nNormal Drag is single CV selection based on the position of the slider.\nShift+Drag is additive selection.\nAlt+Drag is subtractive selection.\nCtrl+Drag to move the slider without selection change.', + 'gsRebuildSlider': 'Interactively rebuilds the selected curves. Slider controls the target number of CVs.\nNOTE: very small scale curves can have issues with distortion. If it is the case, try using Maya curve rebuild command.', + 'gsFactorSlider': 'Adjusts the intensity of the Smooth slider and the power of Extend and Reduce functions', + } + + for widget in customTooltips: + if hasattr(wrap.WIDGETS[widget], "setToolTip") and callable(getattr(wrap.WIDGETS[widget], "setToolTip")): + if enable: + wrap.WIDGETS[widget].setToolTip(customTooltips[widget]) + else: + wrap.WIDGETS[widget].setToolTip('') + +def toggleCustomTooltipsCurveControl(enable=True): + # Custom Tooltips that can't be automatically processed from markdown files (mostly Maya sliders) for Curve Control Window + from . import wrap + customTooltips = { + 'gsPointSizeSlider': 'Controls the size of a CV point highlight', + 'gsCurveWidthSlider': 'Controls the width of the curve highlight', + 'gsHullWidthSlider': 'Controls the width of the hull highlight', + } + + for widget in customTooltips: + if hasattr(wrap.WIDGETS[widget], "setToolTip") and callable(getattr(wrap.WIDGETS[widget], "setToolTip")): + if enable: + wrap.WIDGETS[widget].setToolTip(customTooltips[widget]) + else: + wrap.WIDGETS[widget].setToolTip('') diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/utils.py b/Scripts/Modeling/Edit/gs_curvetools/utils/utils.py new file mode 100644 index 0000000..1831c45 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/utils.py @@ -0,0 +1,1426 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import cProfile +import functools +import inspect +import itertools +import logging +import math +import os +import pstats +import re +import subprocess +import sys +import traceback + +try: + from collections.abc import Iterable +except BaseException: + from collections import Iterable + +from functools import partial as pa +from functools import wraps +from imp import reload +from os import path + +import maya.api.OpenMaya as om +import maya.cmds as mc +import maya.mel as mel + +from ..constants import * + +gs_curvetools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace('\\', '/') + +### Logging ### +class Logger: + """ Create logger, log and print messages""" + + def __init__(self): + logFilePath = os.path.join(gs_curvetools_path, 'log.log').replace('\\', '/') # type: str + # Check if file already there and its size. Delete if too large. + if os.path.exists(logFilePath): + size = os.stat(logFilePath).st_size + if size >= 1024 * 1024: # 1 mb max log size + try: + os.remove(logFilePath) + except BaseException: + pass + # Create logger + self.logger = logging.getLogger("GS_CurveTools") + self.logger.setLevel(logging.DEBUG) + self.logger.propagate = False + # File handler + fileLog = logging.FileHandler(filename=logFilePath, encoding='utf-8') + fileLog.set_name("File Output") + fileLog.setLevel(logging.DEBUG) + # Console handler + consoleLog = logging.StreamHandler(stream=sys.stdout) + consoleLog.set_name("Console Output") + consoleLog.setLevel(logging.INFO) + # Formatter + fileFormatter = logging.Formatter( + fmt='[%(asctime)s,%(msecs)03d | %(levelname)5.5s | %(module)8.8s | %(lineno)5d]: %(funcName)s() : %(message)s', + datefmt='%H:%M:%S') + consoleFormatter = logging.Formatter( + fmt='[GS CurveTools|%(levelname)s]: %(message)s') + fileLog.setFormatter(fileFormatter) + consoleLog.setFormatter(consoleFormatter) + # Add handlers to the logger + self.logger.handlers = [] + self.logger.addHandler(fileLog) + self.logger.addHandler(consoleLog) + + def inView(self, message): + """ InView Message """ + self.logger.debug(message) + mc.inViewMessage(smg=str(message), pos='topCenter', f=1, fit=50, fot=50) + + def warning(self, message): + """ Print Warning Message """ + self.logger.error(message) + trace = traceback.format_stack(limit=2) + self.logger.debug(trace[0].splitlines()[0].strip()) + mc.warning(message) + + def warningInView(self, message): + """ Print Warning and InView message """ + self.logger.error(message) + trace = traceback.format_stack(limit=2) + self.logger.debug(trace[0].splitlines()[0].strip()) + mc.warning(message) + mc.inViewMessage(smg=message, pos='topCenter', f=1, fit=50, fot=50) + + def printInView(self, message): + """ Print to Script Editor and InView message """ + self.logger.info(message) + mc.inViewMessage(smg=message, pos='topCenter', f=1, fit=50, fot=50) + + def openLogFile(self): + if OS == "mac": + self.warning('Opening log file is not supported on Mac. Please open it manually from gs_curvetools folder:') + self.warning('{}'.format(path.join(gs_curvetools_path, 'log.log').replace('\\', '/'))) + return + subprocess.call('notepad "{}"'.format(path.join(gs_curvetools_path, 'log.log').replace('\\', '/')), creationflags=0x00000008) + + +logger = Logger() + +MESSAGE = logger +LOGGER = logger.logger + + +# Some version specific compatibility stuff +if sys.version_info >= (3, 0): + from io import StringIO + fixedWraps = wraps +else: + # Old python import for StringIO + from io import BytesIO as StringIO + + # Legacy wraps for python 2.7 to fix some errors + def wrapsSafely(obj, attr_names=functools.WRAPPER_ASSIGNMENTS): + return wraps(obj, assigned=itertools.ifilter(functools.partial(hasattr, obj), attr_names)) + fixedWraps = wrapsSafely + + +### Decorators ### + +def deferred(func): + """Deferred execution""" + @fixedWraps(func) + def wrapper(*args, **kwargs): + try: + mc.evalDeferred(pa(func, *args, **kwargs)) + except Exception as e: + LOGGER.exception(e) + return wrapper + + +def deferredLp(func): + """Deferred execution (lowest priority)""" + @fixedWraps(func) + def wrapper(*args, **kwargs): + try: + rv = mc.evalDeferred(pa(func, *args, **kwargs), lp=1) + except Exception as e: + LOGGER.exception(e) + return rv + return wrapper + + +def executeNext(func): + @fixedWraps(func) + def wrapper(*args, **kwargs): + try: + mc.evalDeferred(pa(func, *args, **kwargs), en=1) + except Exception as e: + LOGGER.exception(e) + return wrapper + + +def parseMelCommand(pythonCmd, *args, **_): + """ Convert python function call to MEL compatible call with all the args""" + if inspect.ismethod(pythonCmd): + instName = pythonCmd.__self__.name + else: + instName = False + module = pythonCmd.__module__ + function = pythonCmd.__name__ + arguments = "({})".format(','.join(str(x) if not isinstance(x, str) else "'{}'".format(str(x)) for x in args)) + if instName: + cmd = "{}.{}.{}{}".format(module, instName, function, arguments) + else: + cmd = "{}.{}{}".format(module, function, arguments) + melCommand = 'python("import {};{}");'.format(module, cmd) + return melCommand + + +def undo(func): + """ Safe Undo Open/Close Chunk """ + @fixedWraps(func) + def wrapper(*args, **kwargs): + funcName = str(func.__name__) + mc.undoInfo(ock=1, cn=funcName) + rv = None + try: + rv = func(*args, **kwargs) + try: + mc.repeatLast( + ac=parseMelCommand(func, *args, **kwargs), + acl=funcName + ) + except Exception as e: + LOGGER.exception(e) + except Exception as e: + LOGGER.exception(e) + finally: + mc.undoInfo(cck=1) + return rv + return wrapper + + +def noUndo(func): + """ Safe "State Without Flush" """ + @fixedWraps(func) + def wrapper(*args, **kwargs): + mc.undoInfo(swf=0) + rv = None + try: + rv = func(*args, **kwargs) + except Exception as e: + LOGGER.exception(e) + finally: + mc.undoInfo(swf=1) + return rv + return wrapper + + +### Time classes ### + + +class Timer: + """ Timer """ + t0 = 0 + + @classmethod + def increment(cls, sec): + cls.t = mc.timerX() + if cls.t >= cls.t0 + sec: + cls.t0 = cls.t + return True + else: + return False + + +### Debug ### + + +class Profiler(): + + def __init__(self): + self.pr = cProfile.Profile() + self.pr.enable() + + def end(self): + self.pr.disable() + s = StringIO() + sort = 'tottime' + ps = pstats.Stats(self.pr, stream=s).sort_stats(sort) + ps.print_stats() + print(s.getvalue()) # pylint: disable=print-statement + + +def profile(func): + """Profiles the execution speed of the function or method""" + @fixedWraps(func) + def wrapper(*args, **kwargs): + p = Profiler() + rv = func(*args, **kwargs) + p.end() + return rv + return wrapper + + +class DebugDraw: + + timer = Timer() + + @staticmethod + def returnMaterial(color): + matName = 'GS_DEBUG_Material_{}_{}_{}'.format( + str(color[0]).replace('.', ''), + str(color[1]).replace('.', ''), + str(color[2]).replace('.', '') + ) + if not mc.objExists(matName): + lambert = mc.shadingNode('lambert', asShader=1, n=matName, ss=1) + mc.setAttr(lambert + '.color', color[0], color[1], color[2]) + mc.setAttr(lambert + '.incandescence', color[0], color[1], color[2]) + mc.setAttr(lambert + '.ambientColor', color[0], color[1], color[2]) + mc.setAttr(lambert + '.transparency', 0.5, 0.5, 0.5) + lambert_set = mc.sets(r=1, nss=1, em=1, n=matName + '_set') + mc.connectAttr(lambert + '.outColor', lambert_set + '.surfaceShader') + return lambert_set + else: + lambert_set = mc.listConnections(matName + '.outColor', t='shadingEngine')[0] + return lambert_set + + @classmethod + def sceneCleanup(cls, ignoreTimer=False): + # Don't cleanup the scene if multiple objects are created at the same time + if not cls.timer.increment(5) and not ignoreTimer: + return + allNodes = mc.ls() + for node in allNodes: + if 'GS_DEBUG_' in node and mc.objExists(node) and mc.nodeType(node) == 'transform': + mc.delete(node) + + @classmethod + def setGeoStyle(cls, geo, color): + # type: (list[tuple[str, str]], tuple[float, float, float]) -> None + """ Apply debug render style to list of transforms """ + if not isinstance(geo, list) and not isinstance(geo, tuple): + geo = [geo] + for tr in geo: + mc.setAttr(tr + '.overrideEnabled', 1) + mc.setAttr(tr + '.overrideDisplayType', 2) + mc.setAttr(tr + '.overrideRGBColors', 1) + mc.setAttr(tr + '.overrideColorA', 0) + mat = cls.returnMaterial(color) + mc.sets(tr, e=1, fe=mat, nw=1) + + @classmethod + def sphere(cls, center, radius=0.1, dir=(0, 1, 0), color=(0, 1, 0), label=''): + cls.sceneCleanup() + center = (center[0], center[1], center[2]) + sphere = mc.polySphere(n="GS_DEBUG_Sphere#", ax=dir, r=radius, sx=8, sy=8, ch=0, o=1, cuv=0) + mc.move(center[0], center[1], center[2], sphere, xyz=1) + cls.setGeoStyle(sphere, color) + if label: + mc.headsUpMessage(label, o=sphere[0], time=100, vp=1) + + @classmethod + def arrow(cls, origin=(0, 0, 0), target=(1, 0, 0), size=0.1, color=(0, 1, 0), label=''): + cls.sceneCleanup() + origSel = mc.ls(sl=1) + origin = (origin[0], origin[1], origin[2]) + target = (target[0], target[1], target[2]) + cylinderTransform, cylinderCreator = mc.polyCylinder(n='GS_DEBUG_Arrow#', ch=1, ax=(0, 1, 0), r=0.1, h=1, sh=1, sc=1, sa=8) + pyramidTransform, pyramidCreator = mc.polyPyramid(ch=1, ax=(0, 1, 0), w=1, ns=4) + + mc.setAttr(pyramidTransform + '.inheritsTransform', 0) + multX = mc.createNode('multDoubleLinear') + mc.setAttr(multX + '.input2', 0.3) + mc.connectAttr(cylinderTransform + '.scaleX', multX + '.input1', f=1) + mc.connectAttr(multX + '.output', pyramidTransform + '.scaleX') + multZ = mc.createNode('multDoubleLinear') + mc.setAttr(multZ + '.input2', 0.3) + mc.connectAttr(cylinderTransform + '.scaleZ', multZ + '.input1', f=1) + mc.connectAttr(multZ + '.output', pyramidTransform + '.scaleZ') + add = mc.createNode('addDoubleLinear') + mc.connectAttr(cylinderTransform + '.scaleX', add + '.input1') + mc.connectAttr(cylinderTransform + '.scaleZ', add + '.input2') + divide = mc.createNode('multDoubleLinear') + mc.setAttr(divide + '.input2', .5) + mc.connectAttr(add + '.output', divide + '.input1') + mc.connectAttr(divide + '.output', pyramidTransform + '.scaleY') + mc.setAttr(pyramidCreator + '.heightBaseline', -1) + mc.setAttr(cylinderCreator + '.heightBaseline', -1) + mc.select(cylinderTransform + '.vtx[17]', pyramidTransform, r=1) + mc.pointOnPolyConstraint(w=1) + mc.parent(pyramidTransform, cylinderTransform) + mc.move(origin[0], origin[1], origin[2], cylinderTransform, xyz=1) + locator = mc.spaceLocator(p=(0, 0, 0)) + mc.xform(locator, cp=1, t=target, a=1) + aim = mc.aimConstraint(locator, cylinderTransform, aim=(0, 1, 0)) + dist = mc.createNode('distanceBetween') + mc.connectAttr(locator[0] + '.translate', dist + '.point1') + mc.connectAttr(cylinderTransform + '.translate', dist + '.point2') + mult = mc.createNode('multDoubleLinear') + mc.connectAttr(divide + '.output', mult + '.input1') + mc.setAttr(mult + '.input2', -0.7) + subtract = mc.createNode('addDoubleLinear') + mc.connectAttr(mult + '.output', subtract + '.input2') + mc.connectAttr(dist + '.distance', subtract + '.input1') + mc.connectAttr(subtract + '.output', cylinderTransform + '.scaleY') + mc.setAttr(cylinderTransform + ".sx", size) + mc.setAttr(cylinderTransform + ".sz", size) + cls.setGeoStyle([pyramidTransform, cylinderTransform], color) + mc.delete(aim, locator) + mc.select(cylinderTransform, r=1) + mc.select(origSel, r=1) + if label: + mc.headsUpMessage(label, o=cylinderTransform, time=100, vp=1) + + +### Misc utils ### + + +def stopUI(state=False): + from .. import core + windows = [ + core.MAIN_WINDOW_NAME, + core.CURVE_CONTROL_NAME, + core.UV_EDITOR_NAME, + core.SCALE_FACTOR_UI, + 'GSCT_RandomizeCurvePopOut', + 'GSCT_AttributesFilterPopOut', + 'GSCT_TwistGraphPopOut', + 'GSCT_WidthGraphPopOut', + 'GSCT_CurveThicknessWindow', + 'GSCT_LayerEditorWindow', + 'GSCT_CustomLayerColorsWindow', + 'GSCT_OrientToNormalsWindow', + 'GSCT_CardToCurvePopOut', + ] + + # Delete advanced visibility node + if all(mc.getClassification("GSCT_CurveTools_DrawManagerNode")): + sel = mc.ls(typ="GSCT_CurveTools_DrawManagerNode") + for n in sel: + if "GSCT_CurveTools_DrawManager" in n: + mc.delete(mc.listRelatives(n, p=1, pa=1)) + + # Delete workspaces + for window in windows: + if MAYA_VER <= 2017: + if mc.workspaceControl(window, q=1, ex=1): + mc.workspaceControl(window, e=1, floating=1) + mc.deleteUI(window) + return 0 + + if mc.workspaceControl(window, q=1, ex=1): + if mc.workspaceControl(window, q=1, vis=1): + if mc.workspaceControl(window, q=1, fl=1): + mc.workspaceControl(window, e=1, clp=False) + mc.workspaceControl(window, e=1, cl=1) + elif mc.workspaceControl(window, q=1, clp=1): + mc.workspaceControl(window, e=1, clp=False) + mc.workspaceControl(window, e=1, cl=1) + else: + mc.workspaceControl(window, e=1, cl=1) + mc.deleteUI(window) + if state: + if mc.workspaceControlState(window, q=1, ex=1): + LOGGER.info("Deleting window state for %s" % window) + mc.workspaceControlState(window, r=1) + + +@deferred +def resetUI(): + resetOptionVars() + if MAYA_VER >= 2018: + stopUI(True) + + # Reload all files + from importlib import import_module + files = [ + '.constants', + '.core', + '.main', + '.ui', + '.uv_editor', + '.utils.gs_math', + '.utils.style', + '.utils.tooltips', + '.utils.utils', + '.utils.wrap', + ] + modules = {} + for file in files: + modules[file] = import_module(file, 'gs_curvetools') + for module in modules: + reload(modules[module]) + + # Run main function + if '.main' in modules: + modules['.main'].main() + else: + try: + from .. import main + main.main() + except ImportError as e: + LOGGER.exception(e) + raise ImportError('Main not found!') + + +def resetOptionVars(): + mc.optionVar(fv=["GSCT_globalScaleFactor", 1]) + mc.optionVar(fv=["GSCT_globalCurveThickness", -1]) + mc.optionVar(iv=['GSCT_warpSwitch', 1]) + mc.optionVar(iv=["GSCT_syncCurveColor", 0]) + mc.optionVar(iv=['GSCT_colorOnlyDiffuse', 1]) + mc.optionVar(iv=["GSCT_colorizedRegroup", 0]) + mc.optionVar(iv=["GSCT_checkerPattern", 0]) + mc.optionVar(iv=["GSCT_ignoreLastLayer", 1]) + mc.optionVar(iv=["GSCT_ignoreTemplateCollections", 1]) + mc.optionVar(iv=["GSCT_groupTemplateCollections", 1]) + mc.optionVar(iv=["GSCT_syncOutlinerLayerVis", 1]) + mc.optionVar(iv=["GSCT_keepCurveAttributes", 1]) + mc.optionVar(iv=["GSCT_massBindOption", 0]) + mc.optionVar(iv=["GSCT_boundCurvesFollowParent", 1]) + mc.optionVar(iv=["GSCT_bindDuplicatesCurves", 0]) + mc.optionVar(iv=["GSCT_bindFlipUVs", 1]) + mc.optionVar(iv=["GSCT_populateBlendAttributes", 1]) + mc.optionVar(iv=["GSCT_convertInstances", 1]) + mc.optionVar(iv=["GSCT_replacingCurveLayerSelection", 1]) + mc.optionVar(iv=["GSCT_flipUVsAfterMirror", 1]) + mc.optionVar(iv=["GSCT_useAutoRefineOnNewCurves", 1]) + mc.optionVar(iv=["GSCT_layerNumbersOnly", 0]) + mc.optionVar(iv=['GSCT_2layerRows', 1]) + mc.optionVar(iv=['GSCT_3layerRows', 0]) + mc.optionVar(iv=['GSCT_4layerRows', 0]) + mc.optionVar(iv=['GSCT_6layerRows', 0]) + mc.optionVar(iv=['GSCT_8layerRows', 0]) + mc.optionVar(iv=['GSCT_UVBugMessageDismissed', 0]) + mc.optionVar(iv=['GSCT_UVEditorTransparencyToggle', 0]) + mc.optionVar(iv=['GSCT_UVEditorAlphaOnly', 0]) + mc.optionVar(iv=['GSCT_enableTooltips', 1]) + mc.optionVar(iv=['GSCT_showLayerCollectionsMenu', 1]) + mc.optionVar(iv=['GSCT_importIntoANewCollection', 0]) + mc.optionVar(iv=['GSCT_AutoHideCurvesOnInactiveCollections', 0]) + mc.optionVar(sv=['GSCT_UVEditorBGColor', "(36, 36, 36)"]) + mc.optionVar(sv=['GSCT_UVEditorGridColor', "(50, 50, 50)"]) + mc.optionVar(sv=['GSCT_UVEditorFrameColor', "(160, 75, 75)"]) + mc.optionVar(sv=['GSCT_UVEditorUVFrameSelectedColor', "(255, 255, 255)"]) + mc.optionVar(sv=['GSCT_UVEditorUVFrameDeselectedColor', "(128, 128, 128)"]) + mc.optionVar(sv=['GSCT_UVEditorUVCardFillColor', "(96, 100, 160)"]) + mc.optionVar(sv=['GSCT_AttributesFilter', "{'Orientation': False}"]) + mc.optionVar(iv=['GSCT_CardToCurveOutputType', 0]) + mc.optionVar(iv=['GSCT_CardToCurveCardType', 0]) + mc.optionVar(iv=['GSCT_GeometryHighlightEnabled', 0]) + mc.optionVar( + sv=['GSCT_CardToCurveOptions', + "{'gsCardToCurve_horizontalFlip': False, 'gsCardToCurve_verticalFlip': False}"] + ) + # Advanced visibility options + mc.optionVar(fv=['GSCT_' + 'gsPointSizeSlider', 10.5]) + mc.optionVar(fv=['GSCT_' + 'gsCurveWidthSlider', 4.0]) + mc.optionVar(fv=['GSCT_' + 'gsHullWidthSlider', 3.0]) + mc.optionVar(sv=['GSCT_' + 'gsDeselectedCVColor', "[1, 0, 0]"]) + mc.optionVar(fv=['GSCT_' + 'gsDeselectedCVAlpha', 1.0]) + mc.optionVar(sv=['GSCT_' + 'gsSelectedCVColor', "[0, 1, 0]"]) + mc.optionVar(fv=['GSCT_' + 'gsSelectedCVAlpha', 1.0]) + mc.optionVar(sv=['GSCT_' + 'gsCurveHighlightColor', "[0, 0, 1]"]) + mc.optionVar(fv=['GSCT_' + 'gsCurveHighlightAlpha', 1.0]) + mc.optionVar(sv=['GSCT_' + 'gsHullHighlightColor', "[0.5, 0, 0.5]"]) + mc.optionVar(fv=['GSCT_' + 'gsHullHighlightAlpha', 1.0]) + mc.optionVar(iv=['GSCT_' + 'gsCurveVisibilityToggle', 1]) + mc.optionVar(iv=['GSCT_' + 'gsHullVisibilityToggle', 0]) + + mc.optionVar(iv=['GSCT_' + 'gsLazyUpdateToggle', 0]) + mc.optionVar(iv=['GSCT_' + 'gsAlwaysOnTopToggle', 1]) + + mc.optionVar(iv=['GSCT_' + 'gsCVDistanceColor', 1]) + mc.optionVar(iv=['GSCT_' + 'gsHullDistanceColor', 1]) + mc.optionVar(iv=['GSCT_' + 'gsCurveDistanceColor', 1]) + mc.optionVar(fv=['GSCT_' + 'gsDistanceColorMinValue', 0.25]) + mc.optionVar(fv=['GSCT_' + 'gsDistanceColorMaxValue', 1.0]) + + mc.optionVar(iv=['GSCT_' + 'gsEnableCVOcclusion', 0]) + mc.optionVar(sv=['GSCT_' + 'gsOccluderMeshName', ""]) + + +def fixMaya2020UVs(): + success = 0 + total = 0 + for i in range(80): + try: + group = 'curveGrp_%s_Geo' % i + if mc.objExists(group): + geometry = mc.editDisplayLayerMembers(group, q=1, fn=1, nr=1) + for geo in geometry: + total += 1 + allUVNodes = list() + history = mc.listHistory(geo, il=0) + for node in history: + if mc.nodeType(node) == 'polyMoveUV': + allUVNodes.append(node) + for node in allUVNodes: + mc.setAttr(node + '.inputComponents', 1, 'map[*]', type='componentList') + success += 1 + except BaseException: + pass + additionalMessage = '' + if success != total: + additionalMessage = ' Some cards were not fixed!' + MESSAGE.printInView('Fixed %s/%s Cards.%s' % (success, total, additionalMessage)) + + +def fixMaya2020Twist(): + dialog = mc.confirmDialog( + title='Fix Maya 2020.4 Twist', + message='This command will fix Maya 2020.4 Twist Attribute Bug\n\ +Only use it if you have issues with Twist and Inv.Twist Attributes\n\n\ +Proceed?', + button=['Yes', 'No'], + defaultButton='Yes', + cancelButton='No', + dismissString='No', + icon='information' + ) + if dialog == 'No': + return + success = 0 + total = 0 + for i in range(80): + try: + group = 'curveGrp_%s_Curve' % i + if mc.objExists(group): + curve = mc.editDisplayLayerMembers(group, q=1, fn=1, nr=1) + for crv in curve: + if mc.attributeQuery('invTwist', n=crv, ex=1): + total += 1 + twist = mc.listConnections(crv + '.invTwist', d=1, scn=1) + twistHandle = mc.listConnections(twist[0] + '.deformerData', s=1, scn=1) + twistHandleShape = mc.listRelatives(twistHandle[0], c=1, pa=1) + connectionCheck = mc.isConnected(twist[0] + '.startAngle', twistHandleShape[0] + '.startAngle') + if not connectionCheck: + mc.connectAttr(twist[0] + '.startAngle', twistHandleShape[0] + '.startAngle', f=1) + mc.connectAttr(twist[0] + '.endAngle', twistHandleShape[0] + '.endAngle', f=1) + success += 1 + except BaseException: + pass + MESSAGE.printInView('Fixed %s/%s Cards.' % (success, total)) + + +def fixMaya2020Unbind(): + dialog = mc.confirmDialog( + title='Fix Maya 2020.4 Unbind', + message='This command will fix Maya 2020.4 Unbind Function Bug\n\ +Only use it if you have issues with Unbind Function\n\n\ +Proceed?', + button=['Yes', 'No'], + defaultButton='Yes', + cancelButton='No', + dismissString='No', + icon='information' + ) + if dialog == 'No': + return + + success = 0 + total = 0 + for i in range(80): + try: + group = 'curveGrp_%s_Curve' % i + if mc.objExists(group): + curve = mc.editDisplayLayerMembers(group, q=1, fn=1, nr=1) + for crv in curve: + if mc.attributeQuery('profileMagnitude', n=crv, ex=1): + total += 1 + ffd = mc.listConnections(crv + '.profileMagnitude', s=0, d=1) + origGeo = mc.listConnections(ffd[0] + '.originalGeometry[0]', s=1, d=0, p=1) + if origGeo: + mc.disconnectAttr(origGeo[0], ffd[0] + '.originalGeometry[0]') + success += 1 + except BaseException: + pass + MESSAGE.printInView('Fixed %s/%s Cards.' % (success, total)) + + +class GetFolder: + """ Get various folders from Maya """ + + def scripts(self): + return mc.internalVar(usd=1) + + def root(self): + return path.join(gs_curvetools_path, '') + + def fonts(self): + return path.join(gs_curvetools_path, 'fonts', '') + + def icons(self): + return path.join(gs_curvetools_path, 'icons', '') + + def plugins(self): + return path.join(gs_curvetools_path, 'plugins', '') + + +getFolder = GetFolder() + + +def attrExists(obj, attr): + """ Check if attribute exists """ + if mc.objExists(obj): + if mc.attributeQuery(attr, n=obj, ex=1): + return 1 + else: + return 0 + else: + return 0 + + +def getAttr(obj, attr): + """ Check if obj exist, check if attr exist, return attr """ + if mc.objExists(obj): + if mc.attributeQuery(attr, n=obj, ex=1): + return mc.getAttr(obj + '.' + attr) + else: + return None + else: + return None + + +class ProgressBar(): + """Progress bar for Maya UI""" + + def __init__(self, name, maxValue): + self.mainProgressBar = mel.eval('$tmp = $gMainProgressBar') + self.end() + mc.progressBar(self.mainProgressBar, edit=True, beginProgress=True, isInterruptable=True, + status=name, minValue=0, maxValue=maxValue) + + def tick(self, step): + """ Returns "True" if ESC is pressed """ + if mc.progressBar(self.mainProgressBar, query=True, isCancelled=True): + self.end() + MESSAGE.warning('Function is Cancelled!') + return True + mc.progressBar(self.mainProgressBar, edit=True, step=step) + return False + + def end(self): + mc.progressBar(self.mainProgressBar, edit=True, endProgress=True) + + +def objectExists(obj): + """ Check if object exists """ + if obj and len(obj): + return mc.objExists(obj[0]) + else: + return 0 + + +def addAtIndex(addTo, index, add): + """ Adds to source list at index. If list is empty, appends. """ + if len(addTo) <= index: + addTo.append(add) + else: + addTo[index] = add + + +def getClosestPointAndNormal(targetMesh, pointPos=(0, 0, 0)): + """ + (targetMesh, givenPointPosition) -> (MPoint, MVector, int) + + Returns a tuple containing the closest point on the mesh to the given point, + the normal at that point, and the ID of the face in which that point lies. + """ + pointPos = om.MPoint(pointPos) + sel = om.MSelectionList() + sel.add(targetMesh) + fnMesh = om.MFnMesh(sel.getDagPath(0)) + + pointAndNormal = fnMesh.getClosestPointAndNormal(pointPos, space=om.MSpace.kWorld) + return pointAndNormal + + +def getClosestVertexAndNormal(targetMesh, pointPos=(0, 0, 0)): + # type: (str, om.MVector | om.MPoint) -> tuple[om.MPoint, float, str, int, om.MVector] + """ + returns: (vertPos, vertDist, vertName, vertIndex, faceNormal) + + Returns a tuple containing the closest to the pointPos vert on the mesh, the distance + between this vert and pointPos, vertex name and vertex index + """ + pos = om.MPoint(pointPos) + sel = om.MSelectionList() + sel.add(targetMesh) + fn_mesh = om.MFnMesh(sel.getDagPath(0)) + + index = fn_mesh.getClosestPoint(pos, space=om.MSpace.kWorld)[1] + faceNormal = fn_mesh.getPolygonNormal(index, space=om.MSpace.kWorld) + faceVerts = fn_mesh.getPolygonVertices(index) + + vertexDistances = ((vertex, fn_mesh.getPoint(vertex, om.MSpace.kWorld).distanceTo(pos)) + for vertex in faceVerts) + vertIndex, vertDistance = min(vertexDistances, key=lambda t: t[1]) + vertPosition = fn_mesh.getPoint(vertIndex, om.MSpace.kWorld) + vertName = "{}.vtx[{}]".format(targetMesh, vertIndex) + + return (vertPosition, vertDistance, vertName, vertIndex, faceNormal) + + +def resetAttributes(inputCurve, inputGeo): + # type: (str, str) -> None + """ + Resets attributes to a default state suitable for mirroring and orientation adjustment + """ + from .. import core + + if mc.attributeQuery('lengthDivisions', n=inputCurve, ex=1): + mc.setAttr(inputCurve + '.lengthDivisions', 10) + try: + mc.setAttr(inputCurve + '.widthDivisions', 3) + except BaseException: + mc.setAttr(inputCurve + '.widthDivisions', 5) + if mc.attributeQuery('Profile', n=inputCurve, ex=1): + mc.setAttr(inputCurve + '.Profile', 0) + core.updateLattice("0, 0.5, 0.333, 0.5, 0.667, 0.5, 1, 0.5", inputCurve) + if mc.attributeQuery('Twist', n=inputCurve, ex=1): + mc.setAttr(inputCurve + '.Twist', 0) + # if mc.attributeQuery('invTwist', n=inputCurve, ex=1): + # mc.setAttr(inputCurve + '.invTwist', 0) + if mc.attributeQuery('Length', n=inputCurve, ex=1): + targetNode = mc.ls(mc.listHistory(inputGeo, ac=1, il=0), typ='curveWarp')[0] + core.attributes.resetMultiInst(targetNode, 'scaleCurve') + core.attributes.resetMultiInst(targetNode, 'twistCurve') + + +def resetAndReturnAttrs(curve): + # type: (str) -> tuple[str, om.MVector, int, int, float, float, list[list[str]]] + """ + Resets the attributes on a curve and returns original values as a list: + + returns [curve, origVec, lengthDivisions, widthDivisions, twist, invTwist, graphs] + """ + from .. import core + crvAttr = core.attributes.getAttr(curve) + lengthDivisions = crvAttr['lengthDivisions'] if 'lengthDivisions' in crvAttr else None + widthDivisions = crvAttr['widthDivisions'] if 'widthDivisions' in crvAttr else None + twist = crvAttr['Twist'] if 'Twist' in crvAttr else None + invTwist = crvAttr['invTwist'] if 'invTwist' in crvAttr else None + + if lengthDivisions and lengthDivisions > 10: + mc.setAttr(curve + '.lengthDivisions', 10) + if widthDivisions and widthDivisions > 2: + try: + mc.setAttr(curve + '.widthDivisions', 2) + except BaseException: + mc.setAttr(curve + '.widthDivisions', 4) + if twist and twist != 0: + mc.setAttr(curve + '.Twist', 0) + if invTwist and invTwist != 0: + mc.setAttr(curve + '.invTwist', 0) + + firstCv = '%s.cv[0]' % curve + pos = mc.pointPosition(firstCv) + geo = core.selectPart(2, True, curve)[0] + + graphs = None + if mc.attributeQuery('Length', n=curve, ex=1): + targetNode = mc.ls(mc.listHistory(geo, ac=1, il=0), typ='curveWarp')[0] + graphs = core.attributes.getMultiInst(curve) + core.attributes.resetMultiInst(targetNode, 'scaleCurve') + core.attributes.resetMultiInst(targetNode, 'twistCurve') + + origVec = getClosestPointAndNormal(geo, pos)[1] + return [curve, origVec, lengthDivisions, widthDivisions, twist, invTwist, graphs] + + +def getMiddleVertAndNormal(inputCurve, inputGeo, inputFirstCv): + # type: (str, str, om.MVector) -> tuple[om.MVector, om.MVector] + """Returns the middle profile vert of the card and normals of the flat card""" + + from .. import core + + # Getting original values + origAttrs = core.attributes.getAttr(inputCurve) + origGraphs = core.attributes.getMultiInst(inputCurve) + origLattice = core.getLatticeValues(inputCurve) + + # Reset attributes + resetAttributes(inputCurve, inputGeo) + + # Set a new temporary values + if mc.attributeQuery('Profile', n=inputCurve, ex=1): + mc.setAttr(inputCurve + ".Profile", 0) + if origLattice: + core.updateLattice("0, 0.5, 0.333, 0.5, 0.667, 0.5, 1, 0.5", inputCurve) + mc.setAttr(inputCurve + '.lengthDivisions', 10) + if mc.attributeQuery('Width', n=inputCurve, ex=1): + mc.setAttr(inputCurve + '.widthDivisions', 3) + else: + mc.setAttr(inputCurve + '.widthDivisions', 5) + + # Get closest vert and normal value + closestVertAndNormal = getClosestVertexAndNormal(inputGeo, inputFirstCv) + + # Get a position value for the closest vert + if mc.attributeQuery('Profile', n=inputCurve, ex=1): + profileScale = 2 * mc.getAttr(inputCurve + '.Width') + if mc.attributeQuery('scaleFactor', n=inputCurve, ex=1): + profileScale *= mc.getAttr(inputCurve + '.scaleFactor') + mc.setAttr(inputCurve + '.Profile', math.copysign(profileScale, origAttrs['Profile'])) + vertexPos = mc.pointPosition("{}.vtx[{}]".format(inputGeo, closestVertAndNormal[3])) + + # Set the original attributes back + core.attributes.setAttr(inputCurve, origAttrs) + if origGraphs: + for graph in origGraphs: + core.attributes.setMultiInst(inputCurve, graph) + if origLattice: + core.updateLattice(fromDouble2ToString(origLattice), inputCurve) + + return (om.MVector(vertexPos), closestVertAndNormal[4]) + + +def getUnitMult(): + mult = float(mc.convertUnit("1cm", fromUnit="cm", toUnit=mc.currentUnit(q=1, linear=1))) + print("Current Mult:", mult) + return mult + + +def polySelectSp(comps, obj=None): + # type: (list[str], str) -> list[str] + """polySelectSp wrapper""" + if MAYA_VER >= 2023: + return mc.ls(mc.polySelectSp(comps, q=1, loop=1), fl=1) + else: + return selectVertLoop(obj, comps) + + +def selectVertLoop(obj, comps): + # type: (str, list[str]) -> list[str] + """ + Returns vert loop list between two verts on the border + For older Maya versions that had polySelectSp fail do to that + """ + if len(comps) != 2: + raise Exception("Wrong number of input verts") + + allFaces = "{}.f[*]".format(obj) + + def expandToEdges(verts): + if MAYA_VER == 2018: + if isinstance(verts, set): + verts = list(verts) + return set(mc.ls(mc.polyListComponentConversion(verts, fv=1, te=1), fl=1)) + + def expandToVerts(edges): + if MAYA_VER == 2018: + if isinstance(edges, set): + edges = list(edges) + return set(mc.ls(mc.polyListComponentConversion(edges, fe=1, tv=1), fl=1)) + + perimeterEdges = set(mc.ls(mc.polyListComponentConversion(allFaces, ff=1, te=1, bo=1))) + if MAYA_VER == 2018: + perimeterEdges = list(perimeterEdges) + perimeterVerts = set(mc.ls(mc.polyListComponentConversion(perimeterEdges, fe=1, tv=1), fl=1)) + sourceVert = comps[0] # type: str + targetVert = comps[1] # type: str + pathOne = set() + pathTwo = set() + + # Initial expand to find two possible paths + initVerts = expandToVerts(expandToEdges(sourceVert)) + initVerts.remove(sourceVert) + pathOne.add(initVerts.pop()) + pathTwo.add(initVerts.pop()) + + # Check first path + limit = 1000 + count = 0 + currentVert_one = pathOne.copy().pop() + pathOne.add(sourceVert) + currentVert_two = pathTwo.copy().pop() + pathTwo.add(sourceVert) + while count < limit: + count += 1 + # Expand first path + toEdges_one = expandToEdges(currentVert_one) + toVerts_one = expandToVerts(toEdges_one) + nonPerimeterVerts_one = toVerts_one.intersection(perimeterVerts) # Filter out non-perimeter verts + nextVert_one = nonPerimeterVerts_one.difference(pathOne) + pathOne.update(nextVert_one) + currentVert_one = nextVert_one.pop() + if targetVert in pathOne: + return list(pathOne) + # Expand second path + toEdges_two = expandToEdges(currentVert_two) + toVerts_two = expandToVerts(toEdges_two) + nonPerimeterVerts_two = toVerts_two.intersection(perimeterVerts) # Filter out non-perimeter verts + nextVert_two = nonPerimeterVerts_two.difference(pathTwo) + pathTwo.update(nextVert_two) + currentVert_two = nextVert_two.pop() + if targetVert in pathTwo: + return list(pathTwo) + return None + + +def deleteKeysOnAllObjects(): + """Deletes all the keys on all nurbs curve in the scene""" + for obj in mc.ls(typ='nurbsCurve'): + deleteKeys(obj) + + +def deleteKeys(target): + # type: (list[str]|str) -> None + """Deletes keys from target curves and their CVs""" + mc.cutKey(target, cl=1, t=(), hi='both', cp=1, s=1) + + +def colorFrom255to1(clr): + if isinstance(clr, Iterable): + clr = list(clr) + for i in range(len(clr)): + if isinstance(clr[i], float) or isinstance(clr[i], int): + clr[i] = clr[i] / 255.0 + if clr: + return clr + else: + MESSAGE.warning("Invalid color conversion") + elif isinstance(clr, float) or isinstance(clr, int): + return clr / 255.0 + else: + MESSAGE.warning("Invalid color conversion") + + +def colorFrom1to255(clr): + if isinstance(clr, Iterable): + clr = list(clr) + for i in range(len(clr)): + if isinstance(clr[i], float) or isinstance(clr[i], int): + clr[i] = clr[i] * 255.0 + if clr: + return clr + else: + MESSAGE.warning("Invalid color conversion") + elif isinstance(clr, float) or isinstance(clr, int): + return clr * 255.0 + else: + MESSAGE.warning("Invalid color conversion") + + +def fromStringToDouble2(inputString): + splitPoints = inputString.split(',') + splitPoints = filter(None, splitPoints) + splitPoints = [float(point) for point in splitPoints] + arrangedValues = [[splitPoints[i], splitPoints[i + 1]] for i in range(0, len(splitPoints), 2)] + return arrangedValues + + +def fromDouble2ToString(inputList): + newString = '' + for i in range(len(inputList)): + newString += '%s,%s,' % (inputList[i][0], + inputList[i][1]) + return newString + + +def setDouble2Attr(node, attr, values): + for i in range(len(values)): + mc.setAttr('%s.%s[%s]' % (node, attr, i), values[i][0], values[i][1], typ='double2') + + +def convertInstanceToObj(objects=list()): + """ Accepts a list and converts any found instances to objects """ + convert = mc.menuItem('gsConvertInstances', q=1, rb=1) + finalList = list() + if not objects: + return finalList + for obj in objects: + if mc.attributeQuery('Orientation', n=obj, ex=1) and mc.connectionInfo(obj + '.Orientation', isSource=1): + LOGGER.info(obj + ' is skipped. It is a part of existing curveCard/Tube.') + continue + par = mc.listRelatives(mc.listRelatives(obj, pa=1, c=1)[0], pa=1, ap=1) + if len(par) > 1 and convert: + dup = mc.duplicate(obj) + mc.delete(obj) + mc.rename(dup[0], obj) + LOGGER.info(obj + ' is an instance. Converted to object.') + finalList.append(obj) + elif len(par) > 1 and not convert: + LOGGER.info(obj + ' is skipped. It is an instance.') + else: + finalList.append(obj) + mc.evalDeferred("print(' ')") + return finalList + + +def mergeDicts(dict1, dict2, rd=False): + """ Merge dictionaries and keep values of common keys in list. rd - remove duplicates """ + dict3 = dict() + dict3.update(dict1) + dict3.update(dict2) + for key, value in dict3.items(): + if key in dict1 and key in dict2: + if isinstance(dict1[key], list) and isinstance(dict2[key], list): + for geo in dict1[key]: + dict3[key].append(geo) + elif isinstance(dict1[key], list) and not isinstance(dict2[key], list): + dict3[key] = [dict3[key]] + dict1[key] + elif not isinstance(dict1[key], list) and isinstance(dict2[key], list): + dict3[key].append(dict1[key]) + else: + dict3[key] = [value, dict1[key]] + if rd: + for key in dict3: + if isinstance(dict3[key], list): + tmp = dict.fromkeys(dict3[key]) + dict3[key] = list(tmp) + return dict3 + + +def cleanDict(dict0): + """Removes objects from the dict if they were not found in the scene""" + for key in dict0: + tempList = list() + for geo in dict0[key]: + if mc.objExists(geo): + tempList = tempList + [geo] + dict0[key] = tempList + return dict0 + + +def getShader(inputGeo): + """ Returns a dict of shadingEngines from list of geo in format {shader: [geo1,geo2...]} """ + + inputGeo = mc.filterExpand(inputGeo, sm=12) + if not inputGeo: + return {} + + shaderDict = dict() + for geo in inputGeo: + dag = mc.ls(geo, dag=1, s=1) + shader = mc.listConnections(dag, d=1, s=1, t='shadingEngine')[0] + shaderDict.setdefault(shader, []) + shaderDict[shader].append(geo) + + return shaderDict + + +def connectMessage(source, target, message): + """Creates messages in both nodes based on message name and connects them source->target\n + Target node will be a multi-instance message""" + if source == target: + return + if not mc.attributeQuery(message, n=source, ex=1): + mc.addAttr(source, ln=message, at='message', k=0) + if mc.attributeQuery(message, n=target, ex=1) and not mc.attributeQuery(message, n=target, m=1): + mc.deleteAttr('{}.{}'.format(target, message)) + if not mc.attributeQuery(message, n=target, ex=1): + mc.addAttr(target, ln=message, at='message', k=0, m=1, im=0) + mc.connectAttr('{}.{}'.format(source, message), '{}.{}'.format(target, message), na=1) + + +def getMod(): + """ Get modifiers (Shift, Alt, Ctrl and '+' combinations) """ + mods = mc.getModifiers() + if (mods & 1) > 0 and (mods & 4) > 0 and (mods & 8) > 0: + return 'Shift+Ctrl+Alt' + if (mods & 1) > 0 and (mods & 4) > 0: + return 'Shift+Ctrl' + if (mods & 1) > 0 and (mods & 8) > 0: + return 'Shift+Alt' + if (mods & 4) > 0 and (mods & 8) > 0: + return 'Ctrl+Alt' + if (mods & 1) > 0: + return 'Shift' + if (mods & 4) > 0: + return 'Ctrl' + if (mods & 8) > 0: + return 'Alt' + + +def getHotkeyMod(hk, key): + # type: (int, str) -> bool + """Gets key modifiers and checks if hotkey was used""" + return True if hk == 2 or (hk is None and getMod() == key) else False + + +def fixDuplicateNames(nodes=None): + """ Fixes duplicate names conflicts in input nodes or all nodes in the scene """ + if not nodes: + nodes = mc.ls() + duplicates = [node for node in nodes if '|' in node] + duplicates.sort(key=lambda obj: obj.count('|'), reverse=True) + newNames = [] + if not duplicates: + return [] + for obj in duplicates: + regEx1 = re.compile("[^|]*$").search(obj) + shortName = regEx1.group(0) + regEx2 = re.compile(".*[^0-9]").match(shortName) + if regEx2: + stripSuffix = regEx2.group(0) + else: + stripSuffix = shortName + newName = mc.rename(obj, (stripSuffix + "#")) + newNames.append(newName) + return newNames + + +def checkIfBezier(inputCurve): + if mc.nodeType(mc.listRelatives(inputCurve, c=1, pa=1)) == "bezierCurve": + origSel = mc.ls(sl=1) + mc.select(inputCurve, r=1) + result = mc.bezierCurveToNurbs() + origSel.append(result[0]) + if result: + mc.rebuildCurve(result, kr=2, kcp=1, fr=1) + mc.select(origSel, r=1) + return result + return inputCurve + + +def convertBezierToNurbs(): + sel = mc.ls(sl=1, dag=1, typ="bezierCurve") + for crv in sel: + oldConnections = mc.listConnections(crv, c=1, p=1, scn=1) + try: + mc.disconnectAttr(oldConnections[0], oldConnections[1]) + mc.select(crv, r=1) + result = mc.bezierCurveToNurbs() + mc.connectAttr(oldConnections[0], oldConnections[1]) + if result: + mc.rebuildCurve(result, kr=2, kcp=1, fr=1) + except Exception as e: + LOGGER.exception(e) + LOGGER.info("Curve %s was not converted" % crv) + + +@undo +def resetSingleGraph(graph, target=None): + sel = target if target else mc.filterExpand(mc.ls(sl=1, tr=1), sm=9) + if not sel: + sel = mc.filterExpand(mc.listRelatives(mc.ls(sl=1, o=1), p=1, pa=1), sm=9) + if not sel: + return + for curve in sel: + if mc.attributeQuery('Length', n=curve, ex=1) and mc.connectionInfo(curve + '.Length', isSource=1): + warp = mc.listConnections(curve + '.Length') + if warp: + from .. import core + from .wrap import WIDGETS + if graph == 'scale' or graph == 'width': + core.attributes.resetMultiInst(warp[0], 'scaleCurve') + WIDGETS['scaleCurve'].resetGraph() + if graph == 'twist': + core.attributes.resetMultiInst(warp[0], 'twistCurve') + WIDGETS['twistCurve'].resetGraph() + + +def fixBrokenGraphs(): + sel = mc.filterExpand(mc.ls(typ='nurbsCurve'), sm=9) + for curve in sel: + if mc.attributeQuery('Length', n=curve, ex=1) and mc.connectionInfo(curve + '.Length', isSource=1): + warp = mc.listConnections(curve + '.Length') + if warp: + from .. import core + correctScale = None + correctTwist = None + if mc.attributeQuery('scaleCurve', n=curve, ex=1) and mc.attributeQuery('scaleCurve', n=warp[0], ex=1): + correctScale = mc.getAttr(curve + '.scaleCurve') + if mc.attributeQuery('twistCurve', n=curve, ex=1) and mc.attributeQuery('twistCurve', n=warp[0], ex=1): + correctTwist = mc.getAttr(curve + '.twistCurve') + if correctScale: + core.attributes.resetMultiInst(warp[0], 'scaleCurve') + setDouble2Attr(warp[0], 'scaleCurve', fromStringToDouble2(correctScale)) + if correctTwist: + core.attributes.resetMultiInst(warp[0], 'twistCurve') + setDouble2Attr(warp[0], 'twistCurve', fromStringToDouble2(correctTwist)) + + +@undo +def convertToNewLayerSystem(): + """Converts from the old layer system to the new one""" + layers = mc.ls(typ='displayLayer') + from .. import core + if mc.objExists(core.toggleColor.STORAGE_NODE): + mc.delete(core.toggleColor.STORAGE_NODE) + for layer in layers: + if 'curveGrp_' in layer: + connection = mc.listConnections(layer + '.identification', s=1, et=1, t='displayLayerManager', p=1) + if connection: + connectionOut = mc.listConnections(layer + '.drawInfo', d=1, scn=1, et=1, t='transform', p=1) + if connectionOut: + newNode = mc.createNode('displayLayer') + mc.copyAttr(layer, newNode, ic=0, oc=1, v=1, at=['drawInfo']) + mc.delete(layer) + mc.rename(newNode, layer) + + +def createNewDisplayLayer(name=None, objects=None): + # type: (str, list[str]) -> str + """ + Creates a new display layer independent from Maya display layer system. + + name: The name of the Layer + objects: The list of objects to add to the Layer + + returns: Name of the new layer node + """ + newLayer = mc.createNode('displayLayer') + if name: + newLayer = mc.rename(newLayer, name) + if objects: + mc.editDisplayLayerMembers(name, objects, nr=1) + return newLayer + + +def getFormattedLayerNames(collectionID, layerID): + # type: (int, int) -> tuple[str, str, str] + """Gets layer names from the current collection id and layer id""" + id = '' + if collectionID: + id = '%s_' % collectionID + grpCurve = 'curveGrp_%s%s_Curve' % (id, layerID) + grpGeo = 'curveGrp_%s%s_Geo' % (id, layerID) + grpInst = 'curveGrp_%s%s_Inst' % (id, layerID) + return (grpCurve, grpGeo, grpInst) + + +def getFormattedCollectionByID(id): + # type: (int) -> str + """Returns a formatted collection ID, suitable for string concat""" + collection = '' + if int(id) > 0: + collection = '%s_' % id + return collection + + +def getCollectionsSet(): + # type: () -> set + """Returns active collections indexes in a set""" + allLayers = mc.ls(typ='displayLayer') + collections = set() + for layer in allLayers: + if 'curveGrp_' in layer and ('_Geo' in layer or '_Curve' in layer or '_Inst' in layer): + c = layer.split('_') + if len(c) == 4: + collections.add(c[1]) + return collections + + +def AOToggle(): + if (mc.getAttr('hardwareRenderingGlobals.ssaoEnable')): + mc.setAttr('hardwareRenderingGlobals.ssaoEnable', 0) + else: + mc.setAttr('hardwareRenderingGlobals.ssaoEnable', 1) + + +def disableEcho(): + """ Disable Echo All Commands """ + if mc.commandEcho(q=1, st=1) and not mc.optionVar(q='gsEchoAllCommands'): + mc.commandEcho(st=0) + LOGGER.info('Echo All Commands function is disabled for performance reasons.') + LOGGER.info('To disable this functionality, set internalVariable "gsEchoAllCommands" to 1') + LOGGER.info('Example(MEL): optionVar -iv "gsEchoAllCommands" 1') + + +def checkNativePlugins(plugInList, myPluginName): + """Checks if all the required NATIVE maya plugins are loaded""" + for plugin in plugInList: + if mc.pluginInfo(plugin, q=1, loaded=1) == 0: + try: + mc.loadPlugin(plugin, qt=1) + LOGGER.info('{0} plug-in was loaded successfully!'.format(plugin)) + except Exception as e: + message = 'Warning!\n\nMaya native Plug-in "{0}" was not detected and can\'t be activated.\ + \nYou can\'t use some of the functionality of {1} without {0} plug-in.\ + \n\nTry to restart Maya and check your Maya installation.\ + \n\nNOTE: This error can also be triggered if you have maya Plug-in Manager opened. Try to close it and repeat.'\ + .format(plugin, myPluginName) + mc.confirmDialog( + title='{0} Plug-in Not Detected'.format(plugin), + message=message, + button=['OK'], + defaultButton='OK', + cancelButton='OK', + dismissString='OK', + icn='critical') + errorMessage = 'Maya "{0}" plug-in can\'t be loaded. Please check if your Maya install is correct.'.format(plugin) + LOGGER.exception(e) + MESSAGE.warning(errorMessage) + + +def loadCustomPlugin(plugin): + """Checks if all the required CUSTOM maya plugins are loaded""" + if mc.pluginInfo(plugin, q=1, loaded=1) == 0: + split = os.path.split(plugin) + try: + mc.loadPlugin(plugin, qt=1) + LOGGER.info("{0} plug-in was loaded successfully!".format(split[-1])) + return True + except BaseException: + return False + else: + return True + + +def userSetup(): + """ Rewrite userSetup file """ + fileName = mc.internalVar(usd=1) + 'userSetup.mel' + if os.path.exists(fileName): + fileID = open(fileName, 'r') + allLines = fileID.readlines() + fileID.close() + newList = list() + lineDetected = 0 + # Remove old code if any exist + for i in range(len(allLines)): + if 'gs_curvetools' not in allLines[i]: + newList.append(allLines[i]) + else: + lineDetected = 1 + # Write to userSetup if needed + if lineDetected == 1: + LOGGER.info('userSetup.mel was modified successfully!') + newFileID = open(fileName, 'w') + newFileID.writelines(newList) + newFileID.close() + + +def getDPI(): + return mc.mayaDpiSetting(q=1, sd=1) + + +def openDocs(): + """ Open User Documentation """ + if OS == "mac": + os.system('open https://gs-curvetools.readthedocs.io/') + return 1 + os.system('start https://gs-curvetools.readthedocs.io/') + + +def openLink(link): + """ Open User Documentation """ + if OS == "mac": + os.system('open %s' % link) + return 1 + os.system('start %s' % link) diff --git a/Scripts/Modeling/Edit/gs_curvetools/utils/wrap.py b/Scripts/Modeling/Edit/gs_curvetools/utils/wrap.py new file mode 100644 index 0000000..3bdb2e3 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/utils/wrap.py @@ -0,0 +1,1319 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import re +from functools import partial as pa +from imp import reload + +import maya.cmds as mc +import maya.OpenMayaUI as omui +from PySide2 import QtCore, QtGui, QtWidgets +from shiboken2 import wrapInstance + +from . import style, tooltips, utils + +TOOLTIPS_DICT = tooltips.processTooltips() + +reload(utils) +reload(style) +reload(tooltips) + +# Holds the names and pointers to all controls +try: + bool(WIDGETS) # type: ignore # pylint: disable=used-before-assignment +except BaseException: + WIDGETS = {} + + +def getUniqueName(name): + from ..main import MAIN_WINDOW_NAME + return (MAIN_WINDOW_NAME + "_" + name) + + +def wrapControl(name): + name = omui.MQtUtil.findControl(name) + return wrapInstance(int(name), QtWidgets.QWidget) + + +def separator(): + name = omui.MQtUtil.findControl(mc.separator(st='in')) + return wrapInstance(int(name), QtWidgets.QWidget) + +# Layouts + + +class Layout(QtWidgets.QWidget): + + def __init__(self, parent, objName='', spacing=2, + margin=0, margins=[0, 0, 0, 0], vis=True): + super(Layout, self).__init__() + if objName: + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + self.__parent = parent + self.__spacing = spacing + self.__margin = margin + self.__margins = margins + self.__visibility = vis + + def __enter__(self): + layout = QtWidgets.QVBoxLayout(self) + layout.setContentsMargins(*self.__margins) + layout.setMargin(self.__margin) + layout.setSpacing(self.__spacing) + layout.addWidget(self) + self.setVisible(self.__visibility) + return self + + def __exit__(self, *_): + self.__parent.addWidget(self) + + +class Row(QtWidgets.QWidget): + + def __init__(self, parent, objName='', spacing=2, margins=[0, 0, 0, 0]): + super(Row, self).__init__() + if objName: + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.__parent = parent + self.__spacing = spacing + self.__margins = margins + + def __enter__(self): + row = QtWidgets.QHBoxLayout(self) + row.setSpacing(self.__spacing) + row.setContentsMargins(*self.__margins) + return self + + def __exit__(self, *_): + self.__parent.addWidget(self) + + +class Column(QtWidgets.QWidget): + + def __init__(self, parent, objName='', spacing=2, margins=[0, 0, 0, 0]): + super(Column, self).__init__() + if objName: + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + self.__parent = parent + self.__spacing = spacing + self.__margins = margins + + def __enter__(self): + layout = QtWidgets.QVBoxLayout(self) + layout.setSpacing(self.__spacing) + layout.setContentsMargins(*self.__margins) + return self + + def __exit__(self, *_): + self.__parent.addWidget(self) + + +class Frame(QtWidgets.QFrame): + + def __init__(self, parent, objName='', label=None, spacing=2, margins=[0, 0, 0, 0]): + super(Frame, self).__init__() + + self.objName = '' + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.setAutoFillBackground(True) + self.setGeometry(0, 0, 0, 0) + + self.mainParent = parent + self.frameSpacing = style.scale(spacing) + self.frameMargins = style.scale(margins) + self.frameLabel = label + + self.mainLayout = QtWidgets.QVBoxLayout(self) + self.mainLayout.setSpacing(self.frameSpacing) + self.mainLayout.setContentsMargins(0, 0, 0, 0) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def eventFilter(self, object, event): + if object is not self.frameButton and event.type() == QtCore.QEvent.ToolTip: + self.enableTooltip(False) + elif object is self.frameButton and event.type() == QtCore.QEvent.ToolTip: + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + return super(Frame, self).eventFilter(object, event) + + def __enter__(self): + # Layout Button + self.frameButton = Button(self.layout()) + if self.frameLabel: + self.frameButton.setLabel(self.frameLabel) + self.setCollapsible(True) + self.frameButton.clicked.connect(pa(self.toggleCollapsed)) + + # Main Frame for Contents + self.frameWidget = QtWidgets.QWidget() + self.frameWidget.setContentsMargins(*self.frameMargins) + self.frameLayout = QtWidgets.QVBoxLayout(self.frameWidget) + self.frameLayout.setSpacing(self.frameSpacing) + self.frameLayout.setContentsMargins(*self.frameMargins) + self.frameWidget.installEventFilter(self) + self.frameButton.installEventFilter(self) + + self.frameWidget.setHidden(True) + self.layout().addWidget(self.frameWidget) + return self + + def __exit__(self, *_): + self.mainParent.addWidget(self) + + def getFrameLayout(self): + return self.frameLayout + + def setCollapsed(self, hidden=False): + self.frameWidget.setHidden(hidden) + self.frameButton.setChecked(not hidden) + + def toggleCollapsed(self): + palette = QtGui.QPalette() + if self.frameWidget.isVisible(): + self.setFrameStyle(self.NoFrame) + color = QtGui.QColor(0, 0, 0, 0) + palette.setColor(QtGui.QPalette.Background, color) + self.setPalette(palette) + self.frameWidget.setHidden(True) + self.frameButton.setChecked(False) + else: + self.setFrameStyle(self.Panel | self.Sunken) + color = QtGui.QColor(0, 0, 0, 30) + palette.setColor(QtGui.QPalette.Background, color) + self.setPalette(palette) + self.frameWidget.setHidden(False) + self.frameButton.setChecked(True) + + def setCollapsible(self, collapsible): + if collapsible: + self.frameButton.setButtonStyle('frame-button') + self.frameButton.setCheckable(True) + self.frameButton.setChecked(False) + self.frameButton.blockSignals(False) + else: + self.frameButton.setButtonStyle('frame-button-not-collapsible') + self.setCollapsed(False) + self.frameButton.setCheckable(False) + self.frameButton.setChecked(False) + self.frameButton.blockSignals(True) + +# Menu Items: + + +class Menu: + + def __init__(self, label, parent, tearable=True, collapsible=False): + self.label = label + self.parent = parent + self.tearable = tearable + self.collapsible = collapsible + + def __enter__(self): + self.menu = QtWidgets.QMenu(self.label) + self.menu.setToolTipsVisible(True) + self.menu.setTearOffEnabled(self.tearable) + self.menu.setSeparatorsCollapsible(self.collapsible) + return self.menu + + def __exit__(self, *_): + self.parent.addMenu(self.menu) + + +class ActionGroup: + + def __init__(self, objName, parent): + self.objName = objName + self.parent = parent + + def __enter__(self): + self.group = QtWidgets.QActionGroup(self.parent) + if self.objName: + self.group.setObjectName(getUniqueName(self.objName)) + WIDGETS[self.objName] = self.group + return self.group + + def __exit__(self, *_): + pass + + +class MenuItem(QtWidgets.QAction): + + def __init__(self, objName, label, parent, checkable=False, checked=False, collection=None): + super(MenuItem, self).__init__(label, parent) + self.objName = objName + self.setCheckable(checkable) + if checked: + self.setChecked(checked) + if self.objName: + self.setObjectName(getUniqueName(self.objName)) + WIDGETS[self.objName] = self + if collection: + self.setActionGroup(collection) + parent.addAction(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + +def mayaSlider(slider, objName='', layout=None): # TODO: Need to refactor these function to use one class for wrapping + slider = wrapInstance(int(omui.MQtUtil.findControl(slider)), QtWidgets.QWidget) + if objName: + slider.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = slider + if layout: + layout.addWidget(slider) + return slider + + +# Buttons + +class Button(QtWidgets.QPushButton): # Maybe change back to the mode without inheritance + """ Creates a normal button """ + markingPixmap = QtGui.QPixmap(utils.getFolder.icons() + 'marking.png') + modPixmap = QtGui.QPixmap(utils.getFolder.icons() + 'mod.png') + resetPixmap = QtGui.QPixmap(utils.getFolder.icons() + 'reset.png') + + def __init__(self, layout=None, objName=''): + super(Button, self).__init__() + self.objName = '' + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + self.buttonStyle = '' + self.buttonState = 0 + self.buttonLabels = None + + if layout: + layout.addWidget(self) + + self.setButtonStyle() + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def setLabel(self, text, margins=[0, 0, 0, 0], lineHeight=80): + text = self._formatText(text, lineHeight=lineHeight) + + self.label = QtWidgets.QLabel(text, self) + + self.label.setAlignment(QtCore.Qt.AlignCenter) + self.label.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + + layout = QtWidgets.QHBoxLayout(self) + layout.setContentsMargins(*style.scale(margins)) + layout.addWidget(self.label) + self.setLabelStyle() + + def setLabelStyle(self, labelStyle='normal'): + self.font = QtGui.QFont('Roboto') + self.label.setFont(self.font) + if labelStyle == 'normal': + self.setFontSize(11) + elif labelStyle == 'small': + self.setFontSize(10, False) + + def setButtonStyle(self, buttonStyle='normal'): + if buttonStyle == 'normal': + self.setStyleSheet(style.buttonNormal) + self.buttonStyle = 'normal' + elif buttonStyle == 'active-orange': + self.setStyleSheet(style.buttonActiveOrange) + self.buttonStyle = 'active-orange' + elif buttonStyle == 'active-blue': + self.setStyleSheet(style.buttonActiveBlue) + self.buttonStyle = 'active-blue' + elif buttonStyle == 'active-white': + self.setStyleSheet(style.buttonActiveWhite) + self.buttonStyle = 'active-white' + elif buttonStyle == 'small': + self.setStyleSheet(style.smallNormal) + self.buttonStyle = 'small' + elif buttonStyle == 'small-filled': + self.setStyleSheet(style.smallFilled) + self.buttonStyle = 'small-filled' + elif buttonStyle == 'small-no-border': + self.setStyleSheet(style.smallNoBorder) + self.buttonStyle = 'small-no-border' + elif buttonStyle == 'slider-label': + self.setStyleSheet(style.sliderLabel) + self.buttonStyle = 'slider-label' + elif buttonStyle == 'icon': + self.setStyleSheet(style.buttonIcon) + self.buttonStyle = 'icon-button' + elif buttonStyle == 'frame-button': + self.setStyleSheet(style.frameButton) + self.buttonStyle = 'frame-button' + elif buttonStyle == 'frame-button-not-collapsible': + self.setStyleSheet(style.frameButtonNotCollapsable) + self.buttonStyle = 'frame-button-not-collapsible' + elif buttonStyle == 'small-compound-top-left': + self.setStyleSheet(style.smallCompoundTopLeft) + self.buttonStyle = 'small-compound-top-left' + elif buttonStyle == 'small-compound-top-right': + self.setStyleSheet(style.smallCompoundTopRight) + self.buttonStyle = 'small-compound-top-right' + elif buttonStyle == 'small-filled-compound-bottom': + self.setStyleSheet(style.smallFilledCompoundBottom) + self.buttonStyle = 'small-filled-compound-bottom' + + def setIcon(self, icoType): + iconMod = self.modPixmap + iconMarking = self.markingPixmap + self.modLabelMod = QtWidgets.QLabel(self) + self.modLabelMarking = QtWidgets.QLabel(self) + + mult = 7 + iconMod = iconMod.scaled(style.scale(mult), style.scale(mult), QtCore.Qt.KeepAspectRatio) + self.modLabelMod.setContentsMargins(*style.scale([2, 2, 0, 0])) + self.modLabelMod.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.modLabelMod.setPixmap(iconMod) + self.modLabelMod.setVisible(False) + + iconMarking = iconMarking.scaled(style.scale(mult), style.scale(mult), QtCore.Qt.KeepAspectRatio) + self.modLabelMarking.setContentsMargins(*style.scale([2, 2, 0, 0])) + self.modLabelMarking.setAlignment(QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop) + self.modLabelMarking.setPixmap(iconMarking) + self.modLabelMarking.setVisible(False) + + if icoType == 'marking-top': + self.modLabelMarking.setVisible(True) + elif icoType == 'marking-bottom': + self.modLabelMarking.setVisible(True) + self.modLabelMarking.move(*style.scale([0, 16])) + elif icoType == 'mod-top': + self.modLabelMod.setVisible(True) + elif icoType == 'mod-bottom': + self.modLabelMod.setVisible(True) + self.modLabelMod.move(*style.scale([0, 17])) + elif icoType == 'both': + self.modLabelMod.setVisible(True) + self.modLabelMarking.setVisible(True) + self.modLabelMarking.move(*style.scale([1, 13])) + elif icoType == 'reset': + iconReset = self.resetPixmap + iconReset = iconReset.scaled(style.scale(11), style.scale(11), QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + super(Button, self).setIcon(iconReset) + else: + self.modLabelMod.setVisible(False) + self.modLabelMarking.setVisible(False) + + def changeIcon(self, icoType): + if icoType == 'marking': + self.modLabel.setPixmap(self.markingPixmap) + elif icoType == 'mod-top': + self.modLabel.setPixmap(self.modPixmap) + else: + self.modLabel.setPixmap(self.markingModPixmap) + + def setIconVisible(self, vis=True): + self.modLabel.setVisible(vis) + + def setFontSize(self, size, bold=True): + font = self.label.font() + font.setPixelSize(style.scale(size)) + if bold: + font.setBold(bold) + self.label.setFont(font) + + def setLabelOffset(self, offsetX, offsetY, width=None, height=None): + if not width: + width = self.label.geometry().width() + if not height: + height = self.label.geometry().height() + rect = QtCore.QRect(offsetX, offsetY, width, height) + self.label.setFrameRect(rect) + + def setWidthHeight(self, w=None, h=None): + if w: + self.setFixedWidth(style.scale(w)) + if h: + self.setFixedHeight(style.scale(h)) + + def changeText(self, *_): + rect = self.rect().width() + _min = style.scale(75) + _max = style.scale(115) + if rect < _min and self.buttonState != 0: + text = self._formatText(self.buttonLabels[0]) + self.label.setText(text) + self.buttonState = 0 + elif _min <= rect < _max and self.buttonState != 1: + text = self._formatText(self.buttonLabels[1]) + self.label.setText(text) + self.buttonState = 1 + elif rect >= _max and self.buttonState != 2 and len(self.buttonLabels) > 2: + text = self._formatText(self.buttonLabels[2]) + self.label.setText(text) + self.buttonState = 2 + + def setValue(self, value): + if self.isCheckable(): + self.setChecked(bool(value)) + + def addParent(self, parent, stretch=0): + parent.addWidget(self, stretch) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def _formatText(self, text, lineHeight=80): + if lineHeight == 100: + return text + if isinstance(text, list): + self.resizeEvent = self.changeText + self.buttonLabels = text + text = text[0] + text = '

{0}

'.format(text, lineHeight) + return text + + +class IconCheckButton(QtWidgets.QPushButton): + + def __init__(self, parent, objName=''): + super(IconCheckButton, self).__init__() + self.setContentsMargins(0, 0, 0, 0) + self.setStyleSheet(style.buttonIcon) + self.objName = '' + if objName: + self.objName = objName + WIDGETS[objName] = self + self.setObjectName(getUniqueName(objName)) + self.setCheckable(True) + self.clicked.connect(self.toggleIcon) + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def setChecked(self, checked): + super(IconCheckButton, self).setChecked(checked) + self.toggleIcon() + + def setIcons(self, checked, unchecked): + self.icons = [checked, unchecked] + self.switchToIcon(0) + + def switchToIcon(self, iconId): + icon = QtGui.QIcon(self.icons[iconId]) + self.setIcon(icon) + + def setIconWidthHeight(self, w, h): + self.setFixedSize(QtCore.QSize(style.scale(w), style.scale(h))) + self.setIconSize(QtCore.QSize(style.scale(w), style.scale(h))) + + def toggleIcon(self): + if self.isChecked(): + self.switchToIcon(0) + else: + self.switchToIcon(1) + +# Layers + + +class Layer(QtWidgets.QPushButton): + + LAYERS = 20 + + def __init__(self, objName=None, layout=None): + super(Layer, self).__init__() + + self.buttonStyle = 'empty' + + self.setMinimumSize(1, 1) + self.setStyle() + + self.setCheckable(True) + + self.setAcceptDrops(True) + + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + if layout: + layout.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def setLabel(self, label): + self.buttonLabel = QtWidgets.QLabel(label, self) + self.buttonLabel.setStyleSheet(style.layerLabel) + self.buttonLabel.setAlignment(QtCore.Qt.AlignCenter) + self.buttonLabel.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + buttonLayout = QtWidgets.QHBoxLayout(self) + buttonLayout.setContentsMargins(0, 0, 0, 0) + buttonLayout.addWidget(self.buttonLabel) + + def changeLabel(self, text): + self.buttonLabel.setText(text) + + def setNumberOfLayers(self, num): + Layer.LAYERS = num + + # Set custom styles + def setStyle(self, buttonStyle='empty'): + if self.buttonStyle == 'custom': + return 0 + if buttonStyle == self.buttonStyle: + return + if buttonStyle == 'empty': + self.setStyleSheet(style.layer('rgba(0,0,0,0)')) + self.buttonStyle = 'empty' + elif buttonStyle == 'active': + self.setStyleSheet(style.layer('#db9456')) + self.buttonStyle = 'active' + elif buttonStyle == 'hidden': + self.setStyleSheet(style.layer('#a1a1a1')) + self.buttonStyle = 'hidden' + elif buttonStyle == 'curve': + self.setStyleSheet(style.layer('#5285a6')) + self.buttonStyle = 'curve' + elif buttonStyle == 'geo': + self.setStyleSheet(style.layer('#25934e')) + self.buttonStyle = 'geo' + elif buttonStyle == 'edit': + self.setStyleSheet(style.layer('\ + qlineargradient(\ + x1: 0, y1: 0, x2: 1, y2: 1,\ + stop: 0 #db9456, stop: 0.499 #db9456, stop: 0.501 red,stop: 1 red\ + );')) + self.buttonStyle = 'edit' + + def colorize(self, isColorized): + if isColorized: + self.buttonStyle = 'custom' + else: + self.buttonStyle = 'empty' + + def setLayout(self, layout): + layout.addWidget(self) + + def mousePressEvent(self, event): + super(Layer, self).mousePressEvent(event) + if event.button() == QtCore.Qt.MiddleButton: + drag = QtGui.QDrag(self) + mimeData = QtCore.QMimeData() + mimeData.setText(self.objectName()) + drag.setMimeData(mimeData) + drag.exec_() + + def dragEnterEvent(self, event): + super(Layer, self).dragEnterEvent(event) + if event.proposedAction() != QtCore.Qt.MoveAction: + return + if event.source().__class__ != Layer: + return + event.acceptProposedAction() + + def dropEvent(self, event): + super(Layer, self).dropEvent(event) + if event.proposedAction() != QtCore.Qt.MoveAction: + return + source = event.source() + if source != self and source.__class__ == Layer: + from .. import core + core.moveLayers(event.mimeData().text(), self.objectName()) + event.acceptProposedAction() + +# Sliders + + +class MayaSlider: + + """ Just a simple wrapper of normal Maya sliders """ + + def __init__(self, slider, objName='', layout=None): + self.slider = wrapInstance(int(omui.MQtUtil.findControl(slider)), QtWidgets.QWidget) + if objName: + self.slider.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self.slider + if layout: + layout.addWidget(self.slider) + + +class ControlSlider(QtWidgets.QWidget): + + linkIconTop = QtGui.QPixmap(utils.getFolder.icons() + 'marking.png') + linkIconBottom = QtGui.QPixmap(utils.getFolder.icons() + 'link_bottom.png') + + def __init__(self, parent=None, objName='', typ='int'): + super(ControlSlider, self).__init__() + + self.objName = '' + if objName: + self.objName = objName + self.attributeName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.minimum = self.fieldMinimum = 0 + self.maximum = self.fieldMaximum = 0 + + self.typ = typ + + self.layout = QtWidgets.QHBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + self.button = Button(self.layout) + self.button.setButtonStyle('slider-label') + self.button.setCheckable(True) + self.button.setAutoFillBackground(True) + + if self.typ == 'int': # Possibly replace with native sliders for better functionality + m_slider = mc.intSliderGrp(cw=[(1, 45), (2, 1)], adj=2, f=1) + else: + m_slider = mc.floatSliderGrp(cw=[(1, 45), (2, 1)], adj=2, f=1) + + self.slider = wrapInstance(int(omui.MQtUtil.findControl(m_slider)), QtWidgets.QWidget) + self.layout.addWidget(self.slider) + + self.m_slider = self.slider.objectName() + + if parent: + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.button.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.button.setToolTip('') + + def setLabel(self, label): + self.button.setLabel(label) + self.button.label.setAlignment(QtCore.Qt.AlignRight) + self.button.setFixedWidth(style.scale(55)) + + def setLabelWidth(self, width): + self.button.setFixedWidth(style.scale(width)) + + def setValue(self, value): + if self.typ == 'int': + mc.intSliderGrp(self.m_slider, e=1, v=value) + else: + mc.floatSliderGrp(self.m_slider, e=1, v=value) + + def getValue(self): + if self.typ == 'int': + return mc.intSliderGrp(self.m_slider, q=1, v=1) + else: + return mc.floatSliderGrp(self.m_slider, q=1, v=1) + + def setDragCommand(self, cmd): + self.dragCommand = cmd + if self.typ == 'int': + mc.intSliderGrp(self.m_slider, e=1, dc=self.dragCommand) + else: + mc.floatSliderGrp(self.m_slider, e=1, dc=self.dragCommand) + + def setReleaseCommand(self, cmd): + def releaseCommand(*_): + self.dragCommand() + cmd() + if self.typ == 'int': + mc.intSliderGrp(self.m_slider, e=1, cc=releaseCommand) + else: + mc.floatSliderGrp(self.m_slider, e=1, cc=releaseCommand) + + def getAttributeName(self): + return self.attributeName + + def setMinMax(self, minimum, maximum): + self.minimum = minimum + self.maximum = maximum + if self.typ == 'int': + mc.intSliderGrp(self.m_slider, e=1, + min=self.minimum, max=self.maximum, + fmn=self.minimum, fmx=self.maximum) + else: + mc.floatSliderGrp(self.m_slider, e=1, + min=self.minimum, max=self.maximum, + fmn=self.minimum, fmx=self.maximum) + + def setFieldMinMax(self, minimum, maximum): + self.fieldMinimum = minimum + self.fieldMaximum = maximum + if self.typ == 'int': + mc.intSliderGrp(self.m_slider, e=1, fmn=self.fieldMinimum, fmx=self.fieldMaximum) + else: + mc.floatSliderGrp(self.m_slider, e=1, fmn=self.fieldMinimum, fmx=self.fieldMaximum) + + def setPrecision(self, precision): + if self.typ != 'int': + mc.floatSliderGrp(self.m_slider, e=1, pre=precision) + + def setStep(self, step): + if self.typ != 'int': + mc.floatSliderGrp(self.m_slider, e=1, s=step) + + def resetMinMax(self): + if self.typ == 'int': + mc.intSliderGrp(self.m_slider, e=1, + fmn=self.fieldMinimum, fmx=self.fieldMaximum, + min=self.minimum, max=self.maximum) + else: + mc.floatSliderGrp(self.m_slider, e=1, + fmn=self.fieldMinimum, fmx=self.fieldMaximum, + min=self.minimum, max=self.maximum) + +# Falloff curve graph + + +class FallOffCurve(QtWidgets.QWidget): + + def __init__(self, parent, objName='', attr=True): + super(FallOffCurve, self).__init__() + self.objName = objName + self.attr = attr + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + if self.objName: + self.setObjectName(getUniqueName(self.objName)) + WIDGETS[self.objName] = self + if self.attr: + self.m_graph = mc.falloffCurveAttr(ccw=5, stg=0, h=220, hlc=[0.68, 0.68, 0.68]) + else: + self.m_graph = mc.falloffCurve(ccw=5, stg=0, h=220, hlc=[0.68, 0.68, 0.68]) + self.graph = wrapInstance(int(omui.MQtUtil.findControl(self.m_graph)), QtWidgets.QWidget) + self.graph.setStyleSheet('*{background-color:rgba(0,0,0,0);}') + self.layout.addWidget(self.graph) + + parent.addWidget(self) + + def connectGraph(self, path): + if self.attr: + mc.falloffCurveAttr(self.m_graph, e=1, at=path) + else: + mc.falloffCurve(self.m_graph, e=1, at=path) + + def changeCommand(self, command): + if self.attr: + mc.falloffCurveAttr(self.m_graph, e=1, cc=command) + else: + mc.falloffCurve(self.m_graph, e=1, cc=command) + + def getGraph(self): + if self.attr: + return mc.falloffCurveAttr(self.m_graph, q=1, asString=1) + else: + return mc.falloffCurve(self.m_graph, q=1, asString=1) + + def setGraph(self, string): + if self.attr: + mc.falloffCurveAttr(self.m_graph, e=1, asString=string) + else: + mc.falloffCurve(self.m_graph, e=1, asString=string) + + def resetGraph(self, custom=None): + defaults = r"0, 0.5, 0.333, 0.5, 0.667, 0.5, 1, 0.5" + if custom: + defaults = custom + if self.attr: + mc.falloffCurveAttr(self.m_graph, e=1, asString=defaults) + else: + mc.falloffCurve(self.m_graph, e=1, asString=defaults) + +# Color picker button + + +class ColorPicker(QtWidgets.QWidget): + + def __init__(self, objName='', parent=None): + super(ColorPicker, self).__init__() + + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + self.objName = '' + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.m_colorSlider = mc.colorSliderGrp(objName, rgb=[.5, .5, .5]) + + origSlider = wrapInstance(int(omui.MQtUtil.findControl(self.m_colorSlider)), QtWidgets.QWidget) + self.components = origSlider.children() + self.colorRect = self.components[1] + self.layout.addWidget(self.colorRect) + origSlider.setVisible(False) + + if parent: + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def addParent(self, parent, stretch=0): + parent.addWidget(self, stretch) + + def connectCommand(self, command): + mc.colorSliderGrp(self.m_colorSlider, e=1, cc=command) + + def connectDragCommand(self, command): + mc.colorSliderGrp(self.m_colorSlider, e=1, dc=command) + + def resetColors(self): + mc.colorSliderGrp(self.m_colorSlider, e=1, rgb=[0, 0, 0]) + + def setRGBColors(self, clr=[0, 0, 0]): + mc.colorSliderGrp(self.m_colorSlider, e=1, rgb=clr) + + def getRGBColors(self): + return mc.colorSliderGrp(self.m_colorSlider, q=1, rgb=1) + + def setHSVColors(self, clr=[0, 0, 0]): + mc.colorSliderGrp(self.m_colorSlider, e=1, hsv=clr) + + def getHSVColors(self): + return mc.colorSliderGrp(self.m_colorSlider, q=1, hsv=1) + +# Layer selection drop-down menu + + +class LayerSelector(QtWidgets.QComboBox): + + def __init__(self, objName, parent): + super(LayerSelector, self).__init__() + + self.objName = '' + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + symbols = [] + for c in range(ord('a'), ord('j') + 1): + symbols.append(chr(c).capitalize()) + numbers = [str(i) for i in range(10)] + numbers2 = [str(i) for i in range(20, 40)] + numbers3 = [str(i) for i in range(40, 80)] + + self.setContentsMargins(0, 0, 0, 0) + self.addItems(numbers + symbols + numbers2 + numbers3) + + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def setCurrentIndex(self, index): + # Disabling programmatic signal + self.blockSignals(True) + super(LayerSelector, self).setCurrentIndex(index) + self.blockSignals(False) + + def updateLayerList(self): + self.blockSignals(True) + from .. import core + if core.getOption('layerNumbersOnly'): + for i in range(10, 20): + self.removeItem(i) + self.insertItem(i, str(i)) + else: + letters = [chr(c).capitalize() for c in range(ord('a'), ord('j') + 1)] + for i in range(10, 20): + self.removeItem(i) + self.insertItem(i, str(letters[i - 10])) + self.blockSignals(False) + + def wheelEvent(self, event): + super(LayerSelector, self).wheelEvent(event) + + +class LayerCollectionWidget(QtWidgets.QComboBox): + + def __init__(self, objName, parent=None): + super(LayerCollectionWidget, self).__init__() + + self.objName = '' + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + self.setContentsMargins(0, 0, 0, 0) + if parent: + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def clear(self): + super(LayerCollectionWidget, self).clear() + self.addItem("Main") + + +# Line edit input field + + +class LineEdit(QtWidgets.QLineEdit): + + def __init__(self, objName, parent=None): + super(LineEdit, self).__init__() + + self.autoFormat = False + + self.objName = '' + if objName: + self.objName = objName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.setContentsMargins(0, 0, 0, 0) + + self.editingFinished.connect(self.formatText) + + if parent: + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def setAutoFormat(self, shouldFormat): + self.autoFormat = shouldFormat + + @utils.deferred + def formatText(self): + if self.autoFormat: + newName = self.text() + if newName: + if newName[0].isdigit(): + newName = newName[1:] + self.setText(re.sub(r"[^0-9a-zA-Z]+", "_", newName)) + +# Single float field + + +class FloatField(QtWidgets.QWidget): + + def __init__(self, objName, parent=None, attrName=''): + super(FloatField, self).__init__() + + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + + self.objName = '' + if objName: + self.objName = objName + if not attrName: + self.attributeName = objName + else: + self.attributeName = attrName + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.floatField = mc.floatField(min=-10, max=10, pre=1, step=0.25, v=0) + + wrappedField = wrapInstance(int(omui.MQtUtil.findControl(self.floatField)), QtWidgets.QWidget) + self.layout.addWidget(wrappedField) + + if parent: + parent.addWidget(self) + + self.enableTooltip(mc.optionVar(q="GSCT_enableTooltips")) + + def enableTooltip(self, enable): + if enable and self.objName in TOOLTIPS_DICT: + self.setToolTip(TOOLTIPS_DICT[self.objName]) + else: + self.setToolTip('') + + def setDragCommand(self, cmd): + self.dragCommand = cmd + mc.floatField(self.floatField, e=1, dc=cmd) + + def setReleaseCommand(self, cmd): + def releaseCommand(*_): + self.dragCommand() + cmd() + mc.floatField(self.floatField, e=1, cc=releaseCommand) + + def getAttributeName(self): + return self.attributeName + + def getValue(self): + return mc.floatField(self.floatField, q=1, v=1) + + def setRange(self, minimum, maximum): + mc.floatField(self.floatField, e=1, min=minimum, max=maximum) + + def setPrecision(self, pre): + mc.floatField(self.floatField, e=1, pre=pre) + + def setStep(self, step): + mc.floatField(self.floatField, e=1, step=step) + + def setValue(self, value): + mc.floatField(self.floatField, e=1, v=value) + + +class IntField(QtWidgets.QWidget): + + def __init__(self, objName, parent): + super(IntField, self).__init__() + + self.layout = QtWidgets.QVBoxLayout(self) + self.layout.setContentsMargins(0, 0, 0, 0) + if objName: + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + + self.intField = mc.intField(min=-10, max=10, step=1, v=0) + + wrappedField = wrapInstance(int(omui.MQtUtil.findControl(self.intField)), QtWidgets.QWidget) + self.layout.addWidget(wrappedField) + + parent.addWidget(self) + + def setRange(self, minimum, maximum): + mc.intField(self.intField, e=1, min=minimum, max=maximum) + + def setStep(self, step): + mc.intField(self.intField, e=1, step=step) + + def setValue(self, value): + mc.intField(self.intField, e=1, v=value) + +# Label + + +class Label(QtWidgets.QLabel): + + def __init__(self, parent=None, objName=None, margins=[0, 0, 0, 0]): + super(Label, self).__init__() + + if objName: + self.setObjectName(getUniqueName(objName)) + WIDGETS[objName] = self + self.layout = QtWidgets.QHBoxLayout(self) + self.setContentsMargins(*style.scale(margins)) + if parent: + parent.addWidget(self) + + def addParent(self, parent, stretch=0): + parent.addWidget(self, stretch) + + def setLabel(self, text, lineHeight=100): + text = self.__formatText(text, lineHeight=lineHeight) + + self.setText(text) + + self.setAlignment(QtCore.Qt.AlignCenter) + self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) + + self.setLabelStyle() + + def setLabelStyle(self, labelStyle='normal'): + self.font = QtGui.QFont('Roboto') + self.setFont(self.font) + if labelStyle == 'normal': + self.setFontSize(11) + elif labelStyle == 'small': + self.setFontSize(10, False) + + def setFontSize(self, size, bold=True): + font = self.font + font.setPixelSize(style.scale(size)) + if bold: + font.setBold(bold) + self.setFont(font) + + def __formatText(self, text, lineHeight=80): + if lineHeight == 100: + return text + if isinstance(text, list): + self.resizeEvent = self.changeText + self.buttonLabels = text + text = text[0] + text = '

{0}

'.format(text, lineHeight) + return text + + +class UVStandardItem(QtGui.QStandardItem): + + def __init__(self, grpName='', crvName='', fontSize=12, setBold=False, color=[128, 128, 128, 255]): + super(UVStandardItem, self).__init__() + + self.fnt = QtGui.QFont('Open Sans') + self.fnt.setBold(setBold) + self.fnt.setPixelSize(style.scale(fontSize)) + self.curveName = crvName + self.groupName = grpName + + self.setEditable(False) + clr = QtGui.QColor(*color) + self.setForeground(clr) + self.setFont(self.fnt) + self.setText(self.groupName) + + def setBold(self, bold=True): + self.fnt.setBold(bold) + self.setFont(self.fnt) + + +class UVItemList(QtWidgets.QTreeView): + + signal_mouseReleased = QtCore.Signal() + signal_keyPressed = QtCore.Signal() + + def __init__(self, parent=None): + super(UVItemList, self).__init__() + + self.layout = QtWidgets.QHBoxLayout(self) + if parent: + parent.addWidget(self) + + self.treeModel = QtGui.QStandardItemModel() + self.rootNode = self.treeModel.invisibleRootItem() + + self.itemList = [] + + self.setModel(self.treeModel) + self.setHeaderHidden(True) + self.setAnimated(True) + self.setIndentation(style.scale(8)) + self.setSelectionMode(self.ExtendedSelection) + + def mouseReleaseEvent(self, event): + super(UVItemList, self).mouseReleaseEvent(event) + self.expandSelection() + self.signal_mouseReleased.emit() + + def keyPressEvent(self, event): + super(UVItemList, self).keyPressEvent(event) + self.signal_keyPressed.emit() + + def getSelection(self): + selectedItems = self.selectedIndexes() + selectedCurves = [] + for i in selectedItems: + model = i.model() + item = model.itemFromIndex(i) + selectedCurves.append(item.curveName) + return selectedCurves + + def getItemList(self): + return self.itemList + + def expandSelection(self): + indexes = self.selectedIndexes() + for index in indexes: + model = index.model() + item = model.itemFromIndex(index) + if item.hasChildren(): + for row in range(item.rowCount()): + self.selectionModel().select( + self.treeModel.index(row, 0, index), + self.selectionModel().Select + ) + + def selectItems(self, items): + if not items: + return + if not isinstance(items, list): + items = [items] + for item in items: + index = item.index() + self.selectionModel().select( + index, + self.selectionModel().Toggle + ) + + def clearItemList(self): + self.rootNode.removeRows(0, self.treeModel.rowCount()) + + def updateItemList(self, inputDict): + self.itemList *= 0 + self.clearItemList() + for key in inputDict: + parent = mc.listRelatives(key, p=1, pa=1) + rootItem = UVStandardItem(crvName=key, grpName=parent[0]) + self.itemList.append(rootItem) + self.rootNode.appendRow(rootItem) + if inputDict[key]: + rootItem.setBold() + for item in inputDict[key]: + if (mc.attributeQuery('gsmessage', n=item, ex=1) and + mc.connectionInfo(item + '.gsmessage', isSource=1)): + continue + parent = mc.listRelatives(item, p=1, pa=1) + subItem = UVStandardItem(crvName=item, grpName=parent[0]) + self.itemList.append(subItem) + rootItem.appendRow(subItem) + self.expandAll() + self.selectAll() diff --git a/Scripts/Modeling/Edit/gs_curvetools/uv_editor.py b/Scripts/Modeling/Edit/gs_curvetools/uv_editor.py new file mode 100644 index 0000000..58f9029 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/uv_editor.py @@ -0,0 +1,1336 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import random +from functools import partial as pa + +import shiboken2 +from PySide2 import QtCore, QtGui, QtWidgets +from PySide2.QtCore import Qt + +from .utils import gs_math as mt +from .utils import utils +from .utils.wrap import WIDGETS + +# Buttons +LMB = Qt.LeftButton +RMB = Qt.RightButton +MMB = Qt.MiddleButton +ALT = Qt.AltModifier +CTRL = Qt.ControlModifier +SHIFT = Qt.ShiftModifier +NO_MOD = Qt.NoModifier +SCENE_W = 2000 +SCENE_H = 2000 + +# Texture cache +CACHE = {} + + +class Editor(QtWidgets.QGraphicsView): + """ + UV Editor View Main Widget + + """ + + signal_mousePress = QtCore.Signal(object) + signal_mouseMove = QtCore.Signal(object) + signal_mouseRelease = QtCore.Signal(object) + signal_keyPress = QtCore.Signal(object, object) + signal_functionKeyPress = QtCore.Signal(object) + signal_uvDrawEnd = QtCore.Signal() + signal_zoomChangeEvent = QtCore.Signal() + signal_forcedTextureMapUpdate = QtCore.Signal() + + def __init__(self, parent=None): + super(Editor, self).__init__(parent) + self.currentAction = 'NONE' + self.controllerMode = 'SELECT' + self.scaleMode = 'H' + self.initialMousePos = QtCore.QPoint(0, 0) + self.UVDict = dict() + self.textureItem = None + self.diffusePath = '' + self.alphaPath = '' + self.storedAlpha = None + + def init(self): + # Init scene + self.mainScene = EditorScene(self) + self.mainScene.setSceneRect(-SCENE_H / 2, -SCENE_W / 2, SCENE_H, SCENE_W) + self.setScene(self.mainScene) + + # Set initial Flags + self.setRenderHint(QtGui.QPainter.Antialiasing, True) + self.setRenderHint(QtGui.QPainter.TextAntialiasing, True) + self.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, True) + self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, True) + self.setViewportUpdateMode(self.SmartViewportUpdate) + self.setResizeAnchor(self.AnchorViewCenter) + self.setTransformationAnchor(self.AnchorUnderMouse) + self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) + + # Initial scaling and centering + self.centerOn(50, -50) + self.scale(8, 8) + self.focusView() + + # Selection Rect Initialization + self.selectionRect = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self) + + # Mouse Line Init + self.mouseLine = MouseLine() + self.mousePoints = [0, 0, 0, 0] + self.mainScene.addItem(self.mouseLine) + + # Custom mouse cursor + self.customCursor = Cursor() + self.customCursor.sizeVerticalCursor() + self.mainScene.addItem(self.customCursor) + + # Rotation Circle + self.rotationCircle = RotationCircle() + self.mainScene.addItem(self.rotationCircle) + + # Initialize mouse variables + self.previousMouseDelta = 0 + self.zoomDirection = 0 + + # Draw Origin Dot + originBrush = QtGui.QBrush() + originBrush.setStyle(Qt.SolidPattern) + originBrush.setColor(QtGui.QColor(255, 0, 0)) + originPen = QtGui.QPen() + originPen.setWidth(0) + self.originDot = self.mainScene.addEllipse(-5, -5, 10, 10, pen=originPen, brush=originBrush) + self.originDot.setFlag(self.originDot.ItemIgnoresTransformations, True) + + def changeColor(self, + colorBG=(36, 36, 36), + colorGrid=(50, 50, 50), + colorFrame=(160, 75, 75), + uvColorBG=(96, 100, 160), + uvColorSelected=(255, 255, 255), + uvColorDeselected=(128, 128, 128)): + if self.mainScene: + self.mainScene.colorBG = (list(colorBG) + [255]) + self.mainScene.colorGrid = (list(colorGrid) + [255]) + self.mainScene.colorFrame = (list(colorFrame) + [128]) + UVItem.bgColor = QtGui.QColor(*(list(uvColorBG) + [1])) + UVItem.selectedColor = QtGui.QColor(*(list(uvColorSelected) + [255])) + UVItem.deselectedColor = QtGui.QColor(*(list(uvColorDeselected) + [255])) + for uv in self.getAllUVs(): + uv.bgColor = QtGui.QColor(*(list(uvColorBG) + [1])) + uv.selectedColor = QtGui.QColor(*(list(uvColorSelected) + [255])) + uv.deselectedColor = QtGui.QColor(*(list(uvColorDeselected) + [255])) + self.mainScene.update() + + def getCurrentTexture(self): + return self.textureItem + + def toggleAlpha(self, enable): + if self.textureItem: + if enable: + self.textureItem.enableAlpha() + else: + self.textureItem.disableAlpha() + + def setTexture(self, diffuse, alpha=None, coverage=(1.0, 1.0), translation=(0, 0)): + self.removeTexture() + self.diffusePath = diffuse + self.alphaPath = alpha + + self.textureItem = self.createTextureItem(self.diffusePath, self.alphaPath, coverage, translation) + + if not self.textureItem: + return 'NoTexture' + if not (self.textureItem.customPixmap.width() > 0 and + self.textureItem.customPixmap.height() > 0): + return 'ZeroTexture' + + self.setTextureItem(self.textureItem) + + return 'Success' + + def createTextureItem(self, diffusePath, alphaPath, coverage, translation): + # Using cached TextureItem (or creating new one) + diffuseTime = QtCore.QFileInfo(diffusePath).lastModified() + alphaTime = None + if alphaPath: + alphaTime = QtCore.QFileInfo(alphaPath).lastModified() + shouldUpdate = True + if diffusePath in CACHE and shiboken2.isValid(CACHE[diffusePath][0]): # pylint: disable=maybe-no-member + shouldUpdate = False + # Update if Diffuse texture has different timestamp + if CACHE[diffusePath][1] != diffuseTime: + shouldUpdate = True + # Update if Alpha was added + if not CACHE[diffusePath][2] and alphaTime: + shouldUpdate = True + # Update if Alpha was removed + if not alphaTime and CACHE[diffusePath][2]: + shouldUpdate = True + # Update if Alpha has different timestamp + if CACHE[diffusePath][2] and CACHE[diffusePath][2] != alphaTime: + shouldUpdate = True + # Update if coverage or translation changed + if CACHE[diffusePath][0].customCoverage != coverage or CACHE[diffusePath][0].customTranslation != translation: + shouldUpdate = True + if shouldUpdate: + textureItem = TextureItem(diffusePath, alphaPath, coverage, translation) + CACHE[diffusePath] = (textureItem, diffuseTime, alphaTime) + if 'UVEditorTransparencyToggle' in WIDGETS: + if WIDGETS['UVEditorTransparencyToggle'].isChecked(): + textureItem.enableAlpha() + else: + textureItem.disableAlpha() + else: + textureItem = CACHE[diffusePath][0] + return textureItem + + def setTextureItem(self, textureItem): + if textureItem: + textureItem.setScale( + 1.0 / (max(textureItem.customPixmap.width(), + textureItem.customPixmap.height())) * 100) + textureItem.setOffset(0, -100.0 / textureItem.scale()) + + # Avoid multiple connections when setting textures + try: + self.signal_zoomChangeEvent.disconnect() + except Exception: + pass + try: + self.signal_forcedTextureMapUpdate.disconnect() + except Exception: + pass + + # Connecting signals + self.signal_zoomChangeEvent.connect(textureItem.updatePixmap) + self.signal_forcedTextureMapUpdate.connect(pa(textureItem.updatePixmap, True)) + + self.mainScene.addItem(textureItem) + textureItem.updatePixmap(forceUpdate=True) + + return 'Success' + + def removeTexture(self): + for item in self.items(): + if isinstance(item, TextureItem): + self.mainScene.removeItem(item) + + def createUV(self, name): + if name in self.UVDict: + try: + self.mainScene.removeItem(self.UVDict[name]) + except BaseException: + pass + self.UVDict[name] = UVItem(name) + self.mainScene.addItem(self.UVDict[name]) + return self.UVDict[name] + + def purgeUVs(self): + for uv in self.UVDict: + self.mainScene.removeItem(self.UVDict[uv]) + self.UVDict.clear() + + def getUVs(self): + returnDict = dict() + selection = [i.name for i in self.getAllUVs(selected=True)] + for uv in self.UVDict: + if uv in selection: + returnDict[uv] = self.UVDict[uv].getAttrs() + return returnDict + + def wheelEvent(self, event): + # Wheel Zoom + self.setTransformationAnchor(self.AnchorUnderMouse) + self.currentAction = 'ZOOM' + zoomIncrement = 0.1 + if event.delta() > 0: + if self.transform().m11() >= 500: + zoomIncrement = 0 + zoom = 1 + zoomIncrement + else: + if self.transform().m11() <= 1: + zoomIncrement = 0 + zoom = 1 - zoomIncrement + self.scale(zoom, zoom) + self.currentAction = 'NONE' + self.signal_forcedTextureMapUpdate.emit() + + def initializeMouseLine(self): + if self.currentAction in ['SCALE', 'ROTATE']: + self.mouseLine.setVisible(True) + self.mouseLine.setPoints(*self.mousePoints) + else: + self.mouseLine.setVisible(False) + + def initializeRotationCircle(self): + if self.currentAction == 'ROTATE': + self.rotationCircle.setVisible(True) + self.rotationCircle.rotationRect.moveCenter( + self.rotationCircle.mapFromScene(self.mousePoints[0], self.mousePoints[1]) + ) + else: + self.rotationCircle.setVisible(False) + + def mousePressEvent(self, event): + # TODO: Improve selection mode. Should select only one card if clicked, just like in Move mode. + self.signal_mousePress.emit(event) + self.setTransformationAnchor(self.AnchorUnderMouse) + button = event.button() + mod = event.modifiers() + + # Switching between controller flags + items = self.getAllUVs() + if self.controllerMode in ['SELECT', 'ROTATE', 'SCALE']: + for item in items: + item.setFlag(item.ItemIsMovable, False) + else: + for item in items: + item.setFlag(item.ItemIsMovable, True) + + # Initializing controllers: + + # Scale controller + if self.controllerMode == 'SCALE' and button == LMB: + self.currentAction = 'SCALE' + + items = self.getAllUVs(selected=True) + if items: + lastItem = items[-1] + + self.setCursor(Qt.BlankCursor) + self.customCursor.setPos(self.mapToScene(event.pos())) + self.customCursor.setVisible(True) + + if self.scaleMode == 'V': + self.customCursor.setRotation(lastItem.rotation()) + A = lastItem.mapToScene(lastItem.mainRect.bottomLeft()) + B = lastItem.mapToScene(lastItem.mainRect.bottomRight()) + else: + self.customCursor.setRotation(lastItem.rotation() + 90) + A = lastItem.mapToScene(lastItem.yAxisLine.p1().toPoint()) + B = lastItem.mapToScene(lastItem.yAxisLine.p2().toPoint()) + + P = self.mapToScene(event.pos()) + finalPoint = mt.projectPoint(A, B, P) + self.initLength = QtCore.QLineF(P, finalPoint).length() + + scenePos = self.mapToScene(event.pos()) + self.mousePoints = [ + finalPoint.x(), + finalPoint.y(), + scenePos.x(), + scenePos.y() + ] + self.initializeMouseLine() + for item in items: + item.initXScale() + item.initYScale() + + self.initialMousePos = event.pos() + self.setInteractive(False) + + # Rotation controller + elif self.controllerMode == 'ROTATE' and button == LMB: + self.currentAction = 'ROTATE' + + self.setCursor(Qt.BlankCursor) + self.customCursor.setPos(self.mapToScene(event.pos())) + self.customCursor.setVisible(True) + + items = self.getAllUVs(selected=True) + if items: + scenePos = self.mapToScene(event.pos()) + self.mousePoints = [ + items[-1].rotatePivot.x(), + items[-1].rotatePivot.y(), + scenePos.x(), + scenePos.y() + ] + self.initializeMouseLine() + self.initializeRotationCircle() + self.initAngle = self.mouseLine.mouseLine.angleTo(self.scene().xLine) + self.customCursor.setRotation(self.initAngle) + self.setInteractive(False) + + # Move controller + elif self.controllerMode == 'MOVE' and button == LMB: + self.currentAction = 'MOVE' + self.setCursor(Qt.SizeAllCursor) + + # Draw UV controller + elif self.controllerMode == 'DRAW' and button == LMB: + self.setCursor(Qt.CrossCursor) + self.selectionRect.setStyle(QtWidgets.QStyleFactory.create('Fusion')) + self.currentAction = 'DRAW' + self._startSelection(event.pos()) + self.setInteractive(False) + + # Zoom using drag + elif button == RMB and mod == ALT: + self.currentAction = 'ZOOM' + self.initialMousePos = event.pos() + self.zoomInitialPos = event.pos() + self.setInteractive(False) + + # Panning the view + elif button == MMB: + self.currentAction = 'PAN' + self.previousPosition = event.pos() + self.setCursor(Qt.ClosedHandCursor) + self.setInteractive(False) + + # Selection Rect + elif button == LMB and (mod == CTRL or mod == NO_MOD): + self.currentAction = 'SELECT' + self._startSelection(event.pos()) + self.setInteractive(False) + + # Click selection + elif button == LMB and (mod == CTRL and mod == CTRL | SHIFT): + self.currentAction = 'SELECT_ADD' + self._startSelection(event.pos()) + self.setInteractive(False) + + # Click remove selection + elif button == LMB and mod == CTRL: + self.currentAction = 'SELECT_REMOVE' + self._startSelection(event.pos()) + self.setInteractive(False) + + # Click toggle selection + elif button == LMB and mod == SHIFT: + self.currentAction = 'SELECT_TOGGLE' + self._startSelection(event.pos()) + self.setInteractive(False) + + # Default action + else: + self.setCursor(Qt.ArrowCursor) + self.currentAction = 'NONE' + + super(Editor, self).mousePressEvent(event) + + def mouseMoveEvent(self, event): + if not self.isActiveWindow(): + self.activateWindow() + if not self.hasFocus(): + self.setFocus() + + self.initializeMouseLine() + self.initializeRotationCircle() + + # Move controller drag + if self.currentAction == 'MOVE': + pass + + # Scale controller drag + elif self.currentAction == 'SCALE': + # Updating the mouse line position + self.scenePos = self.mapToScene(event.pos()) + + # Calculating delta scale + delta = self.initialMousePos - event.pos() + + # Applying scale to items + items = self.getAllUVs(selected=True) + if items: + self.customCursor.setPos(self.mapToScene(event.pos())) + + lastItem = items[-1] + if self.scaleMode == 'V': + A = lastItem.mapToScene(lastItem.mainRect.bottomLeft()) + B = lastItem.mapToScene(lastItem.mainRect.bottomRight()) + else: + A = lastItem.mapToScene(lastItem.yAxisLine.p1().toPoint()) + B = lastItem.mapToScene(lastItem.yAxisLine.p2().toPoint()) + + P = self.mapToScene(event.pos()) + finalPoint = mt.projectPoint(A, B, P) + length = QtCore.QLineF(P, finalPoint).length() + if self.scaleMode == 'V': + for item in items: + item.changeYScale(length - self.initLength) + else: + for item in items: + item.changeXScale(length - self.initLength) + + self.mousePoints = [ + finalPoint.x(), + finalPoint.y(), + self.scenePos.x(), + self.scenePos.y() + ] + + # Rotate controller drag + elif self.currentAction == 'ROTATE': + self.scenePos = self.mapToScene(event.pos()) + items = self.getAllUVs(selected=True) + + if items: + self.customCursor.setPos(self.mapToScene(event.pos())) + self.mousePoints = [ + items[-1].rotatePivot.x(), + items[-1].rotatePivot.y(), + self.scenePos.x(), + self.scenePos.y() + ] + self.initializeMouseLine() + + currentAngle = self.mouseLine.mouseLine.angleTo(self.scene().xLine) + for item in items: + angleDelta = mt.angleDiff(currentAngle, self.initAngle) + newRotation = item.initRotation + angleDelta + self.customCursor.setRotation(currentAngle) + if event.modifiers() == SHIFT: + newRotation = int(newRotation) + if not abs(newRotation) % 15: + item.setRotation(newRotation) + else: + item.setRotation(newRotation) + + # UV drawing update + elif self.currentAction == 'DRAW': + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + + # Zooming using drag + elif self.currentAction == 'ZOOM': + self.setCursor(Qt.SizeHorCursor) + + delta = self.zoomInitialPos.x() - event.pos().x() + if self.previousMouseDelta > delta: + self.zoomDirection = 1 + elif self.previousMouseDelta < delta: + self.zoomDirection = -1 + else: + if self.zoomDirection == -1: + self.zoomDirection = -1 + else: + self.zoomDirection = 1 + self.previousMouseDelta = delta + + # Zoom factor + zoomAmount = 0.015 + if self.zoomDirection == 1: + if self.transform().m11() >= 500: + zoomAmount = 0 + zoom = 1 + zoomAmount + else: + if self.transform().m11() <= 1: + zoomAmount = 0 + zoom = 1 - zoomAmount + + # Zooming and centering on clicked point + self.setTransformationAnchor(self.AnchorViewCenter) + initPoint = self.mapToScene(self.initialMousePos) + self.scale(zoom, zoom) + afterPoint = self.mapToScene(self.initialMousePos) + pointDelta = afterPoint - initPoint + + self.setTransformationAnchor(self.NoAnchor) + self.translate(pointDelta.x(), pointDelta.y()) + + self.signal_zoomChangeEvent.emit() + + # Panning the view + elif self.currentAction == 'PAN': + delta = self.previousPosition - event.pos() + self.previousPosition = event.pos() + self.translate(self.pos().x() + delta.x(), + self.pos().y() + delta.y()) + + # Creating selection rectangle + elif self.currentAction in ['SELECT', 'SELECT_ADD', 'SELECT_REMOVE', 'SELECT_TOGGLE']: + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + + # Default Action + else: + self.currentAction = 'NONE' + + if self.controllerMode == 'DRAW': + self.setCursor(Qt.CrossCursor) + + super(Editor, self).mouseMoveEvent(event) + + # Emit move signal + if self.currentAction in ['MOVE', 'ROTATE', 'SCALE']: + self.signal_mouseMove.emit(event) + + def mouseReleaseEvent(self, event): + + self.customCursor.setVisible(False) + + # Zooming + if self.currentAction == 'ZOOM': + # Reset the zoom + self.setCursor(Qt.ArrowCursor) + self.zoomDirection = 0 + self.signal_forcedTextureMapUpdate.emit() + + # Panning the view + elif self.currentAction == 'PAN': + # Reset the panning + self.setCursor(Qt.ArrowCursor) + + # UV Drawing + elif self.currentAction == 'DRAW': + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + painterPath = self._releaseSelection() + origGeo = self.selectionRect.geometry() + geo = self.mapToScene(origGeo).boundingRect() + self.drawUV(geo) + self.selectionRect.setStyle(QtWidgets.QStyleFactory.create('adskdarkflatui')) + + # Click selection + elif self.currentAction == 'SELECT': + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + painterPath = self._releaseSelection() + self.scene().setSelectionArea(painterPath) + + # Click add selection + elif self.currentAction == 'SELECT_ADD': + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + painterPath = self._releaseSelection() + for item in self.scene().items(painterPath): + item.setSelected(True) + + # Click remove selection + elif self.currentAction == 'SELECT_REMOVE': + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + painterPath = self._releaseSelection() + for item in self.scene().items(painterPath): + item.setSelected(False) + + # Click toggle selection + elif self.currentAction == 'SELECT_TOGGLE': + self.selectionRect.setGeometry( + QtCore.QRect(self.selectionOrigin, event.pos()).normalized() + ) + painterPath = self._releaseSelection() + for item in self.scene().items(painterPath): + if item.isSelected(): + item.setSelected(False) + else: + item.setSelected(True) + + self.initAngle = self.mouseLine.mouseLine.angleTo(self.scene().xLine) + for item in self.getAllUVs(selected=True): + item.initRotation = item.rotation() + + self.mouseLine.setVisible(False) + self.rotationCircle.setVisible(False) + + self.currentAction = 'NONE' + self.setInteractive(True) + self.setCursor(Qt.ArrowCursor) + + self.scene().update() + self.signal_mouseRelease.emit(event) + + super(Editor, self).mouseReleaseEvent(event) + + def keyPressEvent(self, event): + # Filter all key press events + key = event.key() + mod = event.modifiers() + if event.type() == QtCore.QEvent.KeyPress: + self.setCursor(Qt.ArrowCursor) + if key == Qt.Key_F: + self.focusView() + self.signal_forcedTextureMapUpdate.emit() + elif key == Qt.Key_I: + self.signal_functionKeyPress.emit('I') + elif key == Qt.Key_H: + self.signal_functionKeyPress.emit('H') + elif key == Qt.Key_A: + self.signal_functionKeyPress.emit('A') + elif key == Qt.Key_V: + self.signal_functionKeyPress.emit('V') + elif key == Qt.Key_X: + self.signal_functionKeyPress.emit('X') + elif key == Qt.Key_O: + self.signal_functionKeyPress.emit('O') + elif key == Qt.Key_S: + self.signal_functionKeyPress.emit('S') + elif key == Qt.Key_Q: + self.controllerMode = 'SELECT' + elif key == Qt.Key_W: + self.controllerMode = 'MOVE' + elif key == Qt.Key_E: + self.controllerMode = 'ROTATE' + elif key == Qt.Key_R: + if self.controllerMode == 'SCALE': + self.scaleMode = 'H' if self.scaleMode == 'V' else 'V' + self.controllerMode = 'SCALE' + elif key == Qt.Key_D: + self.controllerMode = 'DRAW' + elif key == Qt.Key_Z and mod == CTRL: + super(Editor, self).keyPressEvent(event) + elif key == Qt.Key_Y and mod == CTRL: + super(Editor, self).keyPressEvent(event) + for item in self.getAllUVs(selected=True): + item.update() + elif mod == CTRL: + pass + else: + super(Editor, self).keyPressEvent(event) + self.signal_keyPress.emit(self.controllerMode, self.scaleMode) + + def controllerModeChange(self, mode, direction=None): + self.controllerMode = mode + if direction: + self.scaleMode = direction + for item in self.getAllUVs(selected=True): + item.update() + + def _startSelection(self, pos): + self.selectionOrigin = pos + self.selectionRect.setGeometry(QtCore.QRect(self.selectionOrigin, QtCore.QSize())) + self.selectionRect.show() + + def _releaseSelection(self): + painterPath = QtGui.QPainterPath() + rect = self.mapToScene(self.selectionRect.geometry()) + painterPath.addPolygon(rect) + self.selectionRect.hide() + return painterPath + + def focusView(self): + items = self.getAllUVs() + selectedItems = self.getAllUVs(selected=True) + finalRect = None + + if selectedItems: + finalRect = selectedItems[0].sceneBoundingRect() + for i in range(1, len(selectedItems)): + finalRect |= selectedItems[i].sceneBoundingRect() + finalRect.adjust(-2, -2, 2, 2) + self.fitInView(finalRect, Qt.KeepAspectRatio) + elif items: + finalRect = items[0].sceneBoundingRect() + for i in range(1, len(items)): + finalRect |= items[i].sceneBoundingRect() + if self.textureItem: + finalRect |= self.textureItem.sceneBoundingRect() + else: + finalRect |= QtCore.QRectF(0, 0, 100, -100) + finalRect.adjust(-2, -2, 2, 2) + self.fitInView(finalRect, Qt.KeepAspectRatio) + + self.signal_zoomChangeEvent.emit() + + def randomizeUVs(self, mod): + selectedItems = set(self.getAllUVs(selected=True)) + finalList = [] + while selectedItems: + ele = selectedItems.pop() + same = set() + same.add(ele) + for item in selectedItems: + if ele.getAttrs() == item.getAttrs(): + same.add(item) + selectedItems = selectedItems - same + finalList.append(list(same)) + + groups = [(len(x), x[0].getAttrs()) for x in finalList] + flattenedList = [element for sublist in finalList for element in sublist] + random.shuffle(flattenedList) + if mod: + for i in range(len(groups)): + flattenedList[0].moveUV( + x=groups[i][1]['moveU'], + y=groups[i][1]['moveV'], + rot=groups[i][1]['rotateUV'], + sx=groups[i][1]['scaleU'], + sy=groups[i][1]['scaleV'] + ) + flattenedList.pop(0) + for item in flattenedList: + randI = random.randint(0, len(groups) - 1) + item.moveUV( + x=groups[randI][1]['moveU'], + y=groups[randI][1]['moveV'], + rot=groups[randI][1]['rotateUV'], + sx=groups[randI][1]['scaleU'], + sy=groups[randI][1]['scaleV'] + ) + else: + for group in groups: + applied = [] + for i in range(group[0]): + flattenedList[i].moveUV( + x=group[1]['moveU'], + y=group[1]['moveV'], + rot=group[1]['rotateUV'], + sx=group[1]['scaleU'], + sy=group[1]['scaleV'] + ) + applied.append(flattenedList[i]) + flattenedList = list(set(flattenedList) - set(applied)) + + def verticalFlipUV(self): + items = self.getAllUVs(selected=True) + for item in items: + pre = item.mapToScene(item.mainRect.topLeft()) + item.setRotation((item.rotation() + 180) % 360) + post = item.mapToScene(item.mainRect.bottomRight()) + delta = post - pre + item.setPos(item.pos() - delta) + item.initRotation = item.rotation() + + def resetUV(self): + for item in self.getAllUVs(selected=True): + item.resetRect() + + def drawUV(self, rect): + x = rect.center().x() - 50 + y = rect.bottom() + sx = (rect.right() - rect.left()) / 100.0 + sy = -(rect.top() - rect.bottom()) / 100.0 + if sx > 0.02 or sy > 0.02: + for item in self.getAllUVs(selected=True): + if item.isVisible(): + item.setRotation(0) + item.setPos(QtCore.QPointF(x, y)) + item.setXScale(sx) + item.setYScale(sy) + self.signal_uvDrawEnd.emit() + + def getAllUVs(self, selected=False): + # type: (bool) -> list[UVItem] + UVs = [] + for item in self.scene().items(): + if isinstance(item, UVItem): + if selected: + if item.isSelected(): + UVs.append(item) + else: + continue + else: + UVs.append(item) + return UVs + + +class EditorScene(QtWidgets.QGraphicsScene): + """ + UV Editor Scene + + """ + + def __init__(self, parent=None): + super(EditorScene, self).__init__(parent) + self.colorBG = (36, 36, 36, 255) + self.colorGrid = (50, 50, 50, 128) + self.colorFrame = (160, 75, 75, 255) + + def drawBackground(self, painter, rect): + # Draw background + self.gridSpacing = 10 + + self._brush = QtGui.QBrush() + self._brush.setStyle(Qt.SolidPattern) + self._brush.setColor(QtGui.QColor(*self.colorBG)) + + leftLine = rect.left() - rect.left() % self.gridSpacing + topLine = rect.top() - rect.top() % self.gridSpacing + + painter.fillRect(rect, self._brush) + + lines = [] + i = int(leftLine) + while i < int(rect.right()): + lines.append(QtCore.QLineF(i, rect.top(), i, rect.bottom())) + i += self.gridSpacing + + u = int(topLine) + while u < int(rect.bottom()): + lines.append(QtCore.QLineF(rect.left(), u, rect.right(), u)) + u += self.gridSpacing + + # Draw Grid Lines + gridPen = QtGui.QPen() + gridPen.setColor(QtGui.QColor(*self.colorGrid)) + gridPen.setWidth(0) + painter.setPen(gridPen) + painter.drawLines(lines) + + # Draw Axis Lines + axisPen = QtGui.QPen() + axisPen.setColor(QtGui.QColor(*self.colorFrame)) + axisPen.setWidth(0) + self.xLine = QtCore.QLineF(rect.left(), 0, rect.right(), 0) + self.yLine = QtCore.QLineF(0, rect.top(), 0, rect.bottom()) + xEndLine = QtCore.QLineF(rect.left(), -100, rect.right(), -100) + yEndLine = QtCore.QLineF(100, rect.top(), 100, rect.bottom()) + painter.setPen(axisPen) + painter.drawLines([self.xLine, self.yLine, xEndLine, yEndLine]) + + +class TextureItem(QtWidgets.QGraphicsPixmapItem): + """ + Creates a texture item for background use in UV Editor + """ + + def __init__(self, pixmapPath, alphaPath=None, coverage=(1.0, 1.0), offset=(0, 0)): + super(TextureItem, self).__init__() + self.UPDATE = True + self.pixmapPath = pixmapPath + self.alphaPath = alphaPath + self.storedAlpha = None + self.timer = utils.Timer() + self.origPixmap = QtGui.QPixmap() + self.customPixmap = QtGui.QPixmap() + self.customCoverage = coverage + self.coverageW = self.customCoverage[0] + self.coverageH = self.customCoverage[1] + self.customTranslation = offset + self.translationX = self.customTranslation[0] + self.translationY = self.customTranslation[1] + self.createPixmap() + + def createPixmap(self): + self.image = QtGui.QImage(self.pixmapPath) + if self.alphaPath: + alphaChannel = QtGui.QImage(self.alphaPath) + self.storedAlpha = self.alphaPath + if self.pixmapPath == self.alphaPath: + alphaChannel = QtGui.QImage(self.alphaPath).convertToFormat(self.image.Format_Alpha8) + if WIDGETS['UVEditorAlphaOnlyToggle'].isChecked(): + self.image = self.image.alphaChannel().convertToFormat(self.image.Format_RGB888) + else: + self.image = self.image.convertToFormat(self.image.Format_RGBA8888) + else: + alphaChannel = QtGui.QImage(self.alphaPath) + if WIDGETS['UVEditorAlphaOnlyToggle'].isChecked(): + self.image = alphaChannel.convertToFormat(self.image.Format_RGB888) + self.image.setAlphaChannel(alphaChannel) + else: + self.image = self.image.convertToFormat(self.image.Format_RGBX8888) + + self.origPixmap.convertFromImage(self.image) + self.setTransformationMode(Qt.SmoothTransformation) + self.customPixmap.convertFromImage(self.image) + self.setPixmap(self.customPixmap) + self.updatePixmap(forceUpdate=True) + + def updatePixmap(self, forceUpdate=False): + timer = self.timer.increment(1.0 / 5.0) + if forceUpdate: + timer = True + if not timer: + return + self.customPixmap = self.origPixmap + scene = self.scene() + if self.customPixmap.width() and self.customPixmap.height(): + if scene: + sceneRect = self.boundingRect() + l = scene.parent().mapToGlobal(self.mapToScene(sceneRect.topLeft()).toPoint()) + r = scene.parent().mapToGlobal(self.mapToScene(sceneRect.topRight()).toPoint()) + w_o = (r - l) * scene.parent().transform().m11() + x = w_o.x() + if x <= 0: + x = 100 + self.mult = self.customPixmap.height() / float(x) + else: + self.mult = 1.0 + if self.mult < 1: + self.mult = 1.0 + scale = 1.0 / (max(self.customPixmap.width(), + self.customPixmap.height())) * 100 * self.mult + self.setScale(scale) + resMax = max(self.customPixmap.width(), self.customPixmap.height()) + # resMin = min(self.customPixmap.width(), self.customPixmap.height()) + aspectWidth = (self.customPixmap.width() / float(resMax)) * (1.0 / self.coverageW) + aspectHeight = (self.customPixmap.height() / float(resMax)) * (1.0 / self.coverageH) + # print(aspectWidth, aspectHeight) + pixWidth = self.customPixmap.width() / float(self.mult * aspectWidth) + pixHeight = self.customPixmap.height() / float(self.mult * aspectHeight) + # self.customPixmap = self.customPixmap.scaledToHeight(pixHeight, Qt.SmoothTransformation) + self.customPixmap = self.customPixmap.scaled( + QtCore.QSize(pixWidth, pixHeight), + Qt.AspectRatioMode.IgnoreAspectRatio, + Qt.SmoothTransformation) + self.setOffset(self.translationX * 100.0 / self.scale(), + (-100.0 * self.coverageH + self.translationY * -100.0) / self.scale()) + self.setPixmap(self.customPixmap) + + def enableAlpha(self): + if self.storedAlpha: + self.alphaPath = self.storedAlpha + self.createPixmap() + self.updatePixmap(True) + + def disableAlpha(self): + if self.alphaPath: + self.storedAlpha = self.alphaPath + self.alphaPath = '' + self.createPixmap() + self.updatePixmap(True) + + +class UVItem(QtWidgets.QGraphicsItem): + """ + UV Rectangle and controls implementation + + """ + bgColor = QtGui.QColor(96, 100, 160, 1) + selectedColor = QtGui.QColor(255, 255, 255, 255) + deselectedColor = QtGui.QColor(128, 128, 128, 255) + + def __init__(self, name=None, x1=0, y1=-100, x2=100, y2=0): + super(UVItem, self).__init__() + + self.name = name + self.flip = False + self.setFlag(self.ItemIsMovable, True) + self.setFlag(self.ItemIsSelectable, True) + self.setZValue(1) + + self.initRotation = self.rotation() + self.initPos = self.pos() + + self.mainRect = QtCore.QRectF(QtCore.QPointF(x1, y1), QtCore.QPointF(x2, y2)) + + self.xAxisLine = QtCore.QLineF(0, -50, 100, -50) + self.yAxisLine = QtCore.QLineF(50, 0, 50, -100) + + self.rotatePivot = QtCore.QPointF(50, -50) + self.rootPoint = QtCore.QRectF(0, 0, 0, 1) + self.flipIndication = QtCore.QRectF(0, 0, 0, 1) + + self.setTransformOriginPoint(50, -50) + + self.initXScale() + self.initYScale() + + def paint(self, painter, *_): + # Main Rect Painting + pen = QtGui.QPen() + if self.isSelected(): + pen.setColor(self.selectedColor) + else: + pen.setColor(self.deselectedColor) + pen.setWidth(2) + pen.setCosmetic(True) + painter.setPen(pen) + brush = QtGui.QBrush() + brush.setStyle(Qt.SolidPattern) + brush.setColor(self.bgColor) + painter.setBrush(brush) + painter.drawRect(self.mainRect) + + if self.isSelected(): + # Root Point Painting + self.rootPoint.setWidth(min(5 * self.mainRect.width() / 20, 5)) + self.rootPoint.moveCenter(QtCore.QPointF(50, -.5)) + painter.drawRect(self.rootPoint) + + # Axis Lines Painting + pen.setStyle(Qt.DashLine) + pen.setWidthF(0) + painter.setPen(pen) + self.updateAxisLines() + if (self.scene().parent().scaleMode == 'V' and + self.scene().parent().controllerMode == 'SCALE'): + painter.drawLine(self.xAxisLine) + elif (self.scene().parent().scaleMode == 'H' and + self.scene().parent().controllerMode == 'SCALE'): + painter.drawLine(self.yAxisLine) + + # Flip indicator Painting + if self.flip: + brush = QtGui.QBrush() + brush.setStyle(Qt.SolidPattern) + brush.setColor(QtGui.QColor(0, 0, 255, 128)) + painter.setBrush(brush) + pen.setStyle(Qt.SolidLine) + pen.setWidthF(0) + painter.setPen(pen) + self.flipIndication.setWidth(.95) + self.flipIndication.setHeight(self.flipIndication.width()) + self.flipIndication.moveCenter(QtCore.QPointF(50, -.5)) + painter.drawEllipse(self.flipIndication) + + self.updatePivotPoint() + + def boundingRect(self): + rect = QtCore.QRectF(self.mainRect) + rect.adjust(-1, -1, 1, 1) + return rect + + def shape(self): + path = QtGui.QPainterPath() + rect = QtCore.QRectF(self.mainRect) + rect.adjust(-1, -1, 1, 1) + path.addRect(rect) + return path + + def updatePivotPoint(self): + self.rotatePivot = QtCore.QPointF(50, -50) + self.pos() + + def updateAxisLines(self): + self.xAxisLine.setLine( + self.mainRect.left(), self.mainRect.top() / 2, + self.mainRect.right(), self.mainRect.top() / 2 + ) + self.yAxisLine.setLine( + 50, 0, + 50, self.mainRect.top() + ) + + def initXScale(self): + self.initLeft = self.mainRect.left() + self.initRight = self.mainRect.right() + + def initYScale(self): + self.initTop = self.mainRect.top() + + def moveUV(self, x=None, y=None, rot=None, sx=None, sy=None): + """ Transform UV object directly from Maya """ + if x is not None and y is not None: + self.setPos(x * 100, -(y * 100)) + elif x is not None: + self.setPos(x * 100, self.pos().y()) + elif y is not None: + self.setPos(self.pos().x(), -(y * 100)) + if rot is not None: + self.setRotation(rot) + if sx is not None: + self.setXScale(sx) + if sy is not None: + self.setYScale(sy) + + def changeYScale(self, delta): + """ Set the scale using controllers """ + self.prepareGeometryChange() + newTop = self.initTop - delta + if newTop <= -1: + self.mainRect.setTop(self.initTop - delta) + self.mainRect = self.mainRect.normalized() + self.update() + + def setYScale(self, scale): + """ Set the scale directly from Maya """ + self.prepareGeometryChange() + top = round(100 * scale, 14) + self.mainRect.setTop(-top) + self.mainRect = self.mainRect.normalized() + self.update() + + def changeXScale(self, delta): + """ Set the scale using controllers """ + self.prepareGeometryChange() + self.mainRect.setLeft(self.initLeft - delta) + self.mainRect.setRight(self.initRight + delta) + self.mainRect = self.mainRect.normalized() + self.update() + + def setXScale(self, scale): + """ Set the scale directly from Maya """ + self.prepareGeometryChange() + sl = round((1 - scale) / 2 * 100, 14) + sr = round((1 - ((1 - scale) / 2)) * 100, 14) + self.mainRect.setLeft(sl) + self.mainRect.setRight(sr) + self.mainRect = self.mainRect.normalized() + self.update() + + def getAttrs(self): + attrs = dict() + attrs['moveU'] = self.pos().x() / 100 + attrs['moveV'] = self.pos().y() / 100 * -1 + attrs['rotateUV'] = self.rotation() * -1 + attrs['scaleU'] = round((100 - (100 - self.mainRect.right()) * 2) / 100, 14) + attrs['scaleV'] = self.mainRect.top() / 100 * -1 + return attrs + + def resetRect(self): + self.setPos(0, 0) + self.setRotation(0) + self.mainRect = QtCore.QRectF(QtCore.QPointF(0, -100), QtCore.QPointF(100, 0)) + self.update() + + +class MouseLine(QtWidgets.QGraphicsItem): + + def __init__(self): + super(MouseLine, self).__init__() + self.setVisible(False) + self.setZValue(100) + + # Mouse Line Init + self.mouseLine = QtCore.QLineF(0, 0, 0, 0) + + # Rotation Pivot Point + self.pivotPoint = QtCore.QRectF(0, 0, 20, 20) + + def setPoints(self, x1=0, y1=0, x2=0, y2=0): + scale = self.scene().parent().transform().m11() + self.prepareGeometryChange() + self.mouseLine.setLine(x1, y1, x2, y2) + self.pivotPoint.setWidth(2 / scale * 10) + self.pivotPoint.setHeight(2 / scale * 10) + self.pivotPoint.moveCenter(QtCore.QPointF(x1, y1)) + self.update() + + def paint(self, painter, *_): + pen = QtGui.QPen() + pen.setColor(QtGui.QColor(128, 0, 0, 128)) + pen.setCosmetic(True) + pen.setStyle(Qt.DashLine) + pen.setWidthF(3) + + painter.setPen(pen) + painter.drawLine(self.mouseLine) + + pen = QtGui.QPen() + pen.setColor(QtGui.QColor(255, 0, 0, 128)) + pen.setCosmetic(True) + pen.setWidthF(0) + + brush = QtGui.QBrush() + brush.setStyle(Qt.SolidPattern) + brush.setColor(QtGui.QColor(255, 0, 0, 128)) + painter.setPen(pen) + painter.setBrush(brush) + + painter.drawEllipse(self.pivotPoint) + + def boundingRect(self): + self.bRect = QtCore.QRectF(self.mouseLine.p1(), self.mouseLine.p2()).united(self.pivotPoint) + return self.bRect.normalized() + + def shape(self): + path = QtGui.QPainterPath() + path.addRect(self.bRect) + return path + + +class RotationCircle(QtWidgets.QGraphicsItem): + + def __init__(self): + super(RotationCircle, self).__init__() + self.setZValue(100) + self.setVisible(False) + + # Rect + self.rotationRect = QtCore.QRectF(0, 0, 20, 20) + + self.rotationRect.moveCenter(self.mapFromScene(QtCore.QPoint(0, 0))) + self.setTransformOriginPoint(self.rotationRect.center()) + + def paint(self, painter, *_): + scale = self.scene().parent().transform().m11() + pen = QtGui.QPen() + pen.setColor(QtGui.QColor(128, 0, 0, 128)) + pen.setCosmetic(True) + pen.setWidthF(3) + + painter.setPen(pen) + self.rotationRect.setWidth(20 / scale * 10) + self.rotationRect.setHeight(20 / scale * 10) + painter.drawEllipse(self.rotationRect) + + def boundingRect(self): + self.bRect = self.rotationRect.normalized() + return self.bRect + + def shape(self): + path = QtGui.QPainterPath() + path.addRect(self.bRect) + return path + + +class Cursor(QtWidgets.QGraphicsItem): + + def __init__(self): + super(Cursor, self).__init__() + self.setFlag(self.ItemIsFocusable, False) + self.setVisible(False) + self.setZValue(101) + self.mainLine = QtGui.QPolygonF() + + def sizeVerticalCursor(self): + self.mainLine.append(QtCore.QPointF(0.2, -1)) + self.mainLine.append(QtCore.QPointF(1, -1)) + self.mainLine.append(QtCore.QPointF(0, -2)) + self.mainLine.append(QtCore.QPointF(-1, -1)) + self.mainLine.append(QtCore.QPointF(-0.2, -1)) + + self.mainLine.append(QtCore.QPointF(-0.2, 1)) + self.mainLine.append(QtCore.QPointF(-1, 1)) + self.mainLine.append(QtCore.QPointF(0, 2)) + self.mainLine.append(QtCore.QPointF(1, 1)) + self.mainLine.append(QtCore.QPointF(0.2, 1)) + + def paint(self, painter, *_): + pen = QtGui.QPen() + pen.setColor(QtGui.QColor(0, 0, 0, 255)) + pen.setWidthF(0) + pen.setCosmetic(True) + brush = QtGui.QBrush() + brush.setColor(QtGui.QColor(255, 255, 255, 255)) + brush.setStyle(Qt.SolidPattern) + painter.setBrush(brush) + painter.setPen(pen) + painter.drawPolygon(self.mainLine) + + scale = self.scene().parent().transform().m11() + self.setScale(10 / scale) + + def boundingRect(self): + self.bRect = self.mainLine.boundingRect() + return self.bRect.normalized() + + def shape(self): + path = QtGui.QPainterPath() + path.addRect(self.bRect) + return path + + +if __name__ == '__main__': + app = QtWidgets.QApplication([]) + + editor = Editor() + editor.init() + editor.resize(1250, 1250) + editor.show() + + app.exec_()

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 literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/2022/cv_manip.mll b/Scripts/Modeling/Edit/gs_curvetools/plugins/2022/cv_manip.mll new file mode 100644 index 0000000000000000000000000000000000000000..eff0140a6d5600f84db10b2d06f20b141bde1e13 GIT binary patch literal 91136 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 LdJlR0|Nr|x|AkI; literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/2023/cv_manip.mll b/Scripts/Modeling/Edit/gs_curvetools/plugins/2023/cv_manip.mll new file mode 100644 index 0000000000000000000000000000000000000000..047dc3c0c136097c77fb6412374321b9b4510bc2 GIT binary patch literal 91136 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 V`ovzx-tN5z_m1qPfdBu0{|99;b!z|s literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/2024/cv_manip.mll b/Scripts/Modeling/Edit/gs_curvetools/plugins/2024/cv_manip.mll new file mode 100644 index 0000000000000000000000000000000000000000..2a1dd2adf09005621d9010b06ae68f0307483529 GIT binary patch literal 91136 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 Ld&uMe|KI-sVcKUP literal 0 HcmV?d00001 diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/__init__.py b/Scripts/Modeling/Edit/gs_curvetools/plugins/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip.py b/Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip.py new file mode 100644 index 0000000..7305716 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip.py @@ -0,0 +1,80 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import sys +from imp import reload + +import maya.api.OpenMaya as om +import maya.api.OpenMayaRender as omr + +from gs_curvetools.plugins import cv_manip_src # pyright: ignore + +reload(cv_manip_src) + +# API parameters +maya_useNewAPI = True + + +# ------------ Init & UnInit Plugin ------------ +def initializePlugin(obj): + plugin = om.MFnPlugin(obj, "GeorgeSladkovsky", "1.3.1", "Any") + try: + plugin.registerNode( + "GSCT_CurveTools_DrawManagerNode", + cv_manip_src.DrawManagerNode.id, + cv_manip_src.DrawManagerNode.creator, + cv_manip_src.DrawManagerNode.initialize, + om.MPxNode.kLocatorNode, + cv_manip_src.DrawManagerNode.drawDbClassification) + except BaseException: + sys.stderr.write("Failed to register node\n") + raise + + try: + omr.MDrawRegistry.registerDrawOverrideCreator( + cv_manip_src.DrawManagerNode.drawDbClassification, + cv_manip_src.DrawManagerNode.drawRegistrantId, + cv_manip_src.DrawOverride.creator) + except BaseException: + sys.stderr.write("Failed to register override\n") + raise + + +def uninitializePlugin(obj): + om.MMessage.removeCallbacks(cv_manip_src.CALLBACK_IDS) + cv_manip_src.CALLBACK_IDS = [] + plugin = om.MFnPlugin(obj) + try: + plugin.deregisterNode(cv_manip_src.DrawManagerNode.id) + except BaseException: + sys.stderr.write("Failed to deregister node\n") + raise + + try: + omr.MDrawRegistry.deregisterGeometryOverrideCreator( + cv_manip_src.DrawManagerNode.drawDbClassification, + cv_manip_src.DrawManagerNode.drawRegistrantId) + except BaseException: + sys.stderr.write("Failed to deregister override\n") + raise diff --git a/Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip_src.py b/Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip_src.py new file mode 100644 index 0000000..2b2ba72 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/plugins/cv_manip_src.py @@ -0,0 +1,770 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import maya.api.OpenMaya as om +import maya.api.OpenMayaRender as omr +import maya.api.OpenMayaUI as omui +import maya.cmds as mc + +# API parameters +maya_useNewAPI = True + +# Cache and globals +CALLBACK_IDS = [] # Callback IDs cache + + +# ------------ Functions ------------ +def onSelectionChanged(*_): + DrawOverride.selectionList.clear() + newSelection = om.MSelectionList() + try: + newSelection = om.MGlobal.getRichSelection(False).getSelection() + DrawOverride.softSelectionActive = True + except BaseException: + DrawOverride.softSelectionActive = False + newSelection = om.MGlobal.getActiveSelectionList() + + hilite = om.MGlobal.getHiliteList() # type: om.MSelectionList + if not hilite.isEmpty(): + newSelection.merge(hilite) + DrawOverride.selectionList = newSelection + + +# ------------ Data Classes ---------------------- +# Holds all the data for a single CV point +PointData = { + "curveIndex": 0, + "cvIndex": 0, + "position": om.MPoint(), + "isSelected": False, + "hasWeight": False, + "weight": 1.0, + "colorMult": 1.0, + "alphaMult": 1.0, + "distance": 0.0, +} + +# Holds all the data for a single curve point +CurveData = { + "curveIndex": 0, + "position": om.MPoint(), + "color": om.MColor(), + "pointIndex": 0, + "distance": 0.0, +} + + +# ------------ Draw Manager (Locator) ------------ +class DrawManagerNode(omui.MPxLocatorNode): + id = om.MTypeId(0x0013bc41) + drawDbClassification = "drawdb/geometry/GSCT_CurveTools_DrawManager" + drawRegistrantId = "GSCT_CurveTools_DrawManagerPlugin" + + # Curve attributes + aDrawAlwaysOnTop = om.MObject() + aPointSize = om.MObject() + aLineWidth = om.MObject() + aHullWidth = om.MObject() + aCurveDivisions = om.MObject() + aDeselectedPointColor = om.MObject() + aDeselectedPointAlpha = om.MObject() + aSelectedPointColor = om.MObject() + aSelectedPointAlpha = om.MObject() + aCurveColor = om.MObject() + aCurveAlpha = om.MObject() + aHullColor = om.MObject() + aHullAlpha = om.MObject() + aUseCVDistanceColor = om.MObject() + aUseCurveDistanceColor = om.MObject() + aUseHullDistanceColor = om.MObject() + aDistanceColorMin = om.MObject() + aDistanceColorMax = om.MObject() + aOccluderMeshName = om.MObject() + aUseCVOcclusion = om.MObject() + aShowCurve = om.MObject() + aShowHull = om.MObject() + aLazyUpdate = om.MObject() + + def __init__(self): + CALLBACK_IDS.append(om.MEventMessage.addEventCallback("SelectionChanged", onSelectionChanged)) + super(DrawManagerNode, self).__init__() + + @staticmethod + def creator(): # Creator method to be passed in initializePlugin + return DrawManagerNode() + + @staticmethod + def initialize(): # Init method to be passed in initializePlugin + # Init plugs attributes + numericAttr = om.MFnNumericAttribute() + typedAttr = om.MFnTypedAttribute() + + # Add attributes + DrawManagerNode.aDrawAlwaysOnTop = numericAttr.create("drawOnTop", "dot", om.MFnNumericData.kBoolean, True) + om.MPxNode.addAttribute(DrawManagerNode.aDrawAlwaysOnTop) + + DrawManagerNode.aPointSize = numericAttr.create("pointSize", "psize", om.MFnNumericData.kFloat, 10.0) + numericAttr.setMin(1) + numericAttr.setMax(100) + om.MPxNode.addAttribute(DrawManagerNode.aPointSize) + + DrawManagerNode.aLineWidth = numericAttr.create("lineWidth", "lwidth", om.MFnNumericData.kFloat, 4.0) + numericAttr.setMin(1) + numericAttr.setMax(100) + om.MPxNode.addAttribute(DrawManagerNode.aLineWidth) + + DrawManagerNode.aHullWidth = numericAttr.create("hullWidth", "hwidth", om.MFnNumericData.kFloat, 3.0) + numericAttr.setMin(1) + numericAttr.setMax(100) + om.MPxNode.addAttribute(DrawManagerNode.aHullWidth) + + DrawManagerNode.aCurveDivisions = numericAttr.create("curveDivisions", "crvdiv", om.MFnNumericData.kInt, 5) + numericAttr.setMin(2) + numericAttr.setMax(64) + om.MPxNode.addAttribute(DrawManagerNode.aCurveDivisions) + + DrawManagerNode.aDeselectedPointColor = numericAttr.create("deselectedPointColor", "dpcolor", om.MFnNumericData.k3Float) + numericAttr.default = (1.0, 0, 0) + numericAttr.usedAsColor = True + om.MPxNode.addAttribute(DrawManagerNode.aDeselectedPointColor) + + DrawManagerNode.aDeselectedPointAlpha = numericAttr.create("deselectedPointAlpha", "dpalpha", om.MFnNumericData.kFloat, 1.0) + numericAttr.setMin(0.0) + numericAttr.setMax(1.0) + om.MPxNode.addAttribute(DrawManagerNode.aDeselectedPointAlpha) + + DrawManagerNode.aSelectedPointColor = numericAttr.create("selectedPointColor", "spcolor", om.MFnNumericData.k3Float) + numericAttr.default = (0, 1.0, 0) + numericAttr.usedAsColor = True + om.MPxNode.addAttribute(DrawManagerNode.aSelectedPointColor) + + DrawManagerNode.aSelectedPointAlpha = numericAttr.create("selectedPointAlpha", "spalpha", om.MFnNumericData.kFloat, 1.0) + numericAttr.setMin(0.0) + numericAttr.setMax(1.0) + om.MPxNode.addAttribute(DrawManagerNode.aSelectedPointAlpha) + + DrawManagerNode.aCurveColor = numericAttr.create("curveColor", "ccolor", om.MFnNumericData.k3Float) + numericAttr.default = (0, 0, 1.0) + numericAttr.usedAsColor = True + om.MPxNode.addAttribute(DrawManagerNode.aCurveColor) + + DrawManagerNode.aCurveAlpha = numericAttr.create("curveAlpha", "calpha", om.MFnNumericData.kFloat, 1.0) + numericAttr.setMin(0.0) + numericAttr.setMax(1.0) + om.MPxNode.addAttribute(DrawManagerNode.aCurveAlpha) + + DrawManagerNode.aHullColor = numericAttr.create("hullColor", "hcolor", om.MFnNumericData.k3Float) + numericAttr.default = (0.5, 0, 0.5) + numericAttr.usedAsColor = True + om.MPxNode.addAttribute(DrawManagerNode.aHullColor) + + DrawManagerNode.aHullAlpha = numericAttr.create("hullAlpha", "halpha", om.MFnNumericData.kFloat, 1.0) + numericAttr.setMin(0.0) + numericAttr.setMax(1.0) + om.MPxNode.addAttribute(DrawManagerNode.aHullAlpha) + + DrawManagerNode.aUseCVDistanceColor = numericAttr.create("useCVDistanceColor", "usecvdcolor", om.MFnNumericData.kBoolean, True) + om.MPxNode.addAttribute(DrawManagerNode.aUseCVDistanceColor) + + DrawManagerNode.aUseHullDistanceColor = numericAttr.create( + "useHullDistanceColor", "usehulldcolor", om.MFnNumericData.kBoolean, True) + om.MPxNode.addAttribute(DrawManagerNode.aUseHullDistanceColor) + + DrawManagerNode.aUseCurveDistanceColor = numericAttr.create( + "useCurveDistanceColor", "usecurvedcolor", om.MFnNumericData.kBoolean, True) + om.MPxNode.addAttribute(DrawManagerNode.aUseCurveDistanceColor) + + DrawManagerNode.aDistanceColorMin = numericAttr.create("distanceColorMin", "dcolormin", om.MFnNumericData.kFloat, 0.25) + numericAttr.setMin(0.0) + numericAttr.setMax(1.0) + om.MPxNode.addAttribute(DrawManagerNode.aDistanceColorMin) + + DrawManagerNode.aDistanceColorMax = numericAttr.create("distanceColorMax", "dcolormax", om.MFnNumericData.kFloat, 1.0) + numericAttr.setMin(0.0) + numericAttr.setMax(1.0) + om.MPxNode.addAttribute(DrawManagerNode.aDistanceColorMax) + + DrawManagerNode.aOccluderMeshName = typedAttr.create("occluderMeshName", "oclmeshname", om.MFnData.kString) + om.MPxNode.addAttribute(DrawManagerNode.aOccluderMeshName) + + DrawManagerNode.aUseCVOcclusion = numericAttr.create("useCVOcclusion", "usecvocclusion", om.MFnNumericData.kBoolean, False) + om.MPxNode.addAttribute(DrawManagerNode.aUseCVOcclusion) + + DrawManagerNode.aShowCurve = numericAttr.create("showCurve", "showcurve", om.MFnNumericData.kBoolean, True) + om.MPxNode.addAttribute(DrawManagerNode.aShowCurve) + + DrawManagerNode.aShowHull = numericAttr.create("showHull", "showhull", om.MFnNumericData.kBoolean, False) + om.MPxNode.addAttribute(DrawManagerNode.aShowHull) + + DrawManagerNode.aLazyUpdate = numericAttr.create("lazyUpdate", "lazyupdate", om.MFnNumericData.kBoolean, False) + om.MPxNode.addAttribute(DrawManagerNode.aLazyUpdate) + + +# ------------ Draw Manager Data ------------ +class UserData(om.MUserData): + + def __init__(self): + super(UserData, self).__init__(False) + + self.curveDataArray = [] # type: list[dict] + self.processedCurvePoints = [] # type: list[om.MPointArray] + self.processedCurveColors = [] # type: list[om.MColorArray] + + self.pointDataArray = [] # type: list[dict] + + # Sorted and colored CV arrays + self.cvPosArray = om.MPointArray() + self.cvColorArray = om.MColorArray() + + self.rootCVsArray = om.MPointArray() + self.rootCVsColors = om.MColorArray() + + # Paired and colored hull arrays + self.hullPosArray = [] # List of MPointArray + self.hullColorArray = [] # list of MColorArray + + self.pointSize = 10.0 + self.lineWidth = 4.0 + self.hullWidth = 3.0 + self.curveDivisions = 5 # type: int + self.selectedPointColor = om.MColor((0, 1.0, 0.0, 1.0)) + self.deselectedPointColor = om.MColor((1.0, 0.0, 0.0, 1.0)) + self.curveColor = om.MColor((0, 0, 1.0, 1.0)) + self.hullColor = om.MColor((.5, 0, .5, 1.0)) + + self.drawAlwaysOnTop = True # type: bool + self.drawCurve = False # type: bool + self.drawHull = False # type: bool + + def clear(self): + self.pointDataArray = [] + self.processedCurvePoints = [] + self.processedCurveColors = [] + self.curveDataArray = [] + self.hullPosArray = [] + self.hullColorArray = [] + self.cvPosArray.clear() + self.cvColorArray.clear() + self.rootCVsArray.clear() + self.rootCVsColors.clear() + + +# ------------ Draw Manager Draw Override ------------ +class DrawOverride(omr.MPxDrawOverride): + + nodeState = 0 + selectionInitialized = False + softSelectionActive = False + selectionList = om.MSelectionList() + previousSelectionString = "" + + def __init__(self, obj): + super(DrawOverride, self).__init__(obj, None) + self.thisMObject = obj # type: om.MObject + + # Cache + self.CVCache = {} # type: dict[str, om.MPointArray] + self.splinePointsCache = {} # type: dict[str, om.MPointArray] + self.oldCurveDivisions = 0 # type: int + + @staticmethod + def creator(obj): + return DrawOverride(obj) + + def supportedDrawAPIs(self): + return omr.MRenderer.kAllDevices + + def hasUIDrawables(self): + return True + + def prepareForDraw(self, objPath, cameraPath, frameContext, oldData): + # type: (om.MDagPath, om.MDagPath, omr.MFrameContext, om.MUserData) -> om.MUserData + if self.thisMObject.isNull(): + return + data = oldData + if not isinstance(data, UserData): + data = UserData() + + data.clear() + + lazyUpdate = om.MPlug(self.thisMObject, DrawManagerNode.aLazyUpdate).asBool() # type: bool + + # Get selection list from callback + if not DrawOverride.selectionList or not lazyUpdate: + onSelectionChanged() + sel = DrawOverride.selectionList + + # Clear cache if no selection exists + if sel.isEmpty(): + self.CVCache.clear() + self.splinePointsCache.clear() + return data + + # Getting the node state and bypassing if not "Normal" + if mc.getAttr(objPath.fullPathName() + '.nodeState') != 0: + return data + + # Get the data from plugs + data = self.updateDataFromPlugs(data) + + # Check if selection strings are the same + if data.drawCurve: + selectionString = "".join(self.selectionList.getSelectionStrings()) # type: str + if selectionString != self.previousSelectionString: + # print("Clearing CV and SplinePointsCache") + self.CVCache.clear() + self.splinePointsCache.clear() + self.previousSelectionString = selectionString + + # Iterate over curves + processedCurves = set() + curveIndex = 0 # type: int + for selIndex in range(sel.length()): + try: + dag = sel.getDagPath(selIndex).extendToShape() # type: om.MDagPath + except BaseException: + continue + + if dag.apiType() != 267: + continue + + fullName = dag.fullPathName() # type: str + + # Check if current curve was processed already + if fullName in processedCurves: + continue + + # Create curve function set and get CVs and curve points + curve = om.MFnNurbsCurve(dag) + cvPos, curvePoints = self.processMayaCurve(curve, fullName, data, curveIndex) + data.curveDataArray += curvePoints + processedCurves.add(fullName) # Prevent from double processing of curves + + # Check selection display status (selection context) + displayStatus = omui.M3dView.displayStatus(dag) # type: int + if displayStatus != 4 and displayStatus != 2: + curveIndex += 1 + continue + + # Check if selection list item has components + comps = sel.getComponent(selIndex) # type: tuple[om.MDagPath, om.MObject] + if comps[1] != om.MObject.kNullObj: + compFn = om.MFnSingleIndexedComponent(comps[1]) + + if compFn.componentType != om.MFn.kCurveCVComponent: + data.clear() + return data + + selectedIDs = om.MIntArray() + selectedWeights = om.MFloatArray() + # Getting soft selection component weights and IDs + for i in range(compFn.elementCount): + weight = 1.0 + if compFn.hasWeights: + weight = compFn.weight(i).influence # type: float + selectedIDs.append(compFn.element(i)) + selectedWeights.append(weight) + # Adding selected CVs and their colors to data + for i, index in enumerate(selectedIDs): + pointData = PointData.copy() + pointData["curveIndex"] = curveIndex + pointData["cvIndex"] = selectedIDs[i] + pointData["position"] = om.MPoint(cvPos[index]) + pointData["isSelected"] = True + pointData["hasWeight"] = True + pointData["weight"] = selectedWeights[i] + data.pointDataArray.append(pointData) + # Adding deselected CVs and their colors to data + for i in range(len(cvPos)): + if i not in selectedIDs: + pointData = PointData.copy() + pointData["curveIndex"] = curveIndex + pointData["cvIndex"] = i + pointData["position"] = om.MPoint(cvPos[i]) + data.pointDataArray.append(pointData) + else: + # If no components selected, just add all the CVs with deselected colors + for i in range(len(cvPos)): + pointData = PointData.copy() + pointData["curveIndex"] = curveIndex + pointData["cvIndex"] = i + pointData["position"] = om.MPoint(cvPos[i]) + data.pointDataArray.append(pointData) + curveIndex += 1 + + # Skip everything else if there were no processed curves + if not processedCurves: + data.clear() + return data + + # Process draw indices (depth sorting) + camera = om.MFnCamera(cameraPath) + camTransform = om.MFnDagNode(camera.parent(0)) + camTransformMatrix = camTransform.transformationMatrix() # type: om.MMatrix + cameraPoint = om.MPoint( + camTransformMatrix.getElement(3, 0), + camTransformMatrix.getElement(3, 1), + camTransformMatrix.getElement(3, 2) + ) + + # Add camera distances + for point in data.pointDataArray: + point["distance"] = point["position"].distanceTo(cameraPoint) + data.pointDataArray.sort(key=lambda p: p["distance"], reverse=True) + + # Ray-casting for occlusion of verts + useOcclusion = om.MPlug(self.thisMObject, DrawManagerNode.aUseCVOcclusion).asBool() # type: bool + if useOcclusion: + self.calculateOcclusion(data, cameraPoint) + + # Optional color changed based on the distance from camera + useCVDistanceColor = om.MPlug(self.thisMObject, DrawManagerNode.aUseCVDistanceColor).asBool() # type: bool + useHullDistanceColor = om.MPlug(self.thisMObject, DrawManagerNode.aUseHullDistanceColor).asBool() # type: bool + useCurveDistanceColor = om.MPlug(self.thisMObject, DrawManagerNode.aUseCurveDistanceColor).asBool() # type: bool + + distanceColorMin = om.MPlug(self.thisMObject, DrawManagerNode.aDistanceColorMin).asFloat() # type: float + distanceColorMax = om.MPlug(self.thisMObject, DrawManagerNode.aDistanceColorMax).asFloat() # type: float + if distanceColorMax < distanceColorMin: + distanceColorMax = distanceColorMin + + # Process color multiplier + if not self.softSelectionActive and (useCVDistanceColor or useHullDistanceColor or useCurveDistanceColor): + for i, point in enumerate(data.pointDataArray): + point["colorMult"] = i / float(len(data.pointDataArray)) + + # Process curve points + if data.drawCurve: + self.processCurvePoints(data, cameraPoint, useCurveDistanceColor, distanceColorMin, distanceColorMax) + + # Process CVs + for point in data.pointDataArray: + if point["cvIndex"] == 0: + x, y, _ = omui.M3dView.active3dView().worldToView(point["position"]) + data.rootCVsArray.append(om.MPoint(x, y, 0)) + rootColor = om.MColor() + if point["isSelected"]: + rootColor = data.selectedPointColor + if useCVDistanceColor: + rootColor = data.selectedPointColor * max(point["weight"] * point["colorMult"] * distanceColorMax, distanceColorMin) + else: + rootColor = data.deselectedPointColor + if useCVDistanceColor: + rootColor = data.deselectedPointColor * max(point["colorMult"] * distanceColorMax, distanceColorMin) + data.rootCVsColors.append(rootColor) + + data.cvPosArray.append(point["position"]) + color = om.MColor() + if point["isSelected"]: + if useCVDistanceColor: + color = data.selectedPointColor * max(point["weight"] * point["colorMult"] * + distanceColorMax, distanceColorMin) # type: om.MColor + else: + color = data.selectedPointColor * max(point["weight"] * distanceColorMax, distanceColorMin) # type: om.MColor + else: + color = data.deselectedPointColor + if useCVDistanceColor: + color = data.deselectedPointColor * max(point["colorMult"] * distanceColorMax, distanceColorMin) + color.a = color.a * point["alphaMult"] + data.cvColorArray.append(color) + + # Process hull points + if data.drawHull: + self.processHullPoints(data, useHullDistanceColor, distanceColorMin, distanceColorMax) + else: + data.hullPosArray = [] + data.hullColorArray = [] + + return data + + def addUIDrawables(self, objPath, drawManager, frameContext, userData): + # type: (om.MDagPath, omr.MUIDrawManager, omr.MFrameContext, om.MUserData) -> None + if not isinstance(userData, UserData): + return + + if mc.getAttr(objPath.fullPathName() + '.nodeState') != 0: + return + + if userData.drawAlwaysOnTop: + drawManager.beginDrawInXray() + else: + drawManager.beginDrawable() + + # Draw curve + if userData.drawCurve: + drawManager.setLineWidth(userData.lineWidth) + for i in range(len(userData.processedCurvePoints)): + drawManager.mesh(drawManager.kLines, userData.processedCurvePoints[i], None, + userData.processedCurveColors[i], None, None) + + # Draw hull + if userData.drawHull: + for i in range(len(userData.hullPosArray)): + drawManager.setLineWidth(userData.hullWidth) + drawManager.mesh(drawManager.kLines, + userData.hullPosArray[i], + None, + userData.hullColorArray[i]) + + # Draw CVs + if len(userData.cvPosArray) > 0: + drawManager.setPointSize(userData.pointSize) + drawManager.mesh(drawManager.kPoints, + userData.cvPosArray, + None, + userData.cvColorArray) + # Draw root CV + drawManager.setLineWidth(1.0) + for i in range(len(userData.rootCVsArray)): + drawManager.setColor(userData.rootCVsColors[i]) + drawManager.circle2d(userData.rootCVsArray[i], userData.pointSize / 2.0 + 4.0, False) + + if userData.drawAlwaysOnTop: + drawManager.endDrawInXray() + else: + drawManager.endDrawable() + + def updateDataFromPlugs(self, data): + # type: (UserData) -> UserData + # Getting attributes + data.drawAlwaysOnTop = om.MPlug(self.thisMObject, DrawManagerNode.aDrawAlwaysOnTop).asBool() + data.pointSize = om.MPlug(self.thisMObject, DrawManagerNode.aPointSize).asFloat() + data.lineWidth = om.MPlug(self.thisMObject, DrawManagerNode.aLineWidth).asFloat() + data.hullWidth = om.MPlug(self.thisMObject, DrawManagerNode.aHullWidth).asFloat() + data.curveDivisions = om.MPlug(self.thisMObject, DrawManagerNode.aCurveDivisions).asInt() + + # Selected points color + selectedPointColorPlug = om.MPlug(self.thisMObject, DrawManagerNode.aSelectedPointColor).asMObject() # type: om.MObject + selectedPointColorData = om.MFnNumericData(selectedPointColorPlug) + data.selectedPointColor = om.MColor(selectedPointColorData.getData()) + pointAlphaPlug = om.MPlug(self.thisMObject, DrawManagerNode.aSelectedPointAlpha) + data.selectedPointColor.a = pointAlphaPlug.asFloat() + + # Deselected points color + deselectedPointColorPlug = om.MPlug(self.thisMObject, DrawManagerNode.aDeselectedPointColor).asMObject() # type: om.MObject + deselectedPointColorData = om.MFnNumericData(deselectedPointColorPlug) + data.deselectedPointColor = om.MColor(deselectedPointColorData.getData()) + pointAlphaPlug = om.MPlug(self.thisMObject, DrawManagerNode.aDeselectedPointAlpha) + data.deselectedPointColor.a = pointAlphaPlug.asFloat() + + # Curve color + curveColorPlug = om.MPlug(self.thisMObject, DrawManagerNode.aCurveColor).asMObject() # type: om.MObject + curveColorData = om.MFnNumericData(curveColorPlug) + data.curveColor = om.MColor(curveColorData.getData()) + curveAlphaPlug = om.MPlug(self.thisMObject, DrawManagerNode.aCurveAlpha) + data.curveColor.a = curveAlphaPlug.asFloat() + + # Hull color + hullColorPlug = om.MPlug(self.thisMObject, DrawManagerNode.aHullColor).asMObject() # type: om.MObject + hullColorPlugData = om.MFnNumericData(hullColorPlug) + data.hullColor = om.MColor(hullColorPlugData.getData()) + hullAlphaPlug = om.MPlug(self.thisMObject, DrawManagerNode.aCurveAlpha) + data.hullColor.a = hullAlphaPlug.asFloat() + + data.drawCurve = om.MPlug(self.thisMObject, DrawManagerNode.aShowCurve).asBool() + data.drawHull = om.MPlug(self.thisMObject, DrawManagerNode.aShowHull).asBool() + + return data + + def processMayaCurve(self, curve, name, data, curveIndex): + # type: (om.MFnNurbsCurve, str, UserData, int) -> tuple[om.MPointArray, list] + cvPos = curve.cvPositions(space=om.MSpace.kWorld) # type: om.MPointArray + + # Check if curve is being drawn at all + if not data.drawCurve: + return cvPos, [] + + needsUpdate = False + if self.oldCurveDivisions != data.curveDivisions: + needsUpdate = True + self.oldCurveDivisions = data.curveDivisions + + if self.CVCache and name in self.CVCache and len(self.CVCache[name]) == len(cvPos) and not needsUpdate: + for i, cv in enumerate(cvPos): + if not cv.isEquivalent(self.CVCache[name][i]): + needsUpdate = True + break + else: + needsUpdate = True + + if not needsUpdate: + return cvPos, self.splinePointsCache[name] + + self.CVCache.update({name: cvPos}) + + splinePoints = [] + knotDomainMin = curve.knotDomain[0] # type: float + knotDomainMax = curve.knotDomain[1] # type: float + r = knotDomainMin # type: float + step = knotDomainMax / (curve.numSpans * data.curveDivisions) # type: float + index = 0 + while r < knotDomainMax: + pointAtParam = curve.getPointAtParam(r, space=om.MSpace.kWorld) # type: om.MPoint + curveData = CurveData.copy() + curveData["curveIndex"] = curveIndex + curveData["position"] = pointAtParam + curveData["color"] = data.curveColor + curveData["pointIndex"] = index + splinePoints.append(curveData) + if r + step >= knotDomainMax: + index += 1 + curveData = CurveData.copy() + pointAtParam = curve.getPointAtParam(knotDomainMax - 0.0001, space=om.MSpace.kWorld) + curveData["curveIndex"] = curveIndex + curveData["position"] = pointAtParam + curveData["color"] = data.curveColor + curveData["pointIndex"] = index + splinePoints.append(curveData) + break + r += step + index += 1 + self.splinePointsCache.update({name: splinePoints}) + return cvPos, splinePoints + + def calculateOcclusion(self, data, cameraPoint): + # type: (UserData, om.MPoint) -> UserData + try: + occluderMeshName = om.MPlug(self.thisMObject, DrawManagerNode.aOccluderMeshName).asString() + meshSel = om.MSelectionList().add(occluderMeshName) # type: om.MSelectionList + meshSel = meshSel.getDagPath(0) + occluderMesh = om.MFnMesh(meshSel) + params = occluderMesh.autoUniformGridParams() # type: om.MMeshIsectAccelParams + for pointData in data.pointDataArray: + result = occluderMesh.anyIntersection(om.MFloatPoint(cameraPoint), + om.MFloatVector(pointData["position"]) - om.MFloatVector(cameraPoint), + om.MSpace.kWorld, + 1.0, + False, + accelParams=params) + if result: + pointData["alphaMult"] = 0.0 + except BaseException: + pass + return data + + def processCurvePoints(self, data, cameraPoint, useCurveDistanceColor, colorMin, colorMax): + # type: (UserData, om.MPoint, bool, float, float) -> UserData + for curve in data.curveDataArray: + curve["distance"] = curve["position"].distanceTo(cameraPoint) + + min_e = min(data.curveDataArray, key=lambda x: x["distance"]) # type: dict + max_e = max(data.curveDataArray, key=lambda x: x["distance"]) # type: dict + + splitCurves = dict() # type: dict[int, list[dict]] + + for curvePoint in data.curveDataArray: + if useCurveDistanceColor: + invLerp = 1 - (curvePoint["distance"] - min_e["distance"]) / (max_e["distance"] - min_e["distance"]) + curvePoint["color"] = data.curveColor * max(invLerp * colorMax, colorMin) + splitCurves.setdefault(curvePoint["curveIndex"], []).append(curvePoint) + + processedPointsArray = om.MPointArray() + processedColorsArray = om.MColorArray() + for curve in splitCurves.values(): + for i in range(1, len(curve)): + processedPointsArray.append(om.MPoint(curve[i - 1]["position"])) + processedPointsArray.append(om.MPoint(curve[i]["position"])) + processedColorsArray.append(curve[i - 1]["color"]) + processedColorsArray.append(curve[i]["color"]) + + data.processedCurvePoints.append(processedPointsArray) + data.processedCurveColors.append(processedColorsArray) + + def processHullPoints(self, data, distanceColors, colorMin, colorMax): + # type: (UserData, bool, float, float) -> UserData + # Split points into different curves + filteredPoints = dict() # type: dict[int, dict[int, om.MPoint]] + filteredColors = dict() # type: dict[int, dict[int, om.MPoint]] + for point in data.pointDataArray: + filteredPoints.setdefault(point["curveIndex"], dict()).update({point["cvIndex"]: point["position"]}) + color = om.MColor() + if point["isSelected"]: + if distanceColors: + color = data.selectedPointColor * max(point["weight"] * point["colorMult"] * colorMax, colorMin) # type: om.MColor + else: + color = data.selectedPointColor * max(point["weight"] * colorMax, colorMin) # type: om.MColor + else: + color = data.hullColor + if distanceColors: + color = data.hullColor * max(point["colorMult"] * colorMax, colorMin) + color.a = point["alphaMult"] * data.hullColor.a + filteredColors.setdefault(point["curveIndex"], dict()).update({point["cvIndex"]: color}) + + for i in range(len(filteredPoints)): # Curve index + try: + pointArray = om.MPointArray() + colorArray = om.MColorArray() + for j in range(1, len(filteredPoints[i])): # CV index + pointArray.append(om.MPoint(filteredPoints[i][j - 1])) + pointArray.append(om.MPoint(filteredPoints[i][j])) + colorArray.append(filteredColors[i][j - 1]) + colorArray.append(filteredColors[i][j]) + data.hullPosArray.append(pointArray) + data.hullColorArray.append(colorArray) + except BaseException: + pass + + +''' +# ------------ Init & UnInit Plugin ------------ +def initializePlugin(obj): + plugin = om.MFnPlugin(obj, "GeorgeSladkovsky", "1.3", "Any") + try: + plugin.registerNode( + "GSCT_CurveTools_DrawManagerNode", + DrawManagerNode.id, + DrawManagerNode.creator, + DrawManagerNode.initialize, + om.MPxNode.kLocatorNode, + DrawManagerNode.drawDbClassification) + except BaseException: + sys.stderr.write("Failed to register node\n") + raise + + try: + omr.MDrawRegistry.registerDrawOverrideCreator( + DrawManagerNode.drawDbClassification, + DrawManagerNode.drawRegistrantId, + DrawOverride.creator) + except BaseException: + sys.stderr.write("Failed to register override\n") + raise + + +def uninitializePlugin(obj): + global CALLBACK_IDS + om.MMessage.removeCallbacks(CALLBACK_IDS) + CALLBACK_IDS = [] + plugin = om.MFnPlugin(obj) + try: + plugin.deregisterNode(DrawManagerNode.id) + except BaseException: + sys.stderr.write("Failed to deregister node\n") + raise + + try: + omr.MDrawRegistry.deregisterGeometryOverrideCreator(DrawManagerNode.drawDbClassification, DrawManagerNode.drawRegistrantId) + except BaseException: + sys.stderr.write("Failed to deregister override\n") + raise +''' diff --git a/Scripts/Modeling/Edit/gs_curvetools/ui.py b/Scripts/Modeling/Edit/gs_curvetools/ui.py new file mode 100644 index 0000000..0eff014 --- /dev/null +++ b/Scripts/Modeling/Edit/gs_curvetools/ui.py @@ -0,0 +1,2924 @@ +""" + +License: +This collection of code named GS CurveTools is a property of George Sladkovsky (Yehor Sladkovskyi) +and can not be copied or distributed without his written permission. + +GS CurveTools v1.3.1 Studio +Copyright 2023, George Sladkovsky (Yehor Sladkovskyi) +All Rights Reserved + +Autodesk Maya is a property of Autodesk, Inc. + +Social Media and Contact Links: + +Discord Server: https://discord.gg/f4DH6HQ +Online Store: https://sladkovsky3d.artstation.com/store +Online Documentation: https://gs-curvetools.readthedocs.io/ +Twitch Channel: https://www.twitch.tv/videonomad +YouTube Channel: https://www.youtube.com/c/GeorgeSladkovsky +ArtStation Portfolio: https://www.artstation.com/sladkovsky3d +Contact Email: george.sladkovsky@gmail.com + +""" + +import os +from functools import partial as pa +from imp import reload + +import maya.cmds as mc +import maya.OpenMayaUI as omui +from PySide2 import QtCore, QtGui, QtWidgets +from shiboken2 import wrapInstance + +from . import core, uv_editor +from .constants import * +from .utils import gs_math as mt +from .utils import style, tooltips, utils, wrap +from .utils.utils import deferred, noUndo, undo +from .utils.wrap import WIDGETS + +reload(core) +reload(uv_editor) +reload(utils) +reload(wrap) +reload(style) + +MESSAGE = utils.logger +LOGGER = utils.logger.logger + +# Maya Main Window and Workspace + + +def mayaMainWindow(): + """ Get Maya main window pointer""" + mayaWindow = wrapInstance(int(omui.MQtUtil.mainWindow()), QtWidgets.QMainWindow) + return mayaWindow + + +def mayaWorkspaceControl(name, label, retain=True, iw=164, ih=783, widthProperty='preferred'): + """ Create Maya Dockable Workspace """ + try: + control = mc.workspaceControl(name, l=label, r=retain, li=True, vis=True, wp=widthProperty, iw=iw, ih=ih, mw=iw, mh=100) + except BaseException: + LOGGER.debug("Using legacy workspace control") + control = mc.workspaceControl(name, l=label, r=retain, li=True, vis=True, wp=widthProperty, iw=iw, ih=ih, mw=iw) + qtControl = omui.MQtUtil.findControl(control) + dock = wrapInstance(int(qtControl), QtWidgets.QWidget) + wrap.WIDGETS[name] = dock + + return dock + + +def curveControlWorkspace(): + if mc.workspaceControl(CURVE_CONTROL_NAME, q=1, ex=1): + if MAYA_VER >= 2018: + if not mc.workspaceControl(CURVE_CONTROL_NAME, q=1, vis=1): + mc.workspaceControl(CURVE_CONTROL_NAME, e=1, rs=1) + deferred(core.updateMainUI)() + else: + mc.workspaceControl(CURVE_CONTROL_NAME, e=1, vis=0) + else: + mc.workspaceControl(CURVE_CONTROL_NAME, e=1, fl=1) + mc.deleteUI(CURVE_CONTROL_NAME) + else: + CurveControlUI() + core.curveControlUI.updateUI() + from . import main + main.checkScriptJobs(CURVE_CONTROL_NAME) + + +class CurveControlUI(QtWidgets.QWidget): + + def __init__(self): + parent = mayaWorkspaceControl(name=CURVE_CONTROL_NAME, + label=CURVE_CONTROL_LABEL, + retain=False, iw=350, ih=750, widthProperty="free") + # Dockable Workspace Connection + super(CurveControlUI, self).__init__(parent) + self.ui() + parent.layout().addWidget(self) + + def ui(self): + # Layout + mainLayout = QtWidgets.QVBoxLayout(self) + mainLayout.setContentsMargins(*style.scale([2, 0, 2, 0])) + + self.scrollWidget = QtWidgets.QWidget() + layout = QtWidgets.QVBoxLayout(self.scrollWidget) + scrollArea = QtWidgets.QScrollArea() + scrollArea.setWidget(self.scrollWidget) + mainLayout.addWidget(scrollArea) + + # Layout Settings + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(style.scale(2)) + layout.setMargin(0) + layout.setAlignment(QtCore.Qt.AlignTop) + + scrollArea.setFrameShape(QtWidgets.QFrame.NoFrame) + scrollArea.setWidgetResizable(True) + + # Main Controls + layout.addWidget(wrap.separator()) + + # Layer Selector, Color Picker, Curve Name and Line Thickness + with wrap.Row(layout, 'gsCurveControlHeader') as row: + layerSelector = wrap.LayerSelector('gsLayerSelector', row.layout()) + layerSelector.setFixedWidth(style.scale(40)) + layerSelector.currentIndexChanged.connect(core.changeLayerViaOptionMenu) + + layerColorPicker = wrap.ColorPicker('gsColorPicker', row.layout()) + layerColorPicker.setContentsMargins(*style.scale([3, 3, 1, 3])) + layerColorPicker.setFixedWidth(style.scale(20)) + layerColorPicker.connectCommand(core.toggleColor.changeLayerColor) + + curveColorPicker = wrap.ColorPicker('gsCurveColorPicker', row.layout()) + curveColorPicker.setContentsMargins(*style.scale([1, 3, 2, 3])) + curveColorPicker.setFixedWidth(style.scale(20)) + curveColorPicker.connectCommand(core.toggleColor.changeCurveColor) + + selectedObjectName = wrap.LineEdit('selectedObjectName', row.layout()) + selectedObjectName.setPlaceholderText('Select Curve') + selectedObjectName.returnPressed.connect(undo(core.renameSelected)) + + lineThickness = wrap.FloatField('lineWidth', row.layout()) + lineThickness.setFixedWidth(style.scale(35)) + lineThickness.setRange(-1, 10) + lineThickness.setValue(-1) + lineThickness.setDragCommand(pa(core.sliders.curveControlSliderDrag, lineThickness)) + lineThickness.setReleaseCommand(core.sliders.release) + + layout.addWidget(wrap.separator()) + + # Select Curves Prompt + label = wrap.Label(layout, 'selectCurvesPrompt') + label.setLabel('Select Compatible Curves') + label.setFontSize(14) + label.setVisible(False) + + # Axis Control + with wrap.Column(layout, 'axisFrame') as column: + label = wrap.Label(column.layout()) + label.setLabel('Axis Control') + label.setFontSize(14) + with wrap.Row(column.layout()) as row: + WIDGETS['Axis'] = axisButtonGrp = QtWidgets.QButtonGroup() + + axisAuto = wrap.Button(row.layout(), 'gsBindAxisAuto') + axisAuto.setLabel('Auto', lineHeight=100) + axisAuto.setCheckable(True) + axisAuto.setButtonStyle('small') + axisAuto.setChecked(True) + axisAuto.clicked.connect(pa(undo(core.applyAxis), 0)) + + axisX = wrap.Button(row.layout(), 'gsBindAxisX') + axisX.setLabel('X', lineHeight=100) + axisX.setCheckable(True) + axisX.setButtonStyle('small') + axisX.clicked.connect(pa(undo(core.applyAxis), 1)) + + axisY = wrap.Button(row.layout(), 'gsBindAxisY') + axisY.setLabel('Y', lineHeight=100) + axisY.setCheckable(True) + axisY.setButtonStyle('small') + axisY.clicked.connect(pa(undo(core.applyAxis), 2)) + + axisZ = wrap.Button(row.layout(), 'gsBindAxisZ') + axisZ.setLabel('Z', lineHeight=100) + axisZ.setCheckable(True) + axisZ.setButtonStyle('small') + axisZ.clicked.connect(pa(undo(core.applyAxis), 3)) + + axisButtonGrp.addButton(axisAuto, 0) + axisButtonGrp.addButton(axisX, 1) + axisButtonGrp.addButton(axisY, 2) + axisButtonGrp.addButton(axisZ, 3) + + axisFlip = wrap.Button(row.layout(), 'AxisFlip') + axisFlip.setLabel('Flip', lineHeight=100) + axisFlip.setCheckable(True) + axisFlip.setButtonStyle('small') + axisFlip.clicked.connect(pa(undo(core.applyAxis), -1)) + + column.layout().addWidget(wrap.separator()) + + # Edit Original Objects + with wrap.Row(column.layout(), margins=style.scale([0, 0, 0, 0]), objName='originalCurvesRow') as originalCurvesRow: + selectOriginalCurves = wrap.Button(originalCurvesRow.layout(), 'selectOriginalCurves') + selectOriginalCurves.setLabel('Select Original Curves', lineHeight=100) + selectOriginalCurves.setButtonStyle('small-filled') + selectOriginalCurves.clicked.connect(undo(core.curveControlUI.selectOriginalObjects)) + + editOrigObj = wrap.Button(originalCurvesRow.layout(), 'editOrigObj') + editOrigObj.setLabel('Edit Original Objects', lineHeight=100) + editOrigObj.setCheckable(True) + editOrigObj.setButtonStyle('small') + editOrigObj.clicked.connect(undo(core.curveControlUI.editOriginalObjects)) + + column.layout().addWidget(wrap.separator()) + + # Length Divisions + with wrap.Row(layout, margins=style.scale([0, 0, 3, 0])) as lengthDivisionsRow: + lengthDivision = wrap.ControlSlider(objName='lengthDivisions', typ='int') + lengthDivision.setLabel('L-Div') + lengthDivision.setMinMax(2, 100) + lengthDivision.setFieldMinMax(2, 10000) + lengthDivision.setValue(2) + lengthDivision.setDragCommand(pa(core.sliders.curveControlSliderDrag, lengthDivision)) + lengthDivision.setReleaseCommand(core.sliders.release) + + dynamicDivisionsToggle = wrap.Button(objName='dynamicDivisions') + dynamicDivisionsToggle.setLabel('Auto') + dynamicDivisionsToggle.setFixedWidth(style.scale(35)) + dynamicDivisionsToggle.setButtonStyle('small') + dynamicDivisionsToggle.setCheckable(True) + dynamicDivisionsToggle.clicked.connect(undo(core.toggleDynamicDivisions)) + + lengthDivisionsRow.layout().addWidget(lengthDivision, 4) + lengthDivisionsRow.layout().addWidget(dynamicDivisionsToggle, 1) + + # Width Divisions + widthDivision = wrap.ControlSlider(layout, 'widthDivisions', 'int') + widthDivision.setLabel('W-Div') + widthDivision.setMinMax(2, 31) + widthDivision.setFieldMinMax(2, 10000) + widthDivision.setValue(2) + widthDivision.setDragCommand(pa(core.sliders.curveControlSliderDrag, widthDivision)) + widthDivision.setReleaseCommand(core.sliders.release) + + # Orientation + orientation = wrap.ControlSlider(layout, 'Orientation', 'float') + orientation.setLabel('Orien') + orientation.setMinMax(-180, 180) + orientation.setFieldMinMax(-36000, 36000) + orientation.setPrecision(1) + orientation.setStep(0.5) + orientation.setDragCommand(pa(core.sliders.curveControlSliderDrag, orientation)) + orientation.setReleaseCommand(core.sliders.release) + + # Twist + twist = wrap.ControlSlider(layout, 'Twist', 'float') + twist.setLabel('Twist') + twist.setMinMax(-180, 180) + twist.setFieldMinMax(-36000, 36000) + twist.setPrecision(1) + twist.setStep(0.5) + twist.setDragCommand(pa(core.sliders.curveControlSliderDrag, twist)) + twist.setReleaseCommand(core.sliders.release) + + # Twist + invTwist = wrap.ControlSlider(layout, 'invTwist', 'float') + invTwist.setLabel('Inv.Twist') + invTwist.setMinMax(-180, 180) + invTwist.setFieldMinMax(-36000, 36000) + invTwist.setPrecision(1) + invTwist.setStep(0.5) + invTwist.setDragCommand(pa(core.sliders.curveControlSliderDrag, invTwist)) + invTwist.setReleaseCommand(core.sliders.release) + + # Twist Curve Graph + with wrap.Frame(layout, 'twistCurveFrame', margins=[0, 1, 0, 2]) as twistCurveFrame: + twistCurveFrame.frameButton.setLabel('Twist Curve Graph') + twistGraph = wrap.FallOffCurve(twistCurveFrame.getFrameLayout(), 'twistCurve') + + def twistGraphCommand(_): # BUG: When undoing multiple edits, last edit is not reverted + core.attributes.propagateGraphs(twistGraph) + core.attributes.storeGraphs(twistGraph) + twistGraph.changeCommand(twistGraphCommand) + + with wrap.Row(twistCurveFrame.getFrameLayout(), margins=style.scale([5, 0, 5, 2])) as row: + row.setFixedHeight(style.BUTTON_HEIGHT) + + magnitude = wrap.FloatField('Magnitude', row.layout()) + magnitude.setFixedWidth(style.scale(45)) + magnitude.setRange(-99, 99) + magnitude.setStep(0.01) + magnitude.setPrecision(2) + magnitude.setDragCommand(pa(core.sliders.curveControlSliderDrag, magnitude)) + magnitude.setReleaseCommand(core.sliders.release) + + resetButton = wrap.Button(row.layout(), 'gsTwistGraphResetButton') + resetButton.setLabel('Reset Curve') + + def resetTwistCmd(): + utils.resetSingleGraph('twist') + core.attributes.storeGraphs(twistGraph) + resetButton.clicked.connect(undo(resetTwistCmd)) + + popOutButton = wrap.Button(row.layout(), 'gsTwistGraphPopOut') + popOutButton.setFixedWidth(style.scale(56)) + popOutButton.setLabel('^') + popOutButton.clicked.connect(self.twistGraphPopOut) + + # Width + width = wrap.ControlSlider(layout, 'Width', 'float') + width.setLabel('Width') + width.setMinMax(0.001, 5) + width.setFieldMinMax(0.001, 1000) + width.setPrecision(3) + width.setStep(0.001) + width.setValue(1) + width.setDragCommand(pa(core.sliders.curveControlSliderDrag, width)) + width.setReleaseCommand(core.sliders.release) + + # Taper + taper = wrap.ControlSlider(layout, 'Taper', 'float') + taper.setLabel('Taper') + taper.setMinMax(0.001, 3) + taper.setFieldMinMax(0.001, 1000) + taper.setPrecision(3) + taper.setStep(0.001) + taper.setValue(1) + taper.setDragCommand(pa(core.sliders.curveControlSliderDrag, taper)) + taper.setReleaseCommand(core.sliders.release) + + # Width for Tubes + with wrap.Row(layout, objName='widthComboSlider', spacing=0) as row: + with wrap.Column(row.layout()) as column: + widthX = wrap.ControlSlider(column.layout(), "WidthX", 'float') + widthX.setLabel("WidthX") + widthX.setMinMax(0.001, 5) + widthX.setFieldMinMax(0.001, 1000) + widthX.setPrecision(3) + widthX.setStep(0.001) + widthX.setValue(1) + widthX.setDragCommand(pa(core.sliders.curveControlSliderDrag, widthX)) + widthX.setReleaseCommand(core.sliders.release) + + widthZ = wrap.ControlSlider(column.layout(), "WidthZ", 'float') + widthZ.setLabel("WidthZ") + widthZ.setMinMax(0.001, 5) + widthZ.setFieldMinMax(0.001, 1000) + widthZ.setPrecision(3) + widthZ.setStep(0.001) + widthZ.setValue(1) + widthZ.setDragCommand(pa(core.sliders.curveControlSliderDrag, widthZ)) + widthZ.setReleaseCommand(core.sliders.release) + + lockButton = wrap.IconCheckButton(row.layout(), 'widthLockSwitch') + lockButton.setIcons(utils.getFolder.icons() + 'sliderLock_en_reversed.png', + utils.getFolder.icons() + 'sliderLock_reversed.png') + lockButton.setIconWidthHeight(10, 30) + lockButton.setChecked(True) + + # Width Curve Graph + with wrap.Frame(layout, 'widthCurveFrame', margins=[0, 1, 0, 2]) as widthCurveFrame: + widthCurveFrame.frameButton.setLabel('Width Curve Graph') + widthGraph = wrap.FallOffCurve(widthCurveFrame.getFrameLayout(), 'scaleCurve') + + def widthGraphCommand(_): # BUG: When undoing multiple edits, last edit is not reverted + core.attributes.propagateGraphs(widthGraph) + core.attributes.storeGraphs(widthGraph) + widthGraph.changeCommand(widthGraphCommand) + + with wrap.Row(widthCurveFrame.getFrameLayout(), margins=style.scale([5, 0, 5, 2])) as row: + row.setFixedHeight(style.BUTTON_HEIGHT) + + resetButton = wrap.Button(row.layout(), 'gsWidthGraphResetButton') + resetButton.setLabel('Reset Curve') + + def resetWidthCmd(): + utils.resetSingleGraph('scale') + core.attributes.storeGraphs(widthGraph) + resetButton.clicked.connect(undo(resetWidthCmd)) + + popOutButton = wrap.Button(row.layout(), 'gsWidthGraphPopOut') + popOutButton.setFixedWidth(style.scale(56)) + popOutButton.setLabel('^') + popOutButton.clicked.connect(self.widthGraphPopOut) + + # Length Unlock Switch + lengthUnlock = wrap.Button(layout, 'LengthLock') + lengthUnlock.setLabel('Length Unlock', lineHeight=100) + lengthUnlock.setCheckable(True) + lengthUnlock.setButtonStyle('small') + lengthUnlock.setFixedWidth(style.scale(106)) + lengthUnlock.clicked.connect(pa(core.curveControlCheckBoxes, 2)) + + # Length + length = wrap.ControlSlider(layout, 'Length', 'float') + length.setLabel('Length') + length.setMinMax(0.001, 3) + length.setFieldMinMax(-1000, 1000) + length.setPrecision(3) + length.setStep(0.001) + length.setValue(1) + length.setDragCommand(pa(core.sliders.curveControlSliderDrag, length)) + length.setReleaseCommand(core.sliders.release) + + # TODO: Offset card from curve attribute + # Offset + offset = wrap.ControlSlider(layout, 'Offset', 'float') + offset.setLabel('Offset') + offset.setMinMax(-1, 1) + offset.setFieldMinMax(-30, 30) + offset.setStep(0.001) + offset.setValue(1) + offset.setDragCommand(pa(core.sliders.curveControlSliderDrag, offset)) + offset.setReleaseCommand(core.sliders.release) + + # Profile + profile = wrap.ControlSlider(layout, 'Profile', 'float') + profile.setLabel('Profile') + profile.setMinMax(-2, 2) + profile.setFieldMinMax(-1000, 1000) + profile.setStep(0.001) + profile.setDragCommand(pa(core.sliders.curveControlSliderDrag, profile)) + profile.setReleaseCommand(core.sliders.release) + + # Profile Curve Graph + with wrap.Frame(layout, 'profileCurveGraph', margins=[0, 1, 0, 2]) as profileCurveFrame: + profileCurveFrame.frameButton.setLabel('Profile Curve Graph') + profileGraph = wrap.FallOffCurve(profileCurveFrame.getFrameLayout(), 'profileCurve', attr=False) + + def changeCommand(value): + core.updateLattice(value) + core.equalizeProfileCurve() + core.attributes.storeGraphs(profileGraph) + profileGraph.changeCommand(changeCommand) + + profileSmoothingSlider = wrap.ControlSlider(profileCurveFrame.getFrameLayout(), 'profileSmoothing', 'int') + profileSmoothingSlider.setLabel('Smoothing') + profileSmoothingSlider.setMinMax(2, 30) + profileSmoothingSlider.setValue(2) + profileSmoothingSlider.setDragCommand(pa(core.sliders.curveControlSliderDrag, profileSmoothingSlider)) + profileSmoothingSlider.setReleaseCommand(core.sliders.release) + + with wrap.Row(profileCurveFrame.getFrameLayout(), margins=style.scale([5, 0, 5, 2])) as row: + row.setFixedHeight(style.BUTTON_HEIGHT) + + profileMagnitude = wrap.FloatField('profileMagnitude', row.layout()) + profileMagnitude.setFixedWidth(style.scale(45)) + profileMagnitude.setValue(1) + profileMagnitude.setRange(-2, 2) + profileMagnitude.setStep(0.01) + profileMagnitude.setPrecision(2) + profileMagnitude.setDragCommand(pa(core.sliders.curveControlSliderDrag, profileMagnitude)) + profileMagnitude.setReleaseCommand(core.sliders.release) + + with wrap.Column(row.layout(), spacing=0) as eqColumn: + eqColumn.setFixedHeight(style.scale(24)) + with wrap.Row(eqColumn.layout(), spacing=0) as eqRow: + def toggleEq(): + WIDGETS['equalizeCurveButton'].setDisabled( + WIDGETS['autoEqualizeSwitchOn'].isChecked() + ) + autoEqualizeGroup = QtWidgets.QButtonGroup(eqRow.layout()) + autoEqualizeGroup.buttonClicked.connect(toggleEq) + autoEqualizeGroup.buttonClicked.connect(undo(core.equalizeProfileCurve)) + + autoEqualize = wrap.Button(eqRow.layout(), 'autoEqualizeSwitchOn') + autoEqualize.setLabel('Auto', lineHeight=100) + autoEqualize.setLabelStyle('small') + autoEqualize.setButtonStyle('small-compound-top-left') + autoEqualize.setCheckable(True) + autoEqualize.setChecked(True) + + autoEqualizeOff = wrap.Button(eqRow.layout(), 'autoEqualizeSwitchOff') + autoEqualizeOff.setLabel('Manual', lineHeight=100) + autoEqualizeOff.setLabelStyle('small') + autoEqualizeOff.setButtonStyle('small-compound-top-right') + autoEqualizeOff.setCheckable(True) + + autoEqualizeGroup.addButton(autoEqualize) + autoEqualizeGroup.addButton(autoEqualizeOff) + + equalizeProfileCurve = wrap.Button(eqColumn.layout(), 'equalizeCurveButton') + equalizeProfileCurve.setLabel('Equalize Curve', lineHeight=100) + equalizeProfileCurve.setLabelStyle('small') + equalizeProfileCurve.setButtonStyle('small-filled-compound-bottom') + equalizeProfileCurve.setDisabled(True) + equalizeProfileCurve.clicked.connect(pa(undo(core.equalizeProfileCurve), True)) + + resetButton = wrap.Button(row.layout(), 'gsResetProfileGraphButton') + resetButton.setLabel('Reset Curve') + + def resetButtonClicked(*_): + core.resetProfileCurve() + core.attributes.storeGraphs(profileGraph) + resetButton.clicked.connect(undo(resetButtonClicked)) + + popOutButton = wrap.Button(row.layout(), 'gsProfileGraphPopOut') + popOutButton.setFixedWidth(style.scale(56)) + popOutButton.setLabel('^') + popOutButton.clicked.connect(self.profileGraphPopOut) + # Normals + normals = wrap.ControlSlider(layout, 'surfaceNormals', 'float') + normals.setLabel('Normals') + normals.setMinMax(0, 180) + normals.setPrecision(1) + normals.setValue(180) + normals.setDragCommand(pa(core.sliders.curveControlSliderDrag, normals)) + normals.setReleaseCommand(core.sliders.release) + + # Reverse Normals Switch + reverseNormals = wrap.Button(layout, 'reverseNormals') + reverseNormals.setLabel('Reverse Normals', lineHeight=100) + reverseNormals.setCheckable(True) + reverseNormals.setButtonStyle('small') + reverseNormals.setFixedWidth(style.scale(106)) + reverseNormals.clicked.connect(pa(core.curveControlCheckBoxes, 0)) + + with wrap.Frame(layout, 'otherFrame', label='Other', margins=[2, 2, 2, 2]) as refineFrame: + samplingAccuracy = wrap.ControlSlider(parent=refineFrame.getFrameLayout(), objName='samplingAccuracy', typ='float') + samplingAccuracy.setLabel('SamplAcc') + samplingAccuracy.setMinMax(0.001, 2) + samplingAccuracy.setStep(0.01) + samplingAccuracy.setValue(0.33) + samplingAccuracy.setDragCommand(pa(core.sliders.curveControlSliderDrag, samplingAccuracy)) + samplingAccuracy.setReleaseCommand(core.sliders.release) + # Refine + with wrap.Row(refineFrame.getFrameLayout(), margins=style.scale([0, 0, 3, 0])) as curveRefineRow: + refine = wrap.ControlSlider(layout, 'curveRefine', 'int') + refine.setLabel('Refine') + refine.setMinMax(0, 100) + refine.setFieldMinMax(0, 10000) + refine.setValue(20) + refine.setDragCommand(pa(core.sliders.curveControlSliderDrag, refine)) + refine.setReleaseCommand(core.sliders.release) + + autoRefineToggle = wrap.Button(objName='autoRefine') + autoRefineToggle.setLabel('Auto') + autoRefineToggle.setFixedWidth(style.scale(35)) + autoRefineToggle.setButtonStyle('small') + autoRefineToggle.setCheckable(True) + autoRefineToggle.clicked.connect(undo(core.toggleAutoRefine)) + + curveRefineRow.layout().addWidget(refine, 4) + curveRefineRow.layout().addWidget(autoRefineToggle, 1) + + # Smooth + smooth = wrap.ControlSlider(refineFrame.getFrameLayout(), 'curveSmooth', 'float') + smooth.setLabel('Smooth') + smooth.setMinMax(0, 10) + smooth.setDragCommand(pa(core.sliders.curveControlSliderDrag, smooth)) + smooth.setReleaseCommand(core.sliders.release) + + with wrap.Frame(layout, 'orientToNormalsFrame', label='Orient to Normals', + margins=[2, 2, 2, 2]) as orientFrame: + + orientFrame.setVisible(False) + + def selectMesh(): + sel = mc.filterExpand(mc.ls(sl=1, l=1), sm=12) + if not sel: + sel = mc.ls(hl=1, o=1, l=1) + if not sel: + MESSAGE.warningInView('Select compatible mesh.') + return + self.meshName.setText(sel[0]) + + with wrap.Row(orientFrame.getFrameLayout()) as row: + selectMeshBtn = wrap.Button(row.layout(), "gsOrientToNormalsSelectTarget") + selectMeshBtn.setFixedWidth(style.scale(100)) + selectMeshBtn.setLabel('Select Target') + selectMeshBtn.clicked.connect(selectMesh) + + self.meshName = wrap.LineEdit('gsOrientMeshName', row.layout()) + self.meshName.setPlaceholderText('Select or Type Target Mesh') + self.meshName.setClearButtonEnabled(True) + + orientFrame.getFrameLayout().addWidget(wrap.separator()) + + with wrap.Row(orientFrame.getFrameLayout()) as row: + iterationsSlider = wrap.ControlSlider(row.layout(), 'gsIterationsSlider', 'int') + iterationsSlider.setMinMax(1, 100) + iterationsSlider.setValue(10) + iterationsSlider.setLabel('Iterations') + + autoRefreshButton = wrap.Button(row.layout(), 'orientRefreshViewport') + autoRefreshButton.setButtonStyle('small') + autoRefreshButton.setCheckable(True) + autoRefreshButton.setChecked(True) + autoRefreshButton.setWidthHeight(w=style.scale(50)) + autoRefreshButton.setLabel('Refresh VP', lineHeight=100) + + with wrap.Row(orientFrame.getFrameLayout()) as row: + minAngleSlider = wrap.ControlSlider(row.layout(), 'gsMinimumAngle', 'float') + minAngleSlider.setMinMax(0.1, 90) + minAngleSlider.setValue(1) + minAngleSlider.setLabel('Min Angle') + + orientFrame.getFrameLayout().addWidget(wrap.separator()) + + with wrap.Row(orientFrame.getFrameLayout()) as row: + orient = wrap.Button(row.layout(), 'gsOrientToNormals') + orient.setLabel('Orient') + orient.clicked.connect(undo(core.orientToFaceNormals)) + + with wrap.Frame(layout, 'solidifyFrame', label='Solidify Controls', margins=[2, 2, 2, 2]) as solidifyFrame: + # Solidify Activate + solidify = wrap.Button(solidifyFrame.getFrameLayout(), 'solidify') + solidify.setLabel('Solidify', lineHeight=100) + solidify.setCheckable(True) + solidify.setButtonStyle('small') + solidify.setFixedWidth(style.scale(106)) + solidify.clicked.connect(pa(core.curveControlCheckBoxes, 1)) + + # Thickness + thickness = wrap.ControlSlider(solidifyFrame.getFrameLayout(), 'solidifyThickness', 'float') + thickness.setLabel('Thickness') + thickness.setMinMax(0.001, 5) + thickness.setFieldMinMax(-100, 100) + thickness.setValue(0.25) + thickness.setStep(0.01) + thickness.setDragCommand(pa(core.sliders.curveControlSliderDrag, thickness)) + thickness.setReleaseCommand(core.sliders.release) + + # Divisions + divisions = wrap.ControlSlider(solidifyFrame.getFrameLayout(), 'solidifyDivisions', 'int') + divisions.setLabel('Divisions') + divisions.setMinMax(0, 10) + divisions.setFieldMinMax(0, 100) + divisions.setDragCommand(pa(core.sliders.curveControlSliderDrag, divisions)) + divisions.setReleaseCommand(core.sliders.release) + + # ScaleX + solidifyScaleX = wrap.ControlSlider(solidifyFrame.getFrameLayout(), 'solidifyScaleX', 'float') + solidifyScaleX.setLabel('ScaleX') + solidifyScaleX.setMinMax(-10, 10) + solidifyScaleX.setFieldMinMax(-100, 100) + solidifyScaleX.setDragCommand(pa(core.sliders.curveControlSliderDrag, solidifyScaleX)) + solidifyScaleX.setReleaseCommand(core.sliders.release) + + # ScaleY + solidifyScaleY = wrap.ControlSlider(solidifyFrame.getFrameLayout(), 'solidifyScaleY', 'float') + solidifyScaleY.setLabel('ScaleY') + solidifyScaleY.setMinMax(-10, 10) + solidifyScaleY.setFieldMinMax(-100, 100) + solidifyScaleY.setDragCommand(pa(core.sliders.curveControlSliderDrag, solidifyScaleY)) + solidifyScaleY.setReleaseCommand(core.sliders.release) + + # Offset + offset = wrap.ControlSlider(solidifyFrame.getFrameLayout(), 'solidifyOffset', 'float') + offset.setLabel('Offset') + offset.setMinMax(-10, 10) + offset.setFieldMinMax(-100, 100) + offset.setDragCommand(pa(core.sliders.curveControlSliderDrag, offset)) + offset.setReleaseCommand(core.sliders.release) + + # Normals + solidifyNormals = wrap.ControlSlider(solidifyFrame.getFrameLayout(), 'solidifyNormals', 'float') + solidifyNormals.setLabel('SNormals') + solidifyNormals.setMinMax(0, 180) + solidifyNormals.setPrecision(1) + solidifyNormals.setValue(180) + solidifyNormals.setDragCommand(pa(core.sliders.curveControlSliderDrag, solidifyNormals)) + solidifyNormals.setReleaseCommand(core.sliders.release) + + with wrap.Frame(layout, 'UVFrame', label='UV Controls', margins=[2, 2, 2, 2]) as UVFrame: + flipUV = wrap.Button(UVFrame.getFrameLayout(), 'flipUV') + flipUV.setLabel('H-Flip UV', lineHeight=100) + flipUV.setCheckable(True) + flipUV.setButtonStyle('small') + flipUV.setFixedWidth(style.scale(106)) + flipUV.clicked.connect(pa(undo(core.curveControlCheckBoxes), 3)) + + # Move U + moveU = wrap.ControlSlider(UVFrame.getFrameLayout(), 'moveU', 'float') + moveU.setLabel('MoveU') + moveU.setMinMax(-0.5, 0.5) + moveU.setStep(0.001) + moveU.setFieldMinMax(-100, 100) + moveU.setDragCommand(pa(core.sliders.curveControlSliderDrag, moveU)) + moveU.setReleaseCommand(core.sliders.release) + + # Move V + moveV = wrap.ControlSlider(UVFrame.getFrameLayout(), 'moveV', 'float') + moveV.setLabel('MoveV') + moveV.setMinMax(-0.5, 0.5) + moveV.setStep(0.001) + moveV.setFieldMinMax(-100, 100) + moveV.setDragCommand(pa(core.sliders.curveControlSliderDrag, moveV)) + moveV.setReleaseCommand(core.sliders.release) + + # Scale U + scaleU = wrap.ControlSlider(UVFrame.getFrameLayout(), 'scaleU', 'float') + scaleU.setLabel('ScaleU') + scaleU.setMinMax(0.001, 1.999) + scaleU.setFieldMinMax(0, 100) + scaleU.setStep(0.001) + scaleU.setValue(1) + scaleU.setDragCommand(pa(core.sliders.curveControlSliderDrag, scaleU)) + scaleU.setReleaseCommand(core.sliders.release) + + # Scale V + scaleV = wrap.ControlSlider(UVFrame.getFrameLayout(), 'scaleV', 'float') + scaleV.setLabel('ScaleV') + scaleV.setMinMax(0.001, 1.999) + scaleV.setFieldMinMax(0, 100) + scaleV.setStep(0.001) + scaleV.setValue(1) + scaleV.setDragCommand(pa(core.sliders.curveControlSliderDrag, scaleV)) + scaleV.setReleaseCommand(core.sliders.release) + + # Rotate UV + rotateUV = wrap.ControlSlider(UVFrame.getFrameLayout(), 'rotateUV', 'float') + rotateUV.setLabel('RotUV') + rotateUV.setMinMax(-180, 180) + rotateUV.setFieldMinMax(-3600, 3600) + rotateUV.setPrecision(2) + rotateUV.setDragCommand(pa(core.sliders.curveControlSliderDrag, rotateUV)) + rotateUV.setReleaseCommand(core.sliders.release) + + # Rotate UV Root (Legacy) + rotateUVRoot = wrap.ControlSlider(UVFrame.getFrameLayout(), 'rotateRootUV', 'float') + rotateUVRoot.setLabel('RotRtUV') + rotateUVRoot.setMinMax(-180, 180) + rotateUVRoot.setFieldMinMax(-3600, 3600) + rotateUVRoot.setPrecision(2) + rotateUVRoot.setDragCommand(pa(core.sliders.curveControlSliderDrag, rotateUVRoot)) + rotateUVRoot.setReleaseCommand(core.sliders.release) + rotateUVRoot.setVisible(False) + + # Rotate Tip UV (Legacy) + rotateUVTip = wrap.ControlSlider(UVFrame.getFrameLayout(), 'rotateTipUV', 'float') + rotateUVTip.setLabel('RotTipUV') + rotateUVTip.setMinMax(-180, 180) + rotateUVTip.setFieldMinMax(-3600, 3600) + rotateUVTip.setPrecision(2) + rotateUVTip.setDragCommand(pa(core.sliders.curveControlSliderDrag, rotateUVTip)) + rotateUVTip.setReleaseCommand(core.sliders.release) + rotateUVRoot.setVisible(False) + + # UV Bug Message + if MAYA_VER in [2020, 2022] and not mc.optionVar(q='GSCT_UVBugMessageDismissed'): + with wrap.Row(UVFrame.getFrameLayout()) as row: + label = wrap.Label(row.layout()) + label.setLabel('