├── GUI ├── __init__.py ├── serializable.py ├── utils.py ├── node_edge_validators.py ├── node_content.py ├── node_edge_graphic_path.py ├── node_edge_dragging.py └── node_features.py ├── ZCore ├── __init__.py ├── Sources │ ├── icons │ │ ├── node │ │ │ ├── add.png │ │ │ ├── in.png │ │ │ ├── mul.png │ │ │ ├── out.png │ │ │ ├── sub.png │ │ │ ├── divide.png │ │ │ ├── invalid.png │ │ │ ├── python.png │ │ │ ├── spline.png │ │ │ ├── math_icon.png │ │ │ └── status_icons.png │ │ ├── shelf │ │ │ ├── folder.png │ │ │ ├── python.png │ │ │ ├── burger_donut.png │ │ │ ├── context │ │ │ │ └── spline.png │ │ │ └── shelf_options.png │ │ ├── zeno_key_rig.png │ │ └── context │ │ │ ├── splineContext.png │ │ │ └── splineContext_Hires.png │ ├── style │ │ └── nodestyle.qss │ └── controlCurves │ │ └── controls.json ├── Shelf │ ├── tools_build.py │ ├── __init__.py │ ├── Tools │ │ ├── __init__.py │ │ ├── tes1.py │ │ ├── tes2.py │ │ └── tes3.py │ ├── Context │ │ ├── __init__.py │ │ └── Spline.py │ ├── userPref.json │ └── context_build.py ├── Face.py ├── nodes_class │ ├── __init__.py │ ├── spline.py │ ├── output.py │ ├── input.py │ └── operations.py ├── Main.py ├── Lip.py ├── Plan.md ├── WorkspaceControl.py ├── Save │ ├── spline_name.json │ ├── tes.json │ ├── graph2.json │ └── graph_math.json ├── ToolsSystem.py ├── Config.py ├── Commands.py ├── Eyebrow.py ├── NodeBase.py ├── ShelfBase.py ├── SplineCtx.py └── MayaUtil.py ├── requirements.txt ├── docs ├── Node Editor UI.PNG ├── Mode Node Editor.gif ├── Example Node Editor.gif ├── Command line Node Editor.gif ├── build.py ├── source │ ├── rst │ │ ├── GUI.utils.rst │ │ ├── GUI.rst │ │ ├── GUI.node_content.rst │ │ ├── GUI.node_editor.rst │ │ ├── GUI.serializable.rst │ │ ├── GUI.node_features.rst │ │ └── GUI.node_creator.rst │ ├── index.rst │ ├── coding_standards.md │ └── conf.py ├── Makefile └── make.bat ├── HISTORY.rst ├── __init__.py ├── tox.ini ├── MANIFEST.in ├── LICENSE ├── .readthedocs.yaml ├── setup.py └── README.rst /GUI/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ZCore/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | QtPy>=1.9.0 2 | sphinx 3 | sphinx_rtd_theme 4 | recommonmark 5 | PySide2 6 | -------------------------------------------------------------------------------- /docs/Node Editor UI.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/docs/Node Editor UI.PNG -------------------------------------------------------------------------------- /docs/Mode Node Editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/docs/Mode Node Editor.gif -------------------------------------------------------------------------------- /docs/Example Node Editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/docs/Example Node Editor.gif -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/add.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/add.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/in.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/mul.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/mul.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/out.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/out.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/sub.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/sub.png -------------------------------------------------------------------------------- /docs/Command line Node Editor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/docs/Command line Node Editor.gif -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/divide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/divide.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/invalid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/invalid.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/python.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/spline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/spline.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/shelf/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/shelf/folder.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/shelf/python.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/shelf/python.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/zeno_key_rig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/zeno_key_rig.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/math_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/math_icon.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/node/status_icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/node/status_icons.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/shelf/burger_donut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/shelf/burger_donut.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/shelf/context/spline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/shelf/context/spline.png -------------------------------------------------------------------------------- /ZCore/Sources/icons/shelf/shelf_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/shelf/shelf_options.png -------------------------------------------------------------------------------- /HISTORY.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | History 3 | ======= 4 | 5 | 0.9.0 (2024-04-15) 6 | ------------------ 7 | 8 | * First release as a library. 9 | -------------------------------------------------------------------------------- /ZCore/Sources/icons/context/splineContext.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/context/splineContext.png -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | sys.dont_write_bytecode = True 5 | sys.path.append(os.path.dirname(os.path.realpath(__file__))) 6 | -------------------------------------------------------------------------------- /ZCore/Sources/icons/context/splineContext_Hires.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Atxada/Node_Editor/HEAD/ZCore/Sources/icons/context/splineContext_Hires.png -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py37 3 | 4 | [testenv] 5 | setenv = 6 | PYTHONPATH = {toxinidir} 7 | 8 | commands = py.test {posargs:tests} 9 | deps = 10 | pytest 11 | PyQt5 12 | -------------------------------------------------------------------------------- /docs/build.py: -------------------------------------------------------------------------------- 1 | ''' 2 | created to automated building html docs 3 | ''' 4 | import os 5 | 6 | os.chdir(os.path.dirname(__file__)) # change current working directory 7 | os.system('make clean') 8 | os.system('make html') -------------------------------------------------------------------------------- /docs/source/rst/GUI.utils.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: GUI.utils 2 | 3 | :py:mod:`utils` Module 4 | ====================== 5 | 6 | .. automodule:: GUI.utils 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /docs/source/rst/GUI.rst: -------------------------------------------------------------------------------- 1 | GUI Package 2 | =========== 3 | 4 | .. toctree:: 5 | :maxdepth: 4 6 | 7 | GUI.node_content 8 | GUI.node_creator 9 | GUI.node_editor 10 | GUI.node_features 11 | GUI.serializable 12 | GUI.utils -------------------------------------------------------------------------------- /docs/source/rst/GUI.node_content.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: GUI.node_content 2 | 3 | :py:mod:`node\_content` Module 4 | ============================== 5 | 6 | .. automodule:: GUI.node_content 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: -------------------------------------------------------------------------------- /docs/source/rst/GUI.node_editor.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: GUI.node_editor 2 | 3 | :py:mod:`node\_editor` Module 4 | ============================= 5 | 6 | .. automodule:: GUI.node_editor 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /docs/source/rst/GUI.serializable.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: GUI.serializable 2 | 3 | :py:mod:`serializable` Module 4 | ============================= 5 | 6 | .. automodule:: GUI.serializable 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /docs/source/rst/GUI.node_features.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: GUI.node_features 2 | 3 | :py:mod:`node\_features` Module 4 | =============================== 5 | 6 | .. automodule:: GUI.node_features 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include HISTORY.rst 2 | include LICENSE 3 | include README.rst 4 | include requrements.rst 5 | 6 | recursive-include tests * 7 | recursive-exclude * __pycache__ 8 | recursive-exclude * *.py[co] 9 | 10 | recursive-include docs *.rst conf.py Makefile make.bat *.jpg *.png *.gif 11 | -------------------------------------------------------------------------------- /ZCore/Shelf/tools_build.py: -------------------------------------------------------------------------------- 1 | from ZCore.Config import * 2 | 3 | import ZCore.ShelfBase as ShelfBase 4 | 5 | @ register_tab(OP_TAB_TOOLS) 6 | class ZenoTab_Tools(ShelfBase.ZenoTabContainer): 7 | title = "Tools" 8 | 9 | def __init__(self, parent=None): 10 | super(ZenoTab_Tools, self).__init__(parent) -------------------------------------------------------------------------------- /docs/source/rst/GUI.node_creator.rst: -------------------------------------------------------------------------------- 1 | .. py:currentmodule:: GUI.node_creator 2 | 3 | :py:mod:`node\_creator` Module 4 | ============================== 5 | 6 | .. automodule:: GUI.node_creator 7 | :members: 8 | :undoc-members: 9 | :show-inheritance: 10 | 11 | .. _socket-position-constants: 12 | 13 | -------------------------------------------------------------------------------- /ZCore/Shelf/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile, join 2 | import glob 3 | modules = glob.glob(join(dirname(__file__), "*.py")) # collect all module inside package 4 | __all__ = [ basename(file)[:-3] for file in modules if isfile(file) and not file.endswith("__init__.py")] # go through all file then append to __all__ -------------------------------------------------------------------------------- /ZCore/Shelf/Tools/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile, join 2 | import glob 3 | modules = glob.glob(join(dirname(__file__), "*.py")) # collect all module inside package 4 | __all__ = [ basename(file)[:-3] for file in modules if isfile(file) and not file.endswith("__init__.py")] # go through all file then append to __all__ -------------------------------------------------------------------------------- /ZCore/Shelf/Context/__init__.py: -------------------------------------------------------------------------------- 1 | from os.path import dirname, basename, isfile, join 2 | import glob 3 | modules = glob.glob(join(dirname(__file__), "*.py")) # collect all module inside package 4 | __all__ = [ basename(file)[:-3] for file in modules if isfile(file) and not file.endswith("__init__.py")] # go through all file then append to __all__ -------------------------------------------------------------------------------- /ZCore/Shelf/Tools/tes1.py: -------------------------------------------------------------------------------- 1 | from ZCore.Shelf.context_build import * 2 | 3 | import ZCore.ShelfBase as ShelfBase 4 | 5 | class tes1_item(ShelfBase.GraphicButton): 6 | def __init__(self, app=None): 7 | 8 | super(tes1_item, self).__init__() 9 | self.app = app 10 | 11 | def onClick(self, event): 12 | self.app.outputLogInfo("tes1") 13 | 14 | item_class = tes1_item -------------------------------------------------------------------------------- /ZCore/Shelf/Tools/tes2.py: -------------------------------------------------------------------------------- 1 | from ZCore.Shelf.context_build import * 2 | 3 | import ZCore.ShelfBase as ShelfBase 4 | 5 | class tes2_item(ShelfBase.GraphicButton): 6 | def __init__(self, app=None): 7 | 8 | super(tes2_item, self).__init__() 9 | self.app = app 10 | 11 | def onClick(self, event): 12 | self.app.outputLogInfo("tes2") 13 | 14 | item_class = tes2_item -------------------------------------------------------------------------------- /ZCore/Shelf/Tools/tes3.py: -------------------------------------------------------------------------------- 1 | from ZCore.Shelf.context_build import * 2 | 3 | import ZCore.ShelfBase as ShelfBase 4 | 5 | class tes3_item(ShelfBase.GraphicButton): 6 | def __init__(self, app=None): 7 | 8 | super(tes3_item, self).__init__() 9 | self.app = app 10 | 11 | def onClick(self, event): 12 | self.app.outputLogInfo("tes3") 13 | 14 | item_class = tes3_item -------------------------------------------------------------------------------- /ZCore/Face.py: -------------------------------------------------------------------------------- 1 | import ZCore.ToolsSystem as ToolsSystem 2 | 3 | ''' 4 | function: 5 | -contains generic face nodes method and hold important face nodes data 6 | ''' 7 | 8 | class FaceSystem(): 9 | 10 | def __init__(self): 11 | self.RIG_NODES_TYPE = ["eyebrow_"] 12 | 13 | # active node 14 | self.eyebrow_nodes = {} 15 | 16 | def mirror(self,plane="YZ"): 17 | pass -------------------------------------------------------------------------------- /ZCore/nodes_class/__init__.py: -------------------------------------------------------------------------------- 1 | # define which module should be imported from nodes_class package, automatically 2 | from os.path import dirname, basename, isfile, join 3 | import glob 4 | modules = glob.glob(join(dirname(__file__), "*.py")) # collect all module inside package 5 | __all__ = [ basename(file)[:-3] for file in modules if isfile(file) and not file.endswith("__init__.py")] # go through all file then append to __all__ -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. NodeEditor documentation master file, created by 2 | sphinx-quickstart on Mon Apr 15 21:20:26 2024. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to NodeEditor's documentation! 7 | ====================================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | coding_standards 14 | rst/GUI 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /ZCore/nodes_class/spline.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore,QtWidgets,QtGui 2 | from ZCore.Config import * 3 | 4 | import GUI.node_content as node_content 5 | import ZCore.NodeBase as NodeBase 6 | import ZCore.ToolsSystem as ToolsSystem 7 | 8 | @register_node(OP_NODE_SPLINE) 9 | class ZenoNode_Spline(NodeBase.ZenoNode): 10 | icon = ToolsSystem.get_path("Sources","icons","node","spline.png") 11 | op_code = OP_NODE_SPLINE 12 | op_title = "ZSpline" 13 | content_label = "read-only" 14 | content_label_objname = "zeno_node_spline" 15 | 16 | def __init__(self, nameID=op_title, scene=None): 17 | super(ZenoNode_Spline, self).__init__(nameID, scene, inputs=[2], outputs=[]) # call init function, cuz this node use custom socket config 18 | 19 | def evalImplementation(self): 20 | self.markInvalid(False) 21 | self.markDirty(False) 22 | return 123 -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | %SPHINXBUILD% >NUL 2>NUL 14 | if errorlevel 9009 ( 15 | echo. 16 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 17 | echo.installed, then set the SPHINXBUILD environment variable to point 18 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 19 | echo.may add the Sphinx directory to PATH. 20 | echo. 21 | echo.If you don't have Sphinx installed, grab it from 22 | echo.https://www.sphinx-doc.org/ 23 | exit /b 1 24 | ) 25 | 26 | if "%1" == "" goto help 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Atxada 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /ZCore/Shelf/Context/Spline.py: -------------------------------------------------------------------------------- 1 | from ZCore.Shelf.context_build import * 2 | from ZCore.ToolsSystem import OUTPUT_INFO, OUTPUT_ERROR, OUTPUT_SUCCESS, OUTPUT_WARNING 3 | 4 | import ZCore.ShelfBase as ShelfBase 5 | import ZCore.ToolsSystem as ToolsSystem 6 | import maya.cmds as cmds 7 | 8 | @register_item(OP_ITEM_SPLINE) 9 | class spline(ShelfBase.GraphicButton): 10 | plugin = ToolsSystem.get_path("SplineCtx.py") 11 | icon = ToolsSystem.get_path("Sources","icons","shelf","context","spline.png") 12 | def __init__(self, app=None): 13 | 14 | super(spline, self).__init__(self.icon) 15 | self.app = app 16 | 17 | # plugin (use context via cmds module after load plugin, must match with corresponding command name) 18 | cmds.loadPlugin(self.plugin) 19 | self.context = cmds.zSplineCtx() 20 | 21 | def onClick(self, event): 22 | if self.app.getCurrentNodeEditorWidget(): 23 | try: cmds.setToolTo(self.context) 24 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) 25 | else: 26 | self.app.outputLogInfo("no graphic scene found", OUTPUT_ERROR) -------------------------------------------------------------------------------- /ZCore/Shelf/userPref.json: -------------------------------------------------------------------------------- 1 | { 2 | "user_script": [ 3 | { 4 | "shelf": "Tools", 5 | "icon": "C:/Users/atxad/Downloads/burger_donut.png", 6 | "command": "import maya.cmds as cmds\nimport maya.mel as mel\n\nsource_mesh = cmds.ls(sl=1)[0]\ntarget_mesh = cmds.ls(sl=1)[1:]\ncmds.select(clear=1)\n\n# Query Joint influence\nsource_mesh_influences = cmds.skinCluster(source_mesh, inf=1, q=1)\n\n# Bind target mesh to source's joint influence\nfor i in target_mesh:\n for jnt in source_mesh_influences:\n cmds.select(jnt, add=1)\n cmds.select(i, add=1)\n cmds.skinCluster(tsb=1)\n cmds.select(clear=1)\n \n # Copy skin\n cmds.select(source_mesh)\n cmds.select(i,add=1)\n mel.eval(\"copySkinWeights -noMirror -surfaceAssociation closestPoint -influenceAssociation closestJoint -influenceAssociation name;\")\n cmds.select(clear=1)\n\nprint (\"Success copy skin weight from {0} to {1} :D\".format(source_mesh,target_mesh))," 7 | }, 8 | { 9 | "shelf": "Tools", 10 | "icon": "C:/Users/atxad/Pictures/2.jpg", 11 | "command": "print(\"hello world\")" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # Read the Docs configuration file for Sphinx projects 2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 3 | 4 | # Required 5 | version: 2 6 | 7 | # Set the OS, Python version and other tools you might need 8 | build: 9 | os: ubuntu-22.04 10 | tools: 11 | python: "3.7" 12 | # You can also specify other tool versions: 13 | # nodejs: "20" 14 | # rust: "1.70" 15 | # golang: "1.20" 16 | 17 | # Build documentation in the "docs/" directory with Sphinx 18 | sphinx: 19 | configuration: docs/source/conf.py 20 | # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs 21 | # builder: "dirhtml" 22 | # Fail on all warnings to avoid broken references 23 | # fail_on_warning: true 24 | 25 | # Optionally build your docs in additional formats such as PDF and ePub 26 | # formats: 27 | # - pdf 28 | # - epub 29 | 30 | # Optional but recommended, declare the Python requirements required 31 | # to build your documentation 32 | # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 33 | python: 34 | install: 35 | - requirements: requirements.txt 36 | -------------------------------------------------------------------------------- /ZCore/Sources/style/nodestyle.qss: -------------------------------------------------------------------------------- 1 | /* QLabel { color: red; } */ 2 | QGraphicsView { selection-background-color: #FFFFA637; } 3 | 4 | NodeContentWidget { 5 | background: transparent; 6 | } 7 | 8 | NodeContentWidget QTextEdit, 9 | NodeContentWidget QLineEdit { 10 | background: #666; 11 | color: #fff; 12 | } 13 | NodeContentWidget QLabel { 14 | color: #e0e0e0; 15 | } 16 | NodeContentWidget QLabel#zeno_node_input, 17 | NodeContentWidget QLabel#zeno_node_output, 18 | NodeContentWidget QLabel#zeno_node_math { 19 | background: transparent; 20 | height: 0px; 21 | color: #373737; 22 | font-size: 72px; 23 | max-height: 49px; 24 | min-height: 49px; 25 | padding-left: 94px; 26 | } 27 | NodeContentWidget QLabel#zeno_node_math, 28 | NodeContentWidget QLabel#zeno_node_spline { 29 | padding-top: 12px; 30 | } 31 | NodeContentWidget QLabel#zeno_node_output { 32 | min-width: 150px; 33 | max-width: 150px; 34 | min-height: 45px; 35 | max-height: 45px; 36 | margin-left: 10px; 37 | margin-top: 5px; 38 | font-size: 28px; 39 | } 40 | NodeContentWidget QLineEdit#zeno_node_input { 41 | width: 140px; 42 | height: 36px; 43 | margin-top: 5px; 44 | margin-left: 5px; 45 | font-size: 28px; 46 | } -------------------------------------------------------------------------------- /ZCore/Shelf/context_build.py: -------------------------------------------------------------------------------- 1 | from ZCore.Config import * 2 | 3 | import ZCore.ShelfBase as ShelfBase 4 | 5 | # item order (factory default) 6 | OP_ITEM_SPLINE = 1 7 | 8 | SHELF_ITEM = {} 9 | 10 | def register_item_now(op_code, class_reference): 11 | if op_code in SHELF_ITEM: 12 | raise InvalidRegistration("Duplicate item registration of '%s'. There is already %s"%(op_code, SHELF_ITEM[op_code])) 13 | SHELF_ITEM[op_code] = class_reference 14 | 15 | def register_item(op_code): 16 | def decorator(original_class): 17 | register_item_now(op_code, original_class) 18 | return original_class 19 | return decorator 20 | 21 | # import all item and trigger automatic registration 22 | from ZCore.Shelf.Context import * 23 | 24 | @ register_tab(OP_TAB_CONTEXT) 25 | class ZenoTab_Context(ShelfBase.ZenoTabContainer): 26 | title = "Context" 27 | 28 | def __init__(self, app): 29 | super(ZenoTab_Context, self).__init__(app) 30 | 31 | self.app = app 32 | 33 | self.initItem() 34 | 35 | def initItem(self): 36 | keys = list(SHELF_ITEM.keys()) 37 | keys.sort() 38 | for key in keys: 39 | self.itemLayout.addWidget(SHELF_ITEM[key](self.app)) -------------------------------------------------------------------------------- /docs/source/coding_standards.md: -------------------------------------------------------------------------------- 1 | # Coding standards 2 | 3 | ## File naming guidelines 4 | 5 | * files in node editor package start with ```node_``` prefix 6 | 7 | ## Tools architecture guidelines 8 | 9 | * GUI package include basic node editor functionality 10 | * ZCore package include Tools UI, Tools System and all Zeno rig nodes 11 | 12 | ## Coding guidelines 13 | 14 | * methods use Camel case naming 15 | * variables/properties use Snake case naming 16 | * The constructor ```__init__``` always contains all class variables for the entire class. This is helpful for new users, so they can 17 | just look at the constructor and read about all properties that class is using in one place. Nobody wants any 18 | surprises hidden in the code later 19 | * methods inheriting (PySide2) Graphical class end with ```Graphics``` 20 | * nodeeditor uses custom callbacks and listeners. Methods for adding callback functions 21 | are usually named ```addXYListener``` 22 | * custom events are usually named ```onXY``` 23 | * methods named ```doXY``` usually do certain tasks and also take care of low level operations 24 | * classes ideally contain methods in this order: 25 | 26 | * ```__init__``` 27 | * python magic methods (i.e. ```__str__```), setters and getters 28 | * ```initXY``` functions 29 | * listener functions 30 | * nodeeditor event fuctions 31 | * nodeeditor ```doXY``` and ```getXY``` helping functions 32 | * Qt5 event functions 33 | * other functions 34 | * optionally overridden Qt ```paint``` method 35 | * ```serialize``` and ```deserialize``` methods at the end* -------------------------------------------------------------------------------- /ZCore/Main.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import os 3 | import sys 4 | 5 | '============================ redirect system path for maya (placeholder) ============================' 6 | sys.path.insert(0, "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig") 7 | sys.dont_write_bytecode = True # prevent by bytecode python being generated (.pyc) 8 | 9 | # extend PYTHONPATH so app executable inside command prompt (optional) 10 | # sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..')) # go 2 level above, hard-coded 11 | '=====================================================================================================' 12 | 13 | """ 14 | from PySide2 import QtCore, QtGui, QtWidgets 15 | 16 | from ZCore.UI import ZenoMainWindow 17 | from GUI.node_editor import NodeEditorWindow 18 | from GUI.utils import loadStylesheet 19 | """ 20 | 21 | ''' MIGHT DELETE LATER 22 | # load node editor 23 | if __name__ == "__main__": 24 | 25 | try: 26 | window.close() 27 | window.deleteLater() # override inside closeEvent mainWindow 28 | except: 29 | pass 30 | 31 | #print (QtWidgets.QStyleFactory.keys()) 32 | window = ZenoMainWindow() 33 | #QtWidgets.QApplication.setStyle('windows') 34 | #module_path = os.path.dirname(inspect.getfile(window.__class__)) # retrieve file path where this class is located in disk 35 | #loadStylesheet(window, os.path.join(module_path, 'Sources/style/nodestyle.qss')) # append path with qss sub path 36 | 37 | window.show() 38 | ''' 39 | 40 | print ("ZCore package initialized"), -------------------------------------------------------------------------------- /GUI/serializable.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A module containing Serializable "Interface". act as an abstract class 4 | """ 5 | 6 | class Serializable(object): 7 | def __init__(self): 8 | """ 9 | Default constructor automatically creates data which are common to any serializable object. 10 | In our case we create ``self.id`` which we use in every object in NodeEditor. 11 | """ 12 | self.id = id(self) 13 | 14 | def serialize(self): 15 | """ 16 | Serialization method to serialize this class data into ``OrderedDict`` which can be easily stored 17 | in memory or file. 18 | 19 | :return: data serialized in ``OrderedDict`` 20 | :rtype: ``OrderedDict`` 21 | """ 22 | raise NotImplemented 23 | 24 | def deserialize(self, data, hashmap={}, restore_id=True): 25 | """ 26 | Deserialization method which take data in python ``dict`` format with helping `hashmap` containing 27 | references to existing entities. 28 | 29 | :param data: Dictionary containing serialized data 30 | :type data: ``dict`` 31 | :param hashmap: Helper dictionary containing references (by id == key) to existing objects 32 | :type hashmap: ``dict`` 33 | :param restore_id: True if we are creating new Sockets. False is useful when loading existing 34 | Sockets of which we want to keep the existing object's `id`. 35 | :type restore_id: bool 36 | :return: ``True`` if deserialization was successful, otherwise ``False`` 37 | :rtype: ``bool`` 38 | """ 39 | raise NotImplemented 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """The setup script.""" 4 | 5 | from setuptools import setup, find_packages 6 | 7 | with open('README.rst') as readme_file: 8 | readme = readme_file.read() 9 | 10 | with open('HISTORY.rst') as history_file: 11 | history = history_file.read() 12 | 13 | with open('requirements.txt') as requirements_file: 14 | requirements = requirements_file.read() 15 | 16 | setup_requirements = [ ] 17 | 18 | test_requirements = [ ] 19 | 20 | import GUI 21 | 22 | setup( 23 | author="Aldo Aldrich", 24 | author_email='atxadaaldo17022@gmail.com', 25 | python_requires='>=2.7', 26 | classifiers=[ 27 | 'Development Status :: 2 - Pre-Alpha', 28 | 'Intended Audience :: Developers', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Natural Language :: English', 31 | 'Programming Language :: Python :: 3', 32 | 'Programming Language :: Python :: 3.6', 33 | 'Programming Language :: Python :: 3.7', 34 | 'Programming Language :: Python :: 3.8', 35 | ], 36 | description="Python Node Editor using Pyside2", 37 | install_requires=requirements, 38 | license="MIT license", 39 | long_description=readme + '\n\n' + history, 40 | include_package_data=True, 41 | keywords='node_editor', 42 | name='node_editor', 43 | #packages=find_packages(include=['template', 'template.*']), 44 | packages=find_packages(include=['GUI*'], exclude=['ZCore*']), 45 | setup_requires=setup_requirements, 46 | test_suite='tests', 47 | tests_require=test_requirements, 48 | url='https://github.com/Atxada/Node_Editor', 49 | version='0.9.0', 50 | zip_safe=False, 51 | ) 52 | -------------------------------------------------------------------------------- /GUI/utils.py: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | """ 3 | Module with some helper functions 4 | """ 5 | 6 | import traceback 7 | from PySide2 import QtCore, QtWidgets, QtGui 8 | from pprint import PrettyPrinter 9 | 10 | pp = PrettyPrinter(indent=4).pprint 11 | 12 | def dumpException(e=None): 13 | """Prints out Exception message with traceback to the console 14 | 15 | :param e: Exception message 16 | :type e: Exception 17 | """ 18 | # print("%s EXCEPTION:"% e.__class__.__name__, e) 19 | # traceback.print_tb(e.__traceback__) python 2.7 incompatible? 20 | print ("EXCEPTION: ", e) 21 | 22 | 23 | def loadStylesheet(instance, filename): 24 | """ 25 | Loads an qss stylesheet to the current QApplication instance 26 | 27 | :param filename: Filename of qss stylesheet 28 | :type filename: str 29 | """ 30 | # print ("load STYLE:", filename) 31 | file = QtCore.QFile(filename) 32 | # print ("open style: " + str(file.open(QtCore.QFile.ReadOnly | QtCore.QFile.Text))) 33 | stylesheet = file.readAll() 34 | # print ("stylesheet content: ", stylesheet) 35 | instance.setStyleSheet(str(stylesheet)) 36 | 37 | def loadStylesheets(instance, *args): 38 | """ 39 | Loads multiple qss stylesheets. Concatenates them together and applies the final stylesheet to the current QApplication instance 40 | 41 | :param args: variable number of filenames of qss stylesheets 42 | :type args: str, str,... 43 | """ 44 | res = "" 45 | for arg in args: 46 | file = QtCore.QFile(arg) 47 | file.open(QtCore.QFile.ReadOnly | QtCore.QFile.Text) 48 | stylesheet = file.readAll() 49 | res += "\n" + str(stylesheet) 50 | instance.setStyleSheet(res) -------------------------------------------------------------------------------- /ZCore/Lip.py: -------------------------------------------------------------------------------- 1 | #from eye_rig_tool.face_rig import FaceRig 2 | 3 | import maya.cmds as cmds 4 | import maya.mel as mel 5 | 6 | """ 7 | Features: 8 | create ribbon based lip with basic ctrl + with proper skinning between upper and lower (WIP) 9 | create basic emotions option (procedural?) + kissing and mmm 10 | create alphabet and lipsync 11 | create nasolabial fold 12 | (advanced) zipping effect 13 | """ 14 | 15 | class LipSystem(): 16 | 17 | # class properties 18 | setup_node = None 19 | 20 | # setup 21 | def __init__(self): 22 | sel = cmds.filterExpand(sm=32) 23 | if sel != None: 24 | crv = cmds.polyToCurve(f=2,dg=1,usm=0,ch=False,n="eyebrow_setup")[0] 25 | cmds.xform(crv,cp=1) 26 | else: 27 | crv = cmds.curve(n="eyebrow_setup", 28 | d=3, 29 | p=[(0, 0, 2),(0, 0, 1.8), (0, 0, 0), (0, 0, -1.8),(0, 0, -2)]) 30 | 31 | crv_cv = cmds.ls('%s.cv[:]'%crv, fl=1) 32 | cmds.select(crv_cv[0], crv_cv[1]) 33 | cluster_a = cmds.cluster(n="cluster_a") 34 | cmds.select(crv_cv[2]) 35 | cluster_b = cmds.cluster(n="cluster_b") 36 | cmds.select(crv_cv[3], crv_cv[4]) 37 | cluster_c = cmds.cluster(n="cluster_c") 38 | cluster_grp = cmds.group(cluster_a,cluster_b,cluster_c,n="setup_cluster") 39 | cmds.setAttr(cluster_grp_+".visibility",0) 40 | 41 | # build rig system from setup 42 | def build(self): 43 | pass 44 | 45 | # mirror setup or system according to plane 46 | def mirror(self): 47 | pass 48 | 49 | instance = LipSystem() 50 | 51 | ''' 52 | 1. select edge to curve 53 | 3. rebuild curve number span as controller needed 54 | 4. create ribbon 55 | ''' 56 | -------------------------------------------------------------------------------- /ZCore/Plan.md: -------------------------------------------------------------------------------- 1 | # DEFINITION 2 | 3 | 4 | # Strength 5 | 1. Simple process (minimize learning curve) 6 | - Simple UI control 7 | - Interactive process (enable to see a bit of jnt structure) 8 | 2. Extendable modular process (object base) 9 | - Treat body part as object (node editor for more extensive rig) 10 | 3. Very easy to modify 11 | 4. Lightweight rig (optimized) 12 | 13 | # Back-End Concept: 14 | Rig part class (mouth/eye/nose/etc) named as **Facial element/element** contain: 15 | - Native procedure/ Natural object (How it arrange function and manage it) (input: selection (face,edge,etc), Position) 16 | - extensive modular attribute with node editor 17 | 18 | # all objects for face rig 19 | - eyes 20 | - eyelid (done) 21 | - lips 22 | - nose 23 | - cheek 24 | - eyebrow 25 | - **experiment** neck muscle (tense) 26 | - **experiment** muscle simulation (need attach to attr for show and hide manual ctrl and trigger effect with auto (follow anatomical behavior) or manual intensity) (many small ctrl) 27 | 28 | # Highlight features: 29 | - import export module (previous build rig) 30 | - build rig system as object/node 31 | - interactive (spline/manual jnt) 32 | - **experiment** muscle simulation 33 | - **experiment** texture driven wrinkle 34 | - **experiment** ikfk snap 35 | 36 | # Element identity: 37 | - **mouth** > lips seal, lipsync, protrude lips, nasolabial fold 38 | - **jaw** > chewing 39 | 40 | # SOP: 41 | - create object class setup 42 | - create object class build 43 | - refactor to maya util/parent class 44 | - design / override color / correct scale 45 | - nodes creation (circuit, data, etc) / attribute + advance customization 46 | 47 | ################################################################################################# 48 | # TESTING 49 | - snarl expression 50 | - stank face 51 | - wide smile/genuine smile 52 | - extreme smile with eyes close 53 | - shock 54 | - horrified 55 | - smirk face/haewonbear face -------------------------------------------------------------------------------- /ZCore/WorkspaceControl.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore 2 | from shiboken2 import getCppPointer 3 | import maya.OpenMayaUI as omui 4 | import maya.cmds as cmds 5 | 6 | class WorkspaceControl(object): 7 | 8 | def __init__(self, name): 9 | self.name = name 10 | self.widget = None 11 | 12 | def create(self, label, widget, ui_script=None): 13 | 14 | cmds.workspaceControl(self.name, label=label) 15 | 16 | if ui_script: 17 | cmds.workspaceControl(self.name, e=True, uiScript=ui_script) 18 | 19 | self.add_widget_to_layout(widget) 20 | self.set_visible(True) 21 | 22 | def restore(self, widget): 23 | self.add_widget_to_layout(widget) 24 | 25 | def add_widget_to_layout(self, widget): 26 | if widget: 27 | self.widget = widget 28 | self.widget.setAttribute(QtCore.Qt.WA_DontCreateNativeAncestors) # prevent widget become native ui to maya, avoid unwanted flicker/slow process/etc 29 | 30 | workspace_control_ptr = long(omui.MQtUtil.findControl(self.name)) 31 | widget_ptr = long(getCppPointer(self.widget)[0]) 32 | 33 | omui.MQtUtil.addWidgetToMayaLayout(widget_ptr, workspace_control_ptr) 34 | 35 | def exists(self): 36 | return cmds.workspaceControl(self.name, q=True, exists=True) 37 | 38 | def is_visible(self): 39 | return cmds.workspaceControl(self.name, q=True, visible=True) 40 | 41 | def set_visible(self, visible): 42 | if visible: 43 | cmds.workspaceControl(self.name, e=True, restore=True) 44 | else: 45 | cmds.workspaceControl(self.name, e=True, visible=False) 46 | 47 | def set_label(self, label): 48 | cmds.workspaceControl(self.name, e=True, label=label) 49 | 50 | def is_floating(self): 51 | return cmds.workspaceControl(self.name, q=True, floating=True) 52 | 53 | def is_collapsed(self): 54 | return cmds.workspaceControl(self.name, q=True, collapse=True) -------------------------------------------------------------------------------- /ZCore/nodes_class/output.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore,QtWidgets,QtGui 2 | from ZCore.Config import * 3 | 4 | import GUI.node_content as node_content 5 | import ZCore.NodeBase as NodeBase 6 | import ZCore.ToolsSystem as ToolsSystem 7 | 8 | class ZenoOutputContent(node_content.NodeContentWidget): 9 | def initUI(self): 10 | self.label = QtWidgets.QLabel("Result: ", self) 11 | self.label.setAlignment(QtCore.Qt.AlignLeft) 12 | self.label.setObjectName(self.node.content_label_objname) 13 | self.label.setStyleSheet("margin-left: 5px; margin-top: 5px;") 14 | self.label.setMinimumWidth(140) # hypothetical 15 | 16 | @register_node(OP_NODE_OUTPUT) 17 | class ZenoNode_Output(NodeBase.ZenoNode): 18 | icon = ToolsSystem.get_path("Sources","icons","node","out.png") 19 | op_code = OP_NODE_OUTPUT 20 | op_title = "Output" 21 | content_label = "out" 22 | content_label_objname = "zeno_node_output" 23 | 24 | def __init__(self, nameID=op_title, scene=None): 25 | super(ZenoNode_Output, self).__init__(self.__class__.op_title, scene, "evaluation", inputs=[1], outputs=[]) # call init function, cuz this node use custom socket config 26 | 27 | def initInnerClasses(self): # append custom class to node content 28 | self.content = ZenoOutputContent(self) 29 | self.node_graphic = NodeBase.ZenoNodeGraphics(self, self.validateIcon(self.icon)) 30 | 31 | def evalImplementation(self): 32 | input_node = self.getInput(0) 33 | if not input_node: 34 | self.node_graphic.setToolTip("Input is not connected") 35 | self.markInvalid() 36 | return 37 | 38 | value = input_node.eval() 39 | if value is None: 40 | self.node_graphic.setToolTip("Input is NaN") # NaN -> not a number 41 | self.markInvalid() 42 | return 43 | self.content.label.setText("Result: %d"%value) 44 | self.markInvalid(False) 45 | self.markDirty(False) 46 | self.node_graphic.setToolTip("") 47 | 48 | return value -------------------------------------------------------------------------------- /ZCore/Save/spline_name.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2461696542144, 3 | "scene_width": 8000, 4 | "scene_height": 8000, 5 | "nodes": [ 6 | { 7 | "id": 2461044689384, 8 | "title": "zSpline1", 9 | "pos_x": -438.43749999999994, 10 | "pos_y": 56.562500000000014, 11 | "inputs": [ 12 | { 13 | "id": 2461713368648, 14 | "index": 0, 15 | "multi_edges": false, 16 | "position": 2, 17 | "socket_type": 2 18 | } 19 | ], 20 | "outputs": [], 21 | "content": {}, 22 | "op_code": 4 23 | }, 24 | { 25 | "id": 1714429154752, 26 | "title": "zSpline1", 27 | "pos_x": -439.93749999999994, 28 | "pos_y": 195.68750000000003, 29 | "inputs": [ 30 | { 31 | "id": 1714447768136, 32 | "index": 0, 33 | "multi_edges": false, 34 | "position": 2, 35 | "socket_type": 2 36 | } 37 | ], 38 | "outputs": [], 39 | "content": {}, 40 | "op_code": 4 41 | }, 42 | { 43 | "id": 1714445105248, 44 | "title": "zSpline2", 45 | "pos_x": -94.75, 46 | "pos_y": 58.37500000000003, 47 | "inputs": [ 48 | { 49 | "id": 1714445894024, 50 | "index": 0, 51 | "multi_edges": false, 52 | "position": 2, 53 | "socket_type": 2 54 | } 55 | ], 56 | "outputs": [], 57 | "content": {}, 58 | "op_code": 4 59 | }, 60 | { 61 | "id": 1713769242128, 62 | "title": "zSpline3", 63 | "pos_x": -109.0625, 64 | "pos_y": 199.62500000000003, 65 | "inputs": [ 66 | { 67 | "id": 1714445896392, 68 | "index": 0, 69 | "multi_edges": false, 70 | "position": 2, 71 | "socket_type": 2 72 | } 73 | ], 74 | "outputs": [], 75 | "content": {}, 76 | "op_code": 4 77 | } 78 | ], 79 | "edges": [] 80 | } -------------------------------------------------------------------------------- /ZCore/nodes_class/input.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore,QtWidgets,QtGui 2 | from ZCore.Config import * # with import it's cuz error, looping problem? 3 | 4 | import GUI.node_content as node_content 5 | import ZCore.NodeBase as NodeBase 6 | import ZCore.ToolsSystem as ToolsSystem 7 | 8 | class ZenoInputContent(node_content.NodeContentWidget): 9 | def initUI(self): 10 | self.edit = QtWidgets.QLineEdit("1", self) 11 | self.edit.setAlignment(QtCore.Qt.AlignRight) 12 | self.edit.setObjectName(self.node.content_label_objname) 13 | self.edit.setStyleSheet("background: #303030; max-height: 17.5; margin-left: 5px; margin-top: 1.25") 14 | 15 | def serialize(self): # serialize because there is content inside (line edit) 16 | res = super(ZenoInputContent,self).serialize() 17 | res['value'] = self.edit.text() 18 | return res 19 | 20 | def deserialize(self, data, hashmap=[]): # deserialize because there is content inside (line edit) 21 | res = super(ZenoInputContent, self).deserialize(data, hashmap) 22 | try: 23 | value = data['value'] 24 | self.edit.setText(value) 25 | return True & res # idk what & difference with , 26 | except Exception as e: pass 27 | return res 28 | 29 | @register_node(OP_NODE_INPUT) 30 | class ZenoNode_Input(NodeBase.ZenoNode): 31 | icon = ToolsSystem.get_path("Sources","icons","node","in.png") 32 | op_code = OP_NODE_INPUT 33 | op_title = "Input" 34 | content_label = "in" 35 | content_label_objname = "zeno_node_input" 36 | 37 | def __init__(self, nameID=op_title, scene=None): 38 | super(ZenoNode_Input, self).__init__(self.__class__.op_title, scene, "evaluation", inputs=[], outputs=[1]) # call init function, cuz this node use custom socket config 39 | self.eval() 40 | 41 | def initInnerClasses(self): # append custom class to node content 42 | self.content = ZenoInputContent(self) 43 | self.node_graphic = NodeBase.ZenoNodeGraphics(self, self.validateIcon(self.icon)) 44 | self.content.edit.textChanged.connect(self.onInputChanged) 45 | 46 | def evalImplementation(self): 47 | unsaved_value = self.content.edit.text() 48 | saved_value = int(unsaved_value) # checking if line edit contain correct type(interger) 49 | self.value = saved_value 50 | # if everything goes well, pass success evaluation 51 | self.markDirty(False) 52 | self.markInvalid(False) 53 | 54 | self.markDescendantsInvalid(False) 55 | self.markDescendantsDirty() 56 | 57 | self.node_graphic.setToolTip("") # if everything ok, make sure tool tip empty (no warning) 58 | 59 | self.evalChildren() 60 | 61 | return self.value -------------------------------------------------------------------------------- /GUI/node_edge_validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | A module containing the Edge Validator functions which can be registered as callbacks to 4 | :class:`~GUI.node_creator.EdgeConfig` class. 5 | 6 | Example of registering Edge Validator callbacks: 7 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 | 9 | You can register validation callbacks once for example on the bottom of node_creator.py (python module which EdgeConfig reside) file or on the 10 | application start with calling this: 11 | 12 | .. code-block:: python 13 | 14 | from GUI.node_edge_validators import * 15 | 16 | node_creator.EdgeConfig.registerEdgeValidator(edge_validator_debug) 17 | node_creator.EdgeConfig.registerEdgeValidator(edge_cannot_connect_two_outputs_or_two_inputs) 18 | node_creator.EdgeConfig.registerEdgeValidator(edge_cannot_connect_input_and_output_of_same_node) 19 | node_creator.EdgeConfig.registerEdgeValidator(edge_cannot_connect_input_and_output_of_different_type) 20 | 21 | """ 22 | 23 | def print_error(*args): 24 | """Helper method which prints to console if `DEBUG` is set to `True`""" 25 | print("Edge Validation Error: ", args) 26 | 27 | def edge_validator_debug(input, output): 28 | """This will consider edge always valid, however writes bunch of debug stuff into console""" 29 | print("VALIDATING:") 30 | print(input, "input" if input.is_input else "output", "of node", input.node) 31 | for s in input.node.inputs+input.node.outputs: print("\t", s, "input" if s.is_input else "output") 32 | print(output, "input" if input.is_input else "output", "of node", output.node) 33 | for s in output.node.inputs+output.node.outputs: print("\t", s, "input" if s.is_input else "output") 34 | 35 | return True 36 | 37 | def edge_cannot_connect_two_outputs_or_two_inputs(input, output): 38 | """Edge is invalid if it connects 2 output sockets or 2 input sockets""" 39 | if input.is_output and output.is_output: 40 | print_error("Connecting 2 outputs") 41 | return False 42 | 43 | if input.is_input and output.is_input: 44 | print_error("Connecting 2 inputs") 45 | return False 46 | 47 | return True 48 | 49 | def edge_cannot_connect_input_and_output_of_same_node(input, output): 50 | """Edge is invalid if it connects the same node""" 51 | if input.node == output.node: 52 | print_error("Connecting the same node") 53 | return False 54 | 55 | return True 56 | 57 | def edge_cannot_connect_input_and_output_of_different_type(input, output): 58 | """Edge is invalid if it connects sockets with different colors/type""" 59 | 60 | if input.socket_type != output.socket_type: 61 | print_error("Connecting sockets with different colors/type") 62 | return False 63 | 64 | return True 65 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Node Editor 2 | ========================== 3 | .. image:: https://github.com/Atxada/Node_Editor/blob/main/docs/Node%20Editor%20UI.PNG 4 | Description 5 | ----------- 6 | 7 | The initial goal of this project is to create an auto-rig for Maya and get a better understanding of the Qt Framework to create a more complex GUI. 8 | 9 | I plan to continue developing this project's architecture by adding more functions and features as I develop my knowledge. 10 | The main reason to create a custom node editor is to visualize the rigging structure and promote reusability. 11 | Each node contains its own data structure that can be deserialized or serialized whenever needed. 12 | This project has been fun and stressful for me, but through it all, I learned so many things. 13 | thanks to all the resources and people online who have helped me when I'm stuck. As a gratitude, I want to share this project with anyone interested. 14 | 15 | Also, **big thanks to Pavel Křupala** for the node editor GUI tutorial he provided. It helps me a lot to start building my basic knowledge of the Qt framework and many important topics (event/callback, debugging process, sphinx documentation, etc.). After the course, I also continued to develop a few more helpful features and remake the node editor's graphic representation. 16 | 17 | The resource link: 18 | https://www.blenderfreak.com/tutorials/node-editor-tutorial-series/ 19 | 20 | Features 21 | -------- 22 | 23 | - full framework for creating customizable graphs, nodes, sockets, and edges 24 | .. image:: https://github.com/Atxada/Node_Editor/blob/main/docs/Example%20Node%20Editor.gif 25 | - support for undo/redo and serialization into files 26 | - support for implementing evaluation logic 27 | - scene mode to edit nodes (dragging edge, rerouting edge, cutting edge, etc.) 28 | .. image:: https://github.com/Atxada/Node_Editor/blob/main/docs/Mode%20Node%20Editor.gif 29 | - simple set of math nodes to use as a demo 30 | - support for saving custom executable scripts 31 | - simple maya context (zSpline)  32 | - command line interpreters consisting of some handy scripts (? for help) 33 | .. image:: https://github.com/Atxada/Node_Editor/blob/main/docs/Command%20line%20Node%20Editor.gif 34 | - tested in Maya 2020.4 (python 2.7) and 2022 (python 3.7). 35 | 36 | Links 37 | ------------- 38 | 39 | - `Documentation `_ 40 | - `Linkedin `_ 41 | 42 | Testing 43 | ------------ 44 | 45 | 1. Download files from the repository 46 | 2. Unzip the files, rename the folder **Node_Editor-main** to **Node_Editor_main** 47 | 3. Place the folder inside maya script directory: 48 | ``C:\Users\Documents\maya\scripts`` 49 | 4. Copy the following code to script editor 50 | :: 51 | from Node_Editor_main.ZCore import UI 52 | 53 | try: 54 | zeno_rig_window.close() 55 | zeno_rig_window.deleteLater() 56 | except: 57 | pass 58 | 59 | zeno_rig_window = UI.ZenoMainWindow() 60 | zeno_rig_window.show() 61 | 5. Node Editor will show up and ready to use! 62 | -------------------------------------------------------------------------------- /ZCore/Save/tes.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2322577337872, 3 | "scene_width": 8000, 4 | "scene_height": 8000, 5 | "nodes": [ 6 | { 7 | "id": 2323343856752, 8 | "title": "Input", 9 | "pos_x": -299.0, 10 | "pos_y": -6.0, 11 | "inputs": [], 12 | "outputs": [ 13 | { 14 | "id": 2323343862728, 15 | "index": 0, 16 | "multi_edges": true, 17 | "position": 5, 18 | "socket_type": 1 19 | } 20 | ], 21 | "content": {}, 22 | "op_code": 1 23 | }, 24 | { 25 | "id": 2323343858096, 26 | "title": "Output", 27 | "pos_x": 158.0, 28 | "pos_y": 4.0, 29 | "inputs": [ 30 | { 31 | "id": 2323286205320, 32 | "index": 0, 33 | "multi_edges": false, 34 | "position": 2, 35 | "socket_type": 1 36 | } 37 | ], 38 | "outputs": [], 39 | "content": {}, 40 | "op_code": 2 41 | }, 42 | { 43 | "id": 2323343856248, 44 | "title": "Math", 45 | "pos_x": -53.0, 46 | "pos_y": -81.0, 47 | "inputs": [ 48 | { 49 | "id": 2323286213320, 50 | "index": 0, 51 | "multi_edges": false, 52 | "position": 2, 53 | "socket_type": 1 54 | }, 55 | { 56 | "id": 2323286214216, 57 | "index": 1, 58 | "multi_edges": false, 59 | "position": 2, 60 | "socket_type": 1 61 | } 62 | ], 63 | "outputs": [ 64 | { 65 | "id": 2323286214856, 66 | "index": 0, 67 | "multi_edges": true, 68 | "position": 5, 69 | "socket_type": 1 70 | } 71 | ], 72 | "content": {}, 73 | "op_code": 3 74 | }, 75 | { 76 | "id": 2610782603472, 77 | "title": "Output", 78 | "pos_x": -55.0, 79 | "pos_y": 83.0, 80 | "inputs": [ 81 | { 82 | "id": 2610782184520, 83 | "index": 0, 84 | "multi_edges": false, 85 | "position": 2, 86 | "socket_type": 1 87 | } 88 | ], 89 | "outputs": [], 90 | "content": {}, 91 | "op_code": 2 92 | } 93 | ], 94 | "edges": [ 95 | { 96 | "id": 2610782420440, 97 | "edge_type": 2, 98 | "start": 2323343862728, 99 | "end": 2323286213320 100 | }, 101 | { 102 | "id": 2610782420776, 103 | "edge_type": 2, 104 | "start": 2323343862728, 105 | "end": 2610782184520 106 | }, 107 | { 108 | "id": 2610782420328, 109 | "edge_type": 2, 110 | "start": 2323286205320, 111 | "end": 2323343862728 112 | } 113 | ] 114 | } -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # For the full list of built-in configuration values, see the documentation: 4 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 5 | 6 | # -- Path setup -------------------------------------------------------------- 7 | 8 | # If extensions (or modules to document with autodoc) are in another directory, 9 | # add these directories to sys.path here. If the directory is relative to the 10 | # documentation root, use os.path.abspath to make it absolute, like shown here. 11 | # 12 | import os 13 | import sys 14 | sys.path.insert(0, os.path.abspath('../..')) 15 | import GUI 16 | 17 | # -- Project information ----------------------------------------------------- 18 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information 19 | 20 | project = 'NodeEditor' 21 | copyright = '2024, Aldo Aldrich' 22 | author = 'Aldo Aldrich' 23 | release = '0.1.0' 24 | 25 | # -- General configuration --------------------------------------------------- 26 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration 27 | 28 | extensions = [ 29 | 'sphinx.ext.autodoc', 30 | 'sphinx_rtd_theme', 31 | 'sphinx.ext.todo', 32 | 'sphinx.ext.coverage', 33 | 'recommonmark', #for enabling markdown 34 | ] 35 | 36 | autosectionlabel_prefix_document = True 37 | 38 | autodoc_member_order = "bysource" 39 | autoclass_content = "both" # force sphinx to show parameter of constructor under each class 40 | 41 | from recommonmark.transform import AutoStructify 42 | github_doc_root = 'https://github.com/rtfd/recommonmark/tree/master/doc/' 43 | def setup(app): 44 | app.add_config_value('recommonmark_config', { 45 | # 'url_resolver': lambda url: github_doc_root + url, 46 | 'auto_toc_tree_section': 'Contents', 47 | }, True) 48 | app.add_transform(AutoStructify) 49 | 50 | templates_path = ['_templates'] 51 | 52 | source_suffix = ['.rst', '.md'] 53 | 54 | # The master toctree document. 55 | master_doc = 'index' 56 | 57 | exclude_patterns = [] 58 | 59 | # The name of the Pygments (syntax highlighting) style to use. 60 | pygments_style = None 61 | 62 | 63 | # -- Options for HTML output ------------------------------------------------- 64 | # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output 65 | 66 | html_theme = 'sphinx_rtd_theme' # [classic / alabaster] 67 | html_theme_path = ["_themes", ] 68 | html_static_path = ['_static'] 69 | 70 | htmlhelp_basename = 'NodeEditordoc' 71 | latex_elements = { 72 | # The paper size ('letterpaper' or 'a4paper'). 73 | # 74 | # 'papersize': 'letterpaper', 75 | 76 | # The font size ('10pt', '11pt' or '12pt'). 77 | # 78 | # 'pointsize': '10pt', 79 | 80 | # Additional stuff for the LaTeX preamble. 81 | # 82 | # 'preamble': '', 83 | 84 | # Latex figure (float) alignment 85 | # 86 | # 'figure_align': 'htbp', 87 | } 88 | 89 | latex_documents = [ 90 | (master_doc, 'NodeEditor.tex', 'NodeEditor Documentation', 91 | 'Pavel Křupala', 'manual'), 92 | ] 93 | 94 | man_pages = [ 95 | (master_doc, 'nodeeditor', 'NodeEditor Documentation', 96 | [author], 1) 97 | ] 98 | 99 | texinfo_documents = [ 100 | (master_doc, 'NodeEditor', 'NodeEditor Documentation', 101 | author, 'NodeEditor', 'One line description of project.', 102 | 'Miscellaneous'), 103 | ] 104 | 105 | epub_title = project 106 | 107 | epub_exclude_files = ['search.html'] 108 | -------------------------------------------------------------------------------- /ZCore/nodes_class/operations.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore,QtWidgets,QtGui 2 | from ZCore.Config import * 3 | 4 | import GUI.node_content as node_content 5 | import ZCore.NodeBase as NodeBase 6 | import ZCore.ToolsSystem as ToolsSystem 7 | 8 | class ZenoOperationsContent(node_content.NodeContentWidget): 9 | def initUI(self): 10 | self.input1 = QtWidgets.QLabel("Input 1", self) 11 | self.input2 = QtWidgets.QLabel("Input 2", self) 12 | self.addWidgetToLayout([self.input1, self.input2]) 13 | 14 | @register_node(OP_NODE_ADD) 15 | class ZenoNode_Add(NodeBase.ZenoNode): 16 | icon = ToolsSystem.get_path("Sources","icons","node","add.png") 17 | op_code = OP_NODE_ADD 18 | op_title = "Add" 19 | content_label_objname = "zeno_node_add" 20 | 21 | def __init__(self, nameID=op_title, scene=None): 22 | super(ZenoNode_Add, self).__init__(self.__class__.op_title, scene, "math", inputs=[1,1],outputs=[1]) # call init function, cuz this node use custom socket config 23 | 24 | def initInnerClasses(self): 25 | self.content = ZenoOperationsContent(self) 26 | self.node_graphic = NodeBase.ZenoNodeGraphics(self, self.validateIcon(self.icon)) 27 | 28 | def evalOperation(self, input1, input2): 29 | return input1 + input2 30 | 31 | @register_node(OP_NODE_SUBTRACT) 32 | class ZenoNode_Subtract(NodeBase.ZenoNode): 33 | icon = ToolsSystem.get_path("Sources","icons","node","sub.png") 34 | op_code = OP_NODE_SUBTRACT 35 | op_title = "Subtract" 36 | content_label_objname = "zeno_node_subtract" 37 | 38 | def __init__(self, nameID=op_title, scene=None): 39 | super(ZenoNode_Subtract, self).__init__(self.__class__.op_title, scene, "math", inputs=[1,1], outputs=[1]) # call init function, cuz this node use custom socket config 40 | 41 | def initInnerClasses(self): 42 | self.content = ZenoOperationsContent(self) 43 | self.node_graphic = NodeBase.ZenoNodeGraphics(self, self.validateIcon(self.icon)) 44 | 45 | def evalOperation(self, input1, input2): 46 | return input1 - input2 47 | 48 | @register_node(OP_NODE_MULTIPLY) 49 | class ZenoNode_Multiply(NodeBase.ZenoNode): 50 | icon = ToolsSystem.get_path("Sources","icons","node","mul.png") 51 | op_code = OP_NODE_MULTIPLY 52 | op_title = "Multiply" 53 | content_label_objname = "zeno_node_multi" 54 | 55 | def __init__(self, nameID=op_title, scene=None): 56 | super(ZenoNode_Multiply, self).__init__(self.__class__.op_title, scene, "math", inputs=[1,1], outputs=[1]) # call init function, cuz this node use custom socket config 57 | 58 | def initInnerClasses(self): 59 | self.content = ZenoOperationsContent(self) 60 | self.node_graphic = NodeBase.ZenoNodeGraphics(self, self.validateIcon(self.icon)) 61 | 62 | def evalOperation(self, input1, input2): 63 | return input1 * input2 64 | 65 | @register_node(OP_NODE_DIVIDE) 66 | class ZenoNode_Divide(NodeBase.ZenoNode): 67 | icon = ToolsSystem.get_path("Sources","icons","node","divide.png") 68 | op_code = OP_NODE_DIVIDE 69 | op_title = "Divide" 70 | content_label_objname = "zeno_node_divide" 71 | 72 | def __init__(self, nameID=op_title, scene=None): 73 | super(ZenoNode_Divide, self).__init__(self.__class__.op_title, scene, "math", inputs=[1,1], outputs=[1]) # call init function, cuz this node use custom socket config 74 | 75 | def initInnerClasses(self): 76 | self.content = ZenoOperationsContent(self) 77 | self.node_graphic = NodeBase.ZenoNodeGraphics(self, self.validateIcon(self.icon)) 78 | 79 | def evalOperation(self, input1, input2): 80 | return input1 / input2 -------------------------------------------------------------------------------- /GUI/node_content.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """This module containing base class for Node's content graphical representation. It also contains example of overriden 3 | Text Widget which can pass to it's parent notification about currently being modified.""" 4 | 5 | from collections import OrderedDict 6 | 7 | from PySide2 import QtCore,QtWidgets,QtGui 8 | 9 | from GUI.serializable import Serializable 10 | 11 | class NodeContentWidget(QtWidgets.QWidget, Serializable): 12 | """Base class: Node's graphics content. This class also provides layout for other widgets inside of :py:class:`~GUI.node_creator.NodeConfig` class""" 13 | def __init__(self, node, parent=None): # colon used to specify inside docs what type to pass ("string if u don't want to import/define") 14 | """ 15 | 16 | :param node: reference to :py:class:`~GUI.node_creator.NodeConfig` 17 | :type node: :py:class:`~GUI.node_creator.NodeConfig` 18 | :param parent: parent widget 19 | :type parent: QtWidgets.QWidget 20 | """ 21 | self.node = node 22 | super(NodeContentWidget,self).__init__(parent) 23 | self.nodeLayout = QtWidgets.QVBoxLayout(self) 24 | self.nodeLayout.setContentsMargins(7.5,0,0,0) 25 | self.nodeLayout.setSpacing(0) 26 | self.nodeLayout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)) 27 | 28 | self.setStyleSheet("background: transparent;") # override maya default background color for QWidget 29 | self.initUI() 30 | 31 | def initUI(self): 32 | """Default layout setup and widgets to be rendered in :py:class:`~GUI.node_creator.NodeGraphics` class""" 33 | self.label = QtWidgets.QLabel("Content Label") 34 | self.text_edit = TextEditOverride("Content Edit") 35 | 36 | # modify widget 37 | self.addWidgetToLayout([self.label, self.text_edit], 40) 38 | 39 | def addWidgetToLayout(self, widgets=[], height=20): 40 | """Let content create default layout configuration""" 41 | for item in widgets: 42 | item.setFixedHeight(height) 43 | self.nodeLayout.addWidget(item) 44 | 45 | # delete if spacer item exist within layout, it should be last 46 | for index in range(self.nodeLayout.count()): 47 | item = self.nodeLayout.itemAt(index) 48 | if isinstance(item, QtWidgets.QSpacerItem): 49 | self.nodeLayout.removeItem(item) 50 | 51 | self.nodeLayout.addItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Expanding)) 52 | 53 | def setEditingFlag(self, value): 54 | """Helper function which sets editingFlag inside :py:class:`~GUI.node_editor.GraphicsView` class 55 | 56 | This is a helper function to handle editing node's content with ``QLineEdits`` or ``QTextEdits`` (use overriden :py:classs:TextEditOverride) 57 | and handle ``keyPressEvent`` inside :py:class:`~GUI.node_editor.GraphicsView` class. 58 | 59 | .. note:: 60 | 61 | If you are handling KeyPress events by default Qt Window's shortcuts and ``QActions``, you can ignore this method 62 | 63 | :param value: new value for editing flag 64 | :type value: ``bool`` 65 | """ 66 | self.node.scene.getView().editingFlag = value 67 | 68 | def serialize(self): 69 | return OrderedDict([ 70 | ]) 71 | 72 | def deserialize(self, data, hashmap={}, restore_id=True): 73 | return True 74 | 75 | class TextEditOverride(QtWidgets.QTextEdit): 76 | """Overriden ``QTextEdit`` which sends notification about being edited to parent widget :py:class:`NodeContentWidget` 77 | 78 | .. note:: 79 | 80 | This class is example of ``QTextEdit`` modification to be able to handle `Delete` key with overriden 81 | Qt's ``keyPressEvent`` (when not using ``QActions`` in menu or toolbar) 82 | """ 83 | def focusInEvent(self, event): 84 | """Example of overriden focusInEvent to mark the start of editing 85 | 86 | :param event: Qt's focus event 87 | :type event: QFocusEvent 88 | """ 89 | super(TextEditOverride,self).focusInEvent(event) 90 | self.parentWidget().setEditingFlag(True) 91 | 92 | def focusOutEvent(self, event): 93 | """Example of overriden focusOutEvent to mark the end of editing 94 | 95 | :param event: Qt's focus event 96 | :type event: QFocusEvent 97 | """ 98 | super(TextEditOverride,self).focusOutEvent(event) 99 | self.parentWidget().setEditingFlag(False) 100 | -------------------------------------------------------------------------------- /ZCore/ToolsSystem.py: -------------------------------------------------------------------------------- 1 | """ This Module containing helper function to bridge between maya to ZenoRig or ZenoRig to operating system """ 2 | import os 3 | import maya.cmds as cmds 4 | 5 | """ 6 | Python 2 Caveat 7 | -still use old class, pyside2 need new style class for multiple inheritance 8 | -setter getter required object inheritanced 9 | -decodeError exception from json not available 10 | -not support unpacking list (*args) 11 | -not support copy list operation python (2.x till 3.3) 12 | """ 13 | 14 | ''' 15 | function: 16 | - initiate the whole ecosystem for the tools 17 | - store all name for each created rig nodes 18 | - first safeguard when create rig nodes 19 | - contains couple specific function for rig nodes 20 | ''' 21 | 22 | #------------------------------------------ NODE EDITOR ------------------------------------------# 23 | zeno_window = None 24 | 25 | #------------------------------------------- NODE DATA -------------------------------------------# 26 | spline_node = [] # def > object store points and connected spline (Type : Structs) 27 | NODE_TYPE = [0,0,0,spline_node] 28 | 29 | #--------------------------------------- NODE DEFAULT NAME ---------------------------------------# 30 | SPLINE_DEF_NAME = "zSpline" 31 | NODE_NAME = [0,0,0,SPLINE_DEF_NAME] 32 | 33 | #------------------------------------------ RUNTIME DATA -----------------------------------------# 34 | live_mesh = [] 35 | 36 | #---------------------------------------- SYSTEM CONSTANT ----------------------------------------# 37 | SETUP_GRP_NAME = "setupGrp" 38 | 39 | #---------------------------------------- DIRECTORY PATH -----------------------------------------# 40 | """ This can be temporary as os.path can't detect __file__ when using python shell, development only""" 41 | ZCORE_DIR = os.path.dirname(os.path.realpath(__file__)) 42 | 43 | #---------------------------------------- OUTPUT LOG ENUM ----------------------------------------# 44 | # output log color coded 45 | OUTPUT_INFO = 0 46 | OUTPUT_SUCCESS = 1 47 | OUTPUT_WARNING = 2 48 | OUTPUT_ERROR = 3 49 | 50 | # return zcore directory if no argument passed 51 | def get_path(*args): 52 | path = ZCORE_DIR 53 | for arg in args: 54 | path = os.path.join(path, arg) 55 | if os.path.exists(path): 56 | return path 57 | else: 58 | return False 59 | 60 | # function search if setup group present and parent it, if setup group not present create one 61 | # (obj = grp/node to parent to setup) 62 | def parent_setup(obj): 63 | if cmds.objExists(SETUP_GRP_NAME): 64 | cmds.parent(obj, SETUP_GRP_NAME) 65 | else: 66 | cmds.group(n=SETUP_GRP_NAME, em=1) 67 | cmds.parent(obj,SETUP_GRP_NAME) 68 | 69 | # Note: nameID always unique, name argument is a suggested nameID. if name unique, name and nameID can be the same 70 | # (index = index {refer to ToolsSystem.NODE_TYPE}, name = "customName") 71 | def generate_nameID(index, name=""): 72 | suffix = 1 73 | if not name: 74 | name = NODE_NAME[index] 75 | while (name+str(suffix)) in NODE_TYPE[index] or cmds.objExists(name+str(suffix)): 76 | suffix+=1 77 | nameID = name + str(suffix) 78 | else: 79 | if name in NODE_TYPE[index] or cmds.objExists(name): 80 | while (name+str(suffix)) in NODE_TYPE[index] or cmds.objExists(name+str(suffix)): 81 | suffix+=1 82 | nameID = name + str(suffix) 83 | NODE_TYPE[index].append(nameID) 84 | generate_node(index, nameID) 85 | return nameID 86 | 87 | # (index = index {refer to ToolsSystem.NODE_TYPE}, nameID = object unique id) 88 | def generate_node(index, nameID): 89 | if zeno_window and zeno_window.getCurrentNodeEditorWidget(): # remember if u switch and statement it will emit error cuz if zeno_window none, none has no given attribute 90 | node_editor = zeno_window.getCurrentNodeEditorWidget() 91 | node_editor.addNode(index, nameID) 92 | 93 | def rename_name(index, new_name): 94 | ''' 95 | TO DO: check if name exist, if so add sufix (like get unique id) 96 | ''' 97 | 98 | def store_data(): 99 | pass 100 | 101 | def load_data(): 102 | pass 103 | 104 | # ZCore_dir = "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig/ZCore/" 105 | # ZCore_curves_dir = "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig/ZCore/Sources/ControlCurves" 106 | # ZCore_save_dir = "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig/ZCore/Save/" 107 | # ZCore_icon_dir = "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig/ZCore/Sources/icons/" 108 | # ZCore_shelf_dir = "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig/ZCore/Shelf/" 109 | # ZCore_user_script_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)),'Shelf','userPref.json') -------------------------------------------------------------------------------- /ZCore/Config.py: -------------------------------------------------------------------------------- 1 | """this module will automatically register necessary component whenever imported to other module""" 2 | 3 | import json 4 | import os 5 | 6 | class ConfException(Exception): pass 7 | class InvalidRegistration(ConfException): pass 8 | class OpCodeNotRegistered(ConfException): pass 9 | 10 | #------------------------------------------------------------------------------------- 11 | #--------------------------------------- Nodes --------------------------------------- 12 | #------------------------------------------------------------------------------------- 13 | 14 | LISTBOX_MIMETYPE = "application/x-item" 15 | 16 | # Change UI ordering by changing the value (factor default) 17 | OP_NODE_INPUT = 1 18 | OP_NODE_OUTPUT = 2 19 | OP_NODE_SPLINE = 3 20 | OP_NODE_ADD = 4 21 | OP_NODE_SUBTRACT = 5 22 | OP_NODE_MULTIPLY = 6 23 | OP_NODE_DIVIDE = 7 24 | 25 | # List of all nodes available 26 | ZENO_NODES = {} 27 | NODES_NAME = {} # list node name as key 28 | 29 | def register_node_now(op_code, class_reference): 30 | if op_code in ZENO_NODES: 31 | raise InvalidRegistration("Duplicate node registration of '%s'. There is already %s" 32 | %(op_code, ZENO_NODES[op_code])) 33 | ZENO_NODES[op_code] = class_reference 34 | NODES_NAME[class_reference.op_title] = op_code 35 | 36 | def register_node(op_code): # this function called automatically with passed argumnet from whatever under @register_node decorator, weird... 37 | def decorator(original_class): # this nested function add extra functionality when register node called, think as it's the syntax u must follow 38 | register_node_now(op_code, original_class) # your custom action when this decorator called 39 | return original_class # return this is a must, to preserve the class/function's behavior underneath this decorator 40 | return decorator # return this decorator to execute it according to https://www.youtube.com/watch?v=MYAEv3JoenI 41 | 42 | def get_class_from_opcode(op_code): 43 | if op_code not in ZENO_NODES: raise OpCodeNotRegistered("OpCode '%d' is not registered" % op_code) 44 | return ZENO_NODES[op_code] 45 | 46 | # import all nodes and trigger automatic registration 47 | from ZCore.nodes_class import * 48 | 49 | #------------------------------------------------------------------------------------- 50 | #--------------------------------------- Shelf --------------------------------------- 51 | #------------------------------------------------------------------------------------- 52 | """ 53 | SHELF_FACTORY_ORDER = ["Context", "Tools"] # control shelf order, name and validity (anything beside this name will be ignored, even tho it's found under shelf) 54 | SHELF_TAB_CLASS = {} # shelf_title : item class list 55 | 56 | # get all module item inside shelf folder's sub-directory, only include directory specify by shelf_factory_order 57 | for subDir in os.walk(SHELF_DIR): 58 | if os.path.basename(subDir[0]) in SHELF_FACTORY_ORDER: 59 | shelf_title = (os.path.basename(subDir[0])) 60 | if shelf_title in SHELF_TAB_CLASS.keys(): raise InvalidRegistration("Duplicate shelf registration of '%s'. There is already %s" %(shelf_title)) 61 | shelf_item_module = [item for item in subDir[2] if not item == "__init__.py" and item[-3:] == ".py"] 62 | item_class = [] 63 | for module in shelf_item_module: 64 | spec = importlib.util.spec_from_file_location(module, SHELF_DIR+"/%s/%s"%(shelf_title, module)) 65 | foo = importlib.util.module_from_spec(spec) 66 | spec.loader.exec_module(foo) 67 | item_class.append(foo.item_class) 68 | SHELF_TAB_CLASS[shelf_title] = item_class 69 | 70 | # initalize it according to shelf factor order list, passing the app, 1 time process 71 | def init_tab_widget(app): 72 | tab_widget_list = [] 73 | for title in SHELF_FACTORY_ORDER: 74 | tab_widget = ShelfBase.ZenoTabContainer() 75 | for shelf_item in SHELF_TAB_CLASS[title]: 76 | tab_widget.itemLayout.addWidget(shelf_item(app)) 77 | tab_widget_list.append(tab_widget) 78 | return tab_widget_list 79 | 80 | """ 81 | OP_TAB_CONTEXT = 1 82 | OP_TAB_TOOLS = 2 83 | SHELF_TAB = {} 84 | 85 | def register_tab_now(op_code, class_reference): 86 | if op_code in SHELF_TAB: 87 | raise InvalidRegistration("Duplicate tab registration of '%s'. There is already %s" 88 | %(op_code, SHELF_TAB[op_code])) 89 | SHELF_TAB[op_code] = class_reference 90 | 91 | def register_tab(op_code): 92 | def decorator(original_class): 93 | register_tab_now(op_code, original_class) 94 | return original_class 95 | return decorator 96 | 97 | def get_tab_from_opcode(op_code): 98 | if op_code not in SHELF_TAB: raise OpCodeNotRegistered("OpCode '%d' is not registered" % op_code) 99 | return SHELF_TAB[op_code] 100 | 101 | # import all tabs and trigger automatic registration 102 | from ZCore.Shelf import * -------------------------------------------------------------------------------- /GUI/node_edge_graphic_path.py: -------------------------------------------------------------------------------- 1 | import math 2 | from PySide2 import QtCore,QtWidgets,QtGui 3 | 4 | DEBUG = False 5 | DEBUG_ITEM = False # delete later when bezier fixed 6 | 7 | EDGE_CP_ROUNDNESS = 100 8 | 9 | class EdgePathBaseGraphics: 10 | """Base Class for calculating the graphics path to draw for an graphics Edge""" 11 | 12 | def __init__(self, owner): 13 | # keep the reference to owner edge_graphics class 14 | self.owner = owner 15 | 16 | def calcPath(self): 17 | """Calculate the Direct line connection 18 | 19 | :return: ``QPainterPath`` of the graphics path to draw 20 | :rtype: ``QPainterPath`` or ``None`` 21 | """ 22 | return None 23 | 24 | class EdgePathDirectGraphics(EdgePathBaseGraphics): 25 | def calcPath(self): 26 | path = QtGui.QPainterPath(QtCore.QPointF(self.owner.pos_source[0], self.owner.pos_source[1])) 27 | path.lineTo(self.owner.pos_destination[0], self.owner.pos_destination[1]) 28 | return path 29 | 30 | class EdgePathBezierGraphics(EdgePathBaseGraphics): 31 | def calcPath(self): 32 | self.start_socket = self.owner.edge.start_socket.is_input 33 | dist = math.fabs(self.owner.pos_source[0] - self.owner.pos_destination[0]) 34 | if self.start_socket == True: 35 | self.input_pos = self.owner.pos_source 36 | self.output_pos = self.owner.pos_destination 37 | if self.input_pos[0] < self.output_pos[0] and dist > 300: dist = 300 38 | path = QtGui.QPainterPath(QtCore.QPointF(self.owner.pos_source[0], self.owner.pos_source[1])) 39 | path.cubicTo((self.input_pos[0] - dist*0.5), self.input_pos[1], 40 | (self.output_pos[0] + dist*0.5), self.output_pos[1], 41 | self.owner.pos_destination[0], self.owner.pos_destination[1]) 42 | else: 43 | self.input_pos = self.owner.pos_destination 44 | self.output_pos = self.owner.pos_source 45 | if self.input_pos[0] < self.output_pos[0] and dist > 300: dist = 300 46 | path = QtGui.QPainterPath(QtCore.QPointF(self.owner.pos_source[0], self.owner.pos_source[1])) 47 | path.cubicTo((self.output_pos[0] + dist*0.5), self.output_pos[1], 48 | (self.input_pos[0] - dist*0.5), self.input_pos[1], 49 | self.owner.pos_destination[0], self.owner.pos_destination[1]) 50 | return path 51 | 52 | ''' 53 | def calcPath(self): 54 | s = self.owner.pos_source 55 | d = self.owner.pos_destination 56 | dist = (d[0] - s[0] * 0.5)*0.5 57 | 58 | cpx_s = +dist 59 | cpx_d = -dist 60 | cpy_s = 0 61 | cpy_d = 0 62 | 63 | if self.owner.edge.start_socket is not None: 64 | ssin = self.owner.edge.start_socket.is_input 65 | ssout = self.owner.edge.start_socket.is_output 66 | 67 | if (s[0] > d[0] and ssout) or (s[0] < d[0] and ssin): 68 | cpx_d *= -1 69 | cpx_s *= -1 70 | 71 | cpy_d = ( 72 | (s[1] - d[1]) / math.fabs( 73 | (s[1] - d[1]) if (s[1] - d[1]) != 0 else 0.00001 74 | ) 75 | ) * EDGE_CP_ROUNDNESS 76 | 77 | cpy_s = ( 78 | (d[1] - s[1]) / math.fabs( 79 | (d[1] - s[1]) if (d[1] - s[1]) != 0 else 0.00001 80 | ) 81 | ) * EDGE_CP_ROUNDNESS 82 | 83 | path = QtGui.QPainterPath(QtCore.QPointF(self.owner.pos_source[0], self.owner.pos_source[1])) 84 | path.cubicTo((s[0] + cpx_s), (s[1] + cpy_s), (d[0] + cpx_d), (d[1] + cpy_d), 85 | self.owner.pos_destination[0], self.owner.pos_destination[1]) 86 | 87 | if DEBUG_ITEM: 88 | if self.ctrl_pt1 == None: 89 | self.ctrl_pt1 = DebugItem((s[0] + cpx_s), (s[1] + cpy_s), 10, 10, QtCore.Qt.white) 90 | self.ctrl_pt2 = DebugItem((d[0] + cpx_d), (d[1] + cpy_d), 10, 10, QtCore.Qt.black) 91 | self.owner.edge.scene.scene_graphic.addItem(self.ctrl_pt1) 92 | self.owner.edge.scene.scene_graphic.addItem(self.ctrl_pt2) 93 | else: 94 | self.ctrl_pt1.setPos((s[0] + cpx_s), (s[1] + cpy_s)) 95 | self.ctrl_pt2.setPos((d[0] + cpx_d), (d[1] + cpy_d)) 96 | 97 | return path 98 | ''' 99 | 100 | # https://stackoverflow.com/questions/52728462/pyqt-add-rectangle-in-qgraphicsscene 101 | class DebugItem(QtWidgets.QGraphicsRectItem): 102 | def __init__(self, x, y, w, h, color=QtCore.Qt.red): 103 | super(DebugItem,self).__init__(x, y, w, h) 104 | self.color = color 105 | 106 | def paint(self, painter, option, widget=None): 107 | super(DebugItem, self).paint(painter, option, widget) 108 | painter.save() 109 | painter.setRenderHint(QtGui.QPainter.Antialiasing) 110 | painter.setBrush(self.color) 111 | painter.drawEllipse(option.rect) 112 | painter.restore() -------------------------------------------------------------------------------- /ZCore/Save/graph2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 2150555730056, 3 | "scene_width": 8000, 4 | "scene_height": 8000, 5 | "nodes": [ 6 | { 7 | "id": 2150590113928, 8 | "title": "Input", 9 | "pos_x": -494.0, 10 | "pos_y": -210.0, 11 | "inputs": [], 12 | "outputs": [ 13 | { 14 | "id": 2150590115528, 15 | "index": 0, 16 | "multi_edges": true, 17 | "position": 5, 18 | "socket_type": 1 19 | } 20 | ], 21 | "content": {}, 22 | "op_code": 1 23 | }, 24 | { 25 | "id": 2150555786504, 26 | "title": "Output", 27 | "pos_x": -173.0, 28 | "pos_y": -223.0, 29 | "inputs": [ 30 | { 31 | "id": 2150555784840, 32 | "index": 0, 33 | "multi_edges": false, 34 | "position": 2, 35 | "socket_type": 1 36 | } 37 | ], 38 | "outputs": [ 39 | { 40 | "id": 2150555785352, 41 | "index": 0, 42 | "multi_edges": true, 43 | "position": 5, 44 | "socket_type": 1 45 | } 46 | ], 47 | "content": {}, 48 | "op_code": 2 49 | }, 50 | { 51 | "id": 2150555843912, 52 | "title": "Math", 53 | "pos_x": -177.0, 54 | "pos_y": -324.0, 55 | "inputs": [ 56 | { 57 | "id": 2150555841480, 58 | "index": 0, 59 | "multi_edges": false, 60 | "position": 2, 61 | "socket_type": 1 62 | }, 63 | { 64 | "id": 2150555842632, 65 | "index": 1, 66 | "multi_edges": false, 67 | "position": 2, 68 | "socket_type": 1 69 | } 70 | ], 71 | "outputs": [ 72 | { 73 | "id": 2150555843592, 74 | "index": 0, 75 | "multi_edges": true, 76 | "position": 5, 77 | "socket_type": 1 78 | } 79 | ], 80 | "content": {}, 81 | "op_code": 3 82 | }, 83 | { 84 | "id": 1914766548496, 85 | "title": "ZSpline", 86 | "pos_x": -172.0, 87 | "pos_y": -44.0, 88 | "inputs": [ 89 | { 90 | "id": 1915452237768, 91 | "index": 0, 92 | "multi_edges": false, 93 | "position": 2, 94 | "socket_type": 2 95 | } 96 | ], 97 | "outputs": [], 98 | "content": {}, 99 | "op_code": 4 100 | }, 101 | { 102 | "id": 2150555828296, 103 | "title": "Output", 104 | "pos_x": -495.2, 105 | "pos_y": -88.2, 106 | "inputs": [ 107 | { 108 | "id": 2150555828616, 109 | "index": 0, 110 | "multi_edges": false, 111 | "position": 2, 112 | "socket_type": 1 113 | } 114 | ], 115 | "outputs": [ 116 | { 117 | "id": 2150555829256, 118 | "index": 0, 119 | "multi_edges": true, 120 | "position": 5, 121 | "socket_type": 1 122 | } 123 | ], 124 | "content": {}, 125 | "op_code": 2 126 | }, 127 | { 128 | "id": 2078837775608, 129 | "title": "zSpline1", 130 | "pos_x": -178.0, 131 | "pos_y": 77.0, 132 | "inputs": [ 133 | { 134 | "id": 2078855404744, 135 | "index": 0, 136 | "multi_edges": false, 137 | "position": 2, 138 | "socket_type": 2 139 | } 140 | ], 141 | "outputs": [], 142 | "content": {}, 143 | "op_code": 4 144 | } 145 | ], 146 | "edges": [ 147 | { 148 | "id": 2078837748736, 149 | "edge_type": 2, 150 | "start": 2150555841480, 151 | "end": 2150590115528 152 | }, 153 | { 154 | "id": 2080126772952, 155 | "edge_type": 2, 156 | "start": 2150555842632, 157 | "end": 2150590115528 158 | }, 159 | { 160 | "id": 2078837793792, 161 | "edge_type": 2, 162 | "start": 2150555784840, 163 | "end": 2150555829256 164 | } 165 | ] 166 | } -------------------------------------------------------------------------------- /ZCore/Commands.py: -------------------------------------------------------------------------------- 1 | from ZCore.ToolsSystem import OUTPUT_INFO, OUTPUT_SUCCESS, OUTPUT_WARNING, OUTPUT_ERROR 2 | 3 | import cmd, math 4 | import ZCore.Config as Config 5 | 6 | class ZenoCommand(cmd.Cmd, object): 7 | prompt ="(Zeno) > " 8 | # use_rawinput = True # good to know this default is true, and let cmdloop accept input()/raw_input() 9 | 10 | def __init__(self, app): 11 | super(ZenoCommand, self).__init__() 12 | self.app = app 13 | self.command_list = [] 14 | 15 | # automate cmd list registration 16 | for func in dir(self): 17 | if func[:3] == "do_": self.command_list.append(func[3:]) 18 | 19 | def default(self, arg): # handle unknown command 20 | self.app.outputLogInfo("unknown command: '%s'"%arg, OUTPUT_ERROR) 21 | 22 | def do_help(self, arg): 23 | # super(ZenoCommand, self).do_help(arg) 24 | # check if help also called with command to return docstring 25 | if arg: 26 | try: 27 | doc = getattr(self, 'do_'+arg).__doc__ 28 | if doc: 29 | self.app.outputLogInfo(doc) 30 | return 31 | except AttributeError: pass 32 | 33 | # get all nice name cmd to list 34 | func = self.command_list 35 | self.app.outputLogInfo("ZENO COMMANDS:", OUTPUT_WARNING, log_detail=False) 36 | self.app.outputLogInfo("============================", OUTPUT_WARNING, log_detail=False) 37 | self.custom_column(func) 38 | 39 | def custom_column(self, item_list, column_size=3, column_padding=4): 40 | ncolumn = int(math.ceil(len(item_list)/float(column_size))) # convert to float to get precise decimal value 41 | column_width = [] 42 | # iterate through all item to find each column longest text 43 | for num in range(column_size): 44 | column_width.append(max(len(item) for item in item_list[num::column_size]) + column_padding) 45 | # print each item with width of longest text of corresponding column 46 | for num in range(ncolumn): 47 | item_in_row = [item for item in item_list if item in item_list[0+(num*column_size):column_size+(num*column_size)]] 48 | self.app.outputLogInfo("".join((item).ljust(column_width[index]) for index, item in enumerate(item_in_row))) 49 | 50 | # required 'do_' as prefix followed by command to bind command with this function 51 | def do_debug_on_screen(self, arg): 52 | """Show debug information on scene graphic""" 53 | try: 54 | active_editor = self.app.getCurrentNodeEditorWidget() 55 | if active_editor: 56 | if active_editor.debug_widget.isVisible(): 57 | active_editor.debug_widget.setVisible(False) 58 | self.app.outputLogInfo("turn off: on-screen debug", OUTPUT_WARNING) 59 | else: 60 | active_editor.debug_widget.setVisible(True) 61 | self.app.outputLogInfo("turn on: on-screen debug", OUTPUT_WARNING) 62 | return 63 | self.app.outputLogInfo("error : no graphic scene found", OUTPUT_ERROR) 64 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) 65 | 66 | def do_debug_on_log(self, arg): 67 | """Show debug information on output log""" 68 | self.app.outputLogInfo("debug_on_log") 69 | 70 | def do_print(self, arg): 71 | """Print some text with argument""" 72 | self.app.outputLogInfo(arg) 73 | 74 | def do_newScene(self, arg): 75 | """Create new scene graph""" 76 | try: self.app.onFileNew() 77 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) 78 | 79 | def do_node(self, arg): 80 | """Create node from given node name""" 81 | args = arg.split() 82 | if len(args) == 0: return 83 | node_name = str(args[0]) 84 | if len(args) > 1: node_amount = int(args[1]) 85 | else: node_amount = 1 86 | reached_limit = False 87 | 88 | if node_name in Config.NODES_NAME: 89 | active_editor = self.app.getCurrentNodeEditorWidget() 90 | if node_amount > 20: 91 | reached_limit = True 92 | args[1] = 20 93 | try: 94 | active_editor.scene.doDeselectItems(silent=True) 95 | x, y = active_editor.scene.getView().width()/2, active_editor.scene.getView().height()/2 96 | scenepos = active_editor.scene.getView().mapToScene(x, y) 97 | for index in range(node_amount): 98 | node = active_editor.addNode(Config.NODES_NAME[node_name]) 99 | node.setPos(scenepos.x(), scenepos.y()+((node.node_graphic.height+10)*index)) 100 | node.doSelect() 101 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) 102 | if reached_limit: self.app.outputLogInfo("only 20 new nodes allowed at a time", OUTPUT_ERROR) 103 | 104 | def do_openScene(self, arg): 105 | """Open scene graph""" 106 | try: self.app.onFileOpen() 107 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) -------------------------------------------------------------------------------- /GUI/node_edge_dragging.py: -------------------------------------------------------------------------------- 1 | import GUI.node_creator as node_creator 2 | 3 | DEBUG = False 4 | 5 | class EdgeDragging: 6 | def __init__(self, view_graphic): 7 | self.view_graphic = view_graphic 8 | # initializing these variable to know we're using them in this class... 9 | self.drag_edge = None 10 | self.drag_start_socket = None 11 | 12 | def getEdgeClass(self): 13 | """Helper function to get the edge class. Using what Scene class provides""" 14 | return self.view_graphic.scene_graphic.scene.getEdgeClass() 15 | 16 | def updateDestination(self, x, y): 17 | """ update the end point of our dragging edge 18 | 19 | :param x: new x scene position 20 | :type x: ``float`` 21 | :param y: new y scene position 22 | :type y: ``float`` 23 | """ 24 | # according to sentry: 'NoneType' object has no attribute 'edge_graphic' 25 | if self.drag_edge is not None and self.drag_edge.edge_graphic is not None: 26 | self.drag_edge.edge_graphic.setDestination(x, y) 27 | self.drag_edge.edge_graphic.update() 28 | else: 29 | if DEBUG: print(">>> trying to update self.drag_edge edge_graphic, ignored. value is None!") 30 | 31 | def edgeDragStart(self, item): 32 | """Code handling the start of a dragging an `Edge` operation""" 33 | if DEBUG: print ("View:edgeDragStart - start dragging edge") 34 | if DEBUG: print ("View:edgeDragStart - assign Start socket to", item.socket) 35 | #self.previousEdge = item.socket.edge 36 | self.drag_start_socket = item.socket 37 | self.drag_edge = self.getEdgeClass()(item.socket.node.scene, 38 | item.socket, 39 | None, 40 | node_creator.EDGE_TYPE_BEZIER) 41 | self.drag_edge.edge_graphic.makeUnselectable() 42 | if DEBUG: print ("View:edgeDragStart - drag_edge", self.drag_edge) 43 | 44 | 45 | def edgeDragEnd(self, item): 46 | """Code handling the end of the dragging an `Edge` operation. If this code returns True then skip the 47 | rest of the mouse event processing. Can be called with ``None`` to cancel the edge dragging mode 48 | 49 | :param item: Item in the `Graphics Scene` where we ended dragging an `Edge` 50 | :type item: ``QGraphicsItem`` 51 | """ 52 | # if clicked on something else than socket 53 | if not isinstance(item, node_creator.SocketGraphics): 54 | self.view_graphic.resetMode() 55 | if DEBUG: print ("View:edgeDragEnd - end dragging edge early") 56 | self.drag_edge.remove(silent=True) # don't notify sockets about removing drag_edge 57 | self.drag_edge = None 58 | 59 | # clicked on socket 60 | if isinstance(item, node_creator.SocketGraphics): 61 | 62 | # check if edge would be valid 63 | if not self.drag_edge.validateEdge(self.drag_start_socket, item.socket): 64 | #print("NOT VALID CONNECTION") 65 | ## if you want behavior when u click on socket and release the drag edge is dissapear 66 | # self.view_graphic.resetMode() 67 | # if DEBUG: print('View::edgeDragEnd ~ End dragging edge') 68 | # self.drag_edge.remove(silent=True) 69 | # self.drag_edge = None 70 | # 71 | return False 72 | 73 | # regular processing of drag edge 74 | self.view_graphic.resetMode() 75 | if DEBUG: print('View::edgeDragEnd ~ End dragging edge') 76 | self.drag_edge.remove(silent=True) 77 | self.drag_edge = None 78 | 79 | try: 80 | if item.socket != self.drag_start_socket: 81 | # if we released dragging on a socket (other then the beginning socket) 82 | 83 | ## First remove old edges / send notifications 84 | for socket in (item.socket, self.drag_start_socket): 85 | if not socket.is_multi_edges: 86 | if socket.is_input: 87 | # print("removing SILENTLY edges from input socket (is_input and !is_multi_edges) [DragStart]:", item.socket.edges) 88 | socket.removeAllEdges(silent=True) 89 | else: 90 | socket.removeAllEdges(silent=False) 91 | 92 | # create new edge 93 | new_edge = self.getEdgeClass()(item.socket.node.scene, 94 | self.drag_start_socket, 95 | item.socket, 96 | edge_type=node_creator.EDGE_TYPE_BEZIER) 97 | if DEBUG: print("View:edgeDragEnd - created new edge:", new_edge, "connecting", new_edge.start_socket, "<--->", new_edge.end_socket) 98 | 99 | # send notifications for the new edge 100 | for socket in (self.drag_start_socket, item.socket): 101 | socket.node.onEdgeConnectionChanged(new_edge) 102 | if socket.is_input: socket.node.onInputChanged(socket) 103 | 104 | self.view_graphic.scene_graphic.scene.history.storeHistory("connect edge", setModified=True) 105 | return True 106 | except Exception as e: print ("ERROR Edge Drag End: %s"%e) 107 | 108 | #if DEBUG: print ("View:edgeDragEnd - about to set socket to previous edge", self.previousEdge) 109 | #if self.previousEdge is not None: 110 | # self.previousEdge.start_socket.edge = self.previousEdge 111 | 112 | if DEBUG: print ("View:edgeDragEnd - everything done...") 113 | return False -------------------------------------------------------------------------------- /ZCore/Eyebrow.py: -------------------------------------------------------------------------------- 1 | from ZCore.Face import FaceSystem 2 | 3 | import maya.api.OpenMaya as om 4 | import maya.cmds as cmds 5 | 6 | ''' 7 | ---------------------------------- Feature expansion ---------------------------------- 8 | -migrate constraint jnt along curve to MayaUtil 9 | -request id from parent 10 | -mpxcontext, create using custom vtx 11 | -when not select anything, will create curve skinned within joint 12 | 13 | --------------------------------------- Problem --------------------------------------- 14 | pass 15 | ''' 16 | 17 | class EyebrowSystem(FaceSystem): # inherit FaceRig later 18 | 19 | def __init__(self): 20 | # constant properties 21 | self.nameID = "None" #FaceRig.generate_nameID() 22 | 23 | # writable properties 24 | self.ctrl_pos = [] 25 | self.joint_num = 3 26 | self.name = self.nameID 27 | self.size = 1 28 | 29 | # read-only properties 30 | self.setup_grp = None 31 | self.setup_crv = None 32 | self.nurbs_surface = None 33 | self.setup_jnt = [] 34 | self.setup_ctrl = [] 35 | self.ribbon_follicles = [] 36 | 37 | # def(vtx=list,pos=xform(ws)) 38 | def setup(self): 39 | if self.setup_crv != None: 40 | cmds.curve(self.setup_crv,a=1,p=pos) 41 | cmds.xform(self.setup_crv,cp=1) 42 | else: 43 | self.setup_crv = cmds.curve(n=self.name+"setup1",d=1,ep=pos) 44 | cmds.xform(self.setup_crv,cp=1) 45 | self.setup_grp = cmds.group(n=self.name+"grp",em=1) 46 | cmds.parent(self.setup_crv,self.setup_grp) 47 | cmds.rebuildCurve(self.setup_crv,ch=0,kcp=1,kr=0,d=1) # set max value to 1 48 | 49 | cmds.select(cl=1) 50 | jnt = cmds.joint(n=self.name+"ctrl1") 51 | cmds.setAttr(jnt+".radius",self.size*2) 52 | self.setup_ctrl.append(jnt) 53 | cmds.parent(self.setup_ctrl,self.setup_grp) 54 | ''' 55 | # setup jnt along setup_crv 56 | for iter in range(self.joint_num): 57 | cmds.select(cl=1) 58 | jnt = cmds.joint(n=self.name+"jnt1") 59 | cmds.setAttr(jnt+".radius",self.size) 60 | self.setup_jnt.append(jnt) 61 | cmds.parent(self.setup_jnt,self.setup_grp) 62 | self.distribute_object_to_curve(self.setup_jnt,self.setup_crv) 63 | for iter in range(self.ctrl_num): 64 | cmds.select(cl=1) 65 | jnt = cmds.joint(n=self.name+"ctrl1") 66 | cmds.setAttr(jnt+".radius",self.size*2) 67 | self.setup_ctrl.append(jnt) 68 | cmds.parent(self.setup_ctrl,self.setup_grp) 69 | self.distribute_object_to_curve(self.setup_ctrl,self.setup_crv) 70 | # create crv, if there is update 71 | self.setup_crv = cmds.polyToCurve(f=2,dg=1,usm=0,ch=0,n=self.name+"setup1")[0] 72 | cmds.xform(self.setup_crv,cp=1) 73 | cmds.rebuildCurve(self.setup_crv,ch=0,kcp=1,kr=0,d=1) # set max value to 1 74 | self.setup_grp = cmds.group(n=self.name+"grp",em=1) 75 | cmds.parent(self.setup_crv,self.setup_grp) 76 | ''' 77 | 78 | def build(self): 79 | self.nurbs_surface = cmds.extrude(self.setup_crv,n=self.name+"surface",ch=False,l=self.size*0.25,et=0)[0] 80 | cmds.extendSurface(self.nurbs_surface,ch=0,et=0,d=self.size*0.25,es=1,ed=1) 81 | cmds.rebuildSurface(self.nurbs_surface,ch=False,dir=1,sv=1,kr=0) # remove v middle spans 82 | cmds.rebuildSurface(self.nurbs_surface,ch=False,kr=1,dir=0 ,su=0) # make u spans cubic 83 | nurbs_surface_shape = cmds.listRelatives(self.nurbs_surface, s=1)[0] 84 | 85 | # setup ribbon using follicle node 86 | for iter in range(len(self.setup_jnt)): 87 | follicle = cmds.createNode("follicle") 88 | follicle = cmds.pickWalk(follicle, d="up")[0] 89 | follicle = cmds.rename(follicle, self.name + "_follicle#") 90 | follicle_index = follicle[len(self.name + "_follicle"):] 91 | follicle_shape = cmds.pickWalk(follicle, d="down")[0] 92 | self.ribbon_follicles.append(follicle) 93 | 94 | # connect follicles to nurbs plane 95 | cmds.connectAttr(nurbs_surface_shape + ".local", follicle_shape + ".inputSurface") 96 | cmds.connectAttr(nurbs_surface_shape + ".worldMatrix[0]", follicle_shape + ".inputWorldMatrix") 97 | cmds.connectAttr(follicle_shape + ".outRotate", follicle + ".rotate") 98 | cmds.connectAttr(follicle_shape + ".outTranslate", follicle + ".translate") 99 | 100 | # UValue (0-1). calculate U and V value to determine where to place follicle on surface 101 | dgpa = om.MDagPath.getAPathTo(om.MSelectionList().add(self.setup_crv).getDependNode(0)) 102 | curve = om.MFnNurbsCurve(dgpa) 103 | cv_pos = curve.cvPosition(iter, om.MSpace.kObject) 104 | U = curve.closestPoint(cv_pos)[1] 105 | cmds.setAttr(follicle_shape+".parameterU",U) 106 | cmds.setAttr(follicle_shape+".parameterV",0.5) 107 | 108 | # migrate to maya util 109 | # def(obj=list,crv) 110 | def distribute_object_to_curve(self,obj,crv): 111 | mopath_list = [] 112 | if len(obj) != 1: 113 | step = 1.0/(len(obj)-1) 114 | for iter in range(len(obj)): 115 | motion_path = cmds.pathAnimation(obj[iter], crv, f=1, fm=1) 116 | mopath_list.append(motion_path) 117 | mopath_input = motion_path + "_uValue.output" 118 | cmds.disconnectAttr(mopath_input, motion_path + ".uValue") 119 | cmds.delete(motion_path + "_uValue") 120 | cmds.setAttr(motion_path + ".uValue", 0+(step*iter)) 121 | return mopath_list 122 | 123 | else: 124 | motion_path = cmds.pathAnimation(obj[0], crv, f=1, fm=1) 125 | mopath_input = motion_path + "_uValue.output" 126 | cmds.disconnectAttr(mopath_input, motion_path + ".uValue") 127 | cmds.delete(motion_path + "_uValue") 128 | cmds.setAttr(motion_path + ".uValue", 0.5) 129 | return [motion_path] 130 | 131 | ''' 132 | instance = EyebrowSystem() 133 | instance.setup() 134 | if instance.setup_crv is not None: 135 | instance.build() 136 | pass 137 | 138 | 1. select edge to curve 139 | 3. rebuild curve number span as controller needed 140 | 4. create ribbon 141 | ''' -------------------------------------------------------------------------------- /ZCore/Sources/controlCurves/controls.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrow_line_cross":[[0.2857142857142857, 0.7142857142857156, -1.3877787807814457e-17], [0.0, 1.0, 0.0], [-0.2857142857142857, 0.7142857142857156, 1.3877787807814457e-17], [0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.2857142857142857, -0.7142857142857156, -1.3877787807814457e-17], [0.0, -1.0, 0.0], [-0.2857142857142857, -0.7142857142857156, 1.3877787807814457e-17], [0.0, -1.0, 0.0], [0.0, 0.0, 0.0], [-1.0, 0.0, 1.6653345369377348e-16], [-0.7142857142857155, -0.28571428571428564, 8.326672684688674e-17], [-1.0, 0.0, 1.6653345369377348e-16], [-0.7142857142857155, 0.28571428571428564, 8.326672684688674e-17], [-1.0, 0.0, 1.6653345369377348e-16], [1.0, 0.0, -1.6653345369377348e-16], [0.7142857142857155, 0.28571428571428564, -8.326672684688674e-17], [1.0, 0.0, -1.6653345369377348e-16], [0.7142857142857155, -0.28571428571428564, -8.326672684688674e-17], [1.0, 0.0, -1.6653345369377348e-16]], 3 | "arrow_rotate":[[0.0, 0.11363549310263743, 1.0], [0.0, 0.8900243135924486, 0.945110195475448], [0.0, 0.5098549392342592, 0.8611526065520864], [0.0, 0.7278108829317862, 0.7277962911283667], [0.0, 0.9509314130520045, 0.3939185011299523], [0.0, 1.0293349967989234, -3.885780586188048e-16], [0.0, 0.9509314130520039, -0.393918501129953], [0.0, 0.7278108829317858, -0.7277962911283671], [0.0, 0.5115393805414652, -0.8629556062620732], [0.0, 0.8900243135924484, -0.9451101954754486], [0.0, 0.11363549310263693, -0.9999999999999994], [0.0, 0.4903922094361548, -0.3189366952848418], [0.0, 0.40198685643306986, -0.7192473183001294], [0.0, 0.5953236917992244, -0.5955261530716661], [0.0, 0.7781270690721265, -0.32225815453814255], [0.0, 0.8420984472497195, -2.220446049250313e-16], [0.0, 0.7781270690721271, 0.3222581545381422], [0.0, 0.5953236917992251, 0.595526153071666], 4 | [0.0, 0.40054409186999484, 0.7164402201173561], [0.0, 0.490392209436155, 0.3189366952848413], [0.0, 0.11363549310263743, 1.0]], 5 | "box":[[-1.0, -0.9999999999999997, 0.9999999999999997], [-0.9999999999999997, 1.0, 0.9999999999999997], [-0.9999999999999997, 1.0, -0.9999999999999997], [-1.0, -0.9999999999999997, -0.9999999999999997], [-1.0, -0.9999999999999997, 0.9999999999999997], [0.9999999999999997, -1.0, 0.9999999999999997], [0.9999999999999997, -1.0, -0.9999999999999997], [1.0, 0.9999999999999997, -0.9999999999999997], [1.0, 0.9999999999999997, 0.9999999999999997], [0.9999999999999997, -1.0, 0.9999999999999997], [1.0, 0.9999999999999997, 0.9999999999999997], [-0.9999999999999997, 1.0, 0.9999999999999997], [-0.9999999999999997, 1.0, -0.9999999999999997], [1.0, 0.9999999999999997, -0.9999999999999997], [0.9999999999999997, -1.0, -0.9999999999999997], [-1.0, -0.9999999999999997, -0.9999999999999997]], 6 | "circle":[[-0.707106781186547, 0.7071067811865483, 0.0], [-1.0, 0.0, -1.6653345369377348e-16], [-0.7071067811865478, -0.7071067811865478, 2.7755575615628914e-17], [0.0, -1.0, 0.0], [0.707106781186547, -0.7071067811865478, 0.0], [1.0, 0.0, 1.6653345369377348e-16], [0.7071067811865478, 0.707106781186547, -2.7755575615628914e-17], [0.0, 1.0, 0.0],[-0.707106781186547, 0.7071067811865483, 0.0]], 7 | "handle_3D":[[0.0, 0.0, 0.0], [0.0, 0.8406483945113074, 0.0], [-0.07687498622552066, 0.9175233807368268, 0.0], [0.0, 0.9943983669623487, 0.0], [0.07687498622552082, 0.9175233807368268, 0.0], [0.0, 0.8406483945113074, 0.0], [0.0, 0.9175233807368268, 0.07687498622552076], [0.0, 0.9943983669623487, 0.0], [0.0, 0.9175233807368268, -0.07687498622552076], [-0.07687498622552066, 0.9175233807368268, 0.0], [0.0, 0.9175233807368268, 0.07687498622552076], [0.07687498622552082, 0.9175233807368268, 0.0], [0.0, 0.9175233807368268, -0.07687498622552076], [0.0, 0.8406483945113074, 0.0], [0.0, 0.0, 0.0], [0.0, -0.8406483945113074, 0.0], [-0.07687498622552082, -0.9175233807368268, 0.0], [0.0, -0.9943983669623487, 0.0], [0.07687498622552066, -0.9175233807368268, 0.0], [0.0, -0.8406483945113074, 0.0], [0.0, -0.9175233807368268, 0.07687498622552076], [0.0, -0.9943983669623487, 0.0], 8 | [0.0, -0.9175233807368268, -0.07687498622552076], [-0.07687498622552082, -0.9175233807368268, 0.0], [0.0, -0.9175233807368268, 0.07687498622552076], [0.07687498622552066, -0.9175233807368268, 0.0], [0.0, -0.9175233807368268, -0.07687498622552076], [0.0, -0.8406483945113074, 0.0], [0.0, 0.0, 0.0], [-0.8355557483070825, 0.0, 0.0], [-0.9124307345326036, 0.0, -0.07687498622552076], [-0.9893057207581257, 0.0, 0.0], [-0.9124307345326036, 0.0, 0.07687498622552076], [-0.8355557483070825, 0.0, 0.0], [-0.9124307345326036, -0.07687498622552066, 0.0], [-0.9893057207581257, 0.0, 0.0], [-0.9124307345326036, 0.07687498622552082, 0.0], [-0.9124307345326036, 0.0, 0.07687498622552076], [-0.9124307345326036, -0.07687498622552066, 0.0], [-0.9124307345326036, 0.0, -0.07687498622552076], [-0.9124307345326036, 0.07687498622552082, 0.0], [-0.8355557483070825, 0.0, 0.0], [0.0, 0.0, 0.0], 9 | [0.8355557483070825, 0.0, 0.0], [0.9124307345326036, 0.0, -0.07687498622552076], [0.9893057207581257, 0.0, 0.0], [0.9124307345326036, 0.0, 0.07687498622552076], [0.8355557483070825, 0.0, 0.0], [0.9124307345326036, -0.07687498622552082, 0.0], [0.9893057207581257, 0.0, 0.0], [0.9124307345326036, 0.07687498622552066, 0.0], [0.9124307345326036, 0.0, -0.07687498622552076], [0.9124307345326036, -0.07687498622552082, 0.0], [0.9124307345326036, 0.0, 0.07687498622552076], [0.9124307345326036, 0.07687498622552066, 0.0], [0.8355557483070825, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, -0.8462500275489583], [-0.07687498622552076, 0.0, -0.9231250137744794], [0.0, 0.0, -0.9999999999999999], [0.07687498622552076, 0.0, -0.9231250137744794], [0.0, 0.0, -0.8462500275489583], [0.0, -0.07687498622552076, -0.9231250137744794], [0.0, 0.0, -0.9999999999999999], [0.0, 0.07687498622552076, -0.9231250137744794], 10 | [-0.07687498622552076, 0.0, -0.9231250137744794], [0.0, -0.07687498622552076, -0.9231250137744794], [0.07687498622552076, 0.0, -0.9231250137744794], [0.0, 0.07687498622552076, -0.9231250137744794], [0.0, 0.0, -0.8462500275489583], [0.0, 0.0, 0.0], [0.0, 0.0, 0.8462500275489583], [-0.07687498622552076, 0.0, 0.9231250137744794], [0.0, 0.0, 0.9999999999999999], [0.07687498622552076, 0.0, 0.9231250137744794], [0.0, 0.0, 0.8462500275489583], [0.0, -0.07687498622552076, 0.9231250137744794], [0.0, 0.0, 0.9999999999999999], [0.0, 0.07687498622552076, 0.9231250137744794], [-0.07687498622552076, 0.0, 0.9231250137744794], [0.0, -0.07687498622552076, 0.9231250137744794], [0.07687498622552076, 0.0, 0.9231250137744794], [0.0, 0.07687498622552076, 0.9231250137744794], [0.0, 0.0, 0.8462500275489583]], 11 | "octahedron":[[-0.9999999999999999, 0.0, 0.0], [0.0, 0.0, 0.9999999999999999], [0.9999999999999999, 0.0, 0.0], [0.0, 0.0, -0.9999999999999999], [-0.9999999999999999, 0.0, 0.0], [0.0, -0.9999999999999999, 0.0], [0.9999999999999999, 0.0, 0.0], [0.0, 0.9999999999999999, 0.0], [0.0, 0.0, -0.9999999999999999], [0.0, -0.9999999999999999, 0.0], [0.0, 0.0, 0.9999999999999999], [0.0, 0.9999999999999999, 0.0], [-0.9999999999999999, 0.0, 0.0]], 12 | "sphere_3D":[[0.0, 0.0, 1.0],[-0.5, 0.0, 0.8660250000000008],[-0.8660250000000008, 0.0, 0.5],[-1.0, 0.0, 0.0],[-0.8660250000000008, 0.0, -0.5],[-0.5, 0.0, -0.8660250000000008],[0.0, 0.0,-1.0],[0.5, 0.0,-0.8660250000000008],[0.8660250000000008, 0.0, -0.5], [1.0, 0.0, 0.0], [0.8660250000000008, 0.0, 0.5], [0.5, 0.0, 0.8660250000000008],[0.0, 0.0, 1.0],[0.0,0.7071070000000004, 0.7071070000000004], [0.0, 1.0, 0.0], [0.0, 0.7071070000000004, -0.7071070000000004], [0.0, 0.0, -1.0], [0.0, -0.7071070000000004, -0.7071070000000004], [0.0, -1.0, 0.0], [-0.5000000000000002, -0.8660250000000008, 0.0], [-0.8660250000000008, -0.4999999999999997, 0.0], [-1.0, 0.0, 0.0], [-0.8660250000000008, 0.5000000000000002, 0.0], 13 | [-0.4999999999999997, 0.8660250000000008, 0.0], [0.0, 1.0, 0.0], [0.5, 0.8660250000000008, 0.0],[0.8660250000000008, 0.4999999999999997, 0.0], [1.0, 0.0, 0.0],[0.8660250000000008, -0.5000000000000002, 0.0], [0.4999999999999997, -0.8660250000000008, 0.0],[0.0, -1.0, 0.0], [0.0, -0.7071070000000004, 0.7071070000000004], [0.0, 0.0, 1.0]] 14 | } -------------------------------------------------------------------------------- /ZCore/NodeBase.py: -------------------------------------------------------------------------------- 1 | from PySide2 import QtCore,QtWidgets,QtGui 2 | 3 | import GUI.node_creator as node_creator 4 | import GUI.node_content as node_content 5 | 6 | import ZCore.ToolsSystem as ToolsSystem 7 | 8 | DEBUG = False 9 | 10 | NODE_COLORS = { 11 | "math" : QtGui.QColor("#303030"), 12 | "evaluation" : QtGui.QColor("#912130"), 13 | "unknown" : QtGui.QColor("#454545"), 14 | } 15 | 16 | class ZenoNodeGraphics(node_creator.NodeGraphics): 17 | def __init__(self, node, icon="", parent=None): 18 | super(ZenoNodeGraphics, self).__init__(node, parent) 19 | self.icon = icon 20 | 21 | @property 22 | def node_type(self): 23 | return self.node.node_type 24 | 25 | def getNodeHeaderColor(self, key): 26 | try: return NODE_COLORS[key] 27 | except: return NODE_COLORS["unknown"] 28 | 29 | def initSizes(self): 30 | super(ZenoNodeGraphics, self).initSizes() 31 | self.width = 160 32 | self.edge_roundness = 5 33 | self.edge_padding = 0 # feels like edge padding is edge/outline width 34 | self.title_height = 20 35 | self.title_horizontal_padding = 5 36 | self.title_vertical_padding = 10 37 | 38 | # calculate node height from input and output amount 39 | self.height = self.title_height + (self.node.segment_amount*self.node.socket_spacing) + (self.node.socket_spacing/4) 40 | 41 | def initAsset(self): 42 | super(ZenoNodeGraphics, self).initAsset() 43 | self._title_font = QtGui.QFont("Ubuntu", 8) 44 | self._brush_title = QtGui.QBrush(self.getNodeHeaderColor(self.node_type)) 45 | self._brush_background = QtGui.QBrush(QtGui.QColor("#4b4c51")) 46 | self._brush_segment = QtGui.QBrush(QtGui.QColor("#595b61")) 47 | self._invalid_icon = QtGui.QImage(ToolsSystem.get_path("Sources","icons","node","invalid.png")) 48 | 49 | def initTitle(self): 50 | super(ZenoNodeGraphics, self).initTitle() 51 | self.title_item.setPos(self.title_horizontal_padding, -3) 52 | 53 | def paint(self, painter, option, widget): 54 | ''' 55 | self._brush_title = QtGui.QBrush(QtGui.QColor("#6db463")) 56 | if self.node.isDirty(): 57 | self._brush_title = QtGui.QBrush(QtGui.QColor("#bbbb4c")) 58 | if self.node.isInvalid(): 59 | self._brush_title = QtGui.QBrush(QtGui.QColor("#de6e5b")) 60 | ''' 61 | 62 | super(ZenoNodeGraphics, self).paint(painter, option, widget) 63 | 64 | # draw node icon 65 | painter.drawImage( 66 | QtCore.QRectF(self.width-self.title_horizontal_padding-20, 2.5, 15, 15), 67 | self.icon, 68 | QtCore.QRectF(0, 0, 32, 32) 69 | ) 70 | 71 | # draw warning if node is invalid 72 | if self.node.isInvalid(): 73 | painter.drawImage( 74 | QtCore.QRectF(-14, -14, 24, 24), 75 | self._invalid_icon, 76 | QtCore.QRectF(0, 0, 64, 64) 77 | ) 78 | 79 | class ZenoNodeContent(node_content.NodeContentWidget): 80 | def initUI(self): 81 | layout = QtWidgets.QVBoxLayout(self) 82 | 83 | class ZenoNode(node_creator.NodeConfig): 84 | icon = "" 85 | op_code = 0 86 | op_title = "undefined" 87 | content_label = "" 88 | content_label_objname = "zeno_node_bg" 89 | 90 | GraphicNode_class = ZenoNodeGraphics 91 | NodeContent_class = ZenoNodeContent 92 | 93 | def __init__(self, title=op_title, scene=None, node_type="unknown", inputs=[1,1], outputs=[2]): 94 | self.node_type = node_type 95 | super(ZenoNode, self).__init__(title, scene, inputs, outputs) # self.__class__ will access instance from derived class 96 | 97 | self.value = None 98 | 99 | # mark node dirty by default, cuz it's needed to be used/evaluated 100 | self.markDirty() 101 | 102 | def initInnerClasses(self): 103 | """Sets up graphics Node and Content Widget""" 104 | node_content_class = self.getNodeContentClass() 105 | graphics_node_class = self.getGraphicsNodeClass() 106 | if node_content_class is not None: self.content = node_content_class(self) 107 | if graphics_node_class is not None: self.node_graphic = graphics_node_class(self, self.validateIcon(self.icon)) 108 | 109 | def initSettings(self): 110 | super(ZenoNode, self).initSettings() 111 | self.input_socket_position = node_creator.LEFT_TOP 112 | self.output_socket_position = node_creator.RIGHT_TOP 113 | 114 | def validateIcon(self, icon): 115 | if icon == "" or QtGui.QImageReader.canRead(QtGui.QImageReader(icon))==False: # check if icon not valid, replace with template icon if true 116 | return ToolsSystem.get_path("Sources","icons","node","python.png") 117 | return icon 118 | 119 | def evalOperation(self, input1, input2): 120 | return 1 121 | 122 | def evalImplementation(self): 123 | # this is where derived class/nodes will implement it's custom evaluation 124 | input1 = self.getInput(0) 125 | input2 = self.getInput(1) 126 | 127 | if input1 is None or input2 is None: 128 | self.markInvalid() 129 | self.markDescendantsDirty() 130 | self.node_graphic.setToolTip("Connect all inputs") 131 | return None 132 | 133 | else: 134 | value = self.evalOperation(input1.eval(), input2.eval()) 135 | self.value = value 136 | self.markDirty(False) 137 | self.markInvalid(False) 138 | self.node_graphic.setToolTip("") 139 | 140 | self.markDescendantsDirty() 141 | self.evalChildren() 142 | 143 | return value 144 | 145 | def eval(self): 146 | if not self.isDirty() and not self.isInvalid(): 147 | if DEBUG: print("_> returning cached %s value:" % self.__class__.__name__, self.value) 148 | return self.value 149 | 150 | try: 151 | value = self.evalImplementation() 152 | return value 153 | except ValueError as e: 154 | self.markInvalid() 155 | self.node_graphic.setToolTip(str(e)) 156 | self.markDescendantsDirty() 157 | except Exception as e: 158 | self.markInvalid() 159 | self.node_graphic.setToolTip(str(e)) 160 | if DEBUG: print("Node Eval::", e) 161 | 162 | def onInputChanged(self, socket=None): 163 | if DEBUG: print("%s::__onInputChanged"% self.__class__.__name__) 164 | self.markDirty() 165 | self.eval() 166 | 167 | def serialize(self): 168 | res = super(ZenoNode, self).serialize() 169 | res['op_code'] = self.__class__.op_code 170 | return res 171 | 172 | def deserialize(self, data, hashmap=[], restore_id=True): 173 | res = super(ZenoNode, self).deserialize(data, hashmap, restore_id) 174 | if DEBUG: print('Deserialized zenoNode "%s"'% self.__class__.__name__, "res:", res) 175 | return res 176 | 177 | """ 178 | class ZenoNodeIcon(QtWidgets.QLabel): 179 | def __init__(self, icon="", size=(32,32), parent=None): 180 | super(ZenoNodeIcon, self).__init__(parent) 181 | 182 | self.icon = icon 183 | self.size = size 184 | 185 | self.initUI() 186 | 187 | def initUI(self): 188 | self.icon_validator = QtGui.QImageReader(self.icon) 189 | if QtGui.QImageReader.canRead(self.icon_validator): 190 | self.item_image = QtGui.QImage(self.icon) 191 | else: 192 | self.item_image = QtGui.QImage(ToolsSystem.get_path("Sources","icons","shelf","python.png")) 193 | 194 | self.item_image = self.item_image.scaled(self.size[0],self.size[1],QtCore.Qt.IgnoreAspectRatio,QtCore.Qt.SmoothTransformation) 195 | self.item_pixmap = QtGui.QPixmap() 196 | self.item_pixmap.convertFromImage(self.item_image) 197 | self.setPixmap(self.item_pixmap) 198 | """ -------------------------------------------------------------------------------- /GUI/node_features.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | This module define all extended features of node editor 4 | """ 5 | 6 | from PySide2 import QtCore,QtWidgets,QtGui 7 | 8 | import GUI.node_creator as node_creator 9 | 10 | DEBUG_REROUTING = False 11 | 12 | class CutLine(QtWidgets.QGraphicsItem): 13 | """Class representing Cutting Line used for cutting multiple `Edges` with one stroke""" 14 | def __init__(self, parent=None): 15 | """ 16 | :param parent: parent widget 17 | :type parent: ``QWidget`` 18 | """ 19 | super(CutLine,self).__init__(parent) 20 | 21 | self.line_points = [] 22 | 23 | self._pen = QtGui.QPen(QtCore.Qt.white) 24 | self._pen.setWidthF(2.0) 25 | self._pen.setDashPattern([3, 3]) 26 | 27 | self.setZValue(2) 28 | 29 | def boundingRect(self): 30 | """Defining Qt' bounding rectangle""" 31 | return self.shape().boundingRect() 32 | 33 | def shape(self): 34 | """Calculate the QPainterPath object from list of line points 35 | 36 | :return: shape function returning ``QPainterPath`` representation of Cutting Line 37 | :rtype: ``QPainterPath`` 38 | """ 39 | poly = QtGui.QPolygonF(self.line_points) 40 | 41 | if len(self.line_points) > 1: 42 | path = QtGui.QPainterPath(self.line_points[0]) 43 | for pt in self.line_points[1:]: 44 | path.lineTo(pt) 45 | else: 46 | path = QtGui.QPainterPath(QtCore.QPointF(0,0)) 47 | path.lineTo(QtCore.QPointF(1,1)) 48 | 49 | return path 50 | 51 | def paint(self, painter, option, widget): 52 | """Paint the Cutting Line""" 53 | painter.setRenderHint(QtGui.QPainter.Antialiasing) 54 | painter.setBrush(QtCore.Qt.NoBrush) 55 | painter.setPen(self._pen) 56 | 57 | poly = QtGui.QPolygonF(self.line_points) 58 | painter.drawPolyline(poly) 59 | 60 | class EdgeRerouting: 61 | def __init__ (self, view_graphic): 62 | self.view_graphic = view_graphic 63 | self.start_socket = None # store where we start rerouting the edges 64 | self.rerouting_edges = [] # edges respresenting the rerouting (dashed edges) 65 | self.is_rerouting = False # state to track if currently rerouting edges or not 66 | self.first_mb_release = False 67 | 68 | def print_debug(self, *args): 69 | if DEBUG_REROUTING: print("REROUTING:", args) 70 | 71 | def getEdgeClass(self): 72 | return self.view_graphic.scene_graphic.scene.getEdgeClass() 73 | 74 | def getAffectedEdges(self): 75 | if self.start_socket is None: 76 | return [] # no starting socket assigned, so no edges to hide 77 | # return edges connected to the socket 78 | return self.start_socket.edges[:] # python 2.7 doesn't support list.copy() use slicing instead 79 | 80 | def setAffectedEdgesVisible(self, visibility=True): 81 | for edge in self.getAffectedEdges(): 82 | if visibility: edge.edge_graphic.show() 83 | else: edge.edge_graphic.hide() 84 | 85 | def resetRerouting(self): 86 | self.is_rerouting = False 87 | self.start_socket = None 88 | self.first_mb_release = False 89 | # holding all rerouting edges should be empty at this point... 90 | # self.rerouting_edges = [] 91 | 92 | def clearReroutingEdges(self): 93 | self.print_debug("clean called") 94 | while self.rerouting_edges != []: 95 | edge = self.rerouting_edges.pop() 96 | self.print_debug("\t cleaning:", edge) 97 | edge.remove(silent=True) 98 | 99 | def updateScenePos(self, x, y): 100 | if self.is_rerouting: 101 | for edge in self.rerouting_edges: 102 | if edge and edge.edge_graphic: 103 | edge.edge_graphic.setDestination(x,y) 104 | edge.edge_graphic.update() 105 | 106 | def startRerouting(self, socket): 107 | self.print_debug("start rerouting on", socket) 108 | self.is_rerouting = True 109 | self.start_socket = socket 110 | 111 | self.print_debug("numEdges:", len(self.getAffectedEdges())) 112 | self.setAffectedEdgesVisible(visibility=False) 113 | 114 | start_position = self.start_socket.node.getSocketScenePosition(self.start_socket) 115 | 116 | for edge in self.getAffectedEdges(): 117 | other_socket = edge.getOtherSocket(self.start_socket) 118 | 119 | new_edge = self.getEdgeClass()(self.start_socket.node.scene, edge_type=edge.edge_type) 120 | new_edge.start_socket = other_socket 121 | new_edge.edge_graphic.setSource(*other_socket.node.getSocketScenePosition(other_socket)) 122 | new_edge.edge_graphic.setDestination(*start_position) 123 | new_edge.edge_graphic.update() 124 | self.rerouting_edges.append(new_edge) 125 | 126 | def stopRerouting(self, target=None): # target is destination which socket user want to reroute 127 | self.print_debug("stop rerouting on", target, "no change" if target==self.start_socket else "") 128 | 129 | if self.start_socket is not None: 130 | # reset start socket highlight 131 | self.start_socket.socket_graphic.isHighlighted = False 132 | 133 | # collect all affected (node, edge) tuples if successfully reroute 134 | affected_nodes = [] 135 | 136 | if target is None or target == self.start_socket: 137 | # canceling rerouting (no change) 138 | self.setAffectedEdgesVisible(visibility=True) 139 | else: 140 | # validate edges before doing anything else 141 | valid_edges, invalid_edges = self.getAffectedEdges(), [] 142 | for edge in self.getAffectedEdges(): 143 | start_sock = edge.getOtherSocket(self.start_socket) 144 | if not edge.validateEdge(start_sock, target): 145 | # not valide edge 146 | self.print_debug("This edge rerouting is not valid!", edge) 147 | invalid_edges.append(edge) 148 | 149 | # remove the invalidated edges from the list 150 | for invalid_edge in invalid_edges: 151 | valid_edges.remove(invalid_edge) 152 | 153 | # reconnect to new socket 154 | self.print_debug("Should reconnect from:", self.start_socket, "-->", target) 155 | 156 | self.setAffectedEdgesVisible(visibility=True) 157 | 158 | for edge in valid_edges: 159 | for node in [edge.start_socket.node, edge.end_socket.node]: 160 | if node not in affected_nodes: 161 | affected_nodes.append((node, edge)) 162 | 163 | if target.is_input: # input is always receive one connection 164 | target.removeAllEdges(silent=True) 165 | 166 | if edge.end_socket == self.start_socket: # assign edge to new socket after rerouting done 167 | edge.end_socket = target 168 | else: 169 | edge.start_socket = target 170 | 171 | edge.updatePositions() 172 | 173 | # hide and remove rerouting edges (cleanup) 174 | self.clearReroutingEdges() 175 | 176 | # send notifications for all affected nodes 177 | for affected_node, edge in affected_nodes: 178 | affected_node.onEdgeConnectionChanged(edge) 179 | if edge.start_socket in affected_node.inputs: 180 | affected_node.onInputChanged(edge.start_socket) 181 | if edge.end_socket in affected_node.inputs: 182 | affected_node.onInputChanged(edge.end_socket) 183 | 184 | # store history stamp 185 | self.start_socket.node.scene.history.storeHistory("rerouted edges", setModified=True) 186 | 187 | # reset variables of this rerouting state (cleanup) 188 | self.resetRerouting() 189 | 190 | class EdgeSnapping: 191 | def __init__(self, view_graphhic, snapping_radius): 192 | self.view_graphic = view_graphhic 193 | self.scene_graphic = self.view_graphic.scene_graphic 194 | self.edge_snapping_radius = snapping_radius 195 | 196 | def getSnappedSocketItem(self, event): 197 | scenepos = self.view_graphic.mapToScene(event.pos()) 198 | socket_graphic, pos = self.getSnappedToSocketPosition(scenepos) 199 | return socket_graphic 200 | 201 | def getSnappedToSocketPosition(self, scenepos): 202 | """Returns socket_graphic and scene position to nearest socket or original position if no nearby socket found 203 | 204 | :param scenepos: scene point to snap (target snap) 205 | :type scenepos: ``QPointF`` 206 | :return: socket_graphic and scene position to nearest socket 207 | """ 208 | 209 | # scan rectangle according to radius to find nearby graphic item 210 | # remember QRectF parameter (x,y,w,h) so don't confused with argument below 211 | scan_rect = QtCore.QRectF( 212 | scenepos.x() - self.edge_snapping_radius, scenepos.y() - self.edge_snapping_radius, 213 | self.edge_snapping_radius*2, self.edge_snapping_radius*2 214 | ) 215 | items = self.scene_graphic.items(scan_rect) # passing scan_rect tell scene_graphic to return items inside rectangle radius 216 | items = list(filter(lambda x: isinstance(x, node_creator.SocketGraphics), items)) # filter (builtin) out only instance of socket graphic class 217 | 218 | if len(items) == 0: 219 | return None, scenepos 220 | 221 | selected_item = items[0] 222 | if len(items) > 1: 223 | # calculate the nearest socket 224 | nearest = 10000000000 225 | for grsocket in items: 226 | grsocket_scenepos = grsocket.socket.node.getSocketScenePosition(grsocket.socket) 227 | qpdist = QtCore.QPointF(*grsocket_scenepos) - scenepos 228 | dist = qpdist.x() * qpdist.x() + qpdist.y() * qpdist.y() # find vector length without sqrt because we just try to find smaller value 229 | if dist < nearest: 230 | nearest, selected_item = dist, grsocket 231 | 232 | selected_item.isHighlighted = True 233 | selected_item.update() 234 | 235 | calcpos = selected_item.socket.node.getSocketScenePosition(selected_item.socket) 236 | 237 | return selected_item, QtCore.QPointF(*calcpos) -------------------------------------------------------------------------------- /ZCore/ShelfBase.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from PySide2 import QtCore,QtWidgets,QtGui 3 | from ZCore.ToolsSystem import OUTPUT_INFO, OUTPUT_ERROR, OUTPUT_SUCCESS, OUTPUT_WARNING 4 | 5 | import json 6 | 7 | import maya.cmds as cmds 8 | 9 | import ZCore.ToolsSystem as ToolsSystem 10 | 11 | class ZenoTabContainer(QtWidgets.QWidget): 12 | title = "" 13 | def __init__(self, app=None): 14 | 15 | super(ZenoTabContainer, self).__init__(app) 16 | 17 | self.app = app 18 | 19 | self.initUI() 20 | self.initInputDialog() 21 | 22 | def initUI(self): 23 | self.mainLayout = QtWidgets.QHBoxLayout() 24 | self.mainLayout.setContentsMargins(0,0,0,0) 25 | 26 | self.optionsLabel = GraphicButton(ToolsSystem.get_path("Sources","icons","shelf","shelf_options.png"), 27 | self.onClick, 28 | QtGui.QColor('white'), 29 | 0.85, 30 | (15,15)) 31 | 32 | self.itemWidget = QtWidgets.QWidget() # contain flow layout 33 | self.itemWidget.setContentsMargins(5,5,5,5) 34 | 35 | self.itemLayout = FlowLayout(self.itemWidget) 36 | 37 | self.scrollWidget = QtWidgets.QScrollArea() 38 | self.scrollWidget.setWidgetResizable(True) 39 | self.scrollWidget.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) 40 | self.scrollWidget.setWidget(self.itemWidget) 41 | self.mainLayout.addWidget(self.optionsLabel) 42 | self.mainLayout.addWidget(self.scrollWidget) 43 | 44 | self.setFocusPolicy(QtCore.Qt.NoFocus) 45 | self.scrollWidget.setFocusPolicy(QtCore.Qt.NoFocus) 46 | self.setLayout(self.mainLayout) 47 | 48 | def initInputDialog(self): 49 | self.customScriptDialog = QtWidgets.QWidget(self.app) 50 | self.customScriptDialog.setWindowFlags(QtCore.Qt.Window) 51 | self.customScriptDialog.setWindowTitle("Custom Script") 52 | 53 | self.inputDialogLayout = QtWidgets.QVBoxLayout(self.customScriptDialog) 54 | self.inputDialogLayout.setContentsMargins(5,5,5,5) 55 | 56 | self.scriptInputEdit = QtWidgets.QPlainTextEdit(self.customScriptDialog) 57 | self.scriptInputEdit.setLineWrapMode(QtWidgets.QPlainTextEdit.NoWrap) 58 | self.saveScriptBtn = QtWidgets.QPushButton("Save Script", self.customScriptDialog) 59 | self.saveScriptBtn.clicked.connect(self.onSaveScript) 60 | 61 | self.textLayout = QtWidgets.QHBoxLayout() 62 | self.iconLabel = QtWidgets.QLabel("Icon :") 63 | self.iconLineEdit = QtWidgets.QLineEdit() 64 | self.iconFolderButton = GraphicButton(ToolsSystem.get_path("Sources","icons","shelf","folder.png"), 65 | self.onFileOpen, 66 | size=(15,15)) 67 | 68 | self.textLayout.addWidget(self.iconLabel) 69 | self.textLayout.addWidget(self.iconLineEdit) 70 | self.textLayout.addWidget(self.iconFolderButton) 71 | 72 | self.inputDialogLayout.addWidget(self.scriptInputEdit) 73 | self.inputDialogLayout.addLayout(self.textLayout) 74 | self.inputDialogLayout.addWidget(self.saveScriptBtn) 75 | 76 | def onFileOpen(self, event): 77 | fnames, filter = QtWidgets.QFileDialog.getOpenFileNames(self, 'Open graph from file', '', 'Supported Types (*.bmp *.jpg *.jpeg *.png *.svg);;All files (*)') 78 | try: self.iconLineEdit.setText(fnames[-1]) 79 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) 80 | 81 | def onSaveScript(self): 82 | new_script_content = self.scriptInputEdit.toPlainText() 83 | new_script_widget = MayaScriptButton(self.iconLineEdit.text(), new_script_content, tab=self) 84 | self.itemLayout.addWidget(new_script_widget) 85 | self.app.user_script.append(new_script_widget) 86 | self.scriptInputEdit.clear() 87 | 88 | with open(ToolsSystem.get_path("Shelf","userPref.json"), "w") as file: 89 | file.write(json.dumps(self.serialize(), indent=4)) 90 | 91 | self.customScriptDialog.close() 92 | 93 | def onClick(self, event): 94 | self.handleShelfMenu(event) 95 | 96 | def onMouseEnter(self, event): 97 | self.highlight.setStrength(0.85) 98 | 99 | def onMouseLeave(self, event): 100 | self.highlight.setStrength(0.0) 101 | 102 | def handleShelfMenu(self, event): 103 | self.shelf_menu = QtWidgets.QMenu(self) 104 | self.addScriptAct = self.shelf_menu.addAction("Add Script Shortcut") 105 | action = self.shelf_menu.exec_(self.mapToGlobal(event.pos())) 106 | if action == self.addScriptAct: 107 | self.showInputDialog(event) 108 | 109 | def showInputDialog(self, event): 110 | self.customScriptDialog.setGeometry(self.mapToGlobal(event.pos()).x(), 111 | self.mapToGlobal(event.pos()).y(), 112 | 600, 113 | self.app.height()-200) 114 | self.customScriptDialog.show() 115 | 116 | def serialize(self): 117 | json_list = [] 118 | try: 119 | for script in self.app.user_script: 120 | hashmap = OrderedDict([('shelf', script.tab.title), 121 | ('icon', script.icon), 122 | ('command', script.evalString)]) 123 | json_list.append(hashmap) 124 | return OrderedDict([('user_script', json_list)]) 125 | 126 | except Exception as e: self.app.outputLogInfo(e, OUTPUT_ERROR) 127 | 128 | class GraphicButton(QtWidgets.QLabel): 129 | def __init__(self, icon="", callback=None, color=QtGui.QColor('silver'), strength=0.25, size=(32,32), tab=None): 130 | super(GraphicButton, self).__init__() 131 | 132 | self.icon = icon 133 | self.callback = [] 134 | if callback: self.callback.append(callback) 135 | self.color = color 136 | self.strength = strength 137 | self.size = size 138 | self.tab = tab 139 | 140 | self.initUI() 141 | 142 | def initUI(self): 143 | self.icon_validator = QtGui.QImageReader(self.icon) 144 | if QtGui.QImageReader.canRead(self.icon_validator): 145 | self.item_image = QtGui.QImage(self.icon) 146 | else: 147 | self.item_image = QtGui.QImage(ToolsSystem.get_path("Sources","icons","shelf","python.png")) 148 | 149 | self.item_image = self.item_image.scaled(self.size[0],self.size[1],QtCore.Qt.IgnoreAspectRatio,QtCore.Qt.SmoothTransformation) 150 | self.item_pixmap = QtGui.QPixmap() 151 | self.item_pixmap.convertFromImage(self.item_image) 152 | self.setPixmap(self.item_pixmap) 153 | 154 | # set highlight mouse hover effect 155 | self.highlight = QtWidgets.QGraphicsColorizeEffect() 156 | self.highlight.setColor(self.color) 157 | self.highlight.setStrength(0.0) 158 | self.setGraphicsEffect(self.highlight) 159 | 160 | self.mousePressEvent = self.onClick 161 | self.enterEvent = self.onMouseEnter 162 | self.leaveEvent = self.onMouseLeave 163 | 164 | def onClick(self, event): 165 | for callback in self.callback: 166 | callback(event) 167 | 168 | def onMouseEnter(self, event): 169 | self.highlight.setStrength(self.strength) 170 | 171 | def onMouseLeave(self, event): 172 | self.highlight.setStrength(0.0) 173 | 174 | class MayaScriptButton(GraphicButton): 175 | def __init__(self, icon="", callback="", color=QtGui.QColor('silver'), strength=0.25, size=(32,32), tab=None): 176 | # we not passing the callback, because this class extend graphic button with different callback execution (using cmds.evalDeffered) 177 | super(MayaScriptButton, self).__init__(icon, color=color, strength=strength, size=size, tab=tab) 178 | self.evalString = callback 179 | 180 | def onClick(self, event): 181 | cmds.evalDeferred(self.evalString) 182 | 183 | class FlowLayout(QtWidgets.QLayout): 184 | def __init__(self, parent=None): 185 | super(FlowLayout, self).__init__(parent) 186 | 187 | if parent is not None: 188 | self.setContentsMargins(QtCore.QMargins(0, 0, 0, 0)) 189 | 190 | self._item_list = [] 191 | self.tolerance = 22 # tolerance size when next widget push to new row 192 | 193 | def __del__(self): 194 | item = self.takeAt(0) 195 | while item: 196 | item = self.takeAt(0) 197 | 198 | def addItem(self, item): 199 | self._item_list.append(item) 200 | 201 | def count(self): 202 | return len(self._item_list) 203 | 204 | def itemAt(self, index): 205 | if 0 <= index < len(self._item_list): 206 | return self._item_list[index] 207 | 208 | return None 209 | 210 | def takeAt(self, index): 211 | if 0 <= index < len(self._item_list): 212 | return self._item_list.pop(index) 213 | 214 | return None 215 | 216 | def expandingDirections(self): 217 | return QtCore.Qt.Orientation(0) 218 | 219 | def hasHeightForWidth(self): 220 | return True 221 | 222 | def heightForWidth(self, width): 223 | height = self._do_layout(QtCore.QRect(0, 0, width, 0), True) 224 | return height 225 | 226 | def setGeometry(self, rect): 227 | super(FlowLayout, self).setGeometry(rect) 228 | self._do_layout(rect, False) 229 | 230 | def sizeHint(self): 231 | return self.minimumSize() 232 | 233 | def minimumSize(self): 234 | size = QtCore.QSize() 235 | 236 | for item in self._item_list: 237 | size = size.expandedTo(item.minimumSize()) 238 | 239 | size += QtCore.QSize(2 * self.contentsMargins().top(), 2 * self.contentsMargins().top()) 240 | return size 241 | 242 | def _do_layout(self, rect, test_only): 243 | x = rect.x() 244 | y = rect.y() 245 | line_height = 0 246 | spacing = self.spacing() 247 | 248 | for item in self._item_list: 249 | style = item.widget().style() 250 | layout_spacing_x = style.layoutSpacing( 251 | QtWidgets.QSizePolicy.PushButton, QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Horizontal 252 | ) 253 | layout_spacing_y = style.layoutSpacing( 254 | QtWidgets.QSizePolicy.PushButton, QtWidgets.QSizePolicy.PushButton, QtCore.Qt.Vertical 255 | ) 256 | space_x = spacing + layout_spacing_x 257 | space_y = spacing + layout_spacing_y 258 | next_x = x + item.sizeHint().width() + space_x 259 | if next_x - space_x - self.tolerance > rect.right() and line_height > 0: 260 | x = rect.x() 261 | y = y + line_height + space_y 262 | next_x = x + item.sizeHint().width() + space_x 263 | line_height = 0 264 | 265 | if not test_only: 266 | item.setGeometry(QtCore.QRect(QtCore.QPoint(x, y), item.sizeHint())) 267 | 268 | x = next_x 269 | line_height = max(line_height, item.sizeHint().height()) 270 | 271 | return y + line_height - rect.y() 272 | -------------------------------------------------------------------------------- /ZCore/SplineCtx.py: -------------------------------------------------------------------------------- 1 | 2 | import operator 3 | 4 | import maya.api.OpenMaya as om 5 | import maya.api.OpenMayaUI as omui 6 | 7 | import maya.cmds as cmds 8 | 9 | import ZCore.ToolsSystem as ToolsSystem 10 | 11 | ''' 12 | ---------------------------------- Feature expansion ---------------------------------- 13 | -ray get closest intersection, optimized and not heavy tested with multiple mesh 14 | -tool settings for advance context (like ep curve tool settings), and query live mesh to flag argument ctx 15 | 16 | --------------------------------------- Problem --------------------------------------- 17 | -undo stack queue problem (crash when undo? with live surface) 18 | -change query from live to live cache (need more testing for error proof) 19 | -in middle of context if user delete crv problem occur 20 | ''' 21 | 22 | """ 23 | # directory 24 | icon_dir = ToolsSystem.ZCore_icon_dir 25 | 26 | class SplineContext(): 27 | def __init__(self,name_id=""): 28 | self.name_id = name_id 29 | self.active_crv = None 30 | self.active_jnt = [] 31 | self.size = 1 32 | 33 | def create_joint(self,pos): 34 | cmds.select(cl=1) 35 | jnt = cmds.joint(n=self.name_id+"joint1",p=pos) 36 | cmds.setAttr(jnt+".radius",self.size*2.5) 37 | self.active_jnt.append(jnt) 38 | 39 | def add_item(self,pos): 40 | if self.active_crv != None: 41 | cmds.curve(self.active_crv,a=1,ep=pos) 42 | cmds.xform(self.active_crv,cp=1) 43 | cmds.rebuildCurve(self.active_crv,ch=0,kcp=1,kr=0,d=1) # set max value to 1 44 | else: 45 | self.active_crv = cmds.curve(n=self.name_id+"setup1",d=1,ep=pos) 46 | cmds.xform(self.active_crv,cp=1) 47 | self.create_joint(pos) 48 | 49 | def remove_item(self): 50 | crv_last_index = len(cmds.getAttr(self.active_crv+".ep[:]"))-1 51 | cmds.delete(self.active_jnt[-1]) 52 | self.active_jnt = self.active_jnt[:-1] 53 | cmds.delete(self.active_crv+".ep[%s]"%crv_last_index) 54 | if crv_last_index == 0: 55 | cmds.delete(self.active_crv) 56 | if not cmds.objExists(self.active_crv): 57 | self.active_crv = None 58 | """ 59 | 60 | # tell maya plugin produces and need to be passed, maya api 2.0 61 | def maya_useNewAPI(): 62 | pass 63 | 64 | class SplineContext(omui.MPxContext): 65 | TITLE = "Ribbon Spline Tool" 66 | HELP_TEXT = "Select vertex to add joint placement. Enter to complete" 67 | CTX_ICON = ToolsSystem.get_path("Sources","icons","context","splineContext.png") 68 | 69 | # context data 70 | SETUP_COLOR = 22 71 | jnt = [] 72 | 73 | def __init__(self): 74 | super(SplineContext, self).__init__() 75 | 76 | # reference to class attr(.), so it will stay consistent to all instance 77 | self.setTitleString(SplineContext.TITLE) 78 | self.setImage(SplineContext.CTX_ICON, omui.MPxContext.kImage1) # up to 3 slot 79 | self.setCursor(omui.MCursor.kDoubleCrossHairCursor) 80 | 81 | def helpStateHasChanged(self, event): 82 | self.setHelpString(SplineContext.HELP_TEXT) 83 | 84 | def toolOnSetup(self,event): 85 | om.MGlobal.selectCommand(om.MSelectionList()) # select nothing, to record undo 86 | self.reset_context() 87 | 88 | def toolOffCleanup(self): 89 | self.reset_context() 90 | 91 | def doPress(self, event, draw_manager, frame_context): 92 | ray_source = om.MPoint() # 3D point with double-precision coordinates 93 | ray_direction = om.MVector() # 3D vector with double-precision coordinates 94 | omui.M3dView().active3dView().viewToWorld(event.position[0],event.position[1],ray_source,ray_direction) 95 | if ToolsSystem.live_mesh: 96 | live_mesh = ToolsSystem.live_mesh 97 | else: 98 | live_mesh = cmds.ls(type="mesh") 99 | if len(live_mesh) > 10: 100 | cmds.warning("truncate live mesh to 10, mesh amount exceed safe perfomance limit. Use live object for accurate result") 101 | live_mesh = live_mesh[:10] 102 | 103 | for mesh in live_mesh: 104 | selectionList = om.MSelectionList() 105 | selectionList.add(mesh) # Add mesh to list 106 | dagPath = selectionList.getDagPath(0) # Path to a DAG node 107 | fnMesh = om.MFnMesh(dagPath) # Function set for operation on meshes 108 | 109 | intersection = fnMesh.closestIntersection(om.MFloatPoint(ray_source), # raySource 110 | om.MFloatVector(ray_direction), # rayDirection 111 | om.MSpace.kWorld, # space 112 | 99999, # maxParam (search radius around the raySource point) 113 | False) # testBothDirections 114 | 115 | # Extract the different values from the intersection result 116 | hitPoint, hitRayParam, hitFace, hitTriangle, hitBary1, hitBary2 = intersection 117 | x, y, z, _ = hitPoint 118 | 119 | """ 120 | obj_distances = [] 121 | if (x, y, z) != (0.0, 0.0, 0.0): 122 | distance_vector = map(lambda i, j: i-j, hitPoint, ray_source) 123 | obj_distances.append(distance_vector[0]*distance_vector[0] + distance_vector[1]*distance_vector[1] + distance_vector[2]*distance_vector[2]) 124 | else: 125 | obj_distances.append(0) 126 | """ 127 | 128 | if (x, y, z) != (0.0, 0.0, 0.0): 129 | cmds.undoInfo(openChunk=True,cn="create spline point") 130 | cmds.select(cl=1) 131 | if not event.isModifierControl(): 132 | jnt = cmds.joint(n="splinePt1",p=(x,y,z)) 133 | cmds.setAttr(jnt+".overrideEnabled",1) 134 | cmds.setAttr(jnt+".overrideColor",self.SETUP_COLOR) 135 | else: 136 | # get closest vertex from hitPoint's face vertices (https://gist.github.com/hdlx/) 137 | index = fnMesh.getClosestPoint(om.MPoint(hitPoint), space=om.MSpace.kWorld)[1] # closest polygon index 138 | face_vertices = fnMesh.getPolygonVertices(index) # get polygon vertices 139 | vertex_distances = ((vertex, fnMesh.getPoint(vertex, om.MSpace.kWorld).distanceTo(om.MPoint(hitPoint))) 140 | for vertex in face_vertices) 141 | closest_vertex = min(vertex_distances, key=operator.itemgetter(1)) # sort by smallest first index list 142 | closest_vertex_pos = cmds.xform(mesh+".vtx[%s]"%closest_vertex[0],q=1,t=1,ws=1) 143 | jnt = cmds.joint(n="splinePt1",p=closest_vertex_pos) 144 | cmds.setAttr(jnt+".overrideEnabled",1) 145 | cmds.setAttr(jnt+".overrideColor",self.SETUP_COLOR) 146 | self.jnt.append(jnt) 147 | cmds.undoInfo(closeChunk=True) 148 | break 149 | else: 150 | if mesh == live_mesh[-1]: 151 | cmds.warning("ray source:%s & ray direction:%s, no intersection found!"%(ray_source,ray_direction)) 152 | 153 | def completeAction(self): 154 | valid_jnt = [] 155 | if self.jnt and cmds.objExists(self.jnt[0]): 156 | name = ToolsSystem.generate_nameID(3) 157 | grp = cmds.group(n=name,em=1) 158 | 159 | # get valid jnt (ignore deleted/missing joint according to database) 160 | for jnt in self.jnt: 161 | try: 162 | cmds.setAttr(jnt+".overrideEnabled",0) 163 | cmds.parent(jnt,grp) 164 | valid_jnt.append(cmds.rename(jnt,name+"_jnt1")) # append renamed jnt 165 | except: 166 | cmds.warning("%s not found, skipped"%jnt) 167 | crv = cmds.curve(n=name+"_crv",d=3,ep=[cmds.xform(jnt,q=1,t=1,ws=1) for jnt in valid_jnt]) 168 | cmds.parent(crv,grp) 169 | 170 | # create cluster control curve 171 | if len(valid_jnt)>1: 172 | for index,jnt in enumerate(valid_jnt): 173 | if jnt == valid_jnt[0]: 174 | cluster = cmds.cluster(crv+".cv[:1]") 175 | cmds.parent(cluster[1],valid_jnt[0]) 176 | elif jnt == valid_jnt[-1]: 177 | cluster = cmds.cluster(crv+".cv[%s:]"%len(valid_jnt)) # cv doesn't work with negative indicates 178 | cmds.parent(cluster[1],valid_jnt[-1]) 179 | else: 180 | cluster = cmds.cluster(crv+".cv[%s]"%(index+1)) 181 | cmds.parent(cluster[1],valid_jnt[index]) 182 | cmds.setAttr(cluster[1]+".visibility",0) 183 | 184 | # cleanup 185 | ToolsSystem.parent_setup(grp) 186 | self.jnt = [] 187 | cmds.select(cl=1) 188 | 189 | def deleteAction(self): 190 | if self.jnt: 191 | try: 192 | cmds.delete(self.jnt[-1]) 193 | self.jnt.pop() 194 | except: 195 | cmds.warning("Can't delete %s, object not found"%self.jnt[-1]) 196 | 197 | def abortAction(self): 198 | for jnt in self.jnt: 199 | try: 200 | cmds.delete(jnt) 201 | except: 202 | cmds.warning("Can't delete %s, object not found"%jnt) 203 | self.jnt = [] 204 | 205 | # user defined function 206 | def reset_context(self): 207 | self.completeAction() 208 | self.jnt = [] 209 | 210 | class SplineContextCmd(omui.MPxContextCommand): 211 | 212 | COMMAND_NAME = "zSplineCtx" # used as mel command to create context 213 | 214 | def __init__(self): 215 | super(SplineContextCmd, self).__init__() 216 | 217 | # required for maya to get instance of context 218 | def makeObj(self): 219 | return SplineContext() # return ribbon spline ctx 220 | 221 | @classmethod 222 | def creator(cls): 223 | return SplineContextCmd() # return ribbon spline ctx cmd 224 | 225 | def initializePlugin(plugin): 226 | author = "Aldo Aldrich" 227 | version = "0.1.0" 228 | 229 | plugin_fn = om.MFnPlugin(plugin, "", author, version) 230 | 231 | try: 232 | plugin_fn.registerContextCommand(SplineContextCmd.COMMAND_NAME, 233 | SplineContextCmd.creator) 234 | 235 | except: 236 | om.MGlobal.displayError("Failed to register context command: %s" 237 | %SplineContextCmd.COMMAND_NAME) 238 | 239 | def uninitializePlugin(plugin): 240 | plugin_fn = om.MFnPlugin(plugin) 241 | 242 | try: 243 | plugin_fn.deregisterContextCommand(SplineContextCmd.COMMAND_NAME) 244 | 245 | except: 246 | om.MGlobal.displayError("Failed to deregister context command: %s" 247 | %SplineContextCmd.COMMAND_NAME) 248 | 249 | ''' 250 | # development phase 251 | if __name__ == "__main__": 252 | # required before unloading the plugin 253 | # cmds.flushUndo() 254 | # cmds.file(new=True, force=True) 255 | 256 | # reload the plugin 257 | plugin_name = "C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/ZenoRig/ZCore/SplineCtx.py" 258 | 259 | #cmds.loadPlugin(plugin_name) 260 | cmds.evalDeferred('if cmds.pluginInfo("{0}", q=True, loaded=True): cmds.unloadPlugin("{0}")'.format(plugin_name)) 261 | cmds.evalDeferred('if not cmds.pluginInfo("{0}", q=True, loaded=True): cmds.loadPlugin("{0}")'.format(plugin_name)) 262 | 263 | # setup code to help speed up testing (e.g. load context) 264 | # cmds.evalDeferred('cmds.file("C:/Users/atxad/Desktop/Maya Scripts Draft/1st Album EPILOGUE/Face Tools/Test Model Subject/Haeri.ma",open=True,force=True)') 265 | cmds.evalDeferred('context = cmds.zSplineCtx(); cmds.setToolTo(context)') # ctx already added to cmds module 266 | ''' -------------------------------------------------------------------------------- /ZCore/Save/graph_math.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": 1904883295680, 3 | "scene_width": 8000, 4 | "scene_height": 8000, 5 | "nodes": [ 6 | { 7 | "id": 1904899704928, 8 | "title": "Input", 9 | "pos_x": -376.0, 10 | "pos_y": -97.0, 11 | "inputs": [], 12 | "outputs": [ 13 | { 14 | "id": 1904902212296, 15 | "index": 0, 16 | "multi_edges": true, 17 | "position": 4, 18 | "socket_type": 1 19 | } 20 | ], 21 | "content": { 22 | "value": "1" 23 | }, 24 | "op_code": 1 25 | }, 26 | { 27 | "id": 1906352004176, 28 | "title": "Output", 29 | "pos_x": 120.03999999999996, 30 | "pos_y": -101.40000000000003, 31 | "inputs": [ 32 | { 33 | "id": 1904902259880, 34 | "index": 0, 35 | "multi_edges": false, 36 | "position": 1, 37 | "socket_type": 1 38 | } 39 | ], 40 | "outputs": [], 41 | "content": {}, 42 | "op_code": 2 43 | }, 44 | { 45 | "id": 1904902259936, 46 | "title": "ZSpline", 47 | "pos_x": 364.16, 48 | "pos_y": -7.679999999999989, 49 | "inputs": [ 50 | { 51 | "id": 1904902259992, 52 | "index": 0, 53 | "multi_edges": false, 54 | "position": 1, 55 | "socket_type": 2 56 | } 57 | ], 58 | "outputs": [], 59 | "content": {}, 60 | "op_code": 3 61 | }, 62 | { 63 | "id": 1904902259824, 64 | "title": "Add", 65 | "pos_x": -115.39999999999998, 66 | "pos_y": -111.72000000000003, 67 | "inputs": [ 68 | { 69 | "id": 1904902260104, 70 | "index": 0, 71 | "multi_edges": false, 72 | "position": 1, 73 | "socket_type": 1 74 | }, 75 | { 76 | "id": 1904902260160, 77 | "index": 1, 78 | "multi_edges": false, 79 | "position": 1, 80 | "socket_type": 1 81 | } 82 | ], 83 | "outputs": [ 84 | { 85 | "id": 1904902260216, 86 | "index": 0, 87 | "multi_edges": true, 88 | "position": 4, 89 | "socket_type": 1 90 | } 91 | ], 92 | "content": {}, 93 | "op_code": 4 94 | }, 95 | { 96 | "id": 1904902260552, 97 | "title": "Input", 98 | "pos_x": -377.2799999999999, 99 | "pos_y": -18.239999999999995, 100 | "inputs": [], 101 | "outputs": [ 102 | { 103 | "id": 1904902260664, 104 | "index": 0, 105 | "multi_edges": true, 106 | "position": 4, 107 | "socket_type": 1 108 | } 109 | ], 110 | "content": { 111 | "value": "2" 112 | }, 113 | "op_code": 1 114 | }, 115 | { 116 | "id": 1904902260496, 117 | "title": "Input", 118 | "pos_x": -372.80000000000007, 119 | "pos_y": 52.8, 120 | "inputs": [], 121 | "outputs": [ 122 | { 123 | "id": 1904902260048, 124 | "index": 0, 125 | "multi_edges": true, 126 | "position": 4, 127 | "socket_type": 1 128 | } 129 | ], 130 | "content": { 131 | "value": "3" 132 | }, 133 | "op_code": 1 134 | }, 135 | { 136 | "id": 1904902260384, 137 | "title": "Input", 138 | "pos_x": -373.5999999999999, 139 | "pos_y": 129.60000000000002, 140 | "inputs": [], 141 | "outputs": [ 142 | { 143 | "id": 1904902260272, 144 | "index": 0, 145 | "multi_edges": true, 146 | "position": 4, 147 | "socket_type": 1 148 | } 149 | ], 150 | "content": { 151 | "value": "0" 152 | }, 153 | "op_code": 1 154 | }, 155 | { 156 | "id": 1904902260608, 157 | "title": "Output", 158 | "pos_x": 122.40000000000003, 159 | "pos_y": -33.60000000000001, 160 | "inputs": [ 161 | { 162 | "id": 1904902261000, 163 | "index": 0, 164 | "multi_edges": false, 165 | "position": 1, 166 | "socket_type": 1 167 | } 168 | ], 169 | "outputs": [], 170 | "content": {}, 171 | "op_code": 2 172 | }, 173 | { 174 | "id": 1904902260440, 175 | "title": "Output", 176 | "pos_x": 118.39999999999998, 177 | "pos_y": 43.19999999999999, 178 | "inputs": [ 179 | { 180 | "id": 1904902260944, 181 | "index": 0, 182 | "multi_edges": false, 183 | "position": 1, 184 | "socket_type": 1 185 | } 186 | ], 187 | "outputs": [], 188 | "content": {}, 189 | "op_code": 2 190 | }, 191 | { 192 | "id": 1904902260832, 193 | "title": "Output", 194 | "pos_x": 119.99999999999997, 195 | "pos_y": 127.20000000000002, 196 | "inputs": [ 197 | { 198 | "id": 1904902260328, 199 | "index": 0, 200 | "multi_edges": false, 201 | "position": 1, 202 | "socket_type": 1 203 | } 204 | ], 205 | "outputs": [], 206 | "content": {}, 207 | "op_code": 2 208 | }, 209 | { 210 | "id": 1904902260720, 211 | "title": "Subtract", 212 | "pos_x": -116.80000000000001, 213 | "pos_y": -26.0, 214 | "inputs": [ 215 | { 216 | "id": 1904902260776, 217 | "index": 0, 218 | "multi_edges": false, 219 | "position": 1, 220 | "socket_type": 1 221 | }, 222 | { 223 | "id": 1904902260888, 224 | "index": 1, 225 | "multi_edges": false, 226 | "position": 1, 227 | "socket_type": 1 228 | } 229 | ], 230 | "outputs": [ 231 | { 232 | "id": 1904902261056, 233 | "index": 0, 234 | "multi_edges": true, 235 | "position": 4, 236 | "socket_type": 1 237 | } 238 | ], 239 | "content": {}, 240 | "op_code": 5 241 | }, 242 | { 243 | "id": 1906352004624, 244 | "title": "Multiply", 245 | "pos_x": -117.60000000000002, 246 | "pos_y": 55.39999999999998, 247 | "inputs": [ 248 | { 249 | "id": 1904902261168, 250 | "index": 0, 251 | "multi_edges": false, 252 | "position": 1, 253 | "socket_type": 1 254 | }, 255 | { 256 | "id": 1904902261224, 257 | "index": 1, 258 | "multi_edges": false, 259 | "position": 1, 260 | "socket_type": 1 261 | } 262 | ], 263 | "outputs": [ 264 | { 265 | "id": 1904902261280, 266 | "index": 0, 267 | "multi_edges": true, 268 | "position": 4, 269 | "socket_type": 1 270 | } 271 | ], 272 | "content": {}, 273 | "op_code": 6 274 | }, 275 | { 276 | "id": 1904902261112, 277 | "title": "Divide", 278 | "pos_x": -117.0, 279 | "pos_y": 140.20000000000005, 280 | "inputs": [ 281 | { 282 | "id": 1904902261392, 283 | "index": 0, 284 | "multi_edges": false, 285 | "position": 1, 286 | "socket_type": 1 287 | }, 288 | { 289 | "id": 1904902261448, 290 | "index": 1, 291 | "multi_edges": false, 292 | "position": 1, 293 | "socket_type": 1 294 | } 295 | ], 296 | "outputs": [ 297 | { 298 | "id": 1904902261504, 299 | "index": 0, 300 | "multi_edges": true, 301 | "position": 4, 302 | "socket_type": 1 303 | } 304 | ], 305 | "content": {}, 306 | "op_code": 7 307 | } 308 | ], 309 | "edges": [ 310 | { 311 | "id": 1904902261616, 312 | "edge_type": 2, 313 | "start": 1904902212296, 314 | "end": 1904902260104 315 | }, 316 | { 317 | "id": 1904902261728, 318 | "edge_type": 2, 319 | "start": 1904902260664, 320 | "end": 1904902260160 321 | }, 322 | { 323 | "id": 1904902261560, 324 | "edge_type": 2, 325 | "start": 1904902260216, 326 | "end": 1904902259880 327 | }, 328 | { 329 | "id": 1904902261896, 330 | "edge_type": 2, 331 | "start": 1904902212296, 332 | "end": 1904902260776 333 | }, 334 | { 335 | "id": 1904902261672, 336 | "edge_type": 2, 337 | "start": 1904902260664, 338 | "end": 1904902260888 339 | }, 340 | { 341 | "id": 1904902261784, 342 | "edge_type": 2, 343 | "start": 1904902261056, 344 | "end": 1904902261000 345 | }, 346 | { 347 | "id": 1904902262008, 348 | "edge_type": 2, 349 | "start": 1904902260048, 350 | "end": 1904902261168 351 | }, 352 | { 353 | "id": 1904902262064, 354 | "edge_type": 2, 355 | "start": 1904902260272, 356 | "end": 1904902261224 357 | }, 358 | { 359 | "id": 1904902262176, 360 | "edge_type": 2, 361 | "start": 1904902261280, 362 | "end": 1904902260944 363 | }, 364 | { 365 | "id": 1904902261952, 366 | "edge_type": 2, 367 | "start": 1904902260048, 368 | "end": 1904902261392 369 | }, 370 | { 371 | "id": 1904902262232, 372 | "edge_type": 2, 373 | "start": 1904902260272, 374 | "end": 1904902261448 375 | }, 376 | { 377 | "id": 1904902262344, 378 | "edge_type": 2, 379 | "start": 1904902261504, 380 | "end": 1904902260328 381 | } 382 | ] 383 | } -------------------------------------------------------------------------------- /ZCore/MayaUtil.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | import maya.cmds as cmds 5 | 6 | import ZCore.ToolsSystem as ToolsSystem 7 | 8 | # directory 9 | module_path = ToolsSystem.get_path("Sources","ControlCurves") 10 | #current_dir = os.path.dirname(__file__) 11 | #module_path = os.path.join(current_dir,"Sources") 12 | 13 | # Color Enum 14 | COLOR_RED = 13 15 | COLOR_YELLOW = 17 16 | COLOR_GREEN = 14 17 | COLOR_CYAN = 18 18 | COLOR_BLUE = 6 19 | COLOR_CREAM = 20 20 | COLOR_WHITE = 16 21 | COLOR_BLACK = 1 22 | 23 | # axis const 24 | WORLD_X = "x" 25 | WORLD_Y = "y" 26 | WORLD_Z = "z" 27 | 28 | def __init__(self): 29 | pass 30 | 31 | # Error (Unusable if obj in world space outliner and already freezed transform) 32 | # parameter(selection = object, group_name = string) 33 | def create_extra_group(self, selection, suffix_name): 34 | cmds.select(selection) 35 | selection_parent = cmds.pickWalk(d="up") 36 | 37 | if selection_parent[0] == selection: 38 | extra_group = cmds.group(n = selection + "_" + suffix_name,w=1,em=1) 39 | cmds.matchTransform(extra_group,selection) 40 | cmds.parent(selection,extra_group) 41 | return extra_group 42 | 43 | else: 44 | extra_group = cmds.group(n = selection + "_" + suffix_name,w=1,em=1) 45 | cmds.parent(extra_group,selection_parent) 46 | self.zero_transform(extra_group) 47 | cmds.makeIdentity(extra_group) 48 | cmds.parent(selection,extra_group) 49 | cmds.select(cl=1) 50 | return extra_group 51 | 52 | #print ("Create extra group for {0}".format(selection)) 53 | 54 | # Beta 55 | def system_group_hierarchy(self, system_group_order): 56 | if system_group_order == 0: 57 | system_group_order = "Extra" 58 | elif system_group_order == 1: 59 | system_group_order = "Flip" 60 | elif system_group_order == 2: 61 | system_group_order = "Global" 62 | elif system_group_order == 3: 63 | system_group_order = "Follow" 64 | elif system_group_order == 4: 65 | system_group_order = "Offset" 66 | else: 67 | cmds.error("please specify valid order input") 68 | 69 | # this function create extra group for follow so you don't need to 70 | # parameter(selection=ctrl,follow_target1,follow_target2,skip_translate=axis to be ignore,skip_rotate=axis to be ignore) 71 | # follow_type = "translate"|"rotate"|"translate_and_rotate") 72 | def follow_system(self,selection,follow_target1,follow_target2,follow_type = "translate_and_rotate",attribute_name=None): 73 | # Beta 74 | cmds.select(selection) 75 | 76 | # Decide follow_type 77 | if follow_type == "translate_and_rotate": 78 | if attribute_name == None: 79 | attribute_name = "Follow" 80 | if cmds.objExists(selection+"_"+attribute_name): 81 | follow_group = selection+"_"+attribute_name 82 | else: 83 | follow_group = self.create_extra_group(selection,attribute_name) 84 | 85 | follow_constraint = cmds.parentConstraint(follow_target2,follow_target1,follow_group,mo=1)[0] 86 | cmds.setAttr(follow_constraint + ".interpType", 2) 87 | setrange_name = "_follow_setrange" 88 | 89 | elif follow_type == "translate": 90 | 91 | 92 | if attribute_name == None: 93 | attribute_name = "Follow_Translate" 94 | if cmds.objExists(selection+"_"+attribute_name): 95 | follow_group = selection+"_"+attribute_name 96 | else: 97 | follow_group = self.create_extra_group(selection,attribute_name) 98 | 99 | follow_constraint = cmds.parentConstraint(follow_target2,follow_target1,follow_group, 100 | mo=1,sr=("x","y","z"))[0] 101 | cmds.setAttr(follow_constraint + ".interpType", 2) 102 | setrange_name = "_follow_trans_setrange" 103 | 104 | else: 105 | if attribute_name == None: 106 | attribute_name = "Follow_Rotate" 107 | if cmds.objExists(selection+"_"+attribute_name): 108 | follow_group = selection+"_"+attribute_name 109 | else: 110 | follow_group = self.create_extra_group(selection,attribute_name) 111 | 112 | follow_constraint = cmds.parentConstraint(follow_target2,follow_target1,follow_group, 113 | mo=1,st=("x","y","z"))[0] 114 | cmds.setAttr(follow_constraint + ".interpType", 2) 115 | setrange_name = "_follow_rot_setrange" 116 | 117 | # Create Follow Attribute 118 | if not cmds.objExists(selection+"."+attribute_name): 119 | cmds.addAttr (selection, ln=attribute_name, at="float",dv=0,min=0, max=10) 120 | cmds.setAttr (selection + ("."+attribute_name),e=1, k=1) 121 | 122 | # connect follow attribute to drive weight constraint 123 | 124 | rev_node = cmds.shadingNode("setRange", n=(selection + setrange_name) , au=1 ) 125 | cmds.setAttr(rev_node + ".oldMaxX", 10) 126 | cmds.setAttr(rev_node + ".oldMaxY", 10) 127 | cmds.setAttr(rev_node + ".minY", 1) 128 | cmds.setAttr(rev_node + ".maxX", 1) 129 | 130 | cmds.connectAttr(selection+"."+attribute_name, rev_node+".value.valueX") 131 | cmds.connectAttr(selection+"."+attribute_name, rev_node+".value.valueY") 132 | cmds.connectAttr(rev_node+".outValue.outValueX",follow_constraint+"."+follow_target1+"W1") 133 | cmds.connectAttr(rev_node+".outValue.outValueY",follow_constraint+"."+follow_target2+"W0") 134 | 135 | cmds.select(cl=1) 136 | return (selection + "." + attribute_name) 137 | 138 | # parameter(object = selected dag_object) 139 | def zero_transform(self,object): 140 | cmds.setAttr(object + ".tx", 0) 141 | cmds.setAttr(object + ".ty", 0) 142 | cmds.setAttr(object + ".tz", 0) 143 | cmds.setAttr(object + ".rx", 0) 144 | cmds.setAttr(object + ".ry", 0) 145 | cmds.setAttr(object + ".rz", 0) 146 | 147 | # parameter(object = selected dag_object, channel to lock and hide(e.g. ".tx",".ty", etc..)) 148 | def lock_hide_attr(self,object,channel): 149 | for i in channel: 150 | cmds.setAttr(object + i, l=1, k=0, cb=0) 151 | 152 | # parameter(source,target,translate,rotate,scale) 153 | def connect_attr(self,source,target,channel=("x","y","z"),translate=True,rotate=True,scale=False): 154 | for i in channel: 155 | if translate==True: 156 | cmds.connectAttr(source+".translate.t"+i,target+".translate.t"+i,f=1) 157 | if rotate==True: 158 | cmds.connectAttr(source+".rotate.r"+i,target+".rotate.r"+i,f=1) 159 | if scale==True: 160 | cmds.connectAttr(source+".scale.s"+i,target+".scale.s"+i,f=1) 161 | 162 | # Beta 163 | # Make sure all argument ordered correctly (top to bottom in hierarchy)! 164 | # parameter(follow_gr p= list or string, fk_ctrl = list or string, follow_grp_parent = string, 165 | # fk_ctrl_parent = list or string, follow_attr = obj_name + attr_name) 166 | def set_follow_for_fk(self,follow_grp,fk_ctrl,follow_grp_parent,fk_ctrl_parent,follow_attr,max_attr=10): 167 | offset_fk = [] 168 | offset_follow = [] 169 | constraint_list = [] 170 | count_follow_grp = 0 171 | count_fk_ctrl = 0 172 | count_offset_follow = 0 173 | count_constraint_list = 0 174 | 175 | for i in fk_ctrl: 176 | new_fk_group = self.create_extra_group(i,"connectFollow") 177 | offset_fk.append(new_fk_group) 178 | cmds.parent(new_fk_group,fk_ctrl_parent[count_fk_ctrl]) 179 | count_fk_ctrl+=1 180 | 181 | for i in follow_grp: 182 | new_follow_grp = self.create_extra_group(i,"off") 183 | offset_follow.append(new_follow_grp) 184 | cmds.parent(new_follow_grp,follow_grp_parent) 185 | cmds.connectAttr(i+".translate",offset_fk[count_follow_grp]+".translate",f=1) 186 | cmds.connectAttr(i+".rotate",offset_fk[count_follow_grp]+".rotate",f=1) 187 | count_follow_grp+=1 188 | 189 | # Exclude first offset 190 | for i in offset_follow[1:]: 191 | constraint = cmds.parentConstraint(follow_grp[count_offset_follow],fk_ctrl[count_offset_follow], 192 | i,mo=1)[0] 193 | cmds.setAttr(constraint + ".interpType", 2) 194 | constraint_list.append(constraint) 195 | count_offset_follow+=1 196 | 197 | # Switch for follow and fk system 198 | rename_follow_attr = follow_attr.replace(".","_") 199 | if cmds.objExists(rename_follow_attr+"_setFK_SR"): 200 | shading_node = rename_follow_attr + "_setFK_SR" 201 | else: 202 | shading_node = cmds.shadingNode("setRange",n=rename_follow_attr+"_setFK_SR",au=1) 203 | cmds.setAttr(shading_node + ".oldMaxX", max_attr) 204 | cmds.setAttr(shading_node + ".oldMaxY", max_attr) 205 | cmds.setAttr(shading_node + ".minY", 1) 206 | cmds.setAttr(shading_node+ ".maxX", 1) 207 | cmds.connectAttr(follow_attr, shading_node+".value.valueX") 208 | cmds.connectAttr(follow_attr, shading_node+".value.valueY") 209 | 210 | for i in constraint_list: 211 | cmds.connectAttr(shading_node+".outValue.outValueX",i+"."+follow_grp[count_constraint_list]+"W0") 212 | cmds.connectAttr(shading_node+".outValue.outValueY",i+"."+fk_ctrl[count_constraint_list]+"W1") 213 | count_constraint_list+=1 214 | 215 | # HEAVY 216 | def are_vertices_connected(self,vertex1, vertex2): 217 | #basically query all edge that connected to first vertex 218 | connected_edges = cmds.polyListComponentConversion(vertex1, toEdge=True) 219 | connected_edges = cmds.filterExpand(connected_edges, sm=32) 220 | 221 | # Check if the second vertex is part of any of the connected edges 222 | for edge in connected_edges: 223 | vertices = cmds.polyListComponentConversion(edge, toVertex=True) 224 | vertices = cmds.filterExpand(vertices, sm=31) 225 | if vertex2 in vertices: 226 | return True 227 | 228 | return False 229 | 230 | # HEAVY 231 | # parameter(vertex, all_vertex = list of selected vertex) 232 | def is_vertex_on_edge(self,vertex, all_vertex): 233 | connect_count = 0 234 | 235 | #basically query all edge that connected to first vertex 236 | connected_edges = cmds.polyListComponentConversion(vertex, toEdge=True) 237 | connected_edges = cmds.filterExpand(connected_edges, sm=32) 238 | cmds.select(cl=1) 239 | 240 | # Check if the second vertex is part of any of the connected edges 241 | for edge in connected_edges: 242 | vertices = cmds.polyListComponentConversion(edge, toVertex=True) 243 | vertices = cmds.filterExpand(vertices, sm=31) 244 | cmds.select(vertices,add=1) 245 | neighbor_vtx = cmds.ls(sl=1,fl=1) 246 | 247 | for vtx in all_vertex: 248 | if vtx in neighbor_vtx: 249 | connect_count +=1 250 | if connect_count == 3: 251 | return False 252 | 253 | return True 254 | 255 | # Beta(Remember only passing the correct parameter for this to work) 256 | # Note(this function can remove element in given argument) 257 | # parameter(selection = vertex selected (cmds.ls(fl=1,os=1)) 258 | def order_selection(self,selection): 259 | selection_initial_length = len(selection) 260 | connected_vtx=[] 261 | 262 | count = 0 263 | for i in range(len(selection)): 264 | if len(selection) == 1: 265 | connected_vtx.append(selection[0]) 266 | selection.remove(selection[0]) 267 | break 268 | for vtx in selection: 269 | first_index = selection[0] 270 | vertex = vtx 271 | if first_index!=vtx: 272 | if self.are_vertices_connected(first_index,vtx): 273 | connected_vtx.append(first_index) 274 | selection.remove(selection[0]) 275 | selection.remove(vtx) 276 | selection.insert(0,vtx) 277 | count+=1 278 | 279 | if len(connected_vtx) != selection_initial_length: 280 | print (connected_vtx) 281 | cmds.warning("some vertices have been excluded, please select clean vertex loop") 282 | 283 | return connected_vtx 284 | 285 | def do_wire_deform(self, shape, deformCurves, baseCurves): 286 | count = len(deformCurves) 287 | wireDef = cmds.wire(shape, wc= count)[0] 288 | for i in range(count): 289 | cmds.connectAttr('%s.worldSpace[0]' % deformCurves[i], '%s.deformedWire[%s]' % (wireDef, i)) 290 | cmds.connectAttr('%s.worldSpace[0]' % baseCurves[i], '%s.baseWire[%s]' % (wireDef, i)) 291 | cmds.setAttr('%s.rotation' % wireDef, 0) 292 | 293 | # Beta 294 | # parameter(name,vtx=single or multiple xform/(x,y,z),degree,width,color=remember to use enum for easier us) 295 | def create_curve_from_pos(self,name,pos,degree=1,width=1,color=0): 296 | curve = cmds.curve(n=name,d=degree,p=pos,k=range(len(pos))) 297 | cmds.xform(curve,cp=1) 298 | curveShape = cmds.listRelatives(curve, s=1)[0] 299 | 300 | cmds.setAttr(curveShape+".lineWidth",width) 301 | cmds.setAttr(curveShape+".overrideEnabled",1) 302 | cmds.setAttr(curveShape+".overrideColor",color) 303 | 304 | return curve 305 | 306 | # parameter(obj) 307 | # Note(use translation flag) 308 | def get_xform_pos(self,obj): 309 | if isinstance(obj, list): 310 | obj_xform = [] 311 | 312 | for i in obj: 313 | pos = cmds.xform(i, q=1,ws=1,t=1) 314 | obj_xform.append(pos) 315 | 316 | return obj_xform 317 | else: 318 | return cmds.xform(obj,q=1,ws=1,t=1) 319 | 320 | # Beta 321 | # Parameter (name,pos,rot,color,width,shape=ctrl shape you want to create) 322 | def create_ctrl_on_pos(self,name,pos=(0,0,0),rot=(0,0,0),color=0,width=1,shape="circle",scale=1): 323 | if shape != "circle": 324 | with open(self.module_path+"/controls.json") as read_file: 325 | json_file = json.load(read_file) 326 | point = json_file[shape] 327 | 328 | if scale != 1: 329 | for i in range(len(point)): 330 | point[i][0] = point[i][0]*scale 331 | point[i][1] = point[i][1]*scale 332 | point[i][2] = point[i][2]*scale 333 | ctrl = cmds.curve(n=name,d=1,p=point) 334 | else: 335 | ctrl = cmds.curve(n=name,d=1,p=point) 336 | else: 337 | ctrl = cmds.circle(n=name,ch=0)[0] 338 | if scale!=1: 339 | for i in range(cmds.getAttr(ctrl+'.spans')): 340 | X_CV = cmds.xform(ctrl+".cv[{0}]".format(i),q=1,ws=1,t=1)[0] 341 | Y_CV = cmds.xform(ctrl+".cv[{0}]".format(i),q=1,ws=1,t=1)[1] 342 | Z_CV = cmds.xform(ctrl+".cv[{0}]".format(i),q=1,ws=1,t=1)[2] 343 | cmds.xform(ctrl+".cv[{0}]".format(i),ws=1,t=(X_CV*scale,Y_CV*scale,Z_CV*scale)) 344 | 345 | group = cmds.group(n=name+"_off",em=1,w=1) 346 | cmds.parent(ctrl,group) 347 | 348 | # Move group on pos and rot 349 | cmds.xform(group,ws=1,t=pos,ro=rot) 350 | 351 | # Change Color if needed 352 | circleShape = cmds.listRelatives(ctrl, s=1) 353 | 354 | for shape in circleShape: 355 | cmds.setAttr(shape+".lineWidth",width) 356 | cmds.setAttr(shape+".overrideEnabled",1) 357 | cmds.setAttr(shape+".overrideColor",color) 358 | 359 | return group, ctrl 360 | 361 | # source: stackoverflow by Kyle baker 362 | def findMiddle(self,input_list): 363 | middle = float(len(input_list))/2 364 | if middle % 2 != 0: 365 | return input_list[int(middle - .5)] # return element in list 366 | else: 367 | return (input_list[int(middle)], input_list[int(middle-1)]) # return tuple between two object 368 | 369 | 370 | # parameter (pos_1 = (x,y,z), pos_2 = (x,y,z)) 371 | def findMiddle_pos(self,pos_1,pos_2): 372 | center = [] 373 | 374 | for i in range(3): 375 | center.append((pos_1[i]+pos_2[i])/2) 376 | return center 377 | 378 | # parameter (obj to add,name) 379 | def add_attr_separator(self,obj,name): 380 | cmds.addAttr(obj, ln=name, at="enum", en="___________:") 381 | cmds.setAttr(obj+"."+name, e=1, k=0, cb=1) 382 | 383 | return obj + "." + name 384 | 385 | # you can assign max_value/min_value as false using string 386 | # parameter (obj to add,name,min_value,max_value) 387 | def add_attr_float(self,obj,name,min_value,max_value,dv=0): 388 | if max_value=="False" and min_value=="False": 389 | cmds.addAttr(obj,ln=name,at="double",dv=dv) 390 | cmds.setAttr(obj+"."+name,e=1,k=1) 391 | return obj + "." + name 392 | 393 | elif max_value=="False": 394 | cmds.addAttr(obj,ln=name,at="double",dv=dv,min=min_value) 395 | cmds.setAttr(obj+"."+name,e=1,k=1) 396 | return obj + "." + name 397 | 398 | elif min_value=="False": 399 | cmds.addAttr(obj,ln=name,at="double",dv=dv,max=max_value) 400 | cmds.setAttr(obj+"."+name,e=1,k=1) 401 | return obj + "." + name 402 | 403 | else: 404 | cmds.addAttr(obj,ln=name,at="double",dv=dv,max=max_value,min=min_value) 405 | cmds.setAttr(obj+"."+name,e=1,k=1) 406 | return obj + "." + name 407 | 408 | # You can pass None to output_attr 409 | # parameter (input_attr=name+attr_name,output_attr=name+attr_name,factor=conversion factor) 410 | def convert_value(self,input_attr,output_attr,factor): 411 | if output_attr == None: 412 | node = cmds.shadingNode("unitConversion",au=1,n="UC_"+input_attr.rpartition(".")[2]) 413 | if not output_attr == None: 414 | node = cmds.shadingNode("unitConversion",au=1,n="UC_"+input_attr.rpartition(".")[2]+"_"+(output_attr.rpartition(".")[2])[:14]) 415 | cmds.connectAttr(node+".output",output_attr,f=1) 416 | cmds.connectAttr(input_attr,node+".input.",f=1) 417 | cmds.setAttr(node+".conversionFactor",factor) 418 | 419 | return node 420 | 421 | # drivers take list 422 | # parameter(drivers=[obj_name+attr_name,...], driven = obj_name+attr_name) 423 | def clamp_multi_input(self,drivers,driven,clamp_min=0,clamp_max=1): 424 | # check passed argument if it list 425 | if not isinstance(drivers, list): 426 | cmds.warning("Only detect one input clamp, skip operation") 427 | return 428 | 429 | # Select all driver first then the driven 430 | rename_driven = driven.replace(".","_") 431 | clamp_node_name = rename_driven + "_clamp" 432 | blendWeight_node_name = rename_driven + "_blendW" 433 | 434 | cmds.shadingNode("blendWeighted",n=blendWeight_node_name,au=1) 435 | cmds.shadingNode("clamp",n=clamp_node_name,au=1) 436 | 437 | cmds.setAttr(clamp_node_name + ".minR",clamp_min) 438 | cmds.setAttr(clamp_node_name + ".maxR",clamp_max) 439 | 440 | count = 0 441 | for i in drivers: 442 | cmds.connectAttr(i,blendWeight_node_name+".input"+str([count]),f=1) 443 | count+=1 444 | 445 | cmds.connectAttr(blendWeight_node_name+".output",clamp_node_name+".input.inputR",f=1) 446 | cmds.connectAttr(clamp_node_name+".output.outputR",driven,f=1) 447 | 448 | return blendWeight_node_name,clamp_node_name 449 | 450 | # you can pass None to output attr and input2 451 | # parameter (input1 = list or single as x input, input2 = float, number, list or single as x input, 452 | # name = node_name,output_attr = list or single as x input) 453 | # example input1 = ["obj.rx","obj.ry"] -> input1[0] and input1[1] will use x and y channel automatically 454 | def multiply_divide(self,input1,input2,name,output_attr=None): 455 | channel = ["X","Y","Z"] 456 | count = 0 457 | 458 | if output_attr == None: 459 | node = cmds.shadingNode("multiplyDivide",au=1,n=name) 460 | if not output_attr == None: 461 | node = cmds.shadingNode("multiplyDivide",au=1,n=name) 462 | if isinstance(output_attr, list or tuple): 463 | for i in output_attr: 464 | cmds.connectAttr(node+".output"+channel[count],i,f=1) 465 | count+=1 466 | else: 467 | cmds.connectAttr(node+".outputX",i,f=1) 468 | 469 | count = 0 470 | if isinstance(input1, list or tuple): 471 | for i in input1: 472 | cmds.connectAttr(i,node+".input1"+channel[count],f=1) 473 | count+=1 474 | else: 475 | cmds.connectAttr(i,node+".input1X",f=1) 476 | 477 | count = 0 478 | if isinstance(input2, list or tuple): 479 | for i in input2: 480 | if type(i) == int or float: 481 | cmds.setAttr(node+".input2"+channel[count],i) 482 | count+=1 483 | else: 484 | cmds.connectAttr(i,node+".input2"+channel[count],f=1) 485 | count+=1 486 | elif input2 == None: 487 | pass 488 | else: 489 | if type(input2) == int or float: 490 | cmds.setAttr(node+".input2X",input2) 491 | else: 492 | cmds.connectAttr(input2,node+".input2X",f=1) 493 | 494 | return node 495 | 496 | # Beta (only 1D available ATM) 497 | # parameter(input_attr = obj attr name single or list, name, output_attr, input_type) 498 | def plus_minus_average(self,input_attr,name,output_attr=None,input_type="1D"): 499 | count = 0 500 | if output_attr == None: 501 | node = cmds.shadingNode("plusMinusAverage",au=1,n=name) 502 | else: 503 | node = cmds.shadingNode("plusMinusAverage",au=1,n=name) 504 | cmds.connectAttr(node+".output"+input_type,output_attr,f=1) 505 | count = 0 506 | if isinstance(input_attr, list or tuple): 507 | for i in input_attr: 508 | cmds.connectAttr(i,node+".input"+input_type+"[{0}]".format(count)) 509 | count +=1 510 | else: 511 | cmds.connectAttr(input_attr,node+".input"+input_type+"[{0}]".format(count)) 512 | 513 | return node 514 | 515 | # you can pass None to output attr 516 | # parameter (input_attr,output_attr) 517 | def reverse_value(self,input_attr,output_attr): 518 | if output_attr == None: 519 | node = cmds.shadingNode("reverse",au=1,n="Rev_"+input_attr.rpartition(".")[2]) 520 | if not output_attr == None: 521 | node = cmds.shadingNode("reverse",au=1,n="Rev_"+input_attr.rpartition(".")[2]+"_"+(output_attr.rpartition(".")[2])[:14]) 522 | cmds.connectAttr(node+".outputX",output_attr,f=1) 523 | cmds.connectAttr(input_attr,node+".inputX",f=1) 524 | 525 | return node 526 | 527 | # parameter (name,source=list/single object,target=obj) 528 | def create_blendshape(self,name,source,target): 529 | cmds.select(cl=1) 530 | if isinstance(source, list or tuple): 531 | for i in source: 532 | cmds.select(i,add=1) 533 | else: 534 | cmds.select(source) 535 | 536 | cmds.select(target,add=1) 537 | return cmds.blendShape(n=name)[0] 538 | 539 | # Beta (Unstable) 540 | # driver attr take 2 level tuple/list and driven also take 2 level tuple 541 | # parameter (driver_attr=((driver attr name, value),(..)), driven attr=((driven attr name, value),(..)) 542 | def set_driven_key(self,driver_attr,driven_attr): 543 | for i in range(len(driver_attr)): 544 | cmds.setAttr(driver_attr[i][0],driver_attr[i][1]) 545 | cmds.setAttr(driven_attr[i][0],driven_attr[i][1]) 546 | cmds.setDrivenKeyframe(driven_attr[i][0],cd=driver_attr[i][0]) 547 | 548 | # Reset to normal 549 | cmds.setAttr(driver_attr[0][0],driver_attr[0][1]) 550 | 551 | # parameter (source_attr = attr_name to connect to input BW, target_attr,name,weight_attr=weight input in BW node) 552 | def blend_weight(self,source_attr,target_attr,name,weight_attr=None): 553 | cmds.shadingNode("blendWeighted",n=name,au=1) 554 | 555 | source_count = 0 556 | weight_count = 0 557 | 558 | if isinstance(source_attr, list): 559 | for i in source_attr: 560 | cmds.connectAttr(i,name+".input"+str[source_count],f=1) 561 | source_count+=1 562 | else: 563 | cmds.connectAttr(source_attr,name+".input[0]",f=1) 564 | 565 | if weight_attr != None: 566 | if isinstance(weight_attr, list): 567 | for i in weight_attr: 568 | cmds.connectAttr(i,name+".weight"+str[weight_count],f=1) 569 | weight_count+=1 570 | else: 571 | cmds.connectAttr(weight_attr,name+".weight[0]",f=1) 572 | 573 | cmds.connectAttr(name+".output",target_attr,f=1) 574 | 575 | return name 576 | 577 | # Beta(Heavy)(not effective) 578 | # parameter (obj,axis= x or y or z or (x,y,z) individual, amount = target pos) 579 | def move_cv(self,obj,axis,amount): 580 | degs = cmds.getAttr(obj+'.degree') 581 | spans = cmds.getAttr(obj+'.spans') 582 | cvs = degs+spans 583 | 584 | for i in range(cvs): 585 | if "x" in axis: 586 | temp_posY = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[1] 587 | temp_posZ = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[2] 588 | temp_posX = cmds.xform(obj+".cv[{0}]".format(i),ws=1,t=(amount,temp_posY,temp_posZ),wd=1) 589 | temp_posX = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[0] 590 | 591 | if "y" in axis: 592 | temp_posX = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[0] 593 | temp_posZ = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[2] 594 | temp_posY = cmds.xform(obj+".cv[{0}]".format(i),ws=1,t=(temp_posX,amount,temp_posZ),wd=1) 595 | temp_posY = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[1] 596 | 597 | if "z" in axis: 598 | temp_posX = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[0] 599 | temp_posY = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[1] 600 | temp_posZ = cmds.xform(obj+".cv[{0}]".format(i),ws=1,t=(temp_posX,temp_posY,amount),wd=1) 601 | temp_posZ = cmds.xform(obj+".cv[{0}]".format(i),q=1,ws=1,t=1)[2] 602 | 603 | # parameter (obj = can be single or multiple) 604 | def check_objExist(self,obj): 605 | if isinstance(obj, list): 606 | for i in obj: 607 | if cmds.objExists(i): 608 | cmds.error("Duplicated object detected: {0}".format(i)) 609 | return i 610 | else: 611 | if cmds.objExists(obj): 612 | cmds.error("Duplicated object detected: {0}".format(obj)) 613 | return obj 614 | 615 | # source: stackoverflow by theodox 616 | # parameter (geo) 617 | def is_obj_skinned(self,geo): 618 | objHist = cmds.listHistory(geo, pdo=True) 619 | skinCluster = cmds.ls(objHist, type="skinCluster") or [None] 620 | cluster = skinCluster[0] 621 | 622 | if cluster == None: 623 | return False 624 | else: 625 | return True 626 | 627 | # parameter (obj = list that ordered(the first index is parent)(work with 1 root hierarchy)) 628 | def order_hierarchy(self,obj_list): 629 | if not isinstance(obj_list, list): 630 | return 631 | 632 | count = 0 633 | for i in range(len(obj_list)): 634 | if count == len(obj_list)-1: 635 | return 636 | 637 | if cmds.listRelatives(obj_list[count+1],p=1) != None: 638 | if cmds.listRelatives(obj_list[count+1],p=1)[0] == obj_list[count]: 639 | count += 1 640 | continue 641 | 642 | cmds.parent(obj_list[count+1],obj_list[count]) 643 | count += 1 644 | 645 | # parameter (joint = list/tuple or single object) 646 | def segment_scale_compensate(self,joint): 647 | if isinstance(joint, list or tuple): 648 | for i in joint: 649 | cmds.setAttr(i+".segmentScaleCompensate", 0) 650 | else: 651 | cmds.setAttr(joint+".segmentScaleCompensate", 0) 652 | 653 | # Parameter (joint = list joint, primary, secondary) 654 | # Primary = The argument can be one of the following strings: xyz, yzx, zxy, zyx, yxz, xzy, none. 655 | # Secondary = The argument can be one of the following strings: xup, xdown, yup, ydown, zup, zdown, none 656 | def set_jnt_axis(self, joint, primary="xzy", secondary="zup"): 657 | if isinstance(joint,list or tuple): 658 | for i in joint: 659 | cmds.joint(i,e=1,oj=primary,sao=secondary,zso=1,ch=0) 660 | else: 661 | cmds.joint(joint,e=1,oj=primary,sao=secondary,zso=1,ch=0) 662 | 663 | # Parameter (joint = single joint) 664 | def set_tip_axis(self, joint): 665 | if isinstance(joint,list or tuple): 666 | for i in joint: 667 | cmds.joint(i,e=1,oj="none",zso=1,ch=0) 668 | else: 669 | cmds.joint(joint,e=1,oj="none",zso=1,ch=0) 670 | 671 | # Beta 672 | # Not yet clean constraint 673 | # parameter(joints=selected lid jnt(w/o root) up_obj, jnt_name = suffix or prefix (jnt or joint) 674 | # aim_vec = (x, y, z), up_vec = (x, y, z)) 675 | def set_aim_loc(self,joints,up_obj,jnt_name=None,aim_vec=(1,0,0),up_vec=(0,1,0)): 676 | lid_locator = [] 677 | 678 | # Beta (unquote below, if head joint available) 679 | """ 680 | up_vector = cmds.spaceLocator(n = dir_x+"_eyeUpVec_Loc", p = (0,0,0)) 681 | up_vector_grp = cmds.group(n = dir_x+"_eyeUpVec_FollowHead", em=1, w=1) 682 | """ 683 | 684 | # Create locator for each joint, and aim constraint 685 | for i in joints: 686 | loc = cmds.spaceLocator()[0] 687 | if jnt_name != None: 688 | loc = cmds.rename(cmds.ls(sl=1)[0], i.replace(jnt_name,"Loc")) 689 | lid_locator.append(loc) 690 | jnt_pos = cmds.xform(i, q=1, ws=1, t=1) 691 | cmds.xform(loc, ws=1, t=jnt_pos) 692 | jnt_parent = cmds.listRelatives(i,p=1)[0] 693 | 694 | cmds.aimConstraint(loc, jnt_parent, mo=1, w=1, aim=aim_vec, u=up_vec, wut="object", wuo=up_obj) 695 | 696 | return lid_locator --------------------------------------------------------------------------------