├── 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
--------------------------------------------------------------------------------