├── .gitignore ├── LICENSE.txt ├── Pipfile ├── Pipfile.lock ├── README.md ├── __init__.py ├── default_config.json ├── nodz.png ├── nodz_demo.py ├── nodz_main.py ├── nodz_utils.py └── release_notes.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 LeGoffLoic 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 | -------------------------------------------------------------------------------- /Pipfile: -------------------------------------------------------------------------------- 1 | [[source]] 2 | name = "pypi" 3 | url = "https://pypi.org/simple" 4 | verify_ssl = true 5 | 6 | [dev-packages] 7 | 8 | [packages] 9 | qt-py = "*" 10 | pyside2 = "*" 11 | 12 | [requires] 13 | -------------------------------------------------------------------------------- /Pipfile.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_meta": { 3 | "hash": { 4 | "sha256": "460c7f807b9d8a22a5304d242732085182062134fafd92e1d868388e1f408bcc" 5 | }, 6 | "pipfile-spec": 6, 7 | "requires": { 8 | "python_version": "3.5" 9 | }, 10 | "sources": [ 11 | { 12 | "name": "pypi", 13 | "url": "https://pypi.org/simple", 14 | "verify_ssl": true 15 | } 16 | ] 17 | }, 18 | "default": { 19 | "pyside2": { 20 | "hashes": [ 21 | "sha256:02986ba0215691980d7b126049c9aa392ffa5174079113bc5e62684bde625cb6", 22 | "sha256:433d8f0251ce3d7200ad5279b378165ad6babb1f10588ce7aae9df86c1e100d1", 23 | "sha256:5c52c9e1916248c16c12ae925f167e6ca2580c514c0d46fca0933df6a371d204", 24 | "sha256:8707fac6088dbf3c7262871a8fb5bf9276500f53ef67438b16c096cd510ce2e5", 25 | "sha256:8d185ba5d84a885eb9aacf25cee4efe1f8e4abff63afb6bc10e7176e475db59e", 26 | "sha256:e7aa2b4aa5f47c0f26c8907a4f308bca54ad8239bcabca904906b8f4a54e596e" 27 | ], 28 | "index": "pypi", 29 | "version": "==5.13.1" 30 | }, 31 | "qt-py": { 32 | "hashes": [ 33 | "sha256:4b972258b25b75ef6eb9cb2b11830fc12c5485f95f92647e033639222bcb8417" 34 | ], 35 | "index": "pypi", 36 | "version": "==1.2.1" 37 | }, 38 | "shiboken2": { 39 | "hashes": [ 40 | "sha256:14ca49878c8d545d1b74b0526af81e4ed5064133c682b57cabfa0ac16e7a2ccb", 41 | "sha256:2aa481ce1097d10f74f7d0570d7f38fa5158e49b2146ae3aae5c8c56623681b1", 42 | "sha256:cfec94e16b289f7abca6bf7dfc88b877098653855914c9f54d6477f47c68a93b", 43 | "sha256:dfbb1f2ea86b4ddff9e91589bc93c9b6e2702ab09285cdc151daa8096442a665", 44 | "sha256:f18ccc6f1870ab3fb7087412a745dfc4275c8afb1b645b88d982a5708032f3e3", 45 | "sha256:f341940069fe764f2a9c3a06a9844c4c353c94227d3dbc304640f34b42c02d7a" 46 | ], 47 | "version": "==5.13.1" 48 | } 49 | }, 50 | "develop": {} 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Screenshot](nodz.png) 2 | 3 | Nodz is a very user friendly python library to create nodes based graphs. It can be connected to anything you want as long as it understands python. Nodz does not hold any data other than its own graphics and attributes types as it is used by the graphics. 4 | Nods provides you with a very simple way to read your graph, it outputs connections as strings ('Node1.attribute1', 'node2.attribute5') 5 | 6 | Nodz is partially customizable via a configuration file that let you change colors and the shape of nodes. 7 | 8 | 9 | ***If you find any errors/bugs/flaws or anything bad, feel free to let me know so I can fix it for the next persons that would like to download nodz.*** 10 | 11 | ***PLEASE MAKE SURE TO CREATE 1 PULL REQUEST PER ISSUE ! THIS IS EASIER AND CLEANER TO PROCESS*** 12 | 13 | Nodz in under the [MIT license](LICENSE.txt). 14 | 15 | [WATCH DEMO HERE](https://vimeo.com/219933604) 16 | 17 | 18 | 19 | 20 | 21 | ### 22 | ## Requirement 23 | The following needs to be installed! 24 | - pip 25 | - pipenv 26 | 27 | 28 | 29 | 30 | ### 31 | ## Installation 32 | - `git clone` 33 | - `cd location` 34 | - `pipenv install` 35 | - enjoy! :) 36 | 37 | 38 | 39 | 40 | ### 41 | ## Configuration file 42 | 43 | Nodz comes with a default [configuration file](default_config.json), it is specified what can be removed and what can't be. 44 | If this file stays in the default location, it is auto loaded BUT you still need to apply it to Nodz (look at [nodz_demo.py](nodz_demo.py) lines 5/6) 45 | Be careful when editing it, if you are missing a "**,**" it will error. So don't screw up. :smile: 46 | 47 | 48 | 49 | 50 | ### 51 | ## Features 52 | 53 | Nodz comes by default with few features, you can toggle the grid visibility and the auto snap mode + some hotkeys. Hotkeys are at the moment based on Autodesk Maya because I developped this library for my personnal use in this specific software but I'm planning on adding that part in the configuration file so everyone can set different hotkeys. 54 | 55 | ```python 56 | nodz.gridVisToggle = True 57 | nodz.gridSnapToggle = False 58 | ``` 59 | 60 | ``` 61 | del : delete the selected nodes 62 | f : zoom focus on selected items, all the items if nothing is selected 63 | s : snap the selected node on the grid 64 | 65 | ``` 66 | 67 | 68 | 69 | 70 | ### 71 | ## API 72 | 73 | Nodz has a very simple API of 12 methods. 74 | For more information on each method, please read [nodz_main.py](nodz_main.py) as it has all the documentation required. 75 | 76 | Initialize 77 | ```python 78 | def loadConfig(filePath=defautConfigPath) 79 | def initialize() 80 | ``` 81 | Nodes 82 | ```python 83 | def createNode(name, preset, position, alternate) 84 | def deleteNode(node) 85 | def editNode(node, newName) 86 | ``` 87 | Attributes 88 | ```python 89 | def createAttribute(node, name, index, preset, plug, socket, dataType, plugMaxConnections, socketMaxConnections) 90 | def deleteAttribute(node, index) 91 | def editAttribute( node, index, newName, newIndex) 92 | ``` 93 | Connections 94 | ```python 95 | def createConnection(sourceNode, sourceAttr, targetNode, targetAttr) 96 | ``` 97 | Graph 98 | ```python 99 | def saveGraph(filePath) 100 | def loadGraph(filePath) 101 | def evaluateGraph() 102 | def clearGraph() 103 | ``` 104 | 105 | ### 106 | ## Signals 107 | 108 | Nodz also offers you some signals, most of them can feel redundant considering the design of the library but I'm sure some of you will find a use for it. It's better to have them just in case than not having them. 109 | **They are absolutly not mandatory in order for nodz to work.** 110 | 111 | Nodes 112 | ```python 113 | signal_NodeCreated(nodeName) 114 | signal_NodeDeleted([nodeNames]) 115 | signal_NodeEdited(oldName, newName) 116 | signal_NodeSelected([nodeNames]) 117 | signal_NodeMoved(nodeName, nodePos) 118 | signal_NodeDoubleClicked(nodeName) 119 | ``` 120 | Attributes 121 | ```Python 122 | signal_AttrCreated(nodeName, attrIndex) 123 | signal_AttrDeleted(nodeName, attrIndex) 124 | signal_AttrEdited(nodeName, oldIndex, newIndex) 125 | ``` 126 | Connections 127 | ```python 128 | signal_PlugConnected(srcNodeName, plugAttribute, dstNodeName, socketAttribue) 129 | signal_PlugDisconnected(srcNodeName, plugAttribute, dstNodeName, socketAttribue) 130 | signal_SocketConnected(srcNodeName, plugAttribute, dstNodeName, socketAttribue) 131 | signal_SocketDisconnected(srcNodeName, plugAttribute, dstNodeName, socketAttribue) 132 | ``` 133 | Graph 134 | ```python 135 | signal_GraphSaved() 136 | signal_GraphLoaded() 137 | signal_GraphCleared() 138 | ``` 139 | View 140 | ```Python 141 | signal_KeyPressed(key) 142 | signal_Dropped(drop position) 143 | ``` 144 | 145 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeGoffLoic/Nodz/0ee255c62883f7a374a9de6cbcf555e3352e5dec/__init__.py -------------------------------------------------------------------------------- /default_config.json: -------------------------------------------------------------------------------- 1 | { 2 | // General > Edit values BUT do not delete. 3 | "scene_width": 2000, 4 | "scene_height": 2000, 5 | "grid_size": 36, 6 | "antialiasing": true, 7 | "antialiasing_boost": true, 8 | "smooth_pixmap": true, 9 | 10 | "node_font": "Arial", 11 | "node_font_size": 12, 12 | "attr_font": "Arial", 13 | "attr_font_size": 10, 14 | "mouse_bounding_box": 80, 15 | 16 | // Shapes > Edit values BUT do not delete. 17 | "node_width": 200, 18 | "node_height": 25, 19 | "node_radius": 10, 20 | "node_border": 2, 21 | "node_attr_height": 30, 22 | "connection_width": 2, 23 | 24 | // Default colors > Edit values BUT do not delete. 25 | "alternate_value": 20, 26 | "bg_color": [40, 40,40, 255], 27 | "grid_color": [50, 50, 50, 255], 28 | "slot_border": [50, 50, 50, 255], 29 | "non_connectable_color": [100, 100, 100, 255], 30 | "connection_color": [255, 155, 0, 255], 31 | 32 | "node_default": { 33 | "bg": [130, 130, 130, 255], 34 | "border": [50, 50, 50, 255], 35 | "border_sel": [250, 250, 250, 255], 36 | "text": [255, 255, 255, 255] 37 | }, 38 | 39 | "attr_default": { 40 | "bg": [160, 160, 160, 255], 41 | "text": [220, 220, 220, 255], 42 | "plug": [255, 155, 0, 255], 43 | "socket": [255, 155, 0, 255] 44 | }, 45 | 46 | // Custom colors > Add and remove as you want. 47 | // Don't forget to edit the end of the dictionary if editing the 48 | // following part. 49 | 50 | "node_preset_1": { 51 | "bg": [80, 80, 80, 255], 52 | "border": [50, 50, 50, 255], 53 | "border_sel": [170, 80, 80, 255], 54 | "text": [230, 230, 230, 255] 55 | }, 56 | 57 | "attr_preset_1": { 58 | "bg": [60, 60, 60, 255], 59 | "text": [220, 220, 220, 255], 60 | "plug": [255, 155, 0, 255], 61 | "socket": [255, 155, 0, 255] 62 | }, 63 | 64 | "attr_preset_2":{ 65 | "bg": [250, 120, 120, 255], 66 | "text": [220, 220, 220, 255], 67 | "plug": [255, 155, 0, 255], 68 | "socket": [255, 155, 0, 255] 69 | }, 70 | 71 | "attr_preset_3":{ 72 | "bg": [160, 160, 160, 255], 73 | "text": [220, 220, 220, 255], 74 | "plug": [255, 155, 0, 255], 75 | "socket": [255, 155, 0, 255] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /nodz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LeGoffLoic/Nodz/0ee255c62883f7a374a9de6cbcf555e3352e5dec/nodz.png -------------------------------------------------------------------------------- /nodz_demo.py: -------------------------------------------------------------------------------- 1 | from Qt import QtCore, QtWidgets 2 | import nodz_main 3 | 4 | try: 5 | app = QtWidgets.QApplication([]) 6 | except: 7 | # I guess we're running somewhere that already has a QApp created 8 | app = None 9 | 10 | nodz = nodz_main.Nodz(None) 11 | # nodz.loadConfig(filePath='') 12 | nodz.initialize() 13 | nodz.show() 14 | 15 | 16 | ###################################################################### 17 | # Test signals 18 | ###################################################################### 19 | 20 | # Nodes 21 | @QtCore.Slot(str) 22 | def on_nodeCreated(nodeName): 23 | print('node created : ', nodeName) 24 | 25 | @QtCore.Slot(str) 26 | def on_nodeDeleted(nodeName): 27 | print('node deleted : ', nodeName) 28 | 29 | @QtCore.Slot(str, str) 30 | def on_nodeEdited(nodeName, newName): 31 | print('node edited : {0}, new name : {1}'.format(nodeName, newName)) 32 | 33 | @QtCore.Slot(str) 34 | def on_nodeSelected(nodesName): 35 | print('node selected : ', nodesName) 36 | 37 | @QtCore.Slot(str, object) 38 | def on_nodeMoved(nodeName, nodePos): 39 | print('node {0} moved to {1}'.format(nodeName, nodePos)) 40 | 41 | @QtCore.Slot(str) 42 | def on_nodeDoubleClick(nodeName): 43 | print('double click on node : {0}'.format(nodeName)) 44 | 45 | # Attrs 46 | @QtCore.Slot(str, int) 47 | def on_attrCreated(nodeName, attrId): 48 | print('attr created : {0} at index : {1}'.format(nodeName, attrId)) 49 | 50 | @QtCore.Slot(str, int) 51 | def on_attrDeleted(nodeName, attrId): 52 | print('attr Deleted : {0} at old index : {1}'.format(nodeName, attrId)) 53 | 54 | @QtCore.Slot(str, int, int) 55 | def on_attrEdited(nodeName, oldId, newId): 56 | print('attr Edited : {0} at old index : {1}, new index : {2}'.format(nodeName, oldId, newId)) 57 | 58 | # Connections 59 | @QtCore.Slot(str, str, str, str) 60 | def on_connected(srcNodeName, srcPlugName, destNodeName, dstSocketName): 61 | print('connected src: "{0}" at "{1}" to dst: "{2}" at "{3}"'.format(srcNodeName, srcPlugName, destNodeName, dstSocketName)) 62 | 63 | @QtCore.Slot(str, str, str, str) 64 | def on_disconnected(srcNodeName, srcPlugName, destNodeName, dstSocketName): 65 | print('disconnected src: "{0}" at "{1}" from dst: "{2}" at "{3}"'.format(srcNodeName, srcPlugName, destNodeName, dstSocketName)) 66 | 67 | # Graph 68 | @QtCore.Slot() 69 | def on_graphSaved(): 70 | print('graph saved !') 71 | 72 | @QtCore.Slot() 73 | def on_graphLoaded(): 74 | print('graph loaded !') 75 | 76 | @QtCore.Slot() 77 | def on_graphCleared(): 78 | print('graph cleared !') 79 | 80 | @QtCore.Slot() 81 | def on_graphEvaluated(): 82 | print('graph evaluated !') 83 | 84 | # Other 85 | @QtCore.Slot(object) 86 | def on_keyPressed(key): 87 | print('key pressed : ', key) 88 | 89 | nodz.signal_NodeCreated.connect(on_nodeCreated) 90 | nodz.signal_NodeDeleted.connect(on_nodeDeleted) 91 | nodz.signal_NodeEdited.connect(on_nodeEdited) 92 | nodz.signal_NodeSelected.connect(on_nodeSelected) 93 | nodz.signal_NodeMoved.connect(on_nodeMoved) 94 | nodz.signal_NodeDoubleClicked.connect(on_nodeDoubleClick) 95 | 96 | nodz.signal_AttrCreated.connect(on_attrCreated) 97 | nodz.signal_AttrDeleted.connect(on_attrDeleted) 98 | nodz.signal_AttrEdited.connect(on_attrEdited) 99 | 100 | nodz.signal_PlugConnected.connect(on_connected) 101 | nodz.signal_SocketConnected.connect(on_connected) 102 | nodz.signal_PlugDisconnected.connect(on_disconnected) 103 | nodz.signal_SocketDisconnected.connect(on_disconnected) 104 | 105 | nodz.signal_GraphSaved.connect(on_graphSaved) 106 | nodz.signal_GraphLoaded.connect(on_graphLoaded) 107 | nodz.signal_GraphCleared.connect(on_graphCleared) 108 | nodz.signal_GraphEvaluated.connect(on_graphEvaluated) 109 | 110 | nodz.signal_KeyPressed.connect(on_keyPressed) 111 | 112 | 113 | ###################################################################### 114 | # Test API 115 | ###################################################################### 116 | 117 | # Node A 118 | nodeA = nodz.createNode(name='nodeA', preset='node_preset_1', position=None) 119 | 120 | nodz.createAttribute(node=nodeA, name='Aattr1', index=-1, preset='attr_preset_1', 121 | plug=True, socket=False, dataType=str) 122 | 123 | nodz.createAttribute(node=nodeA, name='Aattr2', index=-1, preset='attr_preset_1', 124 | plug=False, socket=False, dataType=int) 125 | 126 | nodz.createAttribute(node=nodeA, name='Aattr3', index=-1, preset='attr_preset_2', 127 | plug=True, socket=True, dataType=int) 128 | 129 | nodz.createAttribute(node=nodeA, name='Aattr4', index=-1, preset='attr_preset_2', 130 | plug=True, socket=True, dataType=str) 131 | 132 | nodz.createAttribute(node=nodeA, name='Aattr5', index=-1, preset='attr_preset_3', 133 | plug=True, socket=True, dataType=int, plugMaxConnections=1, socketMaxConnections=-1) 134 | 135 | nodz.createAttribute(node=nodeA, name='Aattr6', index=-1, preset='attr_preset_3', 136 | plug=True, socket=True, dataType=int, plugMaxConnections=1, socketMaxConnections=-1) 137 | 138 | 139 | 140 | # Node B 141 | nodeB = nodz.createNode(name='nodeB', preset='node_preset_1') 142 | 143 | nodz.createAttribute(node=nodeB, name='Battr1', index=-1, preset='attr_preset_1', 144 | plug=True, socket=False, dataType=str) 145 | 146 | nodz.createAttribute(node=nodeB, name='Battr2', index=-1, preset='attr_preset_1', 147 | plug=True, socket=False, dataType=int) 148 | 149 | nodz.createAttribute(node=nodeB, name='Battr3', index=-1, preset='attr_preset_2', 150 | plug=True, socket=False, dataType=int) 151 | 152 | nodz.createAttribute(node=nodeB, name='Battr4', index=-1, preset='attr_preset_3', 153 | plug=True, socket=False, dataType=int, plugMaxConnections=1, socketMaxConnections=-1) 154 | 155 | 156 | 157 | # Node C 158 | nodeC = nodz.createNode(name='nodeC', preset='node_preset_1') 159 | 160 | nodz.createAttribute(node=nodeC, name='Cattr1', index=-1, preset='attr_preset_1', 161 | plug=False, socket=True, dataType=str) 162 | 163 | nodz.createAttribute(node=nodeC, name='Cattr2', index=-1, preset='attr_preset_1', 164 | plug=True, socket=False, dataType=int) 165 | 166 | nodz.createAttribute(node=nodeC, name='Cattr3', index=-1, preset='attr_preset_1', 167 | plug=True, socket=False, dataType=str) 168 | 169 | nodz.createAttribute(node=nodeC, name='Cattr4', index=-1, preset='attr_preset_2', 170 | plug=False, socket=True, dataType=str) 171 | 172 | nodz.createAttribute(node=nodeC, name='Cattr5', index=-1, preset='attr_preset_2', 173 | plug=False, socket=True, dataType=int) 174 | 175 | nodz.createAttribute(node=nodeC, name='Cattr6', index=-1, preset='attr_preset_3', 176 | plug=True, socket=False, dataType=str) 177 | 178 | nodz.createAttribute(node=nodeC, name='Cattr7', index=-1, preset='attr_preset_3', 179 | plug=True, socket=False, dataType=str) 180 | 181 | nodz.createAttribute(node=nodeC, name='Cattr8', index=-1, preset='attr_preset_3', 182 | plug=True, socket=False, dataType=int) 183 | 184 | 185 | # Please note that this is a local test so once the graph is cleared 186 | # and reloaded, all the local variables are not valid anymore, which 187 | # means the following code to alter nodes won't work but saving/loading/ 188 | # clearing/evaluating will. 189 | 190 | # Connection creation 191 | nodz.createConnection('nodeB', 'Battr2', 'nodeA', 'Aattr3') 192 | nodz.createConnection('nodeB', 'Battr1', 'nodeA', 'Aattr4') 193 | 194 | # Attributes Edition 195 | nodz.editAttribute(node=nodeC, index=0, newName=None, newIndex=-1) 196 | nodz.editAttribute(node=nodeC, index=-1, newName='NewAttrName', newIndex=None) 197 | 198 | # Attributes Deletion 199 | nodz.deleteAttribute(node=nodeC, index=-1) 200 | 201 | 202 | # Nodes Edition 203 | nodz.editNode(node=nodeC, newName='newNodeName') 204 | 205 | # Nodes Deletion 206 | nodz.deleteNode(node=nodeC) 207 | 208 | 209 | # Graph 210 | print( nodz.evaluateGraph()) 211 | 212 | nodz.saveGraph(filePath='Enter your path') 213 | 214 | nodz.clearGraph() 215 | 216 | nodz.loadGraph(filePath='Enter your path') 217 | 218 | 219 | 220 | if app: 221 | # command line stand alone test... run our own event loop 222 | app.exec_() 223 | -------------------------------------------------------------------------------- /nodz_main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import json 4 | 5 | from Qt import QtGui, QtCore, QtWidgets 6 | import nodz_utils as utils 7 | 8 | 9 | 10 | defaultConfigPath = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'default_config.json') 11 | 12 | 13 | class Nodz(QtWidgets.QGraphicsView): 14 | 15 | """ 16 | The main view for the node graph representation. 17 | 18 | The node view implements a state pattern to control all the 19 | different user interactions. 20 | 21 | """ 22 | 23 | signal_NodeCreated = QtCore.Signal(object) 24 | signal_NodeDeleted = QtCore.Signal(object) 25 | signal_NodeEdited = QtCore.Signal(object, object) 26 | signal_NodeSelected = QtCore.Signal(object) 27 | signal_NodeMoved = QtCore.Signal(str, object) 28 | signal_NodeDoubleClicked = QtCore.Signal(str) 29 | 30 | signal_AttrCreated = QtCore.Signal(object, object) 31 | signal_AttrDeleted = QtCore.Signal(object, object) 32 | signal_AttrEdited = QtCore.Signal(object, object, object) 33 | 34 | signal_PlugConnected = QtCore.Signal(object, object, object, object) 35 | signal_PlugDisconnected = QtCore.Signal(object, object, object, object) 36 | signal_SocketConnected = QtCore.Signal(object, object, object, object) 37 | signal_SocketDisconnected = QtCore.Signal(object, object, object, object) 38 | 39 | signal_GraphSaved = QtCore.Signal() 40 | signal_GraphLoaded = QtCore.Signal() 41 | signal_GraphCleared = QtCore.Signal() 42 | signal_GraphEvaluated = QtCore.Signal() 43 | 44 | signal_KeyPressed = QtCore.Signal(object) 45 | signal_Dropped = QtCore.Signal() 46 | 47 | def __init__(self, parent, configPath=defaultConfigPath): 48 | """ 49 | Initialize the graphics view. 50 | 51 | """ 52 | super(Nodz, self).__init__(parent) 53 | 54 | # Load nodz configuration. 55 | self.loadConfig(configPath) 56 | 57 | # General data. 58 | self.gridVisToggle = True 59 | self.gridSnapToggle = False 60 | self._nodeSnap = False 61 | self.selectedNodes = None 62 | 63 | # Connections data. 64 | self.drawingConnection = False 65 | self.currentHoveredNode = None 66 | self.sourceSlot = None 67 | 68 | # Display options. 69 | self.currentState = 'DEFAULT' 70 | self.pressedKeys = list() 71 | 72 | def wheelEvent(self, event): 73 | """ 74 | Zoom in the view with the mouse wheel. 75 | 76 | """ 77 | self.currentState = 'ZOOM_VIEW' 78 | self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 79 | 80 | inFactor = 1.15 81 | outFactor = 1 / inFactor 82 | 83 | if event.delta() > 0: 84 | zoomFactor = inFactor 85 | else: 86 | zoomFactor = outFactor 87 | 88 | self.scale(zoomFactor, zoomFactor) 89 | self.currentState = 'DEFAULT' 90 | 91 | def mousePressEvent(self, event): 92 | """ 93 | Initialize tablet zoom, drag canvas and the selection. 94 | 95 | """ 96 | # Tablet zoom 97 | if (event.button() == QtCore.Qt.RightButton and 98 | event.modifiers() == QtCore.Qt.AltModifier): 99 | self.currentState = 'ZOOM_VIEW' 100 | self.initMousePos = event.pos() 101 | self.zoomInitialPos = event.pos() 102 | self.initMouse = QtGui.QCursor.pos() 103 | self.setInteractive(False) 104 | 105 | 106 | # Drag view 107 | elif (event.button() == QtCore.Qt.MiddleButton and 108 | event.modifiers() == QtCore.Qt.AltModifier): 109 | self.currentState = 'DRAG_VIEW' 110 | self.prevPos = event.pos() 111 | self.setCursor(QtCore.Qt.ClosedHandCursor) 112 | self.setInteractive(False) 113 | 114 | 115 | # Rubber band selection 116 | elif (event.button() == QtCore.Qt.LeftButton and 117 | event.modifiers() == QtCore.Qt.NoModifier and 118 | self.scene().itemAt(self.mapToScene(event.pos()), QtGui.QTransform()) is None): 119 | self.currentState = 'SELECTION' 120 | self._initRubberband(event.pos()) 121 | self.setInteractive(False) 122 | 123 | 124 | # Drag Item 125 | elif (event.button() == QtCore.Qt.LeftButton and 126 | event.modifiers() == QtCore.Qt.NoModifier and 127 | self.scene().itemAt(self.mapToScene(event.pos()), QtGui.QTransform()) is not None): 128 | self.currentState = 'DRAG_ITEM' 129 | self.setInteractive(True) 130 | 131 | 132 | # Add selection 133 | elif (event.button() == QtCore.Qt.LeftButton and 134 | QtCore.Qt.Key_Shift in self.pressedKeys and 135 | QtCore.Qt.Key_Control in self.pressedKeys): 136 | self.currentState = 'ADD_SELECTION' 137 | self._initRubberband(event.pos()) 138 | self.setInteractive(False) 139 | 140 | 141 | # Subtract selection 142 | elif (event.button() == QtCore.Qt.LeftButton and 143 | event.modifiers() == QtCore.Qt.ControlModifier): 144 | self.currentState = 'SUBTRACT_SELECTION' 145 | self._initRubberband(event.pos()) 146 | self.setInteractive(False) 147 | 148 | 149 | # Toggle selection 150 | elif (event.button() == QtCore.Qt.LeftButton and 151 | event.modifiers() == QtCore.Qt.ShiftModifier): 152 | self.currentState = 'TOGGLE_SELECTION' 153 | self._initRubberband(event.pos()) 154 | self.setInteractive(False) 155 | 156 | 157 | else: 158 | self.currentState = 'DEFAULT' 159 | 160 | super(Nodz, self).mousePressEvent(event) 161 | 162 | def mouseMoveEvent(self, event): 163 | """ 164 | Update tablet zoom, canvas dragging and selection. 165 | 166 | """ 167 | # Zoom. 168 | if self.currentState == 'ZOOM_VIEW': 169 | offset = self.zoomInitialPos.x() - event.pos().x() 170 | 171 | if offset > self.previousMouseOffset: 172 | self.previousMouseOffset = offset 173 | self.zoomDirection = -1 174 | self.zoomIncr -= 1 175 | 176 | elif offset == self.previousMouseOffset: 177 | self.previousMouseOffset = offset 178 | if self.zoomDirection == -1: 179 | self.zoomDirection = -1 180 | else: 181 | self.zoomDirection = 1 182 | 183 | else: 184 | self.previousMouseOffset = offset 185 | self.zoomDirection = 1 186 | self.zoomIncr += 1 187 | 188 | if self.zoomDirection == 1: 189 | zoomFactor = 1.03 190 | else: 191 | zoomFactor = 1 / 1.03 192 | 193 | # Perform zoom and re-center on initial click position. 194 | pBefore = self.mapToScene(self.initMousePos) 195 | self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) 196 | self.scale(zoomFactor, zoomFactor) 197 | pAfter = self.mapToScene(self.initMousePos) 198 | diff = pAfter - pBefore 199 | 200 | self.setTransformationAnchor(QtWidgets.QGraphicsView.NoAnchor) 201 | self.translate(diff.x(), diff.y()) 202 | 203 | # Drag canvas. 204 | elif self.currentState == 'DRAG_VIEW': 205 | offset = self.prevPos - event.pos() 206 | self.prevPos = event.pos() 207 | self.verticalScrollBar().setValue(self.verticalScrollBar().value() + offset.y()) 208 | self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + offset.x()) 209 | 210 | # RuberBand selection. 211 | elif (self.currentState == 'SELECTION' or 212 | self.currentState == 'ADD_SELECTION' or 213 | self.currentState == 'SUBTRACT_SELECTION' or 214 | self.currentState == 'TOGGLE_SELECTION'): 215 | self.rubberband.setGeometry(QtCore.QRect(self.origin, event.pos()).normalized()) 216 | 217 | super(Nodz, self).mouseMoveEvent(event) 218 | 219 | def mouseReleaseEvent(self, event): 220 | """ 221 | Apply tablet zoom, dragging and selection. 222 | 223 | """ 224 | # Zoom the View. 225 | if self.currentState == '.ZOOM_VIEW': 226 | self.offset = 0 227 | self.zoomDirection = 0 228 | self.zoomIncr = 0 229 | self.setInteractive(True) 230 | 231 | 232 | # Drag View. 233 | elif self.currentState == 'DRAG_VIEW': 234 | self.setCursor(QtCore.Qt.ArrowCursor) 235 | self.setInteractive(True) 236 | 237 | 238 | # Selection. 239 | elif self.currentState == 'SELECTION': 240 | self.rubberband.setGeometry(QtCore.QRect(self.origin, 241 | event.pos()).normalized()) 242 | painterPath = self._releaseRubberband() 243 | self.setInteractive(True) 244 | self.scene().setSelectionArea(painterPath) 245 | 246 | 247 | # Add Selection. 248 | elif self.currentState == 'ADD_SELECTION': 249 | self.rubberband.setGeometry(QtCore.QRect(self.origin, 250 | event.pos()).normalized()) 251 | painterPath = self._releaseRubberband() 252 | self.setInteractive(True) 253 | for item in self.scene().items(painterPath): 254 | item.setSelected(True) 255 | 256 | 257 | # Subtract Selection. 258 | elif self.currentState == 'SUBTRACT_SELECTION': 259 | self.rubberband.setGeometry(QtCore.QRect(self.origin, 260 | event.pos()).normalized()) 261 | painterPath = self._releaseRubberband() 262 | self.setInteractive(True) 263 | for item in self.scene().items(painterPath): 264 | item.setSelected(False) 265 | 266 | 267 | # Toggle Selection 268 | elif self.currentState == 'TOGGLE_SELECTION': 269 | self.rubberband.setGeometry(QtCore.QRect(self.origin, 270 | event.pos()).normalized()) 271 | painterPath = self._releaseRubberband() 272 | self.setInteractive(True) 273 | for item in self.scene().items(painterPath): 274 | if item.isSelected(): 275 | item.setSelected(False) 276 | else: 277 | item.setSelected(True) 278 | 279 | self.currentState = 'DEFAULT' 280 | 281 | super(Nodz, self).mouseReleaseEvent(event) 282 | 283 | def keyPressEvent(self, event): 284 | """ 285 | Save pressed key and apply shortcuts. 286 | 287 | Shortcuts are: 288 | DEL - Delete the selected nodes 289 | F - Focus view on the selection 290 | 291 | """ 292 | if event.key() not in self.pressedKeys: 293 | self.pressedKeys.append(event.key()) 294 | 295 | if event.key() in (QtCore.Qt.Key_Delete, QtCore.Qt.Key_Backspace): 296 | self._deleteSelectedNodes() 297 | 298 | if event.key() == QtCore.Qt.Key_F: 299 | self._focus() 300 | 301 | if event.key() == QtCore.Qt.Key_S: 302 | self._nodeSnap = True 303 | 304 | # Emit signal. 305 | self.signal_KeyPressed.emit(event.key()) 306 | 307 | def keyReleaseEvent(self, event): 308 | """ 309 | Clear the key from the pressed key list. 310 | 311 | """ 312 | if event.key() == QtCore.Qt.Key_S: 313 | self._nodeSnap = False 314 | 315 | if event.key() in self.pressedKeys: 316 | self.pressedKeys.remove(event.key()) 317 | 318 | def _initRubberband(self, position): 319 | """ 320 | Initialize the rubber band at the given position. 321 | 322 | """ 323 | self.rubberBandStart = position 324 | self.origin = position 325 | self.rubberband.setGeometry(QtCore.QRect(self.origin, QtCore.QSize())) 326 | self.rubberband.show() 327 | 328 | def _releaseRubberband(self): 329 | """ 330 | Hide the rubber band and return the path. 331 | 332 | """ 333 | painterPath = QtGui.QPainterPath() 334 | rect = self.mapToScene(self.rubberband.geometry()) 335 | painterPath.addPolygon(rect) 336 | self.rubberband.hide() 337 | return painterPath 338 | 339 | def _focus(self): 340 | """ 341 | Center on selected nodes or all of them if no active selection. 342 | 343 | """ 344 | if self.scene().selectedItems(): 345 | itemsArea = self._getSelectionBoundingbox() 346 | self.fitInView(itemsArea, QtCore.Qt.KeepAspectRatio) 347 | else: 348 | itemsArea = self.scene().itemsBoundingRect() 349 | self.fitInView(itemsArea, QtCore.Qt.KeepAspectRatio) 350 | 351 | def _getSelectionBoundingbox(self): 352 | """ 353 | Return the bounding box of the selection. 354 | 355 | """ 356 | bbx_min = None 357 | bbx_max = None 358 | bby_min = None 359 | bby_max = None 360 | bbw = 0 361 | bbh = 0 362 | for item in self.scene().selectedItems(): 363 | pos = item.scenePos() 364 | x = pos.x() 365 | y = pos.y() 366 | w = x + item.boundingRect().width() 367 | h = y + item.boundingRect().height() 368 | 369 | # bbx min 370 | if bbx_min is None: 371 | bbx_min = x 372 | elif x < bbx_min: 373 | bbx_min = x 374 | # end if 375 | 376 | # bbx max 377 | if bbx_max is None: 378 | bbx_max = w 379 | elif w > bbx_max: 380 | bbx_max = w 381 | # end if 382 | 383 | # bby min 384 | if bby_min is None: 385 | bby_min = y 386 | elif y < bby_min: 387 | bby_min = y 388 | # end if 389 | 390 | # bby max 391 | if bby_max is None: 392 | bby_max = h 393 | elif h > bby_max: 394 | bby_max = h 395 | # end if 396 | # end if 397 | bbw = bbx_max - bbx_min 398 | bbh = bby_max - bby_min 399 | return QtCore.QRectF(QtCore.QRect(bbx_min, bby_min, bbw, bbh)) 400 | 401 | def _deleteSelectedNodes(self): 402 | """ 403 | Delete selected nodes. 404 | 405 | """ 406 | selected_nodes = list() 407 | for node in self.scene().selectedItems(): 408 | selected_nodes.append(node.name) 409 | node._remove() 410 | 411 | # Emit signal. 412 | self.signal_NodeDeleted.emit(selected_nodes) 413 | 414 | def _returnSelection(self): 415 | """ 416 | Wrapper to return selected items. 417 | 418 | """ 419 | selected_nodes = list() 420 | if self.scene().selectedItems(): 421 | for node in self.scene().selectedItems(): 422 | selected_nodes.append(node.name) 423 | 424 | # Emit signal. 425 | self.signal_NodeSelected.emit(selected_nodes) 426 | 427 | 428 | ################################################################## 429 | # API 430 | ################################################################## 431 | 432 | def loadConfig(self, filePath): 433 | """ 434 | Set a specific configuration for this instance of Nodz. 435 | 436 | :type filePath: str. 437 | :param filePath: The path to the config file that you want to 438 | use. 439 | 440 | """ 441 | self.config = utils._loadConfig(filePath) 442 | 443 | def initialize(self): 444 | """ 445 | Setup the view's behavior. 446 | 447 | """ 448 | # Setup view. 449 | config = self.config 450 | self.setRenderHint(QtGui.QPainter.Antialiasing, config['antialiasing']) 451 | self.setRenderHint(QtGui.QPainter.TextAntialiasing, config['antialiasing']) 452 | self.setRenderHint(QtGui.QPainter.HighQualityAntialiasing, config['antialiasing_boost']) 453 | self.setRenderHint(QtGui.QPainter.SmoothPixmapTransform, config['smooth_pixmap']) 454 | self.setRenderHint(QtGui.QPainter.NonCosmeticDefaultPen, True) 455 | self.setViewportUpdateMode(QtWidgets.QGraphicsView.FullViewportUpdate) 456 | self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) 457 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 458 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 459 | self.rubberband = QtWidgets.QRubberBand(QtWidgets.QRubberBand.Rectangle, self) 460 | 461 | # Setup scene. 462 | scene = NodeScene(self) 463 | sceneWidth = config['scene_width'] 464 | sceneHeight = config['scene_height'] 465 | scene.setSceneRect(0, 0, sceneWidth, sceneHeight) 466 | self.setScene(scene) 467 | # Connect scene node moved signal 468 | scene.signal_NodeMoved.connect(self.signal_NodeMoved) 469 | 470 | # Tablet zoom. 471 | self.previousMouseOffset = 0 472 | self.zoomDirection = 0 473 | self.zoomIncr = 0 474 | 475 | # Connect signals. 476 | self.scene().selectionChanged.connect(self._returnSelection) 477 | 478 | 479 | # NODES 480 | def createNode(self, name='default', preset='node_default', position=None, alternate=True): 481 | """ 482 | Create a new node with a given name, position and color. 483 | 484 | :type name: str. 485 | :param name: The name of the node. The name has to be unique 486 | as it is used as a key to store the node object. 487 | 488 | :type preset: str. 489 | :param preset: The name of graphical preset in the config file. 490 | 491 | :type position: QtCore.QPoint. 492 | :param position: The position of the node once created. If None, 493 | it will be created at the center of the scene. 494 | 495 | :type alternate: bool. 496 | :param alternate: The attribute color alternate state, if True, 497 | every 2 attribute the color will be slightly 498 | darker. 499 | 500 | :return : The created node 501 | 502 | """ 503 | # Check for name clashes 504 | if name in self.scene().nodes.keys(): 505 | print('A node with the same name already exists : {0}'.format(name)) 506 | print('Node creation aborted !') 507 | return 508 | else: 509 | nodeItem = NodeItem(name=name, alternate=alternate, preset=preset, 510 | config=self.config) 511 | 512 | # Store node in scene. 513 | self.scene().nodes[name] = nodeItem 514 | 515 | if not position: 516 | # Get the center of the view. 517 | position = self.mapToScene(self.viewport().rect().center()) 518 | 519 | # Set node position. 520 | self.scene().addItem(nodeItem) 521 | nodeItem.setPos(position - nodeItem.nodeCenter) 522 | 523 | # Emit signal. 524 | self.signal_NodeCreated.emit(name) 525 | 526 | return nodeItem 527 | 528 | def deleteNode(self, node): 529 | """ 530 | Delete the specified node from the view. 531 | 532 | :type node: class. 533 | :param node: The node instance that you want to delete. 534 | 535 | """ 536 | if not node in self.scene().nodes.values(): 537 | print('Node object does not exist !') 538 | print('Node deletion aborted !') 539 | return 540 | 541 | if node in self.scene().nodes.values(): 542 | nodeName = node.name 543 | node._remove() 544 | 545 | # Emit signal. 546 | self.signal_NodeDeleted.emit([nodeName]) 547 | 548 | def editNode(self, node, newName=None): 549 | """ 550 | Rename an existing node. 551 | 552 | :type node: class. 553 | :param node: The node instance that you want to delete. 554 | 555 | :type newName: str. 556 | :param newName: The new name for the given node. 557 | 558 | """ 559 | if not node in self.scene().nodes.values(): 560 | print('Node object does not exist !') 561 | print('Node edition aborted !') 562 | return 563 | 564 | oldName = node.name 565 | 566 | if newName != None: 567 | # Check for name clashes 568 | if newName in self.scene().nodes.keys(): 569 | print('A node with the same name already exists : {0}'.format(newName)) 570 | print('Node edition aborted !') 571 | return 572 | else: 573 | node.name = newName 574 | 575 | # Replace node data. 576 | self.scene().nodes[newName] = self.scene().nodes[oldName] 577 | self.scene().nodes.pop(oldName) 578 | 579 | # Store new node name in the connections 580 | if node.sockets: 581 | for socket in node.sockets.values(): 582 | for connection in socket.connections: 583 | connection.socketNode = newName 584 | 585 | if node.plugs: 586 | for plug in node.plugs.values(): 587 | for connection in plug.connections: 588 | connection.plugNode = newName 589 | 590 | node.update() 591 | 592 | # Emit signal. 593 | self.signal_NodeEdited.emit(oldName, newName) 594 | 595 | 596 | # ATTRS 597 | def createAttribute(self, node, name='default', index=-1, preset='attr_default', plug=True, socket=True, dataType=None, plugMaxConnections=-1, socketMaxConnections=1): 598 | """ 599 | Create a new attribute with a given name. 600 | 601 | :type node: class. 602 | :param node: The node instance that you want to delete. 603 | 604 | :type name: str. 605 | :param name: The name of the attribute. The name has to be 606 | unique as it is used as a key to store the node 607 | object. 608 | 609 | :type index: int. 610 | :param index: The index of the attribute in the node. 611 | 612 | :type preset: str. 613 | :param preset: The name of graphical preset in the config file. 614 | 615 | :type plug: bool. 616 | :param plug: Whether or not this attribute can emit connections. 617 | 618 | :type socket: bool. 619 | :param socket: Whether or not this attribute can receive 620 | connections. 621 | 622 | :type dataType: type. 623 | :param dataType: Type of the data represented by this attribute 624 | in order to highlight attributes of the same 625 | type while performing a connection. 626 | 627 | :type plugMaxConnections: int. 628 | :param plugMaxConnections: The maximum connections that the plug can have (-1 for infinite). 629 | 630 | :type socketMaxConnections: int. 631 | :param socketMaxConnections: The maximum connections that the socket can have (-1 for infinite). 632 | 633 | """ 634 | if not node in self.scene().nodes.values(): 635 | print('Node object does not exist !') 636 | print('Attribute creation aborted !') 637 | return 638 | 639 | if name in node.attrs: 640 | print('An attribute with the same name already exists : {0}'.format(name)) 641 | print('Attribute creation aborted !') 642 | return 643 | 644 | node._createAttribute(name=name, index=index, preset=preset, plug=plug, socket=socket, dataType=dataType, plugMaxConnections=plugMaxConnections, socketMaxConnections=socketMaxConnections) 645 | 646 | # Emit signal. 647 | self.signal_AttrCreated.emit(node.name, index) 648 | 649 | def deleteAttribute(self, node, index): 650 | """ 651 | Delete the specified attribute. 652 | 653 | :type node: class. 654 | :param node: The node instance that you want to delete. 655 | 656 | :type index: int. 657 | :param index: The index of the attribute in the node. 658 | 659 | """ 660 | if not node in self.scene().nodes.values(): 661 | print('Node object does not exist !') 662 | print('Attribute deletion aborted !') 663 | return 664 | 665 | node._deleteAttribute(index) 666 | 667 | # Emit signal. 668 | self.signal_AttrDeleted.emit(node.name, index) 669 | 670 | def editAttribute(self, node, index, newName=None, newIndex=None): 671 | """ 672 | Edit the specified attribute. 673 | 674 | :type node: class. 675 | :param node: The node instance that you want to delete. 676 | 677 | :type index: int. 678 | :param index: The index of the attribute in the node. 679 | 680 | :type newName: str. 681 | :param newName: The new name for the given attribute. 682 | 683 | :type newIndex: int. 684 | :param newIndex: The index for the given attribute. 685 | 686 | """ 687 | if not node in self.scene().nodes.values(): 688 | print('Node object does not exist !') 689 | print('Attribute creation aborted !') 690 | return 691 | 692 | if newName != None: 693 | if newName in node.attrs: 694 | print('An attribute with the same name already exists : {0}'.format(newName)) 695 | print('Attribute edition aborted !') 696 | return 697 | else: 698 | oldName = node.attrs[index] 699 | 700 | # Rename in the slot item(s). 701 | if node.attrsData[oldName]['plug']: 702 | node.plugs[oldName].attribute = newName 703 | node.plugs[newName] = node.plugs[oldName] 704 | node.plugs.pop(oldName) 705 | for connection in node.plugs[newName].connections: 706 | connection.plugAttr = newName 707 | 708 | if node.attrsData[oldName]['socket']: 709 | node.sockets[oldName].attribute = newName 710 | node.sockets[newName] = node.sockets[oldName] 711 | node.sockets.pop(oldName) 712 | for connection in node.sockets[newName].connections: 713 | connection.socketAttr = newName 714 | 715 | # Replace attribute data. 716 | node.attrsData[oldName]['name'] = newName 717 | node.attrsData[newName] = node.attrsData[oldName] 718 | node.attrsData.pop(oldName) 719 | node.attrs[index] = newName 720 | 721 | if isinstance(newIndex, int): 722 | attrName = node.attrs[index] 723 | 724 | utils._swapListIndices(node.attrs, index, newIndex) 725 | 726 | # Refresh connections. 727 | for plug in node.plugs.values(): 728 | plug.update() 729 | if plug.connections: 730 | for connection in plug.connections: 731 | if isinstance(connection.source, PlugItem): 732 | connection.source = plug 733 | connection.source_point = plug.center() 734 | else: 735 | connection.target = plug 736 | connection.target_point = plug.center() 737 | if newName: 738 | connection.plugAttr = newName 739 | connection.updatePath() 740 | 741 | for socket in node.sockets.values(): 742 | socket.update() 743 | if socket.connections: 744 | for connection in socket.connections: 745 | if isinstance(connection.source, SocketItem): 746 | connection.source = socket 747 | connection.source_point = socket.center() 748 | else: 749 | connection.target = socket 750 | connection.target_point = socket.center() 751 | if newName: 752 | connection.socketAttr = newName 753 | connection.updatePath() 754 | 755 | self.scene().update() 756 | 757 | node.update() 758 | 759 | # Emit signal. 760 | if newIndex: 761 | self.signal_AttrEdited.emit(node.name, index, newIndex) 762 | else: 763 | self.signal_AttrEdited.emit(node.name, index, index) 764 | 765 | 766 | # GRAPH 767 | def saveGraph(self, filePath='path'): 768 | """ 769 | Get all the current graph infos and store them in a .json file 770 | at the given location. 771 | 772 | :type filePath: str. 773 | :param filePath: The path where you want to save your graph at. 774 | 775 | """ 776 | data = dict() 777 | 778 | # Store nodes data. 779 | data['NODES'] = dict() 780 | 781 | nodes = self.scene().nodes.keys() 782 | for node in nodes: 783 | nodeInst = self.scene().nodes[node] 784 | preset = nodeInst.nodePreset 785 | nodeAlternate = nodeInst.alternate 786 | 787 | data['NODES'][node] = {'preset': preset, 788 | 'position': [nodeInst.pos().x(), nodeInst.pos().y()], 789 | 'alternate': nodeAlternate, 790 | 'attributes': []} 791 | 792 | attrs = nodeInst.attrs 793 | for attr in attrs: 794 | attrData = nodeInst.attrsData[attr] 795 | 796 | # serialize dataType if needed. 797 | if isinstance(attrData['dataType'], type): 798 | attrData['dataType'] = str(attrData['dataType']) 799 | 800 | data['NODES'][node]['attributes'].append(attrData) 801 | 802 | 803 | # Store connections data. 804 | data['CONNECTIONS'] = self.evaluateGraph() 805 | 806 | 807 | # Save data. 808 | try: 809 | utils._saveData(filePath=filePath, data=data) 810 | except: 811 | print('Invalid path : {0}'.format(filePath)) 812 | print('Save aborted !') 813 | return False 814 | 815 | # Emit signal. 816 | self.signal_GraphSaved.emit() 817 | 818 | def loadGraph(self, filePath='path'): 819 | """ 820 | Get all the stored info from the .json file at the given location 821 | and recreate the graph as saved. 822 | 823 | :type filePath: str. 824 | :param filePath: The path where you want to load your graph from. 825 | 826 | """ 827 | # Load data. 828 | if os.path.exists(filePath): 829 | data = utils._loadData(filePath=filePath) 830 | else: 831 | print('Invalid path : {0}'.format(filePath)) 832 | print('Load aborted !') 833 | return False 834 | 835 | # Apply nodes data. 836 | nodesData = data['NODES'] 837 | nodesName = nodesData.keys() 838 | 839 | for name in nodesName: 840 | preset = nodesData[name]['preset'] 841 | position = nodesData[name]['position'] 842 | position = QtCore.QPointF(position[0], position[1]) 843 | alternate = nodesData[name]['alternate'] 844 | 845 | node = self.createNode(name=name, 846 | preset=preset, 847 | position=position, 848 | alternate=alternate) 849 | 850 | # Apply attributes data. 851 | attrsData = nodesData[name]['attributes'] 852 | 853 | for attrData in attrsData: 854 | index = attrsData.index(attrData) 855 | name = attrData['name'] 856 | plug = attrData['plug'] 857 | socket = attrData['socket'] 858 | preset = attrData['preset'] 859 | dataType = attrData['dataType'] 860 | plugMaxConnections = attrData['plugMaxConnections'] 861 | socketMaxConnections = attrData['socketMaxConnections'] 862 | 863 | # un-serialize data type if needed 864 | if (isinstance(dataType, str) and dataType.find('<') == 0): 865 | dataType = eval(str(dataType.split('\'')[1])) 866 | 867 | self.createAttribute(node=node, 868 | name=name, 869 | index=index, 870 | preset=preset, 871 | plug=plug, 872 | socket=socket, 873 | dataType=dataType, 874 | plugMaxConnections=plugMaxConnections, 875 | socketMaxConnections=socketMaxConnections 876 | ) 877 | 878 | # Apply connections data. 879 | connectionsData = data['CONNECTIONS'] 880 | 881 | for connection in connectionsData: 882 | source = connection[0] 883 | sourceNode = source.split('.')[0] 884 | sourceAttr = source.split('.')[1] 885 | 886 | target = connection[1] 887 | targetNode = target.split('.')[0] 888 | targetAttr = target.split('.')[1] 889 | 890 | self.createConnection(sourceNode, sourceAttr, 891 | targetNode, targetAttr) 892 | 893 | self.scene().update() 894 | 895 | # Emit signal. 896 | self.signal_GraphLoaded.emit() 897 | 898 | def createConnection(self, sourceNode, sourceAttr, targetNode, targetAttr): 899 | """ 900 | Create a manual connection. 901 | 902 | :type sourceNode: str. 903 | :param sourceNode: Node that emits the connection. 904 | 905 | :type sourceAttr: str. 906 | :param sourceAttr: Attribute that emits the connection. 907 | 908 | :type targetNode: str. 909 | :param targetNode: Node that receives the connection. 910 | 911 | :type targetAttr: str. 912 | :param targetAttr: Attribute that receives the connection. 913 | 914 | """ 915 | plug = self.scene().nodes[sourceNode].plugs[sourceAttr] 916 | socket = self.scene().nodes[targetNode].sockets[targetAttr] 917 | 918 | connection = ConnectionItem(plug.center(), socket.center(), plug, socket) 919 | 920 | connection.plugNode = plug.parentItem().name 921 | connection.plugAttr = plug.attribute 922 | connection.socketNode = socket.parentItem().name 923 | connection.socketAttr = socket.attribute 924 | 925 | plug.connect(socket, connection) 926 | socket.connect(plug, connection) 927 | 928 | connection.updatePath() 929 | 930 | self.scene().addItem(connection) 931 | 932 | return connection 933 | 934 | def evaluateGraph(self): 935 | """ 936 | Create a list of connection tuples. 937 | [("sourceNode.attribute", "TargetNode.attribute"), ...] 938 | 939 | """ 940 | scene = self.scene() 941 | 942 | data = list() 943 | 944 | for item in scene.items(): 945 | if isinstance(item, ConnectionItem): 946 | connection = item 947 | 948 | data.append(connection._outputConnectionData()) 949 | 950 | # Emit Signal 951 | self.signal_GraphEvaluated.emit() 952 | 953 | return data 954 | 955 | def clearGraph(self): 956 | """ 957 | Clear the graph. 958 | 959 | """ 960 | self.scene().clear() 961 | self.scene().nodes = dict() 962 | 963 | # Emit signal. 964 | self.signal_GraphCleared.emit() 965 | 966 | ################################################################## 967 | # END API 968 | ################################################################## 969 | 970 | 971 | class NodeScene(QtWidgets.QGraphicsScene): 972 | 973 | """ 974 | The scene displaying all the nodes. 975 | 976 | """ 977 | signal_NodeMoved = QtCore.Signal(str, object) 978 | 979 | def __init__(self, parent): 980 | """ 981 | Initialize the class. 982 | 983 | """ 984 | super(NodeScene, self).__init__(parent) 985 | 986 | # General. 987 | self.gridSize = parent.config['grid_size'] 988 | 989 | # Nodes storage. 990 | self.nodes = dict() 991 | 992 | def dragEnterEvent(self, event): 993 | """ 994 | Make the dragging of nodes into the scene possible. 995 | 996 | """ 997 | event.setDropAction(QtCore.Qt.MoveAction) 998 | event.accept() 999 | 1000 | def dragMoveEvent(self, event): 1001 | """ 1002 | Make the dragging of nodes into the scene possible. 1003 | 1004 | """ 1005 | event.setDropAction(QtCore.Qt.MoveAction) 1006 | event.accept() 1007 | 1008 | def dropEvent(self, event): 1009 | """ 1010 | Create a node from the dropped item. 1011 | 1012 | """ 1013 | # Emit signal. 1014 | self.signal_Dropped.emit(event.scenePos()) 1015 | 1016 | event.accept() 1017 | 1018 | def drawBackground(self, painter, rect): 1019 | """ 1020 | Draw a grid in the background. 1021 | 1022 | """ 1023 | config = self.parent().config 1024 | 1025 | self._brush = QtGui.QBrush() 1026 | self._brush.setStyle(QtCore.Qt.SolidPattern) 1027 | self._brush.setColor(utils._convertDataToColor(config['bg_color'])) 1028 | 1029 | painter.fillRect(rect, self._brush) 1030 | 1031 | if self.views()[0].gridVisToggle: 1032 | leftLine = rect.left() - rect.left() % self.gridSize 1033 | topLine = rect.top() - rect.top() % self.gridSize 1034 | lines = list() 1035 | 1036 | i = int(leftLine) 1037 | while i < int(rect.right()): 1038 | lines.append(QtCore.QLineF(i, rect.top(), i, rect.bottom())) 1039 | i += self.gridSize 1040 | 1041 | u = int(topLine) 1042 | while u < int(rect.bottom()): 1043 | lines.append(QtCore.QLineF(rect.left(), u, rect.right(), u)) 1044 | u += self.gridSize 1045 | 1046 | self.pen = QtGui.QPen() 1047 | self.pen.setColor(utils._convertDataToColor(config['grid_color'])) 1048 | self.pen.setWidth(0) 1049 | painter.setPen(self.pen) 1050 | painter.drawLines(lines) 1051 | 1052 | def updateScene(self): 1053 | """ 1054 | Update the connections position. 1055 | 1056 | """ 1057 | for connection in [i for i in self.items() if isinstance(i, ConnectionItem)]: 1058 | connection.target_point = connection.target.center() 1059 | connection.source_point = connection.source.center() 1060 | connection.updatePath() 1061 | 1062 | 1063 | class NodeItem(QtWidgets.QGraphicsItem): 1064 | 1065 | """ 1066 | A graphic representation of a node containing attributes. 1067 | 1068 | """ 1069 | 1070 | def __init__(self, name, alternate, preset, config): 1071 | """ 1072 | Initialize the class. 1073 | 1074 | :type name: str. 1075 | :param name: The name of the node. The name has to be unique 1076 | as it is used as a key to store the node object. 1077 | 1078 | :type alternate: bool. 1079 | :param alternate: The attribute color alternate state, if True, 1080 | every 2 attribute the color will be slightly 1081 | darker. 1082 | 1083 | :type preset: str. 1084 | :param preset: The name of graphical preset in the config file. 1085 | 1086 | """ 1087 | super(NodeItem, self).__init__() 1088 | 1089 | self.setZValue(1) 1090 | 1091 | # Storage 1092 | self.name = name 1093 | self.alternate = alternate 1094 | self.nodePreset = preset 1095 | self.attrPreset = None 1096 | 1097 | # Attributes storage. 1098 | self.attrs = list() 1099 | self.attrsData = dict() 1100 | self.attrCount = 0 1101 | self.currentDataType = None 1102 | 1103 | self.plugs = dict() 1104 | self.sockets = dict() 1105 | 1106 | # Methods. 1107 | self._createStyle(config) 1108 | 1109 | @property 1110 | def height(self): 1111 | """ 1112 | Increment the final height of the node every time an attribute 1113 | is created. 1114 | 1115 | """ 1116 | if self.attrCount > 0: 1117 | return (self.baseHeight + 1118 | self.attrHeight * self.attrCount + 1119 | self.border + 1120 | 0.5 * self.radius) 1121 | else: 1122 | return self.baseHeight 1123 | 1124 | @property 1125 | def pen(self): 1126 | """ 1127 | Return the pen based on the selection state of the node. 1128 | 1129 | """ 1130 | if self.isSelected(): 1131 | return self._penSel 1132 | else: 1133 | return self._pen 1134 | 1135 | def _createStyle(self, config): 1136 | """ 1137 | Read the node style from the configuration file. 1138 | 1139 | """ 1140 | self.setAcceptHoverEvents(True) 1141 | self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable) 1142 | self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable) 1143 | 1144 | # Dimensions. 1145 | self.baseWidth = config['node_width'] 1146 | self.baseHeight = config['node_height'] 1147 | self.attrHeight = config['node_attr_height'] 1148 | self.border = config['node_border'] 1149 | self.radius = config['node_radius'] 1150 | 1151 | self.nodeCenter = QtCore.QPointF() 1152 | self.nodeCenter.setX(self.baseWidth / 2.0) 1153 | self.nodeCenter.setY(self.height / 2.0) 1154 | 1155 | self._brush = QtGui.QBrush() 1156 | self._brush.setStyle(QtCore.Qt.SolidPattern) 1157 | self._brush.setColor(utils._convertDataToColor(config[self.nodePreset]['bg'])) 1158 | 1159 | self._pen = QtGui.QPen() 1160 | self._pen.setStyle(QtCore.Qt.SolidLine) 1161 | self._pen.setWidth(self.border) 1162 | self._pen.setColor(utils._convertDataToColor(config[self.nodePreset]['border'])) 1163 | 1164 | self._penSel = QtGui.QPen() 1165 | self._penSel.setStyle(QtCore.Qt.SolidLine) 1166 | self._penSel.setWidth(self.border) 1167 | self._penSel.setColor(utils._convertDataToColor(config[self.nodePreset]['border_sel'])) 1168 | 1169 | self._textPen = QtGui.QPen() 1170 | self._textPen.setStyle(QtCore.Qt.SolidLine) 1171 | self._textPen.setColor(utils._convertDataToColor(config[self.nodePreset]['text'])) 1172 | 1173 | self._nodeTextFont = QtGui.QFont(config['node_font'], config['node_font_size'], QtGui.QFont.Bold) 1174 | self._attrTextFont = QtGui.QFont(config['attr_font'], config['attr_font_size'], QtGui.QFont.Normal) 1175 | 1176 | self._attrBrush = QtGui.QBrush() 1177 | self._attrBrush.setStyle(QtCore.Qt.SolidPattern) 1178 | 1179 | self._attrBrushAlt = QtGui.QBrush() 1180 | self._attrBrushAlt.setStyle(QtCore.Qt.SolidPattern) 1181 | 1182 | self._attrPen = QtGui.QPen() 1183 | self._attrPen.setStyle(QtCore.Qt.SolidLine) 1184 | 1185 | def _createAttribute(self, name, index, preset, plug, socket, dataType, plugMaxConnections, socketMaxConnections): 1186 | """ 1187 | Create an attribute by expanding the node, adding a label and 1188 | connection items. 1189 | 1190 | :type name: str. 1191 | :param name: The name of the attribute. The name has to be 1192 | unique as it is used as a key to store the node 1193 | object. 1194 | 1195 | :type index: int. 1196 | :param index: The index of the attribute in the node. 1197 | 1198 | :type preset: str. 1199 | :param preset: The name of graphical preset in the config file. 1200 | 1201 | :type plug: bool. 1202 | :param plug: Whether or not this attribute can emit connections. 1203 | 1204 | :type socket: bool. 1205 | :param socket: Whether or not this attribute can receive 1206 | connections. 1207 | 1208 | :type dataType: type. 1209 | :param dataType: Type of the data represented by this attribute 1210 | in order to highlight attributes of the same 1211 | type while performing a connection. 1212 | 1213 | """ 1214 | if name in self.attrs: 1215 | print('An attribute with the same name already exists on this node : {0}'.format(name)) 1216 | print('Attribute creation aborted !') 1217 | return 1218 | 1219 | self.attrPreset = preset 1220 | 1221 | # Create a plug connection item. 1222 | if plug: 1223 | plugInst = PlugItem(parent=self, 1224 | attribute=name, 1225 | index=self.attrCount, 1226 | preset=preset, 1227 | dataType=dataType, 1228 | maxConnections=plugMaxConnections) 1229 | 1230 | self.plugs[name] = plugInst 1231 | 1232 | # Create a socket connection item. 1233 | if socket: 1234 | socketInst = SocketItem(parent=self, 1235 | attribute=name, 1236 | index=self.attrCount, 1237 | preset=preset, 1238 | dataType=dataType, 1239 | maxConnections=socketMaxConnections) 1240 | 1241 | self.sockets[name] = socketInst 1242 | 1243 | self.attrCount += 1 1244 | 1245 | # Add the attribute based on its index. 1246 | if index == -1 or index > self.attrCount: 1247 | self.attrs.append(name) 1248 | else: 1249 | self.attrs.insert(index, name) 1250 | 1251 | # Store attr data. 1252 | self.attrsData[name] = {'name': name, 1253 | 'socket': socket, 1254 | 'plug': plug, 1255 | 'preset': preset, 1256 | 'dataType': dataType, 1257 | 'plugMaxConnections': plugMaxConnections, 1258 | 'socketMaxConnections': socketMaxConnections 1259 | } 1260 | 1261 | # Update node height. 1262 | self.update() 1263 | 1264 | def _deleteAttribute(self, index): 1265 | """ 1266 | Remove an attribute by reducing the node, removing the label 1267 | and the connection items. 1268 | 1269 | :type index: int. 1270 | :param index: The index of the attribute in the node. 1271 | 1272 | """ 1273 | name = self.attrs[index] 1274 | 1275 | # Remove socket and its connections. 1276 | if name in self.sockets.keys(): 1277 | for connection in self.sockets[name].connections: 1278 | connection._remove() 1279 | 1280 | self.scene().removeItem(self.sockets[name]) 1281 | self.sockets.pop(name) 1282 | 1283 | # Remove plug and its connections. 1284 | if name in self.plugs.keys(): 1285 | for connection in self.plugs[name].connections: 1286 | connection._remove() 1287 | 1288 | self.scene().removeItem(self.plugs[name]) 1289 | self.plugs.pop(name) 1290 | 1291 | # Reduce node height. 1292 | if self.attrCount > 0: 1293 | self.attrCount -= 1 1294 | 1295 | # Remove attribute from node. 1296 | if name in self.attrs: 1297 | self.attrs.remove(name) 1298 | 1299 | self.update() 1300 | 1301 | def _remove(self): 1302 | """ 1303 | Remove this node instance from the scene. 1304 | 1305 | Make sure that all the connections to this node are also removed 1306 | in the process 1307 | 1308 | """ 1309 | self.scene().nodes.pop(self.name) 1310 | 1311 | # Remove all sockets connections. 1312 | for socket in self.sockets.values(): 1313 | while len(socket.connections)>0: 1314 | socket.connections[0]._remove() 1315 | 1316 | # Remove all plugs connections. 1317 | for plug in self.plugs.values(): 1318 | while len(plug.connections)>0: 1319 | plug.connections[0]._remove() 1320 | 1321 | # Remove node. 1322 | scene = self.scene() 1323 | scene.removeItem(self) 1324 | scene.update() 1325 | 1326 | def boundingRect(self): 1327 | """ 1328 | The bounding rect based on the width and height variables. 1329 | 1330 | """ 1331 | rect = QtCore.QRect(0, 0, self.baseWidth, self.height) 1332 | rect = QtCore.QRectF(rect) 1333 | return rect 1334 | 1335 | def shape(self): 1336 | """ 1337 | The shape of the item. 1338 | 1339 | """ 1340 | path = QtGui.QPainterPath() 1341 | path.addRect(self.boundingRect()) 1342 | return path 1343 | 1344 | def paint(self, painter, option, widget): 1345 | """ 1346 | Paint the node and attributes. 1347 | 1348 | """ 1349 | # Node base. 1350 | painter.setBrush(self._brush) 1351 | painter.setPen(self.pen) 1352 | 1353 | painter.drawRoundedRect(0, 0, 1354 | self.baseWidth, 1355 | self.height, 1356 | self.radius, 1357 | self.radius) 1358 | 1359 | # Node label. 1360 | painter.setPen(self._textPen) 1361 | painter.setFont(self._nodeTextFont) 1362 | 1363 | metrics = QtGui.QFontMetrics(painter.font()) 1364 | text_width = metrics.boundingRect(self.name).width() + 14 1365 | text_height = metrics.boundingRect(self.name).height() + 14 1366 | margin = (text_width - self.baseWidth) * 0.5 1367 | textRect = QtCore.QRect(-margin, 1368 | -text_height, 1369 | text_width, 1370 | text_height) 1371 | 1372 | painter.drawText(textRect, 1373 | QtCore.Qt.AlignCenter, 1374 | self.name) 1375 | 1376 | 1377 | # Attributes. 1378 | offset = 0 1379 | for attr in self.attrs: 1380 | nodzInst = self.scene().views()[0] 1381 | config = nodzInst.config 1382 | 1383 | # Attribute rect. 1384 | rect = QtCore.QRect(self.border / 2, 1385 | self.baseHeight - self.radius + offset, 1386 | self.baseWidth - self.border, 1387 | self.attrHeight) 1388 | 1389 | 1390 | 1391 | attrData = self.attrsData[attr] 1392 | name = attr 1393 | 1394 | preset = attrData['preset'] 1395 | 1396 | 1397 | # Attribute base. 1398 | self._attrBrush.setColor(utils._convertDataToColor(config[preset]['bg'])) 1399 | if self.alternate: 1400 | self._attrBrushAlt.setColor(utils._convertDataToColor(config[preset]['bg'], True, config['alternate_value'])) 1401 | 1402 | self._attrPen.setColor(utils._convertDataToColor([0, 0, 0, 0])) 1403 | painter.setPen(self._attrPen) 1404 | painter.setBrush(self._attrBrush) 1405 | if (offset / self.attrHeight) % 2: 1406 | painter.setBrush(self._attrBrushAlt) 1407 | 1408 | painter.drawRect(rect) 1409 | 1410 | # Attribute label. 1411 | painter.setPen(utils._convertDataToColor(config[preset]['text'])) 1412 | painter.setFont(self._attrTextFont) 1413 | 1414 | # Search non-connectable attributes. 1415 | if nodzInst.drawingConnection: 1416 | if self == nodzInst.currentHoveredNode: 1417 | if (attrData['dataType'] != nodzInst.sourceSlot.dataType or 1418 | (nodzInst.sourceSlot.slotType == 'plug' and attrData['socket'] == False or 1419 | nodzInst.sourceSlot.slotType == 'socket' and attrData['plug'] == False)): 1420 | # Set non-connectable attributes color. 1421 | painter.setPen(utils._convertDataToColor(config['non_connectable_color'])) 1422 | 1423 | textRect = QtCore.QRect(rect.left() + self.radius, 1424 | rect.top(), 1425 | rect.width() - 2*self.radius, 1426 | rect.height()) 1427 | painter.drawText(textRect, QtCore.Qt.AlignVCenter, name) 1428 | 1429 | offset += self.attrHeight 1430 | 1431 | def mousePressEvent(self, event): 1432 | """ 1433 | Keep the selected node on top of the others. 1434 | 1435 | """ 1436 | nodes = self.scene().nodes 1437 | for node in nodes.values(): 1438 | node.setZValue(1) 1439 | 1440 | for item in self.scene().items(): 1441 | if isinstance(item, ConnectionItem): 1442 | item.setZValue(1) 1443 | 1444 | self.setZValue(2) 1445 | 1446 | super(NodeItem, self).mousePressEvent(event) 1447 | 1448 | def mouseDoubleClickEvent(self, event): 1449 | """ 1450 | Emit a signal. 1451 | 1452 | """ 1453 | super(NodeItem, self).mouseDoubleClickEvent(event) 1454 | self.scene().parent().signal_NodeDoubleClicked.emit(self.name) 1455 | 1456 | def mouseMoveEvent(self, event): 1457 | """ 1458 | . 1459 | 1460 | """ 1461 | if self.scene().views()[0].gridVisToggle: 1462 | if self.scene().views()[0].gridSnapToggle or self.scene().views()[0]._nodeSnap: 1463 | gridSize = self.scene().gridSize 1464 | 1465 | currentPos = self.mapToScene(event.pos().x() - self.baseWidth / 2, 1466 | event.pos().y() - self.height / 2) 1467 | 1468 | snap_x = (round(currentPos.x() / gridSize) * gridSize) - gridSize/4 1469 | snap_y = (round(currentPos.y() / gridSize) * gridSize) - gridSize/4 1470 | snap_pos = QtCore.QPointF(snap_x, snap_y) 1471 | self.setPos(snap_pos) 1472 | 1473 | self.scene().updateScene() 1474 | else: 1475 | self.scene().updateScene() 1476 | super(NodeItem, self).mouseMoveEvent(event) 1477 | 1478 | def mouseReleaseEvent(self, event): 1479 | """ 1480 | . 1481 | 1482 | """ 1483 | # Emit node moved signal. 1484 | self.scene().signal_NodeMoved.emit(self.name, self.pos()) 1485 | super(NodeItem, self).mouseReleaseEvent(event) 1486 | 1487 | def hoverLeaveEvent(self, event): 1488 | """ 1489 | . 1490 | 1491 | """ 1492 | nodzInst = self.scene().views()[0] 1493 | 1494 | for item in nodzInst.scene().items(): 1495 | if isinstance(item, ConnectionItem): 1496 | item.setZValue(0) 1497 | 1498 | super(NodeItem, self).hoverLeaveEvent(event) 1499 | 1500 | 1501 | class SlotItem(QtWidgets.QGraphicsItem): 1502 | 1503 | """ 1504 | The base class for graphics item representing attributes hook. 1505 | 1506 | """ 1507 | 1508 | def __init__(self, parent, attribute, preset, index, dataType, maxConnections): 1509 | """ 1510 | Initialize the class. 1511 | 1512 | :param parent: The parent item of the slot. 1513 | :type parent: QtWidgets.QGraphicsItem instance. 1514 | 1515 | :param attribute: The attribute associated to the slot. 1516 | :type attribute: String. 1517 | 1518 | :param index: int. 1519 | :type index: The index of the attribute in the node. 1520 | 1521 | :type preset: str. 1522 | :param preset: The name of graphical preset in the config file. 1523 | 1524 | :param dataType: The data type associated to the attribute. 1525 | :type dataType: Type. 1526 | 1527 | """ 1528 | super(SlotItem, self).__init__(parent) 1529 | 1530 | # Status. 1531 | self.setAcceptHoverEvents(True) 1532 | 1533 | # Storage. 1534 | self.slotType = None 1535 | self.attribute = attribute 1536 | self.preset = preset 1537 | self.index = index 1538 | self.dataType = dataType 1539 | 1540 | # Style. 1541 | self.brush = QtGui.QBrush() 1542 | self.brush.setStyle(QtCore.Qt.SolidPattern) 1543 | 1544 | self.pen = QtGui.QPen() 1545 | self.pen.setStyle(QtCore.Qt.SolidLine) 1546 | 1547 | # Connections storage. 1548 | self.connected_slots = list() 1549 | self.newConnection = None 1550 | self.connections = list() 1551 | self.maxConnections = maxConnections 1552 | 1553 | def accepts(self, slot_item): 1554 | """ 1555 | Only accepts plug items that belong to other nodes, and only if the max connections count is not reached yet. 1556 | 1557 | """ 1558 | # no plug on plug or socket on socket 1559 | hasPlugItem = isinstance(self, PlugItem) or isinstance(slot_item, PlugItem) 1560 | hasSocketItem = isinstance(self, SocketItem) or isinstance(slot_item, SocketItem) 1561 | if not (hasPlugItem and hasSocketItem): 1562 | return False 1563 | 1564 | # no self connection 1565 | if self.parentItem() == slot_item.parentItem(): 1566 | return False 1567 | 1568 | #no more than maxConnections 1569 | if self.maxConnections>0 and len(self.connected_slots) >= self.maxConnections: 1570 | return False 1571 | 1572 | #no connection with different types 1573 | if slot_item.dataType != self.dataType: 1574 | return False 1575 | 1576 | #otherwize, all fine. 1577 | return True 1578 | 1579 | def mousePressEvent(self, event): 1580 | """ 1581 | Start the connection process. 1582 | 1583 | """ 1584 | if event.button() == QtCore.Qt.LeftButton: 1585 | self.newConnection = ConnectionItem(self.center(), 1586 | self.mapToScene(event.pos()), 1587 | self, 1588 | None) 1589 | 1590 | self.connections.append(self.newConnection) 1591 | self.scene().addItem(self.newConnection) 1592 | 1593 | nodzInst = self.scene().views()[0] 1594 | nodzInst.drawingConnection = True 1595 | nodzInst.sourceSlot = self 1596 | nodzInst.currentDataType = self.dataType 1597 | else: 1598 | super(SlotItem, self).mousePressEvent(event) 1599 | 1600 | def mouseMoveEvent(self, event): 1601 | """ 1602 | Update the new connection's end point position. 1603 | 1604 | """ 1605 | nodzInst = self.scene().views()[0] 1606 | config = nodzInst.config 1607 | if nodzInst.drawingConnection: 1608 | mbb = utils._createPointerBoundingBox(pointerPos=event.scenePos().toPoint(), 1609 | bbSize=config['mouse_bounding_box']) 1610 | 1611 | # Get nodes in pointer's bounding box. 1612 | targets = self.scene().items(mbb) 1613 | 1614 | if any(isinstance(target, NodeItem) for target in targets): 1615 | if self.parentItem() not in targets: 1616 | for target in targets: 1617 | if isinstance(target, NodeItem): 1618 | nodzInst.currentHoveredNode = target 1619 | else: 1620 | nodzInst.currentHoveredNode = None 1621 | 1622 | # Set connection's end point. 1623 | self.newConnection.target_point = self.mapToScene(event.pos()) 1624 | self.newConnection.updatePath() 1625 | else: 1626 | super(SlotItem, self).mouseMoveEvent(event) 1627 | 1628 | def mouseReleaseEvent(self, event): 1629 | """ 1630 | Apply the connection if target_slot is valid. 1631 | 1632 | """ 1633 | nodzInst = self.scene().views()[0] 1634 | if event.button() == QtCore.Qt.LeftButton: 1635 | nodzInst.drawingConnection = False 1636 | nodzInst.currentDataType = None 1637 | 1638 | target = self.scene().itemAt(event.scenePos().toPoint(), QtGui.QTransform()) 1639 | 1640 | if not isinstance(target, SlotItem): 1641 | self.newConnection._remove() 1642 | super(SlotItem, self).mouseReleaseEvent(event) 1643 | return 1644 | 1645 | if target.accepts(self): 1646 | self.newConnection.target = target 1647 | self.newConnection.source = self 1648 | self.newConnection.target_point = target.center() 1649 | self.newConnection.source_point = self.center() 1650 | 1651 | # Perform the ConnectionItem. 1652 | self.connect(target, self.newConnection) 1653 | target.connect(self, self.newConnection) 1654 | 1655 | self.newConnection.updatePath() 1656 | else: 1657 | self.newConnection._remove() 1658 | else: 1659 | super(SlotItem, self).mouseReleaseEvent(event) 1660 | 1661 | nodzInst.currentHoveredNode = None 1662 | 1663 | def shape(self): 1664 | """ 1665 | The shape of the Slot is a circle. 1666 | 1667 | """ 1668 | path = QtGui.QPainterPath() 1669 | path.addRect(self.boundingRect()) 1670 | return path 1671 | 1672 | def paint(self, painter, option, widget): 1673 | """ 1674 | Paint the Slot. 1675 | 1676 | """ 1677 | painter.setBrush(self.brush) 1678 | painter.setPen(self.pen) 1679 | 1680 | nodzInst = self.scene().views()[0] 1681 | config = nodzInst.config 1682 | if nodzInst.drawingConnection: 1683 | if self.parentItem() == nodzInst.currentHoveredNode: 1684 | painter.setBrush(utils._convertDataToColor(config['non_connectable_color'])) 1685 | if (self.slotType == nodzInst.sourceSlot.slotType or (self.slotType != nodzInst.sourceSlot.slotType and self.dataType != nodzInst.sourceSlot.dataType)): 1686 | painter.setBrush(utils._convertDataToColor(config['non_connectable_color'])) 1687 | else: 1688 | _penValid = QtGui.QPen() 1689 | _penValid.setStyle(QtCore.Qt.SolidLine) 1690 | _penValid.setWidth(2) 1691 | _penValid.setColor(QtGui.QColor(255, 255, 255, 255)) 1692 | painter.setPen(_penValid) 1693 | painter.setBrush(self.brush) 1694 | 1695 | painter.drawEllipse(self.boundingRect()) 1696 | 1697 | def center(self): 1698 | """ 1699 | Return The center of the Slot. 1700 | 1701 | """ 1702 | rect = self.boundingRect() 1703 | center = QtCore.QPointF(rect.x() + rect.width() * 0.5, 1704 | rect.y() + rect.height() * 0.5) 1705 | 1706 | return self.mapToScene(center) 1707 | 1708 | 1709 | class PlugItem(SlotItem): 1710 | 1711 | """ 1712 | A graphics item representing an attribute out hook. 1713 | 1714 | """ 1715 | 1716 | def __init__(self, parent, attribute, index, preset, dataType, maxConnections): 1717 | """ 1718 | Initialize the class. 1719 | 1720 | :param parent: The parent item of the slot. 1721 | :type parent: QtWidgets.QGraphicsItem instance. 1722 | 1723 | :param attribute: The attribute associated to the slot. 1724 | :type attribute: String. 1725 | 1726 | :param index: int. 1727 | :type index: The index of the attribute in the node. 1728 | 1729 | :type preset: str. 1730 | :param preset: The name of graphical preset in the config file. 1731 | 1732 | :param dataType: The data type associated to the attribute. 1733 | :type dataType: Type. 1734 | 1735 | """ 1736 | super(PlugItem, self).__init__(parent, attribute, preset, index, dataType, maxConnections) 1737 | 1738 | # Storage. 1739 | self.attributte = attribute 1740 | self.preset = preset 1741 | self.slotType = 'plug' 1742 | 1743 | # Methods. 1744 | self._createStyle(parent) 1745 | 1746 | def _createStyle(self, parent): 1747 | """ 1748 | Read the attribute style from the configuration file. 1749 | 1750 | """ 1751 | config = parent.scene().views()[0].config 1752 | self.brush = QtGui.QBrush() 1753 | self.brush.setStyle(QtCore.Qt.SolidPattern) 1754 | self.brush.setColor(utils._convertDataToColor(config[self.preset]['plug'])) 1755 | 1756 | def boundingRect(self): 1757 | """ 1758 | The bounding rect based on the width and height variables. 1759 | 1760 | """ 1761 | width = height = self.parentItem().attrHeight / 2.0 1762 | 1763 | nodzInst = self.scene().views()[0] 1764 | config = nodzInst.config 1765 | 1766 | x = self.parentItem().baseWidth - (width / 2.0) 1767 | y = (self.parentItem().baseHeight - config['node_radius'] + 1768 | self.parentItem().attrHeight / 4 + 1769 | self.parentItem().attrs.index(self.attribute) * self.parentItem().attrHeight) 1770 | 1771 | rect = QtCore.QRectF(QtCore.QRect(x, y, width, height)) 1772 | return rect 1773 | 1774 | def connect(self, socket_item, connection): 1775 | """ 1776 | Connect to the given socket_item. 1777 | 1778 | """ 1779 | if self.maxConnections>0 and len(self.connected_slots) >= self.maxConnections: 1780 | # Already connected. 1781 | self.connections[self.maxConnections-1]._remove() 1782 | 1783 | # Populate connection. 1784 | connection.socketItem = socket_item 1785 | connection.plugNode = self.parentItem().name 1786 | connection.plugAttr = self.attribute 1787 | 1788 | # Add socket to connected slots. 1789 | if socket_item in self.connected_slots: 1790 | self.connected_slots.remove(socket_item) 1791 | self.connected_slots.append(socket_item) 1792 | 1793 | # Add connection. 1794 | if connection not in self.connections: 1795 | self.connections.append(connection) 1796 | 1797 | # Emit signal. 1798 | nodzInst = self.scene().views()[0] 1799 | nodzInst.signal_PlugConnected.emit(connection.plugNode, connection.plugAttr, connection.socketNode, connection.socketAttr) 1800 | 1801 | def disconnect(self, connection): 1802 | """ 1803 | Disconnect the given connection from this plug item. 1804 | 1805 | """ 1806 | # Emit signal. 1807 | nodzInst = self.scene().views()[0] 1808 | nodzInst.signal_PlugDisconnected.emit(connection.plugNode, connection.plugAttr, connection.socketNode, connection.socketAttr) 1809 | 1810 | # Remove connected socket from plug 1811 | if connection.socketItem in self.connected_slots: 1812 | self.connected_slots.remove(connection.socketItem) 1813 | # Remove connection 1814 | self.connections.remove(connection) 1815 | 1816 | 1817 | class SocketItem(SlotItem): 1818 | 1819 | """ 1820 | A graphics item representing an attribute in hook. 1821 | 1822 | """ 1823 | 1824 | def __init__(self, parent, attribute, index, preset, dataType, maxConnections): 1825 | """ 1826 | Initialize the socket. 1827 | 1828 | :param parent: The parent item of the slot. 1829 | :type parent: QtWidgets.QGraphicsItem instance. 1830 | 1831 | :param attribute: The attribute associated to the slot. 1832 | :type attribute: String. 1833 | 1834 | :param index: int. 1835 | :type index: The index of the attribute in the node. 1836 | 1837 | :type preset: str. 1838 | :param preset: The name of graphical preset in the config file. 1839 | 1840 | :param dataType: The data type associated to the attribute. 1841 | :type dataType: Type. 1842 | 1843 | """ 1844 | super(SocketItem, self).__init__(parent, attribute, preset, index, dataType, maxConnections) 1845 | 1846 | # Storage. 1847 | self.attributte = attribute 1848 | self.preset = preset 1849 | self.slotType = 'socket' 1850 | 1851 | # Methods. 1852 | self._createStyle(parent) 1853 | 1854 | def _createStyle(self, parent): 1855 | """ 1856 | Read the attribute style from the configuration file. 1857 | 1858 | """ 1859 | config = parent.scene().views()[0].config 1860 | self.brush = QtGui.QBrush() 1861 | self.brush.setStyle(QtCore.Qt.SolidPattern) 1862 | self.brush.setColor(utils._convertDataToColor(config[self.preset]['socket'])) 1863 | 1864 | def boundingRect(self): 1865 | """ 1866 | The bounding rect based on the width and height variables. 1867 | 1868 | """ 1869 | width = height = self.parentItem().attrHeight / 2.0 1870 | 1871 | nodzInst = self.scene().views()[0] 1872 | config = nodzInst.config 1873 | 1874 | x = - width / 2.0 1875 | y = (self.parentItem().baseHeight - config['node_radius'] + 1876 | (self.parentItem().attrHeight/4) + 1877 | self.parentItem().attrs.index(self.attribute) * self.parentItem().attrHeight ) 1878 | 1879 | rect = QtCore.QRectF(QtCore.QRect(x, y, width, height)) 1880 | return rect 1881 | 1882 | def connect(self, plug_item, connection): 1883 | """ 1884 | Connect to the given plug item. 1885 | 1886 | """ 1887 | if self.maxConnections>0 and len(self.connected_slots) >= self.maxConnections: 1888 | # Already connected. 1889 | self.connections[self.maxConnections-1]._remove() 1890 | 1891 | # Populate connection. 1892 | connection.plugItem = plug_item 1893 | connection.socketNode = self.parentItem().name 1894 | connection.socketAttr = self.attribute 1895 | 1896 | # Add plug to connected slots. 1897 | self.connected_slots.append(plug_item) 1898 | 1899 | # Add connection. 1900 | if connection not in self.connections: 1901 | self.connections.append(connection) 1902 | 1903 | # Emit signal. 1904 | nodzInst = self.scene().views()[0] 1905 | nodzInst.signal_SocketConnected.emit(connection.plugNode, connection.plugAttr, connection.socketNode, connection.socketAttr) 1906 | 1907 | def disconnect(self, connection): 1908 | """ 1909 | Disconnect the given connection from this socket item. 1910 | 1911 | """ 1912 | # Emit signal. 1913 | nodzInst = self.scene().views()[0] 1914 | nodzInst.signal_SocketDisconnected.emit(connection.plugNode, connection.plugAttr, connection.socketNode, connection.socketAttr) 1915 | 1916 | # Remove connected plugs 1917 | if connection.plugItem in self.connected_slots: 1918 | self.connected_slots.remove(connection.plugItem) 1919 | # Remove connections 1920 | self.connections.remove(connection) 1921 | 1922 | 1923 | class ConnectionItem(QtWidgets.QGraphicsPathItem): 1924 | 1925 | """ 1926 | A graphics path representing a connection between two attributes. 1927 | 1928 | """ 1929 | 1930 | def __init__(self, source_point, target_point, source, target): 1931 | """ 1932 | Initialize the class. 1933 | 1934 | :param sourcePoint: Source position of the connection. 1935 | :type sourcePoint: QPoint. 1936 | 1937 | :param targetPoint: Target position of the connection 1938 | :type targetPoint: QPoint. 1939 | 1940 | :param source: Source item (plug or socket). 1941 | :type source: class. 1942 | 1943 | :param target: Target item (plug or socket). 1944 | :type target: class. 1945 | 1946 | """ 1947 | super(ConnectionItem, self).__init__() 1948 | 1949 | self.setZValue(1) 1950 | 1951 | # Storage. 1952 | self.socketNode = None 1953 | self.socketAttr = None 1954 | self.plugNode = None 1955 | self.plugAttr = None 1956 | 1957 | self.source_point = source_point 1958 | self.target_point = target_point 1959 | self.source = source 1960 | self.target = target 1961 | 1962 | self.plugItem = None 1963 | self.socketItem = None 1964 | 1965 | self.movable_point = None 1966 | 1967 | self.data = tuple() 1968 | 1969 | # Methods. 1970 | self._createStyle() 1971 | 1972 | def _createStyle(self): 1973 | """ 1974 | Read the connection style from the configuration file. 1975 | 1976 | """ 1977 | config = self.source.scene().views()[0].config 1978 | self.setAcceptHoverEvents(True) 1979 | self.setZValue(-1) 1980 | 1981 | self._pen = QtGui.QPen(utils._convertDataToColor(config['connection_color'])) 1982 | self._pen.setWidth(config['connection_width']) 1983 | 1984 | def _outputConnectionData(self): 1985 | """ 1986 | . 1987 | 1988 | """ 1989 | return ("{0}.{1}".format(self.plugNode, self.plugAttr), 1990 | "{0}.{1}".format(self.socketNode, self.socketAttr)) 1991 | 1992 | def mousePressEvent(self, event): 1993 | """ 1994 | Snap the Connection to the mouse. 1995 | 1996 | """ 1997 | nodzInst = self.scene().views()[0] 1998 | 1999 | for item in nodzInst.scene().items(): 2000 | if isinstance(item, ConnectionItem): 2001 | item.setZValue(0) 2002 | 2003 | nodzInst.drawingConnection = True 2004 | 2005 | d_to_target = (event.pos() - self.target_point).manhattanLength() 2006 | d_to_source = (event.pos() - self.source_point).manhattanLength() 2007 | if d_to_target < d_to_source: 2008 | self.target_point = event.pos() 2009 | self.movable_point = 'target_point' 2010 | self.target.disconnect(self) 2011 | self.target = None 2012 | nodzInst.sourceSlot = self.source 2013 | else: 2014 | self.source_point = event.pos() 2015 | self.movable_point = 'source_point' 2016 | self.source.disconnect(self) 2017 | self.source = None 2018 | nodzInst.sourceSlot = self.target 2019 | 2020 | self.updatePath() 2021 | 2022 | def mouseMoveEvent(self, event): 2023 | """ 2024 | Move the Connection with the mouse. 2025 | 2026 | """ 2027 | nodzInst = self.scene().views()[0] 2028 | config = nodzInst.config 2029 | 2030 | mbb = utils._createPointerBoundingBox(pointerPos=event.scenePos().toPoint(), 2031 | bbSize=config['mouse_bounding_box']) 2032 | 2033 | # Get nodes in pointer's bounding box. 2034 | targets = self.scene().items(mbb) 2035 | 2036 | if any(isinstance(target, NodeItem) for target in targets): 2037 | 2038 | if nodzInst.sourceSlot.parentItem() not in targets: 2039 | for target in targets: 2040 | if isinstance(target, NodeItem): 2041 | nodzInst.currentHoveredNode = target 2042 | else: 2043 | nodzInst.currentHoveredNode = None 2044 | 2045 | if self.movable_point == 'target_point': 2046 | self.target_point = event.pos() 2047 | else: 2048 | self.source_point = event.pos() 2049 | 2050 | self.updatePath() 2051 | 2052 | def mouseReleaseEvent(self, event): 2053 | """ 2054 | Create a Connection if possible, otherwise delete it. 2055 | 2056 | """ 2057 | nodzInst = self.scene().views()[0] 2058 | nodzInst.drawingConnection = False 2059 | 2060 | slot = self.scene().itemAt(event.scenePos().toPoint(), QtGui.QTransform()) 2061 | 2062 | if not isinstance(slot, SlotItem): 2063 | self._remove() 2064 | self.updatePath() 2065 | super(ConnectionItem, self).mouseReleaseEvent(event) 2066 | return 2067 | 2068 | if self.movable_point == 'target_point': 2069 | if slot.accepts(self.source): 2070 | # Plug reconnection. 2071 | self.target = slot 2072 | self.target_point = slot.center() 2073 | plug = self.source 2074 | socket = self.target 2075 | 2076 | # Reconnect. 2077 | socket.connect(plug, self) 2078 | 2079 | self.updatePath() 2080 | else: 2081 | self._remove() 2082 | 2083 | else: 2084 | if slot.accepts(self.target): 2085 | # Socket Reconnection 2086 | self.source = slot 2087 | self.source_point = slot.center() 2088 | socket = self.target 2089 | plug = self.source 2090 | 2091 | # Reconnect. 2092 | plug.connect(socket, self) 2093 | 2094 | self.updatePath() 2095 | else: 2096 | self._remove() 2097 | 2098 | def _remove(self): 2099 | """ 2100 | Remove this Connection from the scene. 2101 | 2102 | """ 2103 | if self.source is not None: 2104 | self.source.disconnect(self) 2105 | if self.target is not None: 2106 | self.target.disconnect(self) 2107 | 2108 | scene = self.scene() 2109 | scene.removeItem(self) 2110 | scene.update() 2111 | 2112 | def updatePath(self): 2113 | """ 2114 | Update the path. 2115 | 2116 | """ 2117 | self.setPen(self._pen) 2118 | 2119 | path = QtGui.QPainterPath() 2120 | path.moveTo(self.source_point) 2121 | dx = (self.target_point.x() - self.source_point.x()) * 0.5 2122 | dy = self.target_point.y() - self.source_point.y() 2123 | ctrl1 = QtCore.QPointF(self.source_point.x() + dx, self.source_point.y() + dy * 0) 2124 | ctrl2 = QtCore.QPointF(self.source_point.x() + dx, self.source_point.y() + dy * 1) 2125 | path.cubicTo(ctrl1, ctrl2, self.target_point) 2126 | 2127 | self.setPath(path) 2128 | -------------------------------------------------------------------------------- /nodz_utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import re 4 | from Qt import QtCore, QtGui 5 | 6 | 7 | def _convertDataToColor(data=None, alternate=False, av=20): 8 | """ 9 | Convert a list of 3 (rgb) or 4(rgba) values from the configuration 10 | file into a QColor. 11 | 12 | :param data: Input color. 13 | :type data: List. 14 | 15 | :param alternate: Whether or not this is an alternate color. 16 | :type alternate: Bool. 17 | 18 | :param av: Alternate value. 19 | :type av: Int. 20 | 21 | """ 22 | # rgb 23 | if len(data) == 3: 24 | color = QtGui.QColor(data[0], data[1], data[2]) 25 | if alternate: 26 | mult = _generateAlternateColorMultiplier(color, av) 27 | 28 | 29 | color = QtGui.QColor(max(0, data[0]-(av*mult)), max(0, data[1]-(av*mult)), max(0, data[2]-(av*mult))) 30 | return color 31 | 32 | # rgba 33 | elif len(data) == 4: 34 | color = QtGui.QColor(data[0], data[1], data[2], data[3]) 35 | if alternate: 36 | mult = _generateAlternateColorMultiplier(color, av) 37 | color = QtGui.QColor(max(0, data[0]-(av*mult)), max(0, data[1]-(av*mult)), max(0, data[2]-(av*mult)), data[3]) 38 | return color 39 | 40 | # wrong 41 | else: 42 | print('Color from configuration is not recognized : ', data) 43 | print('Can only be [R, G, B] or [R, G, B, A]') 44 | print('Using default color !') 45 | color = QtGui.QColor(120, 120, 120) 46 | if alternate: 47 | color = QtGui.QColor(120-av, 120-av, 120-av) 48 | return color 49 | 50 | def _generateAlternateColorMultiplier(color, av): 51 | """ 52 | Generate a multiplier based on the input color lighness to increase 53 | the alternate value for dark color or reduce it for bright colors. 54 | 55 | :param color: Input color. 56 | :type color: QColor. 57 | 58 | :param av: Alternate value. 59 | :type av: Int. 60 | 61 | """ 62 | lightness = color.lightness() 63 | mult = float(lightness)/255 64 | 65 | return mult 66 | 67 | def _createPointerBoundingBox(pointerPos, bbSize): 68 | """ 69 | generate a bounding box around the pointer. 70 | 71 | :param pointerPos: Pointer position. 72 | :type pointerPos: QPoint. 73 | 74 | :param bbSize: Width and Height of the bounding box. 75 | :type bbSize: Int. 76 | 77 | """ 78 | # Create pointer's bounding box. 79 | point = pointerPos 80 | 81 | mbbPos = point 82 | point.setX(point.x() - bbSize / 2) 83 | point.setY(point.y() - bbSize / 2) 84 | 85 | size = QtCore.QSize(bbSize, bbSize) 86 | bb = QtCore.QRect(mbbPos, size) 87 | bb = QtCore.QRectF(bb) 88 | 89 | return bb 90 | 91 | def _swapListIndices(inputList, oldIndex, newIndex): 92 | """ 93 | Simply swap 2 indices in a the specified list. 94 | 95 | :param inputList: List that contains the elements to swap. 96 | :type inputList: List. 97 | 98 | :param oldIndex: Index of the element to move. 99 | :type oldIndex: Int. 100 | 101 | :param newIndex: Destination index of the element. 102 | :type newIndex: Int. 103 | 104 | """ 105 | if oldIndex == -1: 106 | oldIndex = len(inputList)-1 107 | 108 | 109 | if newIndex == -1: 110 | newIndex = len(inputList) 111 | 112 | value = inputList[oldIndex] 113 | inputList.pop(oldIndex) 114 | inputList.insert(newIndex, value) 115 | 116 | # IO 117 | def _loadConfig(filePath): 118 | """ 119 | Read the configuration file and strips out comments. 120 | 121 | :param filePath: File path. 122 | :type filePath: Str. 123 | 124 | """ 125 | with open(filePath, 'r') as myfile: 126 | fileString = myfile.read() 127 | 128 | # remove comments 129 | cleanString = re.sub('//.*?\n|/\*.*?\*/', '', fileString, re.S) 130 | 131 | data = json.loads(cleanString) 132 | 133 | return data 134 | 135 | def _saveData(filePath, data): 136 | """ 137 | save data as a .json file 138 | 139 | :param filePath: Path of the .json file. 140 | :type filePath: Str. 141 | 142 | :param data: Data you want to save. 143 | :type data: Dict or List. 144 | 145 | """ 146 | f = open(filePath, "w") 147 | f.write(json.dumps(data, 148 | sort_keys = True, 149 | indent = 4, 150 | ensure_ascii=False)) 151 | f.close() 152 | 153 | print("Data successfully saved !") 154 | 155 | def _loadData(filePath): 156 | """ 157 | load data from a .json file. 158 | 159 | :param filePath: Path of the .json file. 160 | :type filePath: Str. 161 | 162 | """ 163 | with open(filePath) as json_file: 164 | j_data = json.load(json_file) 165 | 166 | json_file.close() 167 | 168 | print("Data successfully loaded !") 169 | return j_data 170 | 171 | -------------------------------------------------------------------------------- /release_notes.txt: -------------------------------------------------------------------------------- 1 | RELEASE NOTES 2 | 3 | v1.3.0 - 02/07/2018: 4 | + Feature : Added a signal when a node is double-clicked 5 | 6 | v1.2.0 - 02/07/2018: 7 | + Feature : Now allowing to declare the maximum connections count of plugs and sockets 8 | 9 | v1.1.0 - 28/06/2018: 10 | Catchup, from now on, I will try to mark releases 11 | 12 | --------------------------------------------------------------------------------