├── .gitignore ├── LICENSE.txt ├── README.md ├── examples ├── addition.json ├── calculation.json ├── large.json ├── max_scenegraph.json ├── max_scenegraph.py ├── medium.json ├── pleaselayoutme.json └── startup.json ├── inspiration ├── 3dsmax_mcg.jpg ├── 3dsmax_particle_view.jpg ├── 3dsmax_slate.jpg ├── audulus.jpg ├── blender.png ├── blender_tex.jpg ├── bloodhound.png ├── coral.jpg ├── countdown.png ├── dreammaker.jpg ├── dynamo.png ├── flexmonkey.PNG ├── flowlab.jpg ├── flowlab_noflo.jpg ├── fusion.jpg ├── generative_components.png ├── houdini.jpg ├── l.jpg ├── lightwave.jpg ├── mamba_fx.png ├── maya_hypergraph.jpg ├── minko_shaderlab.png ├── modkit.png ├── modo.jpg ├── natron.jpg ├── neuromorphic_robotics.png ├── nodebox.png ├── noflo.jpg ├── nuke.png ├── num3sis.png ├── pipes.png ├── pypes.jpg ├── quadrigam.png ├── quartz_composer.jpg ├── reactable.png ├── scheme_bricks.png ├── shake.jpg ├── softimage_ice.jpg ├── sql_server_integration.png ├── streamtools.png ├── substance_designer.jpg ├── thinkbox_magma.png ├── touch_designer.jpg ├── udk.png ├── udk_kismet.jpg ├── unix_tools.png ├── vis_trails.png ├── visual_jforex.jpg ├── vrl_studio.png ├── vuo.png ├── vvvv.png ├── world_machine.jpg └── xpresso.jpg ├── qtnodes ├── __init__.py ├── __main__.py ├── edge.py ├── exceptions.py ├── header.py ├── helpers.py ├── knob.py ├── layout.py ├── node.py ├── serializer.py ├── view.py └── widget.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | venv 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Christoph Buelter 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qtnodes 2 | 3 | Node graph visualization and editing with PySide. 4 | 5 | Very **WIP** right now, the goal is to have a bunch of premade components that make it easy to implement a node graph to store and modify arbitrary data. 6 | 7 | The UI part is coming along nicely, but no actual data handling is attached to it yet. 8 | 9 | ## UI Example 10 | 11 | Although this graph makes no sense, it shows the current look and feel: 12 | 13 | ![](http://i.imgur.com/oBj0FBJ.png) 14 | 15 | ## Code Example 16 | 17 | ```python 18 | from PySide import QtGui 19 | from qtnodes import (Header, Node, InputKnob, 20 | OutputKnob, NodeGraphWidget) 21 | 22 | class Multiply(Node): 23 | 24 | def __init__(self, *args, **kwargs): 25 | super(Multiply, self).__init__(*args, **kwargs) 26 | self.addHeader(Header(node=self, text=self.__class__.__name__)) 27 | self.addKnob(InputKnob(name="x")) 28 | self.addKnob(InputKnob(name="y")) 29 | self.addKnob(OutputKnob(name="value")) 30 | 31 | app = QtGui.QApplication([]) 32 | graph = NodeGraphWidget() 33 | graph.registerNodeClass(Multiply) 34 | graph.addNode(Multiply()) 35 | graph.show() 36 | app.exec_() 37 | ``` 38 | 39 | ## Usage 40 | 41 | To start a small demo: 42 | 43 | $ python -m qtnodes 44 | 45 | You can load example **.json** files from `examples/`. 46 | 47 | ### Scene 48 | 49 | - **Pan the viewport**: Hold the middle mousebutton and drag. 50 | - **Zoom the viewport**: Use the mouse wheel. 51 | - **Save scene to file:** Rightclick > Scene > Save As... 52 | - **Load scene from file:** Rightclick > Scene > Open File... 53 | - **Merge scene from file:** Rightclick > Scene > Merge File... 54 | - **Clear complete scene:** Rightclick > Scene > Clear Scene 55 | - **Hold scene state:** Rightclick > Scene > Hold 56 | - **Fetch scene state:** Rightclick > Scene > Fetch 57 | - **Autolayout scene:** Rightclick > Scene > Auto Layout 58 | 59 | Please note: The automatic layout needs [graphviz](http://www.graphviz.org), which means its `dot` command must be on **PATH**. 60 | 61 | ### Nodes 62 | 63 | - **Select a node**: Leftclick its header. 64 | - **Select multiple nodes**: Leftclick and drag a rectangle over the nodes, release to select. 65 | - **Create a node**: Rightclick > Nodes, choose a node type. 66 | - **Move a node**: Leftclick and drag its header. 67 | - **Delete a node**: Select it then press `DELETE`. 68 | - **Create a connection**: Hover over a knob, then leftclick and drag to another knob. You can only connect inputs to outputs and vice versa. 69 | - **Remove a connection**: Hold `ALT` (on Windows, use `CTRL` on Linux/MacOs), the connections turn red, click one to remove it. 70 | 71 | ## Credits 72 | 73 | This started as a port of the original Qt/C++ tool `qnodeseditor` by Stanislaw Adaszewski, see: 74 | 75 | http://algoholic.eu/qnodeseditor-qt-nodesports-based-data-processing-flow-editor/ 76 | 77 | Additional sources and inspirations: 78 | 79 | - http://austinjbaker.com/node-editor-prototype 80 | - http://nukengine.com/blog/category/all/qt-node-editor/ 81 | - http://blog.interfacevision.com/design/design-visual-progarmming-languages-snapshots/ 82 | - https://github.com/Tillsten/qt-dataflow 83 | - https://gist.github.com/dbr/1255776 (Nuke node layout with graphviz) 84 | 85 | ## License 86 | 87 | **MIT**, see [LICENSE.txt](LICENSE.txt) 88 | -------------------------------------------------------------------------------- /examples/addition.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "y": 92.72727272727269, 5 | "x": -150.90909090909082, 6 | "class": "Integer", 7 | "uuid": "320a2a28-79d8-4741-aded-9a8dc1c1d07f" 8 | }, 9 | { 10 | "y": 151.93508436909605, 11 | "x": -147.99860687834052, 12 | "class": "Integer", 13 | "uuid": "1b5aa89f-a18b-4c63-8dbc-8963afc5eecb" 14 | }, 15 | { 16 | "y": 92.25091317770037, 17 | "x": -62.674929059738645, 18 | "class": "Multiply", 19 | "uuid": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb" 20 | }, 21 | { 22 | "y": 115.2879071056542, 23 | "x": 694.4407672103866, 24 | "class": "Output", 25 | "uuid": "b1873c3b-6f7f-48ee-bffb-1e81114a4b6b" 26 | }, 27 | { 28 | "y": 5.406559525751604, 29 | "x": 180.89600075865303, 30 | "class": "BigNode", 31 | "uuid": "818233c9-9d7f-4286-9638-6ef055659eef" 32 | }, 33 | { 34 | "y": -64.08829275424323, 35 | "x": 308.03001985254804, 36 | "class": "Add", 37 | "uuid": "59b20a84-e20d-41b4-909d-909a14cbe6c5" 38 | }, 39 | { 40 | "y": 16.631479243446833, 41 | "x": 306.90107199244056, 42 | "class": "Add", 43 | "uuid": "2a469c4a-0257-4612-9c00-08c1f40372f9" 44 | }, 45 | { 46 | "y": 16.067005313393096, 47 | "x": 393.2655832906682, 48 | "class": "Add", 49 | "uuid": "b3f47882-e194-42cd-a808-201185f29a28" 50 | }, 51 | { 52 | "y": 96.78677731108318, 53 | "x": 308.03001985254804, 54 | "class": "Add", 55 | "uuid": "1dfedc80-f793-40de-b179-1fe77133a76f" 56 | }, 57 | { 58 | "y": 108.64072984221256, 59 | "x": 394.39453115077566, 60 | "class": "Add", 61 | "uuid": "ff4655c2-e55f-4f57-8eae-73e6b25e9064" 62 | }, 63 | { 64 | "y": 177.50654930877346, 65 | "x": 307.4655459224943, 66 | "class": "Add", 67 | "uuid": "d307cee4-1010-4361-8e76-5c9d49cee6d3" 68 | }, 69 | { 70 | "y": 71.94992438871692, 71 | "x": 466.6471941976592, 72 | "class": "Add", 73 | "uuid": "381b5d78-f1bb-4dac-be44-64ea5d9c7070" 74 | }, 75 | { 76 | "y": 135.88990161783863, 77 | "x": 571.3837175236976, 78 | "class": "Add", 79 | "uuid": "c2337304-dc86-4941-980c-cf6ea0fad03d" 80 | } 81 | ], 82 | "edges": [ 83 | { 84 | "source_nodeId": "320a2a28-79d8-4741-aded-9a8dc1c1d07f", 85 | "source_name": "value", 86 | "target_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 87 | "target_name": "x" 88 | }, 89 | { 90 | "source_nodeId": "1b5aa89f-a18b-4c63-8dbc-8963afc5eecb", 91 | "source_name": "value", 92 | "target_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 93 | "target_name": "y" 94 | }, 95 | { 96 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 97 | "source_name": "value", 98 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 99 | "target_name": "i1" 100 | }, 101 | { 102 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 103 | "source_name": "value", 104 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 105 | "target_name": "i2" 106 | }, 107 | { 108 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 109 | "source_name": "value", 110 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 111 | "target_name": "i3" 112 | }, 113 | { 114 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 115 | "source_name": "value", 116 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 117 | "target_name": "i4" 118 | }, 119 | { 120 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 121 | "source_name": "value", 122 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 123 | "target_name": "i5" 124 | }, 125 | { 126 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 127 | "source_name": "value", 128 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 129 | "target_name": "i6" 130 | }, 131 | { 132 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 133 | "source_name": "value", 134 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 135 | "target_name": "i7" 136 | }, 137 | { 138 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 139 | "source_name": "value", 140 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 141 | "target_name": "i8" 142 | }, 143 | { 144 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 145 | "source_name": "value", 146 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 147 | "target_name": "i9" 148 | }, 149 | { 150 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 151 | "source_name": "o1", 152 | "target_nodeId": "59b20a84-e20d-41b4-909d-909a14cbe6c5", 153 | "target_name": "x" 154 | }, 155 | { 156 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 157 | "source_name": "o2", 158 | "target_nodeId": "59b20a84-e20d-41b4-909d-909a14cbe6c5", 159 | "target_name": "y" 160 | }, 161 | { 162 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 163 | "source_name": "o3", 164 | "target_nodeId": "2a469c4a-0257-4612-9c00-08c1f40372f9", 165 | "target_name": "x" 166 | }, 167 | { 168 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 169 | "source_name": "o4", 170 | "target_nodeId": "2a469c4a-0257-4612-9c00-08c1f40372f9", 171 | "target_name": "y" 172 | }, 173 | { 174 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 175 | "source_name": "o5", 176 | "target_nodeId": "1dfedc80-f793-40de-b179-1fe77133a76f", 177 | "target_name": "x" 178 | }, 179 | { 180 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 181 | "source_name": "o6", 182 | "target_nodeId": "1dfedc80-f793-40de-b179-1fe77133a76f", 183 | "target_name": "y" 184 | }, 185 | { 186 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 187 | "source_name": "o7", 188 | "target_nodeId": "d307cee4-1010-4361-8e76-5c9d49cee6d3", 189 | "target_name": "x" 190 | }, 191 | { 192 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 193 | "source_name": "o8", 194 | "target_nodeId": "d307cee4-1010-4361-8e76-5c9d49cee6d3", 195 | "target_name": "y" 196 | }, 197 | { 198 | "source_nodeId": "59b20a84-e20d-41b4-909d-909a14cbe6c5", 199 | "source_name": "value", 200 | "target_nodeId": "b3f47882-e194-42cd-a808-201185f29a28", 201 | "target_name": "x" 202 | }, 203 | { 204 | "source_nodeId": "2a469c4a-0257-4612-9c00-08c1f40372f9", 205 | "source_name": "value", 206 | "target_nodeId": "b3f47882-e194-42cd-a808-201185f29a28", 207 | "target_name": "y" 208 | }, 209 | { 210 | "source_nodeId": "1dfedc80-f793-40de-b179-1fe77133a76f", 211 | "source_name": "value", 212 | "target_nodeId": "ff4655c2-e55f-4f57-8eae-73e6b25e9064", 213 | "target_name": "x" 214 | }, 215 | { 216 | "source_nodeId": "d307cee4-1010-4361-8e76-5c9d49cee6d3", 217 | "source_name": "value", 218 | "target_nodeId": "ff4655c2-e55f-4f57-8eae-73e6b25e9064", 219 | "target_name": "y" 220 | }, 221 | { 222 | "source_nodeId": "b3f47882-e194-42cd-a808-201185f29a28", 223 | "source_name": "value", 224 | "target_nodeId": "381b5d78-f1bb-4dac-be44-64ea5d9c7070", 225 | "target_name": "x" 226 | }, 227 | { 228 | "source_nodeId": "ff4655c2-e55f-4f57-8eae-73e6b25e9064", 229 | "source_name": "value", 230 | "target_nodeId": "381b5d78-f1bb-4dac-be44-64ea5d9c7070", 231 | "target_name": "y" 232 | }, 233 | { 234 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 235 | "source_name": "o9", 236 | "target_nodeId": "c2337304-dc86-4941-980c-cf6ea0fad03d", 237 | "target_name": "y" 238 | }, 239 | { 240 | "source_nodeId": "c2337304-dc86-4941-980c-cf6ea0fad03d", 241 | "source_name": "value", 242 | "target_nodeId": "b1873c3b-6f7f-48ee-bffb-1e81114a4b6b", 243 | "target_name": "output" 244 | }, 245 | { 246 | "source_nodeId": "381b5d78-f1bb-4dac-be44-64ea5d9c7070", 247 | "source_name": "value", 248 | "target_nodeId": "c2337304-dc86-4941-980c-cf6ea0fad03d", 249 | "target_name": "x" 250 | } 251 | ] 252 | } 253 | -------------------------------------------------------------------------------- /examples/calculation.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "y": -196.0, 5 | "x": 12.0, 6 | "class": "Integer", 7 | "uuid": "6795f217-343e-40a0-be13-cce24a981365" 8 | }, 9 | { 10 | "y": -13.0, 11 | "x": 160.0, 12 | "class": "Float", 13 | "uuid": "25d026c9-d667-46c4-8d7d-9a0e16f090f7" 14 | }, 15 | { 16 | "y": -56.0, 17 | "x": 273.0, 18 | "class": "Multiply", 19 | "uuid": "acdf7cfa-b2d3-4750-8147-71f57d054416" 20 | }, 21 | { 22 | "y": 22.0, 23 | "x": 348.0, 24 | "class": "Divide", 25 | "uuid": "560a443d-def6-4a25-a661-737f1024b911" 26 | }, 27 | { 28 | "y": -188.0, 29 | "x": 111.0, 30 | "class": "Add", 31 | "uuid": "46889931-55c6-4929-ad59-f8d044805fb0" 32 | }, 33 | { 34 | "y": -103.0, 35 | "x": 182.0, 36 | "class": "Subtract", 37 | "uuid": "7ad90c86-0608-41e7-94c5-ed91c4ecfb4f" 38 | }, 39 | { 40 | "y": 82.0, 41 | "x": 445.0, 42 | "class": "Output", 43 | "uuid": "24ec24b9-6499-4b66-b042-46c571ad740e" 44 | }, 45 | { 46 | "y": -142.0, 47 | "x": 12.0, 48 | "class": "Integer", 49 | "uuid": "8cbea8db-2b20-44da-a8b7-9ba9acf53170" 50 | }, 51 | { 52 | "y": -84.0, 53 | "x": 90.0, 54 | "class": "Integer", 55 | "uuid": "274afe2a-73eb-4f29-8db0-b47d6e2bf0ee" 56 | }, 57 | { 58 | "y": 38.0, 59 | "x": 249.0, 60 | "class": "Float", 61 | "uuid": "24242bea-38db-407a-a8b9-9e6ace263eb4" 62 | } 63 | ], 64 | "edges": [ 65 | { 66 | "source_nodeId": "6795f217-343e-40a0-be13-cce24a981365", 67 | "source_name": "value", 68 | "target_nodeId": "46889931-55c6-4929-ad59-f8d044805fb0", 69 | "target_name": "x" 70 | }, 71 | { 72 | "source_nodeId": "8cbea8db-2b20-44da-a8b7-9ba9acf53170", 73 | "source_name": "value", 74 | "target_nodeId": "46889931-55c6-4929-ad59-f8d044805fb0", 75 | "target_name": "y" 76 | }, 77 | { 78 | "source_nodeId": "46889931-55c6-4929-ad59-f8d044805fb0", 79 | "source_name": "value", 80 | "target_nodeId": "7ad90c86-0608-41e7-94c5-ed91c4ecfb4f", 81 | "target_name": "x" 82 | }, 83 | { 84 | "source_nodeId": "274afe2a-73eb-4f29-8db0-b47d6e2bf0ee", 85 | "source_name": "value", 86 | "target_nodeId": "7ad90c86-0608-41e7-94c5-ed91c4ecfb4f", 87 | "target_name": "y" 88 | }, 89 | { 90 | "source_nodeId": "7ad90c86-0608-41e7-94c5-ed91c4ecfb4f", 91 | "source_name": "value", 92 | "target_nodeId": "acdf7cfa-b2d3-4750-8147-71f57d054416", 93 | "target_name": "x" 94 | }, 95 | { 96 | "source_nodeId": "25d026c9-d667-46c4-8d7d-9a0e16f090f7", 97 | "source_name": "value", 98 | "target_nodeId": "acdf7cfa-b2d3-4750-8147-71f57d054416", 99 | "target_name": "y" 100 | }, 101 | { 102 | "source_nodeId": "acdf7cfa-b2d3-4750-8147-71f57d054416", 103 | "source_name": "value", 104 | "target_nodeId": "560a443d-def6-4a25-a661-737f1024b911", 105 | "target_name": "x" 106 | }, 107 | { 108 | "source_nodeId": "24242bea-38db-407a-a8b9-9e6ace263eb4", 109 | "source_name": "value", 110 | "target_nodeId": "560a443d-def6-4a25-a661-737f1024b911", 111 | "target_name": "y" 112 | }, 113 | { 114 | "source_nodeId": "560a443d-def6-4a25-a661-737f1024b911", 115 | "source_name": "value", 116 | "target_nodeId": "24ec24b9-6499-4b66-b042-46c571ad740e", 117 | "target_name": "output" 118 | } 119 | ] 120 | } 121 | -------------------------------------------------------------------------------- /examples/max_scenegraph.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "y": 936.0, 5 | "x": -208.0, 6 | "class": "MaxObject", 7 | "uuid": "35ebc4c6-b115-4fcf-857d-55d17e781dfc" 8 | }, 9 | { 10 | "y": 858.0, 11 | "x": -208.0, 12 | "class": "MaxObject", 13 | "uuid": "115b7905-c4a1-4ddf-bd0b-2cb6d6c071cf" 14 | }, 15 | { 16 | "y": 858.0, 17 | "x": -624.0, 18 | "class": "MaxObject", 19 | "uuid": "0cfc101d-1680-40a7-8f75-b6979333a5fd" 20 | }, 21 | { 22 | "y": 780.0, 23 | "x": -416.0, 24 | "class": "MaxObject", 25 | "uuid": "19df4d6f-383f-4862-ab6b-406080346896" 26 | }, 27 | { 28 | "y": 312.0, 29 | "x": -208.0, 30 | "class": "MaxObject", 31 | "uuid": "4f84a96b-d2f3-43fc-b6ca-3dc5234a4222" 32 | }, 33 | { 34 | "y": 1716.0, 35 | "x": -416.0, 36 | "class": "MaxObject", 37 | "uuid": "58815273-77aa-4954-aed8-dce400528eef" 38 | }, 39 | { 40 | "y": 1638.0, 41 | "x": -416.0, 42 | "class": "MaxObject", 43 | "uuid": "b1f33d9d-5d57-453d-a511-eec19c3394ae" 44 | }, 45 | { 46 | "y": 1404.0, 47 | "x": -416.0, 48 | "class": "MaxObject", 49 | "uuid": "3808d99d-199f-411e-b8ce-9b5e1eca5ff7" 50 | }, 51 | { 52 | "y": 1014.0, 53 | "x": -416.0, 54 | "class": "MaxObject", 55 | "uuid": "a7d3b7c8-a97a-49b1-8eac-4bc2c67b1481" 56 | }, 57 | { 58 | "y": 624.0, 59 | "x": -208.0, 60 | "class": "MaxObject", 61 | "uuid": "94638aa8-6145-4bf9-9fc0-96122a15f12c" 62 | }, 63 | { 64 | "y": 390.0, 65 | "x": -208.0, 66 | "class": "MaxObject", 67 | "uuid": "9387420f-a1ff-4df4-b62b-bb1fc652db4a" 68 | }, 69 | { 70 | "y": 546.0, 71 | "x": -208.0, 72 | "class": "MaxObject", 73 | "uuid": "6e8ab4ba-b6b5-4f90-9330-f5cc86a5244b" 74 | }, 75 | { 76 | "y": 0.0, 77 | "x": -1456.0, 78 | "class": "MaxObject", 79 | "uuid": "09981dfd-7e5c-4bfd-b016-e190281d7cef" 80 | }, 81 | { 82 | "y": 624.0, 83 | "x": -1248.0, 84 | "class": "MaxObject", 85 | "uuid": "be338283-1500-4831-88ac-2bef42c7cb63" 86 | }, 87 | { 88 | "y": 780.0, 89 | "x": -1040.0, 90 | "class": "MaxObject", 91 | "uuid": "a8a4d5e1-5699-43b3-a6be-e4900d40a6f6" 92 | }, 93 | { 94 | "y": 936.0, 95 | "x": -832.0, 96 | "class": "MaxObject", 97 | "uuid": "aa1fe695-1492-4aac-877f-29918bcaccfd" 98 | }, 99 | { 100 | "y": 0.0, 101 | "x": -624.0, 102 | "class": "MaxObject", 103 | "uuid": "7440e5e0-4a4e-4a4d-8ee7-9cbbff729915" 104 | }, 105 | { 106 | "y": 78.0, 107 | "x": -416.0, 108 | "class": "MaxObject", 109 | "uuid": "7e341257-7295-4ba8-91d4-ebf8db75687b" 110 | }, 111 | { 112 | "y": 156.0, 113 | "x": -1456.0, 114 | "class": "MaxObject", 115 | "uuid": "dde05dbc-7682-4b8b-8c6d-22f105b44d7b" 116 | }, 117 | { 118 | "y": 0.0, 119 | "x": -1248.0, 120 | "class": "MaxObject", 121 | "uuid": "2f61ebfa-c463-478f-9377-b64a81251723" 122 | }, 123 | { 124 | "y": 858.0, 125 | "x": -1040.0, 126 | "class": "MaxObject", 127 | "uuid": "299a61f7-39fe-49c1-a231-0d82b1685db1" 128 | }, 129 | { 130 | "y": 780.0, 131 | "x": -832.0, 132 | "class": "MaxObject", 133 | "uuid": "eebe2e1b-4ed3-45ff-a165-d0cc64a1e486" 134 | }, 135 | { 136 | "y": 780.0, 137 | "x": -624.0, 138 | "class": "MaxObject", 139 | "uuid": "401031ff-28ff-46be-89d6-8686434e49f0" 140 | }, 141 | { 142 | "y": 702.0, 143 | "x": -416.0, 144 | "class": "MaxObject", 145 | "uuid": "5551e0aa-8437-4eeb-95ba-9e9ac49f9c4e" 146 | }, 147 | { 148 | "y": 0.0, 149 | "x": -1664.0, 150 | "class": "MaxObject", 151 | "uuid": "2c29633f-87b3-416b-8515-9eed5dfe42d9" 152 | }, 153 | { 154 | "y": 78.0, 155 | "x": -1456.0, 156 | "class": "MaxObject", 157 | "uuid": "7fbbad71-2c17-4f47-9855-a8b13fc47010" 158 | }, 159 | { 160 | "y": 78.0, 161 | "x": -1872.0, 162 | "class": "MaxObject", 163 | "uuid": "7815c295-749a-40c9-b9b1-fb600a817c92" 164 | }, 165 | { 166 | "y": 156.0, 167 | "x": -1664.0, 168 | "class": "MaxObject", 169 | "uuid": "04793d64-f99c-4a5b-94f9-d7d3b5572f0f" 170 | }, 171 | { 172 | "y": 390.0, 173 | "x": -1456.0, 174 | "class": "MaxObject", 175 | "uuid": "dd37d2cf-39b1-4078-9924-d58d427cd26d" 176 | }, 177 | { 178 | "y": 468.0, 179 | "x": -1248.0, 180 | "class": "MaxObject", 181 | "uuid": "263129d4-e4e7-4b0d-8869-a9cc25b9e916" 182 | }, 183 | { 184 | "y": 78.0, 185 | "x": -1664.0, 186 | "class": "MaxObject", 187 | "uuid": "95265615-571e-4fac-a519-be04f1f0e96a" 188 | }, 189 | { 190 | "y": 234.0, 191 | "x": -1456.0, 192 | "class": "MaxObject", 193 | "uuid": "76655a57-9b41-4c54-b809-be4c7974d71a" 194 | }, 195 | { 196 | "y": 0.0, 197 | "x": -1872.0, 198 | "class": "MaxObject", 199 | "uuid": "3b28990b-2836-4001-a9a1-8bdc39713071" 200 | }, 201 | { 202 | "y": 234.0, 203 | "x": -1664.0, 204 | "class": "MaxObject", 205 | "uuid": "86db2861-7437-468b-8adc-2fbfb9c42eaa" 206 | }, 207 | { 208 | "y": 468.0, 209 | "x": -1456.0, 210 | "class": "MaxObject", 211 | "uuid": "65b9ac25-ddb4-4983-a46d-ec64fd1cf733" 212 | }, 213 | { 214 | "y": 780.0, 215 | "x": -1248.0, 216 | "class": "MaxObject", 217 | "uuid": "0b946613-7388-44dc-a986-86d935a7ed86" 218 | }, 219 | { 220 | "y": 312.0, 221 | "x": -1456.0, 222 | "class": "MaxObject", 223 | "uuid": "0f098f76-c09c-41c1-b2bb-2e62f50cdcc7" 224 | }, 225 | { 226 | "y": 312.0, 227 | "x": -1248.0, 228 | "class": "MaxObject", 229 | "uuid": "857dfa79-0ebb-4a40-bce2-16c6b4beb3e7" 230 | }, 231 | { 232 | "y": 0.0, 233 | "x": -1040.0, 234 | "class": "MaxObject", 235 | "uuid": "0d4c91cc-5022-4a2e-aafe-7ee1d277e32a" 236 | }, 237 | { 238 | "y": 78.0, 239 | "x": -832.0, 240 | "class": "MaxObject", 241 | "uuid": "e7df8d93-8b3b-4704-8864-ef7659e9450a" 242 | }, 243 | { 244 | "y": 468.0, 245 | "x": -624.0, 246 | "class": "MaxObject", 247 | "uuid": "968a5c22-20fb-4065-aafd-cee08101a17d" 248 | }, 249 | { 250 | "y": 390.0, 251 | "x": -416.0, 252 | "class": "MaxObject", 253 | "uuid": "01efdfd2-70cb-4375-a588-9ca5843da1b2" 254 | }, 255 | { 256 | "y": 0.0, 257 | "x": -208.0, 258 | "class": "MaxObject", 259 | "uuid": "9b71482d-3c1d-44ad-b0f0-9878b459c0b5" 260 | }, 261 | { 262 | "y": 468.0, 263 | "x": -208.0, 264 | "class": "MaxObject", 265 | "uuid": "cb4b56b1-349e-4f0e-bccb-883521ad98ec" 266 | }, 267 | { 268 | "y": 390.0, 269 | "x": -624.0, 270 | "class": "MaxObject", 271 | "uuid": "3a717f8e-07cd-4990-8647-8c57689a776e" 272 | }, 273 | { 274 | "y": 156.0, 275 | "x": -416.0, 276 | "class": "MaxObject", 277 | "uuid": "d1ac4d12-6358-459c-896b-217e631c08ca" 278 | }, 279 | { 280 | "y": 1092.0, 281 | "x": -832.0, 282 | "class": "MaxObject", 283 | "uuid": "bf4278b1-38a0-48e6-97c5-a7ab22c0e36d" 284 | }, 285 | { 286 | "y": 1092.0, 287 | "x": -624.0, 288 | "class": "MaxObject", 289 | "uuid": "6f57edd8-dcc0-47da-acfe-8c0acb86e829" 290 | }, 291 | { 292 | "y": 858.0, 293 | "x": -416.0, 294 | "class": "MaxObject", 295 | "uuid": "0f6909bd-75e6-498d-bbe8-475c466f53d5" 296 | }, 297 | { 298 | "y": 78.0, 299 | "x": -208.0, 300 | "class": "MaxObject", 301 | "uuid": "c7844059-d883-4363-acb2-771d7a2535d4" 302 | }, 303 | { 304 | "y": 936.0, 305 | "x": -624.0, 306 | "class": "MaxObject", 307 | "uuid": "d4ca40c1-f7c6-402e-bdaf-a54a68063f31" 308 | }, 309 | { 310 | "y": 312.0, 311 | "x": -416.0, 312 | "class": "MaxObject", 313 | "uuid": "3e692fbf-6242-454e-a1fa-38bfa857041f" 314 | }, 315 | { 316 | "y": 234.0, 317 | "x": -832.0, 318 | "class": "MaxObject", 319 | "uuid": "cf3f08a0-fee5-426e-ad45-98296b923e35" 320 | }, 321 | { 322 | "y": 1248.0, 323 | "x": -624.0, 324 | "class": "MaxObject", 325 | "uuid": "45e3cb6b-291e-4589-97da-b5d08b1ede79" 326 | }, 327 | { 328 | "y": 1170.0, 329 | "x": -416.0, 330 | "class": "MaxObject", 331 | "uuid": "0960fd7d-faa8-4a5f-983a-b55344c09818" 332 | }, 333 | { 334 | "y": 702.0, 335 | "x": -208.0, 336 | "class": "MaxObject", 337 | "uuid": "8aee64e5-889b-40e3-9931-f0c75e0e7709" 338 | }, 339 | { 340 | "y": 1404.0, 341 | "x": -624.0, 342 | "class": "MaxObject", 343 | "uuid": "1e919ed0-6dd0-4170-9847-bfb5a33bc91a" 344 | }, 345 | { 346 | "y": 1326.0, 347 | "x": -416.0, 348 | "class": "MaxObject", 349 | "uuid": "dc0edfcc-832f-48d9-89f2-8d865a86f97b" 350 | }, 351 | { 352 | "y": 780.0, 353 | "x": -208.0, 354 | "class": "MaxObject", 355 | "uuid": "f5d52b53-d24f-416e-a8c7-f87d113f859e" 356 | }, 357 | { 358 | "y": 936.0, 359 | "x": -1040.0, 360 | "class": "MaxObject", 361 | "uuid": "1ad20e87-c5fe-4f1b-8b2b-5831b0628236" 362 | }, 363 | { 364 | "y": 312.0, 365 | "x": -832.0, 366 | "class": "MaxObject", 367 | "uuid": "441ab6ca-83d7-422a-9776-823b6f6c04f7" 368 | }, 369 | { 370 | "y": 234.0, 371 | "x": -624.0, 372 | "class": "MaxObject", 373 | "uuid": "7a798e7a-866d-49e7-8898-8d9d031d671d" 374 | }, 375 | { 376 | "y": 0.0, 377 | "x": -416.0, 378 | "class": "MaxObject", 379 | "uuid": "c21e1b25-d391-4977-bb50-80daea24dc0a" 380 | }, 381 | { 382 | "y": 702.0, 383 | "x": -1248.0, 384 | "class": "MaxObject", 385 | "uuid": "898870db-8f37-4f17-8cd3-a8d10d510364" 386 | }, 387 | { 388 | "y": 546.0, 389 | "x": -1040.0, 390 | "class": "MaxObject", 391 | "uuid": "ad13fdfc-2112-4b6d-885e-4e2e2cb5f8ee" 392 | }, 393 | { 394 | "y": 702.0, 395 | "x": -832.0, 396 | "class": "MaxObject", 397 | "uuid": "b320f3cc-8963-44b2-9f34-504b83a26240" 398 | }, 399 | { 400 | "y": 1326.0, 401 | "x": -624.0, 402 | "class": "MaxObject", 403 | "uuid": "83b09c04-0f1c-4759-bd30-165e297a64a3" 404 | }, 405 | { 406 | "y": 1248.0, 407 | "x": -416.0, 408 | "class": "MaxObject", 409 | "uuid": "78e0bba5-0086-4ead-b168-0de4041a4b19" 410 | }, 411 | { 412 | "y": 390.0, 413 | "x": -1248.0, 414 | "class": "MaxObject", 415 | "uuid": "adbe744d-7def-40a3-8587-3a643c78d2d6" 416 | }, 417 | { 418 | "y": 390.0, 419 | "x": -1040.0, 420 | "class": "MaxObject", 421 | "uuid": "556b94d6-6fae-408c-b100-b857b68d83cd" 422 | }, 423 | { 424 | "y": 1014.0, 425 | "x": -832.0, 426 | "class": "MaxObject", 427 | "uuid": "a19eb3ea-d0f2-4242-9eea-697ea4d431f2" 428 | }, 429 | { 430 | "y": 702.0, 431 | "x": -624.0, 432 | "class": "MaxObject", 433 | "uuid": "4db93f52-7303-48c0-9d21-dd0b646f804f" 434 | }, 435 | { 436 | "y": 1092.0, 437 | "x": -416.0, 438 | "class": "MaxObject", 439 | "uuid": "c2f5a964-b26c-4624-8601-72da6b6ac64d" 440 | }, 441 | { 442 | "y": 858.0, 443 | "x": -1248.0, 444 | "class": "MaxObject", 445 | "uuid": "681763b9-45c2-4851-80a3-66cc2c177897" 446 | }, 447 | { 448 | "y": 702.0, 449 | "x": -1040.0, 450 | "class": "MaxObject", 451 | "uuid": "8ed351cf-b383-45b9-a26e-9c4f4c9a3ef1" 452 | }, 453 | { 454 | "y": 546.0, 455 | "x": -832.0, 456 | "class": "MaxObject", 457 | "uuid": "8be7a3c3-5d43-4715-8fc1-56e449a23e2b" 458 | }, 459 | { 460 | "y": 546.0, 461 | "x": -624.0, 462 | "class": "MaxObject", 463 | "uuid": "06e88de8-bf99-4b17-9a11-7bd1dcf098ea" 464 | }, 465 | { 466 | "y": 624.0, 467 | "x": -416.0, 468 | "class": "MaxObject", 469 | "uuid": "15b2c350-a447-4a4e-906c-8e8d9a8932c2" 470 | }, 471 | { 472 | "y": 156.0, 473 | "x": -1248.0, 474 | "class": "MaxObject", 475 | "uuid": "dba09ea8-ccb2-4990-af64-af02776a5c2d" 476 | }, 477 | { 478 | "y": 156.0, 479 | "x": -1040.0, 480 | "class": "MaxObject", 481 | "uuid": "ceaab406-6076-4fd4-b57d-1c524a4460de" 482 | }, 483 | { 484 | "y": 390.0, 485 | "x": -832.0, 486 | "class": "MaxObject", 487 | "uuid": "374a869e-4a18-48c6-95f7-de2c27e0d838" 488 | }, 489 | { 490 | "y": 78.0, 491 | "x": -624.0, 492 | "class": "MaxObject", 493 | "uuid": "cc585654-e0e5-4f23-8347-0a5f332ce8a9" 494 | }, 495 | { 496 | "y": 936.0, 497 | "x": -416.0, 498 | "class": "MaxObject", 499 | "uuid": "e905f6cf-59a1-4440-bedf-e10780d4a1f7" 500 | }, 501 | { 502 | "y": 234.0, 503 | "x": -208.0, 504 | "class": "MaxObject", 505 | "uuid": "39f46ff0-98bf-4921-8f86-c63efccd73ae" 506 | }, 507 | { 508 | "y": 468.0, 509 | "x": -1040.0, 510 | "class": "MaxObject", 511 | "uuid": "3c7fc6bd-4598-4586-b6f9-c55f2b699858" 512 | }, 513 | { 514 | "y": 624.0, 515 | "x": -832.0, 516 | "class": "MaxObject", 517 | "uuid": "16989c94-9e55-4832-8a52-bcc8bd8daf9c" 518 | }, 519 | { 520 | "y": 1014.0, 521 | "x": -624.0, 522 | "class": "MaxObject", 523 | "uuid": "d8afe5ce-3f02-443b-b601-989a4e05adab" 524 | }, 525 | { 526 | "y": 1560.0, 527 | "x": -416.0, 528 | "class": "MaxObject", 529 | "uuid": "d0946b3c-3b29-4a79-8695-ea9bbf9c1def" 530 | }, 531 | { 532 | "y": 78.0, 533 | "x": -1248.0, 534 | "class": "MaxObject", 535 | "uuid": "25f7bf8e-7dbe-4bfa-9881-d121954efbec" 536 | }, 537 | { 538 | "y": 312.0, 539 | "x": -1040.0, 540 | "class": "MaxObject", 541 | "uuid": "568d80e5-d9a3-40ae-bf36-f10779519e65" 542 | }, 543 | { 544 | "y": 0.0, 545 | "x": -832.0, 546 | "class": "MaxObject", 547 | "uuid": "fcefcdf4-7463-4aa2-8e50-c61ec2f6801a" 548 | }, 549 | { 550 | "y": 156.0, 551 | "x": -624.0, 552 | "class": "MaxObject", 553 | "uuid": "d94c4dff-f74e-44d1-9ce4-80aba4eeb4e4" 554 | }, 555 | { 556 | "y": 468.0, 557 | "x": -416.0, 558 | "class": "MaxObject", 559 | "uuid": "c5079071-03a8-49e9-bca4-5f6f81d6e723" 560 | }, 561 | { 562 | "y": 936.0, 563 | "x": -1248.0, 564 | "class": "MaxObject", 565 | "uuid": "a77a26df-3770-4fc4-8eeb-eab5e129dd47" 566 | }, 567 | { 568 | "y": 78.0, 569 | "x": -1040.0, 570 | "class": "MaxObject", 571 | "uuid": "295a27f8-1369-4b2f-aec9-6d6d492117fc" 572 | }, 573 | { 574 | "y": 156.0, 575 | "x": -832.0, 576 | "class": "MaxObject", 577 | "uuid": "2220a87b-9aba-4160-bcb3-bdb206182a4f" 578 | }, 579 | { 580 | "y": 312.0, 581 | "x": -624.0, 582 | "class": "MaxObject", 583 | "uuid": "85dfb154-59d2-442b-8a56-d219c763e999" 584 | }, 585 | { 586 | "y": 234.0, 587 | "x": -416.0, 588 | "class": "MaxObject", 589 | "uuid": "7777251e-4c1b-4984-81d8-0e246ae42097" 590 | }, 591 | { 592 | "y": 546.0, 593 | "x": -1248.0, 594 | "class": "MaxObject", 595 | "uuid": "906ab2bf-e49f-47fa-8035-3816f6b9df12" 596 | }, 597 | { 598 | "y": 234.0, 599 | "x": -1040.0, 600 | "class": "MaxObject", 601 | "uuid": "4dc5ae4e-9843-4276-a15f-7d69707fd229" 602 | }, 603 | { 604 | "y": 858.0, 605 | "x": -832.0, 606 | "class": "MaxObject", 607 | "uuid": "a8f8f13c-5f94-4a66-b039-57587820681b" 608 | }, 609 | { 610 | "y": 1170.0, 611 | "x": -624.0, 612 | "class": "MaxObject", 613 | "uuid": "423628f3-88cb-43d7-82ef-60b9e9e56069" 614 | }, 615 | { 616 | "y": 546.0, 617 | "x": -416.0, 618 | "class": "MaxObject", 619 | "uuid": "67bc36d6-4548-4c66-ba79-1ffe00a12d1e" 620 | }, 621 | { 622 | "y": 234.0, 623 | "x": -1248.0, 624 | "class": "MaxObject", 625 | "uuid": "0fb8fa5e-051c-4d35-b1b9-2d4161782167" 626 | }, 627 | { 628 | "y": 624.0, 629 | "x": -1040.0, 630 | "class": "MaxObject", 631 | "uuid": "615fb4e1-2bc7-4e89-ac53-277a93b7d98d" 632 | }, 633 | { 634 | "y": 468.0, 635 | "x": -832.0, 636 | "class": "MaxObject", 637 | "uuid": "59299d4a-3b8a-4902-825b-668791fcc2e6" 638 | }, 639 | { 640 | "y": 624.0, 641 | "x": -624.0, 642 | "class": "MaxObject", 643 | "uuid": "9630690e-5edb-49bd-a0d4-9b3ddf490dd5" 644 | }, 645 | { 646 | "y": 1482.0, 647 | "x": -416.0, 648 | "class": "MaxObject", 649 | "uuid": "2aa1a6e9-2c84-4359-9056-4c9f70a85276" 650 | }, 651 | { 652 | "y": 156.0, 653 | "x": -208.0, 654 | "class": "MaxObject", 655 | "uuid": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d" 656 | }, 657 | { 658 | "y": 0.0, 659 | "x": 0.0, 660 | "class": "MaxObject", 661 | "uuid": "11f1f247-852d-4e08-9af9-8b9b3aed28fc" 662 | } 663 | ], 664 | "edges": [ 665 | { 666 | "source_nodeId": "35ebc4c6-b115-4fcf-857d-55d17e781dfc", 667 | "source_name": "parent", 668 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 669 | "target_name": "children" 670 | }, 671 | { 672 | "source_nodeId": "115b7905-c4a1-4ddf-bd0b-2cb6d6c071cf", 673 | "source_name": "parent", 674 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 675 | "target_name": "children" 676 | }, 677 | { 678 | "source_nodeId": "0cfc101d-1680-40a7-8f75-b6979333a5fd", 679 | "source_name": "parent", 680 | "target_nodeId": "19df4d6f-383f-4862-ab6b-406080346896", 681 | "target_name": "children" 682 | }, 683 | { 684 | "source_nodeId": "19df4d6f-383f-4862-ab6b-406080346896", 685 | "source_name": "parent", 686 | "target_nodeId": "4f84a96b-d2f3-43fc-b6ca-3dc5234a4222", 687 | "target_name": "children" 688 | }, 689 | { 690 | "source_nodeId": "4f84a96b-d2f3-43fc-b6ca-3dc5234a4222", 691 | "source_name": "parent", 692 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 693 | "target_name": "children" 694 | }, 695 | { 696 | "source_nodeId": "58815273-77aa-4954-aed8-dce400528eef", 697 | "source_name": "parent", 698 | "target_nodeId": "94638aa8-6145-4bf9-9fc0-96122a15f12c", 699 | "target_name": "children" 700 | }, 701 | { 702 | "source_nodeId": "b1f33d9d-5d57-453d-a511-eec19c3394ae", 703 | "source_name": "parent", 704 | "target_nodeId": "94638aa8-6145-4bf9-9fc0-96122a15f12c", 705 | "target_name": "children" 706 | }, 707 | { 708 | "source_nodeId": "3808d99d-199f-411e-b8ce-9b5e1eca5ff7", 709 | "source_name": "parent", 710 | "target_nodeId": "94638aa8-6145-4bf9-9fc0-96122a15f12c", 711 | "target_name": "children" 712 | }, 713 | { 714 | "source_nodeId": "a7d3b7c8-a97a-49b1-8eac-4bc2c67b1481", 715 | "source_name": "parent", 716 | "target_nodeId": "94638aa8-6145-4bf9-9fc0-96122a15f12c", 717 | "target_name": "children" 718 | }, 719 | { 720 | "source_nodeId": "94638aa8-6145-4bf9-9fc0-96122a15f12c", 721 | "source_name": "parent", 722 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 723 | "target_name": "children" 724 | }, 725 | { 726 | "source_nodeId": "9387420f-a1ff-4df4-b62b-bb1fc652db4a", 727 | "source_name": "parent", 728 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 729 | "target_name": "children" 730 | }, 731 | { 732 | "source_nodeId": "6e8ab4ba-b6b5-4f90-9330-f5cc86a5244b", 733 | "source_name": "parent", 734 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 735 | "target_name": "children" 736 | }, 737 | { 738 | "source_nodeId": "09981dfd-7e5c-4bfd-b016-e190281d7cef", 739 | "source_name": "parent", 740 | "target_nodeId": "be338283-1500-4831-88ac-2bef42c7cb63", 741 | "target_name": "children" 742 | }, 743 | { 744 | "source_nodeId": "be338283-1500-4831-88ac-2bef42c7cb63", 745 | "source_name": "parent", 746 | "target_nodeId": "a8a4d5e1-5699-43b3-a6be-e4900d40a6f6", 747 | "target_name": "children" 748 | }, 749 | { 750 | "source_nodeId": "a8a4d5e1-5699-43b3-a6be-e4900d40a6f6", 751 | "source_name": "parent", 752 | "target_nodeId": "aa1fe695-1492-4aac-877f-29918bcaccfd", 753 | "target_name": "children" 754 | }, 755 | { 756 | "source_nodeId": "aa1fe695-1492-4aac-877f-29918bcaccfd", 757 | "source_name": "parent", 758 | "target_nodeId": "7440e5e0-4a4e-4a4d-8ee7-9cbbff729915", 759 | "target_name": "children" 760 | }, 761 | { 762 | "source_nodeId": "7440e5e0-4a4e-4a4d-8ee7-9cbbff729915", 763 | "source_name": "parent", 764 | "target_nodeId": "7e341257-7295-4ba8-91d4-ebf8db75687b", 765 | "target_name": "children" 766 | }, 767 | { 768 | "source_nodeId": "7e341257-7295-4ba8-91d4-ebf8db75687b", 769 | "source_name": "parent", 770 | "target_nodeId": "9b71482d-3c1d-44ad-b0f0-9878b459c0b5", 771 | "target_name": "children" 772 | }, 773 | { 774 | "source_nodeId": "dde05dbc-7682-4b8b-8c6d-22f105b44d7b", 775 | "source_name": "parent", 776 | "target_nodeId": "2f61ebfa-c463-478f-9377-b64a81251723", 777 | "target_name": "children" 778 | }, 779 | { 780 | "source_nodeId": "2f61ebfa-c463-478f-9377-b64a81251723", 781 | "source_name": "parent", 782 | "target_nodeId": "299a61f7-39fe-49c1-a231-0d82b1685db1", 783 | "target_name": "children" 784 | }, 785 | { 786 | "source_nodeId": "299a61f7-39fe-49c1-a231-0d82b1685db1", 787 | "source_name": "parent", 788 | "target_nodeId": "eebe2e1b-4ed3-45ff-a165-d0cc64a1e486", 789 | "target_name": "children" 790 | }, 791 | { 792 | "source_nodeId": "eebe2e1b-4ed3-45ff-a165-d0cc64a1e486", 793 | "source_name": "parent", 794 | "target_nodeId": "401031ff-28ff-46be-89d6-8686434e49f0", 795 | "target_name": "children" 796 | }, 797 | { 798 | "source_nodeId": "401031ff-28ff-46be-89d6-8686434e49f0", 799 | "source_name": "parent", 800 | "target_nodeId": "5551e0aa-8437-4eeb-95ba-9e9ac49f9c4e", 801 | "target_name": "children" 802 | }, 803 | { 804 | "source_nodeId": "5551e0aa-8437-4eeb-95ba-9e9ac49f9c4e", 805 | "source_name": "parent", 806 | "target_nodeId": "9b71482d-3c1d-44ad-b0f0-9878b459c0b5", 807 | "target_name": "children" 808 | }, 809 | { 810 | "source_nodeId": "2c29633f-87b3-416b-8515-9eed5dfe42d9", 811 | "source_name": "parent", 812 | "target_nodeId": "7fbbad71-2c17-4f47-9855-a8b13fc47010", 813 | "target_name": "children" 814 | }, 815 | { 816 | "source_nodeId": "7fbbad71-2c17-4f47-9855-a8b13fc47010", 817 | "source_name": "parent", 818 | "target_nodeId": "263129d4-e4e7-4b0d-8869-a9cc25b9e916", 819 | "target_name": "children" 820 | }, 821 | { 822 | "source_nodeId": "7815c295-749a-40c9-b9b1-fb600a817c92", 823 | "source_name": "parent", 824 | "target_nodeId": "04793d64-f99c-4a5b-94f9-d7d3b5572f0f", 825 | "target_name": "children" 826 | }, 827 | { 828 | "source_nodeId": "04793d64-f99c-4a5b-94f9-d7d3b5572f0f", 829 | "source_name": "parent", 830 | "target_nodeId": "dd37d2cf-39b1-4078-9924-d58d427cd26d", 831 | "target_name": "children" 832 | }, 833 | { 834 | "source_nodeId": "dd37d2cf-39b1-4078-9924-d58d427cd26d", 835 | "source_name": "parent", 836 | "target_nodeId": "263129d4-e4e7-4b0d-8869-a9cc25b9e916", 837 | "target_name": "children" 838 | }, 839 | { 840 | "source_nodeId": "263129d4-e4e7-4b0d-8869-a9cc25b9e916", 841 | "source_name": "parent", 842 | "target_nodeId": "0d4c91cc-5022-4a2e-aafe-7ee1d277e32a", 843 | "target_name": "children" 844 | }, 845 | { 846 | "source_nodeId": "95265615-571e-4fac-a519-be04f1f0e96a", 847 | "source_name": "parent", 848 | "target_nodeId": "76655a57-9b41-4c54-b809-be4c7974d71a", 849 | "target_name": "children" 850 | }, 851 | { 852 | "source_nodeId": "76655a57-9b41-4c54-b809-be4c7974d71a", 853 | "source_name": "parent", 854 | "target_nodeId": "0b946613-7388-44dc-a986-86d935a7ed86", 855 | "target_name": "children" 856 | }, 857 | { 858 | "source_nodeId": "3b28990b-2836-4001-a9a1-8bdc39713071", 859 | "source_name": "parent", 860 | "target_nodeId": "86db2861-7437-468b-8adc-2fbfb9c42eaa", 861 | "target_name": "children" 862 | }, 863 | { 864 | "source_nodeId": "86db2861-7437-468b-8adc-2fbfb9c42eaa", 865 | "source_name": "parent", 866 | "target_nodeId": "65b9ac25-ddb4-4983-a46d-ec64fd1cf733", 867 | "target_name": "children" 868 | }, 869 | { 870 | "source_nodeId": "65b9ac25-ddb4-4983-a46d-ec64fd1cf733", 871 | "source_name": "parent", 872 | "target_nodeId": "0b946613-7388-44dc-a986-86d935a7ed86", 873 | "target_name": "children" 874 | }, 875 | { 876 | "source_nodeId": "0b946613-7388-44dc-a986-86d935a7ed86", 877 | "source_name": "parent", 878 | "target_nodeId": "0d4c91cc-5022-4a2e-aafe-7ee1d277e32a", 879 | "target_name": "children" 880 | }, 881 | { 882 | "source_nodeId": "0f098f76-c09c-41c1-b2bb-2e62f50cdcc7", 883 | "source_name": "parent", 884 | "target_nodeId": "857dfa79-0ebb-4a40-bce2-16c6b4beb3e7", 885 | "target_name": "children" 886 | }, 887 | { 888 | "source_nodeId": "857dfa79-0ebb-4a40-bce2-16c6b4beb3e7", 889 | "source_name": "parent", 890 | "target_nodeId": "0d4c91cc-5022-4a2e-aafe-7ee1d277e32a", 891 | "target_name": "children" 892 | }, 893 | { 894 | "source_nodeId": "0d4c91cc-5022-4a2e-aafe-7ee1d277e32a", 895 | "source_name": "parent", 896 | "target_nodeId": "e7df8d93-8b3b-4704-8864-ef7659e9450a", 897 | "target_name": "children" 898 | }, 899 | { 900 | "source_nodeId": "e7df8d93-8b3b-4704-8864-ef7659e9450a", 901 | "source_name": "parent", 902 | "target_nodeId": "968a5c22-20fb-4065-aafd-cee08101a17d", 903 | "target_name": "children" 904 | }, 905 | { 906 | "source_nodeId": "968a5c22-20fb-4065-aafd-cee08101a17d", 907 | "source_name": "parent", 908 | "target_nodeId": "01efdfd2-70cb-4375-a588-9ca5843da1b2", 909 | "target_name": "children" 910 | }, 911 | { 912 | "source_nodeId": "01efdfd2-70cb-4375-a588-9ca5843da1b2", 913 | "source_name": "parent", 914 | "target_nodeId": "9b71482d-3c1d-44ad-b0f0-9878b459c0b5", 915 | "target_name": "children" 916 | }, 917 | { 918 | "source_nodeId": "9b71482d-3c1d-44ad-b0f0-9878b459c0b5", 919 | "source_name": "parent", 920 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 921 | "target_name": "children" 922 | }, 923 | { 924 | "source_nodeId": "cb4b56b1-349e-4f0e-bccb-883521ad98ec", 925 | "source_name": "parent", 926 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 927 | "target_name": "children" 928 | }, 929 | { 930 | "source_nodeId": "3a717f8e-07cd-4990-8647-8c57689a776e", 931 | "source_name": "parent", 932 | "target_nodeId": "d1ac4d12-6358-459c-896b-217e631c08ca", 933 | "target_name": "children" 934 | }, 935 | { 936 | "source_nodeId": "d1ac4d12-6358-459c-896b-217e631c08ca", 937 | "source_name": "parent", 938 | "target_nodeId": "c7844059-d883-4363-acb2-771d7a2535d4", 939 | "target_name": "children" 940 | }, 941 | { 942 | "source_nodeId": "bf4278b1-38a0-48e6-97c5-a7ab22c0e36d", 943 | "source_name": "parent", 944 | "target_nodeId": "6f57edd8-dcc0-47da-acfe-8c0acb86e829", 945 | "target_name": "children" 946 | }, 947 | { 948 | "source_nodeId": "6f57edd8-dcc0-47da-acfe-8c0acb86e829", 949 | "source_name": "parent", 950 | "target_nodeId": "0f6909bd-75e6-498d-bbe8-475c466f53d5", 951 | "target_name": "children" 952 | }, 953 | { 954 | "source_nodeId": "0f6909bd-75e6-498d-bbe8-475c466f53d5", 955 | "source_name": "parent", 956 | "target_nodeId": "c7844059-d883-4363-acb2-771d7a2535d4", 957 | "target_name": "children" 958 | }, 959 | { 960 | "source_nodeId": "c7844059-d883-4363-acb2-771d7a2535d4", 961 | "source_name": "parent", 962 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 963 | "target_name": "children" 964 | }, 965 | { 966 | "source_nodeId": "d4ca40c1-f7c6-402e-bdaf-a54a68063f31", 967 | "source_name": "parent", 968 | "target_nodeId": "3e692fbf-6242-454e-a1fa-38bfa857041f", 969 | "target_name": "children" 970 | }, 971 | { 972 | "source_nodeId": "3e692fbf-6242-454e-a1fa-38bfa857041f", 973 | "source_name": "parent", 974 | "target_nodeId": "8aee64e5-889b-40e3-9931-f0c75e0e7709", 975 | "target_name": "children" 976 | }, 977 | { 978 | "source_nodeId": "cf3f08a0-fee5-426e-ad45-98296b923e35", 979 | "source_name": "parent", 980 | "target_nodeId": "45e3cb6b-291e-4589-97da-b5d08b1ede79", 981 | "target_name": "children" 982 | }, 983 | { 984 | "source_nodeId": "45e3cb6b-291e-4589-97da-b5d08b1ede79", 985 | "source_name": "parent", 986 | "target_nodeId": "0960fd7d-faa8-4a5f-983a-b55344c09818", 987 | "target_name": "children" 988 | }, 989 | { 990 | "source_nodeId": "0960fd7d-faa8-4a5f-983a-b55344c09818", 991 | "source_name": "parent", 992 | "target_nodeId": "8aee64e5-889b-40e3-9931-f0c75e0e7709", 993 | "target_name": "children" 994 | }, 995 | { 996 | "source_nodeId": "8aee64e5-889b-40e3-9931-f0c75e0e7709", 997 | "source_name": "parent", 998 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 999 | "target_name": "children" 1000 | }, 1001 | { 1002 | "source_nodeId": "1e919ed0-6dd0-4170-9847-bfb5a33bc91a", 1003 | "source_name": "parent", 1004 | "target_nodeId": "dc0edfcc-832f-48d9-89f2-8d865a86f97b", 1005 | "target_name": "children" 1006 | }, 1007 | { 1008 | "source_nodeId": "dc0edfcc-832f-48d9-89f2-8d865a86f97b", 1009 | "source_name": "parent", 1010 | "target_nodeId": "f5d52b53-d24f-416e-a8c7-f87d113f859e", 1011 | "target_name": "children" 1012 | }, 1013 | { 1014 | "source_nodeId": "f5d52b53-d24f-416e-a8c7-f87d113f859e", 1015 | "source_name": "parent", 1016 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 1017 | "target_name": "children" 1018 | }, 1019 | { 1020 | "source_nodeId": "1ad20e87-c5fe-4f1b-8b2b-5831b0628236", 1021 | "source_name": "parent", 1022 | "target_nodeId": "441ab6ca-83d7-422a-9776-823b6f6c04f7", 1023 | "target_name": "children" 1024 | }, 1025 | { 1026 | "source_nodeId": "441ab6ca-83d7-422a-9776-823b6f6c04f7", 1027 | "source_name": "parent", 1028 | "target_nodeId": "7a798e7a-866d-49e7-8898-8d9d031d671d", 1029 | "target_name": "children" 1030 | }, 1031 | { 1032 | "source_nodeId": "7a798e7a-866d-49e7-8898-8d9d031d671d", 1033 | "source_name": "parent", 1034 | "target_nodeId": "c21e1b25-d391-4977-bb50-80daea24dc0a", 1035 | "target_name": "children" 1036 | }, 1037 | { 1038 | "source_nodeId": "c21e1b25-d391-4977-bb50-80daea24dc0a", 1039 | "source_name": "parent", 1040 | "target_nodeId": "39f46ff0-98bf-4921-8f86-c63efccd73ae", 1041 | "target_name": "children" 1042 | }, 1043 | { 1044 | "source_nodeId": "898870db-8f37-4f17-8cd3-a8d10d510364", 1045 | "source_name": "parent", 1046 | "target_nodeId": "ad13fdfc-2112-4b6d-885e-4e2e2cb5f8ee", 1047 | "target_name": "children" 1048 | }, 1049 | { 1050 | "source_nodeId": "ad13fdfc-2112-4b6d-885e-4e2e2cb5f8ee", 1051 | "source_name": "parent", 1052 | "target_nodeId": "b320f3cc-8963-44b2-9f34-504b83a26240", 1053 | "target_name": "children" 1054 | }, 1055 | { 1056 | "source_nodeId": "b320f3cc-8963-44b2-9f34-504b83a26240", 1057 | "source_name": "parent", 1058 | "target_nodeId": "83b09c04-0f1c-4759-bd30-165e297a64a3", 1059 | "target_name": "children" 1060 | }, 1061 | { 1062 | "source_nodeId": "83b09c04-0f1c-4759-bd30-165e297a64a3", 1063 | "source_name": "parent", 1064 | "target_nodeId": "78e0bba5-0086-4ead-b168-0de4041a4b19", 1065 | "target_name": "children" 1066 | }, 1067 | { 1068 | "source_nodeId": "78e0bba5-0086-4ead-b168-0de4041a4b19", 1069 | "source_name": "parent", 1070 | "target_nodeId": "39f46ff0-98bf-4921-8f86-c63efccd73ae", 1071 | "target_name": "children" 1072 | }, 1073 | { 1074 | "source_nodeId": "adbe744d-7def-40a3-8587-3a643c78d2d6", 1075 | "source_name": "parent", 1076 | "target_nodeId": "556b94d6-6fae-408c-b100-b857b68d83cd", 1077 | "target_name": "children" 1078 | }, 1079 | { 1080 | "source_nodeId": "556b94d6-6fae-408c-b100-b857b68d83cd", 1081 | "source_name": "parent", 1082 | "target_nodeId": "a19eb3ea-d0f2-4242-9eea-697ea4d431f2", 1083 | "target_name": "children" 1084 | }, 1085 | { 1086 | "source_nodeId": "a19eb3ea-d0f2-4242-9eea-697ea4d431f2", 1087 | "source_name": "parent", 1088 | "target_nodeId": "4db93f52-7303-48c0-9d21-dd0b646f804f", 1089 | "target_name": "children" 1090 | }, 1091 | { 1092 | "source_nodeId": "4db93f52-7303-48c0-9d21-dd0b646f804f", 1093 | "source_name": "parent", 1094 | "target_nodeId": "c2f5a964-b26c-4624-8601-72da6b6ac64d", 1095 | "target_name": "children" 1096 | }, 1097 | { 1098 | "source_nodeId": "c2f5a964-b26c-4624-8601-72da6b6ac64d", 1099 | "source_name": "parent", 1100 | "target_nodeId": "39f46ff0-98bf-4921-8f86-c63efccd73ae", 1101 | "target_name": "children" 1102 | }, 1103 | { 1104 | "source_nodeId": "681763b9-45c2-4851-80a3-66cc2c177897", 1105 | "source_name": "parent", 1106 | "target_nodeId": "8ed351cf-b383-45b9-a26e-9c4f4c9a3ef1", 1107 | "target_name": "children" 1108 | }, 1109 | { 1110 | "source_nodeId": "8ed351cf-b383-45b9-a26e-9c4f4c9a3ef1", 1111 | "source_name": "parent", 1112 | "target_nodeId": "8be7a3c3-5d43-4715-8fc1-56e449a23e2b", 1113 | "target_name": "children" 1114 | }, 1115 | { 1116 | "source_nodeId": "8be7a3c3-5d43-4715-8fc1-56e449a23e2b", 1117 | "source_name": "parent", 1118 | "target_nodeId": "06e88de8-bf99-4b17-9a11-7bd1dcf098ea", 1119 | "target_name": "children" 1120 | }, 1121 | { 1122 | "source_nodeId": "06e88de8-bf99-4b17-9a11-7bd1dcf098ea", 1123 | "source_name": "parent", 1124 | "target_nodeId": "15b2c350-a447-4a4e-906c-8e8d9a8932c2", 1125 | "target_name": "children" 1126 | }, 1127 | { 1128 | "source_nodeId": "15b2c350-a447-4a4e-906c-8e8d9a8932c2", 1129 | "source_name": "parent", 1130 | "target_nodeId": "39f46ff0-98bf-4921-8f86-c63efccd73ae", 1131 | "target_name": "children" 1132 | }, 1133 | { 1134 | "source_nodeId": "dba09ea8-ccb2-4990-af64-af02776a5c2d", 1135 | "source_name": "parent", 1136 | "target_nodeId": "ceaab406-6076-4fd4-b57d-1c524a4460de", 1137 | "target_name": "children" 1138 | }, 1139 | { 1140 | "source_nodeId": "ceaab406-6076-4fd4-b57d-1c524a4460de", 1141 | "source_name": "parent", 1142 | "target_nodeId": "374a869e-4a18-48c6-95f7-de2c27e0d838", 1143 | "target_name": "children" 1144 | }, 1145 | { 1146 | "source_nodeId": "374a869e-4a18-48c6-95f7-de2c27e0d838", 1147 | "source_name": "parent", 1148 | "target_nodeId": "cc585654-e0e5-4f23-8347-0a5f332ce8a9", 1149 | "target_name": "children" 1150 | }, 1151 | { 1152 | "source_nodeId": "cc585654-e0e5-4f23-8347-0a5f332ce8a9", 1153 | "source_name": "parent", 1154 | "target_nodeId": "e905f6cf-59a1-4440-bedf-e10780d4a1f7", 1155 | "target_name": "children" 1156 | }, 1157 | { 1158 | "source_nodeId": "e905f6cf-59a1-4440-bedf-e10780d4a1f7", 1159 | "source_name": "parent", 1160 | "target_nodeId": "39f46ff0-98bf-4921-8f86-c63efccd73ae", 1161 | "target_name": "children" 1162 | }, 1163 | { 1164 | "source_nodeId": "39f46ff0-98bf-4921-8f86-c63efccd73ae", 1165 | "source_name": "parent", 1166 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 1167 | "target_name": "children" 1168 | }, 1169 | { 1170 | "source_nodeId": "3c7fc6bd-4598-4586-b6f9-c55f2b699858", 1171 | "source_name": "parent", 1172 | "target_nodeId": "16989c94-9e55-4832-8a52-bcc8bd8daf9c", 1173 | "target_name": "children" 1174 | }, 1175 | { 1176 | "source_nodeId": "16989c94-9e55-4832-8a52-bcc8bd8daf9c", 1177 | "source_name": "parent", 1178 | "target_nodeId": "d8afe5ce-3f02-443b-b601-989a4e05adab", 1179 | "target_name": "children" 1180 | }, 1181 | { 1182 | "source_nodeId": "d8afe5ce-3f02-443b-b601-989a4e05adab", 1183 | "source_name": "parent", 1184 | "target_nodeId": "d0946b3c-3b29-4a79-8695-ea9bbf9c1def", 1185 | "target_name": "children" 1186 | }, 1187 | { 1188 | "source_nodeId": "d0946b3c-3b29-4a79-8695-ea9bbf9c1def", 1189 | "source_name": "parent", 1190 | "target_nodeId": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d", 1191 | "target_name": "children" 1192 | }, 1193 | { 1194 | "source_nodeId": "25f7bf8e-7dbe-4bfa-9881-d121954efbec", 1195 | "source_name": "parent", 1196 | "target_nodeId": "568d80e5-d9a3-40ae-bf36-f10779519e65", 1197 | "target_name": "children" 1198 | }, 1199 | { 1200 | "source_nodeId": "568d80e5-d9a3-40ae-bf36-f10779519e65", 1201 | "source_name": "parent", 1202 | "target_nodeId": "fcefcdf4-7463-4aa2-8e50-c61ec2f6801a", 1203 | "target_name": "children" 1204 | }, 1205 | { 1206 | "source_nodeId": "fcefcdf4-7463-4aa2-8e50-c61ec2f6801a", 1207 | "source_name": "parent", 1208 | "target_nodeId": "d94c4dff-f74e-44d1-9ce4-80aba4eeb4e4", 1209 | "target_name": "children" 1210 | }, 1211 | { 1212 | "source_nodeId": "d94c4dff-f74e-44d1-9ce4-80aba4eeb4e4", 1213 | "source_name": "parent", 1214 | "target_nodeId": "c5079071-03a8-49e9-bca4-5f6f81d6e723", 1215 | "target_name": "children" 1216 | }, 1217 | { 1218 | "source_nodeId": "c5079071-03a8-49e9-bca4-5f6f81d6e723", 1219 | "source_name": "parent", 1220 | "target_nodeId": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d", 1221 | "target_name": "children" 1222 | }, 1223 | { 1224 | "source_nodeId": "a77a26df-3770-4fc4-8eeb-eab5e129dd47", 1225 | "source_name": "parent", 1226 | "target_nodeId": "295a27f8-1369-4b2f-aec9-6d6d492117fc", 1227 | "target_name": "children" 1228 | }, 1229 | { 1230 | "source_nodeId": "295a27f8-1369-4b2f-aec9-6d6d492117fc", 1231 | "source_name": "parent", 1232 | "target_nodeId": "2220a87b-9aba-4160-bcb3-bdb206182a4f", 1233 | "target_name": "children" 1234 | }, 1235 | { 1236 | "source_nodeId": "2220a87b-9aba-4160-bcb3-bdb206182a4f", 1237 | "source_name": "parent", 1238 | "target_nodeId": "85dfb154-59d2-442b-8a56-d219c763e999", 1239 | "target_name": "children" 1240 | }, 1241 | { 1242 | "source_nodeId": "85dfb154-59d2-442b-8a56-d219c763e999", 1243 | "source_name": "parent", 1244 | "target_nodeId": "7777251e-4c1b-4984-81d8-0e246ae42097", 1245 | "target_name": "children" 1246 | }, 1247 | { 1248 | "source_nodeId": "7777251e-4c1b-4984-81d8-0e246ae42097", 1249 | "source_name": "parent", 1250 | "target_nodeId": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d", 1251 | "target_name": "children" 1252 | }, 1253 | { 1254 | "source_nodeId": "906ab2bf-e49f-47fa-8035-3816f6b9df12", 1255 | "source_name": "parent", 1256 | "target_nodeId": "4dc5ae4e-9843-4276-a15f-7d69707fd229", 1257 | "target_name": "children" 1258 | }, 1259 | { 1260 | "source_nodeId": "4dc5ae4e-9843-4276-a15f-7d69707fd229", 1261 | "source_name": "parent", 1262 | "target_nodeId": "a8f8f13c-5f94-4a66-b039-57587820681b", 1263 | "target_name": "children" 1264 | }, 1265 | { 1266 | "source_nodeId": "a8f8f13c-5f94-4a66-b039-57587820681b", 1267 | "source_name": "parent", 1268 | "target_nodeId": "423628f3-88cb-43d7-82ef-60b9e9e56069", 1269 | "target_name": "children" 1270 | }, 1271 | { 1272 | "source_nodeId": "423628f3-88cb-43d7-82ef-60b9e9e56069", 1273 | "source_name": "parent", 1274 | "target_nodeId": "67bc36d6-4548-4c66-ba79-1ffe00a12d1e", 1275 | "target_name": "children" 1276 | }, 1277 | { 1278 | "source_nodeId": "67bc36d6-4548-4c66-ba79-1ffe00a12d1e", 1279 | "source_name": "parent", 1280 | "target_nodeId": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d", 1281 | "target_name": "children" 1282 | }, 1283 | { 1284 | "source_nodeId": "0fb8fa5e-051c-4d35-b1b9-2d4161782167", 1285 | "source_name": "parent", 1286 | "target_nodeId": "615fb4e1-2bc7-4e89-ac53-277a93b7d98d", 1287 | "target_name": "children" 1288 | }, 1289 | { 1290 | "source_nodeId": "615fb4e1-2bc7-4e89-ac53-277a93b7d98d", 1291 | "source_name": "parent", 1292 | "target_nodeId": "59299d4a-3b8a-4902-825b-668791fcc2e6", 1293 | "target_name": "children" 1294 | }, 1295 | { 1296 | "source_nodeId": "59299d4a-3b8a-4902-825b-668791fcc2e6", 1297 | "source_name": "parent", 1298 | "target_nodeId": "9630690e-5edb-49bd-a0d4-9b3ddf490dd5", 1299 | "target_name": "children" 1300 | }, 1301 | { 1302 | "source_nodeId": "9630690e-5edb-49bd-a0d4-9b3ddf490dd5", 1303 | "source_name": "parent", 1304 | "target_nodeId": "2aa1a6e9-2c84-4359-9056-4c9f70a85276", 1305 | "target_name": "children" 1306 | }, 1307 | { 1308 | "source_nodeId": "2aa1a6e9-2c84-4359-9056-4c9f70a85276", 1309 | "source_name": "parent", 1310 | "target_nodeId": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d", 1311 | "target_name": "children" 1312 | }, 1313 | { 1314 | "source_nodeId": "796daaaa-87f3-44ce-b4e0-f7b233b6b78d", 1315 | "source_name": "parent", 1316 | "target_nodeId": "11f1f247-852d-4e08-9af9-8b9b3aed28fc", 1317 | "target_name": "children" 1318 | } 1319 | ] 1320 | } 1321 | -------------------------------------------------------------------------------- /examples/max_scenegraph.py: -------------------------------------------------------------------------------- 1 | """Run this in 3ds Max: 2 | 3 | python.executeFile @"thisfile.py" 4 | 5 | """ 6 | 7 | import sys 8 | sys.path.insert(0, r"C:\dev\qtnodes") 9 | 10 | 11 | import MaxPlus 12 | 13 | from qtnodes import (Header, Node, InputKnob, 14 | OutputKnob, NodeGraphWidget) 15 | 16 | 17 | class MaxObject(Node): 18 | 19 | def __init__(self, name): 20 | super(MaxObject, self).__init__() 21 | self.addHeader(Header(node=self, text=name)) 22 | self.addKnob(InputKnob(labelText="children")) 23 | self.addKnob(OutputKnob(labelText="parent")) 24 | 25 | 26 | def createObject(graph, node, parent=None): 27 | obj = MaxObject(node.Name) 28 | graph.addNode(obj) 29 | if parent: 30 | obj.knob("parent").connectTo(parent.knob("children")) 31 | 32 | for c in node.Children: 33 | createObject(graph, c, parent=obj) 34 | 35 | 36 | if __name__ == '__main__': 37 | graph = NodeGraphWidget() 38 | graph.registerNodeClass(MaxObject) 39 | 40 | rootNode = MaxPlus.Core.GetRootNode() 41 | createObject(graph, rootNode) 42 | 43 | graph.show() 44 | 45 | 46 | -------------------------------------------------------------------------------- /examples/medium.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "y": 258.9400000000001, 5 | "x": 707.8500000000001, 6 | "class": "Output", 7 | "uuid": "788d6505-c24f-41d0-9592-a707338bf3ad" 8 | }, 9 | { 10 | "y": 260.15000000000003, 11 | "x": 411.4000000000001, 12 | "class": "Divide", 13 | "uuid": "501b2ced-d96a-426c-9a35-315bbe64553c" 14 | }, 15 | { 16 | "y": 440.44000000000005, 17 | "x": 284.34999999999997, 18 | "class": "Add", 19 | "uuid": "4b0dd98b-6048-42b7-bb5c-8519ee866308" 20 | }, 21 | { 22 | "y": 84.69999999999996, 23 | "x": 278.29999999999995, 24 | "class": "Add", 25 | "uuid": "241b8044-b59d-4579-9a43-42c6c622134a" 26 | }, 27 | { 28 | "y": 26.620000000000005, 29 | "x": -177.87, 30 | "class": "Integer", 31 | "uuid": "457bead8-2bc3-4b34-ac3d-c09c7a7a37fb" 32 | }, 33 | { 34 | "y": 276.62, 35 | "x": -77.87, 36 | "class": "Integer", 37 | "uuid": "82dbcf61-76db-4a5b-a593-f4376efeadf2" 38 | }, 39 | { 40 | "y": 126.62, 41 | "x": 22.129999999999967, 42 | "class": "Multiply", 43 | "uuid": "7f8594e6-ab77-4d90-ac40-ff83280532af" 44 | }, 45 | { 46 | "y": 76.62, 47 | "x": 122.13, 48 | "class": "BigNode", 49 | "uuid": "c503d62c-4799-4198-988d-b4036e7cf9d4" 50 | }, 51 | { 52 | "y": 526.488883427273, 53 | "x": -621.6738931090911, 54 | "class": "Integer", 55 | "uuid": "6908e282-c6ec-44cc-b04a-c04e5b601ff6" 56 | }, 57 | { 58 | "y": 585.6966950690963, 59 | "x": -618.7634090783408, 60 | "class": "Integer", 61 | "uuid": "e266789b-849d-4fbe-ad82-af07462e74db" 62 | }, 63 | { 64 | "y": 526.0125238777006, 65 | "x": -533.4397312597387, 66 | "class": "Multiply", 67 | "uuid": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3" 68 | }, 69 | { 70 | "y": 439.16817022575185, 71 | "x": -289.86880144134705, 72 | "class": "BigNode", 73 | "uuid": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50" 74 | }, 75 | { 76 | "y": 369.67331794575693, 77 | "x": -162.73478234745215, 78 | "class": "Add", 79 | "uuid": "5ba173d8-131b-4604-815b-0c1c081cd8dd" 80 | }, 81 | { 82 | "y": 450.3930899434471, 83 | "x": -163.86373020755963, 84 | "class": "Add", 85 | "uuid": "74036279-2646-4f08-8eab-296a82e4fb2c" 86 | }, 87 | { 88 | "y": 449.82861601339334, 89 | "x": -77.499218909332, 90 | "class": "Add", 91 | "uuid": "dc97f5f5-e40b-4986-bdff-f88689a47179" 92 | }, 93 | { 94 | "y": 530.5483880110834, 95 | "x": -162.73478234745215, 96 | "class": "Add", 97 | "uuid": "2b9f55cd-ad7b-4841-8542-65e8ba5fd8f6" 98 | }, 99 | { 100 | "y": 542.4023405422128, 101 | "x": -76.37027104922453, 102 | "class": "Add", 103 | "uuid": "bde20a06-c1f2-44f2-ab46-282dc624c177" 104 | }, 105 | { 106 | "y": 611.2681600087736, 107 | "x": -163.2992562775059, 108 | "class": "Add", 109 | "uuid": "4317a8df-f11b-49bd-9d42-65958571a13c" 110 | }, 111 | { 112 | "y": 505.71153508871714, 113 | "x": -4.117608002340944, 114 | "class": "Add", 115 | "uuid": "012f81e5-9b4a-4b71-af96-4e4d9c563704" 116 | }, 117 | { 118 | "y": 569.6515123178389, 119 | "x": 100.61891532369748, 120 | "class": "Add", 121 | "uuid": "725305bb-6f64-4916-81f8-703c4aa6326f" 122 | }, 123 | { 124 | "y": -292.6619946599999, 125 | "x": -211.08586321999996, 126 | "class": "Integer", 127 | "uuid": "b513b161-5987-42f8-958a-6f6711f87075" 128 | }, 129 | { 130 | "y": -109.66199465999989, 131 | "x": -63.085863219999965, 132 | "class": "Float", 133 | "uuid": "070cda86-122e-4e0f-a9fb-a620440fbdd8" 134 | }, 135 | { 136 | "y": -152.6619946599999, 137 | "x": 49.91413678000015, 138 | "class": "Multiply", 139 | "uuid": "7708a799-ddc8-4158-b5e3-dfec4e9d6220" 140 | }, 141 | { 142 | "y": -74.66199465999989, 143 | "x": 124.91413677999992, 144 | "class": "Divide", 145 | "uuid": "825909e4-6f51-4c74-a329-5892f38ebb89" 146 | }, 147 | { 148 | "y": -284.6619946599999, 149 | "x": -112.08586321999996, 150 | "class": "Add", 151 | "uuid": "31f0df44-fde8-4da5-9898-e312c596f1bf" 152 | }, 153 | { 154 | "y": -199.66199466, 155 | "x": -41.085863219999965, 156 | "class": "Subtract", 157 | "uuid": "a2b1e048-a969-4aad-a6d4-b5702831ca5f" 158 | }, 159 | { 160 | "y": -238.66199466, 161 | "x": -211.08586321999996, 162 | "class": "Integer", 163 | "uuid": "5711fc19-122e-4a2a-af4f-500924427b67" 164 | }, 165 | { 166 | "y": -180.66199466, 167 | "x": -133.08586321999985, 168 | "class": "Integer", 169 | "uuid": "04c8437d-f7c9-4d94-94a9-5d1d4b2f9330" 170 | }, 171 | { 172 | "y": -58.661994660000005, 173 | "x": 25.914136780000035, 174 | "class": "Float", 175 | "uuid": "6d4af025-a2fd-4708-9670-bbf402c9d157" 176 | } 177 | ], 178 | "edges": [ 179 | { 180 | "source_nodeId": "501b2ced-d96a-426c-9a35-315bbe64553c", 181 | "source_name": "value", 182 | "target_nodeId": "788d6505-c24f-41d0-9592-a707338bf3ad", 183 | "target_name": "output" 184 | }, 185 | { 186 | "source_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 187 | "source_name": "value", 188 | "target_nodeId": "501b2ced-d96a-426c-9a35-315bbe64553c", 189 | "target_name": "y" 190 | }, 191 | { 192 | "source_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 193 | "source_name": "value", 194 | "target_nodeId": "501b2ced-d96a-426c-9a35-315bbe64553c", 195 | "target_name": "x" 196 | }, 197 | { 198 | "source_nodeId": "825909e4-6f51-4c74-a329-5892f38ebb89", 199 | "source_name": "value", 200 | "target_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 201 | "target_name": "x" 202 | }, 203 | { 204 | "source_nodeId": "457bead8-2bc3-4b34-ac3d-c09c7a7a37fb", 205 | "source_name": "value", 206 | "target_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 207 | "target_name": "x" 208 | }, 209 | { 210 | "source_nodeId": "82dbcf61-76db-4a5b-a593-f4376efeadf2", 211 | "source_name": "value", 212 | "target_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 213 | "target_name": "y" 214 | }, 215 | { 216 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 217 | "source_name": "o9", 218 | "target_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 219 | "target_name": "x" 220 | }, 221 | { 222 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 223 | "source_name": "value", 224 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 225 | "target_name": "i1" 226 | }, 227 | { 228 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 229 | "source_name": "o8", 230 | "target_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 231 | "target_name": "x" 232 | }, 233 | { 234 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 235 | "source_name": "value", 236 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 237 | "target_name": "i2" 238 | }, 239 | { 240 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 241 | "source_name": "o7", 242 | "target_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 243 | "target_name": "x" 244 | }, 245 | { 246 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 247 | "source_name": "o6", 248 | "target_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 249 | "target_name": "x" 250 | }, 251 | { 252 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 253 | "source_name": "value", 254 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 255 | "target_name": "i3" 256 | }, 257 | { 258 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 259 | "source_name": "o5", 260 | "target_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 261 | "target_name": "x" 262 | }, 263 | { 264 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 265 | "source_name": "value", 266 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 267 | "target_name": "i4" 268 | }, 269 | { 270 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 271 | "source_name": "value", 272 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 273 | "target_name": "i5" 274 | }, 275 | { 276 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 277 | "source_name": "o5", 278 | "target_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 279 | "target_name": "y" 280 | }, 281 | { 282 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 283 | "source_name": "value", 284 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 285 | "target_name": "i6" 286 | }, 287 | { 288 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 289 | "source_name": "o4", 290 | "target_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 291 | "target_name": "y" 292 | }, 293 | { 294 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 295 | "source_name": "o3", 296 | "target_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 297 | "target_name": "y" 298 | }, 299 | { 300 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 301 | "source_name": "value", 302 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 303 | "target_name": "i7" 304 | }, 305 | { 306 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 307 | "source_name": "o2", 308 | "target_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 309 | "target_name": "y" 310 | }, 311 | { 312 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 313 | "source_name": "value", 314 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 315 | "target_name": "i8" 316 | }, 317 | { 318 | "source_nodeId": "7f8594e6-ab77-4d90-ac40-ff83280532af", 319 | "source_name": "value", 320 | "target_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 321 | "target_name": "i9" 322 | }, 323 | { 324 | "source_nodeId": "c503d62c-4799-4198-988d-b4036e7cf9d4", 325 | "source_name": "o1", 326 | "target_nodeId": "241b8044-b59d-4579-9a43-42c6c622134a", 327 | "target_name": "y" 328 | }, 329 | { 330 | "source_nodeId": "725305bb-6f64-4916-81f8-703c4aa6326f", 331 | "source_name": "value", 332 | "target_nodeId": "4b0dd98b-6048-42b7-bb5c-8519ee866308", 333 | "target_name": "y" 334 | }, 335 | { 336 | "source_nodeId": "6908e282-c6ec-44cc-b04a-c04e5b601ff6", 337 | "source_name": "value", 338 | "target_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 339 | "target_name": "x" 340 | }, 341 | { 342 | "source_nodeId": "e266789b-849d-4fbe-ad82-af07462e74db", 343 | "source_name": "value", 344 | "target_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 345 | "target_name": "y" 346 | }, 347 | { 348 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 349 | "source_name": "value", 350 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 351 | "target_name": "i1" 352 | }, 353 | { 354 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 355 | "source_name": "value", 356 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 357 | "target_name": "i2" 358 | }, 359 | { 360 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 361 | "source_name": "value", 362 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 363 | "target_name": "i3" 364 | }, 365 | { 366 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 367 | "source_name": "value", 368 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 369 | "target_name": "i4" 370 | }, 371 | { 372 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 373 | "source_name": "value", 374 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 375 | "target_name": "i5" 376 | }, 377 | { 378 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 379 | "source_name": "value", 380 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 381 | "target_name": "i6" 382 | }, 383 | { 384 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 385 | "source_name": "value", 386 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 387 | "target_name": "i7" 388 | }, 389 | { 390 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 391 | "source_name": "value", 392 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 393 | "target_name": "i8" 394 | }, 395 | { 396 | "source_nodeId": "4be6aefe-db07-4a70-92a5-6b99a6c7d3e3", 397 | "source_name": "value", 398 | "target_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 399 | "target_name": "i9" 400 | }, 401 | { 402 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 403 | "source_name": "o1", 404 | "target_nodeId": "5ba173d8-131b-4604-815b-0c1c081cd8dd", 405 | "target_name": "x" 406 | }, 407 | { 408 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 409 | "source_name": "o2", 410 | "target_nodeId": "5ba173d8-131b-4604-815b-0c1c081cd8dd", 411 | "target_name": "y" 412 | }, 413 | { 414 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 415 | "source_name": "o3", 416 | "target_nodeId": "74036279-2646-4f08-8eab-296a82e4fb2c", 417 | "target_name": "x" 418 | }, 419 | { 420 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 421 | "source_name": "o4", 422 | "target_nodeId": "74036279-2646-4f08-8eab-296a82e4fb2c", 423 | "target_name": "y" 424 | }, 425 | { 426 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 427 | "source_name": "o5", 428 | "target_nodeId": "2b9f55cd-ad7b-4841-8542-65e8ba5fd8f6", 429 | "target_name": "x" 430 | }, 431 | { 432 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 433 | "source_name": "o6", 434 | "target_nodeId": "2b9f55cd-ad7b-4841-8542-65e8ba5fd8f6", 435 | "target_name": "y" 436 | }, 437 | { 438 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 439 | "source_name": "o7", 440 | "target_nodeId": "4317a8df-f11b-49bd-9d42-65958571a13c", 441 | "target_name": "x" 442 | }, 443 | { 444 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 445 | "source_name": "o8", 446 | "target_nodeId": "4317a8df-f11b-49bd-9d42-65958571a13c", 447 | "target_name": "y" 448 | }, 449 | { 450 | "source_nodeId": "5ba173d8-131b-4604-815b-0c1c081cd8dd", 451 | "source_name": "value", 452 | "target_nodeId": "dc97f5f5-e40b-4986-bdff-f88689a47179", 453 | "target_name": "x" 454 | }, 455 | { 456 | "source_nodeId": "74036279-2646-4f08-8eab-296a82e4fb2c", 457 | "source_name": "value", 458 | "target_nodeId": "dc97f5f5-e40b-4986-bdff-f88689a47179", 459 | "target_name": "y" 460 | }, 461 | { 462 | "source_nodeId": "2b9f55cd-ad7b-4841-8542-65e8ba5fd8f6", 463 | "source_name": "value", 464 | "target_nodeId": "bde20a06-c1f2-44f2-ab46-282dc624c177", 465 | "target_name": "x" 466 | }, 467 | { 468 | "source_nodeId": "4317a8df-f11b-49bd-9d42-65958571a13c", 469 | "source_name": "value", 470 | "target_nodeId": "bde20a06-c1f2-44f2-ab46-282dc624c177", 471 | "target_name": "y" 472 | }, 473 | { 474 | "source_nodeId": "dc97f5f5-e40b-4986-bdff-f88689a47179", 475 | "source_name": "value", 476 | "target_nodeId": "012f81e5-9b4a-4b71-af96-4e4d9c563704", 477 | "target_name": "x" 478 | }, 479 | { 480 | "source_nodeId": "bde20a06-c1f2-44f2-ab46-282dc624c177", 481 | "source_name": "value", 482 | "target_nodeId": "012f81e5-9b4a-4b71-af96-4e4d9c563704", 483 | "target_name": "y" 484 | }, 485 | { 486 | "source_nodeId": "1ed0280e-a9b6-48fb-82d1-1717f0f1dd50", 487 | "source_name": "o9", 488 | "target_nodeId": "725305bb-6f64-4916-81f8-703c4aa6326f", 489 | "target_name": "y" 490 | }, 491 | { 492 | "source_nodeId": "012f81e5-9b4a-4b71-af96-4e4d9c563704", 493 | "source_name": "value", 494 | "target_nodeId": "725305bb-6f64-4916-81f8-703c4aa6326f", 495 | "target_name": "x" 496 | }, 497 | { 498 | "source_nodeId": "b513b161-5987-42f8-958a-6f6711f87075", 499 | "source_name": "value", 500 | "target_nodeId": "31f0df44-fde8-4da5-9898-e312c596f1bf", 501 | "target_name": "x" 502 | }, 503 | { 504 | "source_nodeId": "5711fc19-122e-4a2a-af4f-500924427b67", 505 | "source_name": "value", 506 | "target_nodeId": "31f0df44-fde8-4da5-9898-e312c596f1bf", 507 | "target_name": "y" 508 | }, 509 | { 510 | "source_nodeId": "31f0df44-fde8-4da5-9898-e312c596f1bf", 511 | "source_name": "value", 512 | "target_nodeId": "a2b1e048-a969-4aad-a6d4-b5702831ca5f", 513 | "target_name": "x" 514 | }, 515 | { 516 | "source_nodeId": "04c8437d-f7c9-4d94-94a9-5d1d4b2f9330", 517 | "source_name": "value", 518 | "target_nodeId": "a2b1e048-a969-4aad-a6d4-b5702831ca5f", 519 | "target_name": "y" 520 | }, 521 | { 522 | "source_nodeId": "a2b1e048-a969-4aad-a6d4-b5702831ca5f", 523 | "source_name": "value", 524 | "target_nodeId": "7708a799-ddc8-4158-b5e3-dfec4e9d6220", 525 | "target_name": "x" 526 | }, 527 | { 528 | "source_nodeId": "070cda86-122e-4e0f-a9fb-a620440fbdd8", 529 | "source_name": "value", 530 | "target_nodeId": "7708a799-ddc8-4158-b5e3-dfec4e9d6220", 531 | "target_name": "y" 532 | }, 533 | { 534 | "source_nodeId": "7708a799-ddc8-4158-b5e3-dfec4e9d6220", 535 | "source_name": "value", 536 | "target_nodeId": "825909e4-6f51-4c74-a329-5892f38ebb89", 537 | "target_name": "x" 538 | }, 539 | { 540 | "source_nodeId": "6d4af025-a2fd-4708-9670-bbf402c9d157", 541 | "source_name": "value", 542 | "target_nodeId": "825909e4-6f51-4c74-a329-5892f38ebb89", 543 | "target_name": "y" 544 | } 545 | ] 546 | } 547 | -------------------------------------------------------------------------------- /examples/pleaselayoutme.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "y": -48.81899999999996, 5 | "x": 308.6210000000002, 6 | "class": "Add", 7 | "uuid": "c2337304-dc86-4941-980c-cf6ea0fad03d" 8 | }, 9 | { 10 | "y": 351.54399999999987, 11 | "x": 40.08100000000013, 12 | "class": "Add", 13 | "uuid": "381b5d78-f1bb-4dac-be44-64ea5d9c7070" 14 | }, 15 | { 16 | "y": 261.41599999999994, 17 | "x": 140.47800000000007, 18 | "class": "Add", 19 | "uuid": "d307cee4-1010-4361-8e76-5c9d49cee6d3" 20 | }, 21 | { 22 | "y": 81.35100000000001, 23 | "x": 208.0780000000001, 24 | "class": "Add", 25 | "uuid": "ff4655c2-e55f-4f57-8eae-73e6b25e9064" 26 | }, 27 | { 28 | "y": 357.81899999999985, 29 | "x": 165.76700000000017, 30 | "class": "Add", 31 | "uuid": "1dfedc80-f793-40de-b179-1fe77133a76f" 32 | }, 33 | { 34 | "y": 179.654, 35 | "x": 76.3090000000002, 36 | "class": "Add", 37 | "uuid": "b3f47882-e194-42cd-a808-201185f29a28" 38 | }, 39 | { 40 | "y": 414.66999999999996, 41 | "x": 225.66200000000003, 42 | "class": "Add", 43 | "uuid": "2a469c4a-0257-4612-9c00-08c1f40372f9" 44 | }, 45 | { 46 | "y": 319.4089999999999, 47 | "x": 219.00700000000018, 48 | "class": "Add", 49 | "uuid": "59b20a84-e20d-41b4-909d-909a14cbe6c5" 50 | }, 51 | { 52 | "y": 80.4, 53 | "x": 304.245, 54 | "class": "BigNode", 55 | "uuid": "818233c9-9d7f-4286-9638-6ef055659eef" 56 | }, 57 | { 58 | "y": 197.41599999999994, 59 | "x": 128.41700000000014, 60 | "class": "Output", 61 | "uuid": "b1873c3b-6f7f-48ee-bffb-1e81114a4b6b" 62 | }, 63 | { 64 | "y": 197.5279999999999, 65 | "x": 383.80899999999997, 66 | "class": "Multiply", 67 | "uuid": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb" 68 | }, 69 | { 70 | "y": 150.56299999999993, 71 | "x": 178.65799999999993, 72 | "class": "Integer", 73 | "uuid": "1b5aa89f-a18b-4c63-8dbc-8963afc5eecb" 74 | }, 75 | { 76 | "y": 271.113, 77 | "x": 343.7019999999999, 78 | "class": "Integer", 79 | "uuid": "320a2a28-79d8-4741-aded-9a8dc1c1d07f" 80 | } 81 | ], 82 | "edges": [ 83 | { 84 | "source_nodeId": "381b5d78-f1bb-4dac-be44-64ea5d9c7070", 85 | "source_name": "value", 86 | "target_nodeId": "c2337304-dc86-4941-980c-cf6ea0fad03d", 87 | "target_name": "x" 88 | }, 89 | { 90 | "source_nodeId": "c2337304-dc86-4941-980c-cf6ea0fad03d", 91 | "source_name": "value", 92 | "target_nodeId": "b1873c3b-6f7f-48ee-bffb-1e81114a4b6b", 93 | "target_name": "output" 94 | }, 95 | { 96 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 97 | "source_name": "o9", 98 | "target_nodeId": "c2337304-dc86-4941-980c-cf6ea0fad03d", 99 | "target_name": "y" 100 | }, 101 | { 102 | "source_nodeId": "ff4655c2-e55f-4f57-8eae-73e6b25e9064", 103 | "source_name": "value", 104 | "target_nodeId": "381b5d78-f1bb-4dac-be44-64ea5d9c7070", 105 | "target_name": "y" 106 | }, 107 | { 108 | "source_nodeId": "b3f47882-e194-42cd-a808-201185f29a28", 109 | "source_name": "value", 110 | "target_nodeId": "381b5d78-f1bb-4dac-be44-64ea5d9c7070", 111 | "target_name": "x" 112 | }, 113 | { 114 | "source_nodeId": "d307cee4-1010-4361-8e76-5c9d49cee6d3", 115 | "source_name": "value", 116 | "target_nodeId": "ff4655c2-e55f-4f57-8eae-73e6b25e9064", 117 | "target_name": "y" 118 | }, 119 | { 120 | "source_nodeId": "1dfedc80-f793-40de-b179-1fe77133a76f", 121 | "source_name": "value", 122 | "target_nodeId": "ff4655c2-e55f-4f57-8eae-73e6b25e9064", 123 | "target_name": "x" 124 | }, 125 | { 126 | "source_nodeId": "2a469c4a-0257-4612-9c00-08c1f40372f9", 127 | "source_name": "value", 128 | "target_nodeId": "b3f47882-e194-42cd-a808-201185f29a28", 129 | "target_name": "y" 130 | }, 131 | { 132 | "source_nodeId": "59b20a84-e20d-41b4-909d-909a14cbe6c5", 133 | "source_name": "value", 134 | "target_nodeId": "b3f47882-e194-42cd-a808-201185f29a28", 135 | "target_name": "x" 136 | }, 137 | { 138 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 139 | "source_name": "o8", 140 | "target_nodeId": "d307cee4-1010-4361-8e76-5c9d49cee6d3", 141 | "target_name": "y" 142 | }, 143 | { 144 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 145 | "source_name": "o7", 146 | "target_nodeId": "d307cee4-1010-4361-8e76-5c9d49cee6d3", 147 | "target_name": "x" 148 | }, 149 | { 150 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 151 | "source_name": "o6", 152 | "target_nodeId": "1dfedc80-f793-40de-b179-1fe77133a76f", 153 | "target_name": "y" 154 | }, 155 | { 156 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 157 | "source_name": "o5", 158 | "target_nodeId": "1dfedc80-f793-40de-b179-1fe77133a76f", 159 | "target_name": "x" 160 | }, 161 | { 162 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 163 | "source_name": "o4", 164 | "target_nodeId": "2a469c4a-0257-4612-9c00-08c1f40372f9", 165 | "target_name": "y" 166 | }, 167 | { 168 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 169 | "source_name": "o3", 170 | "target_nodeId": "2a469c4a-0257-4612-9c00-08c1f40372f9", 171 | "target_name": "x" 172 | }, 173 | { 174 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 175 | "source_name": "o2", 176 | "target_nodeId": "59b20a84-e20d-41b4-909d-909a14cbe6c5", 177 | "target_name": "y" 178 | }, 179 | { 180 | "source_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 181 | "source_name": "o1", 182 | "target_nodeId": "59b20a84-e20d-41b4-909d-909a14cbe6c5", 183 | "target_name": "x" 184 | }, 185 | { 186 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 187 | "source_name": "value", 188 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 189 | "target_name": "i9" 190 | }, 191 | { 192 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 193 | "source_name": "value", 194 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 195 | "target_name": "i8" 196 | }, 197 | { 198 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 199 | "source_name": "value", 200 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 201 | "target_name": "i7" 202 | }, 203 | { 204 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 205 | "source_name": "value", 206 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 207 | "target_name": "i6" 208 | }, 209 | { 210 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 211 | "source_name": "value", 212 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 213 | "target_name": "i5" 214 | }, 215 | { 216 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 217 | "source_name": "value", 218 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 219 | "target_name": "i4" 220 | }, 221 | { 222 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 223 | "source_name": "value", 224 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 225 | "target_name": "i3" 226 | }, 227 | { 228 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 229 | "source_name": "value", 230 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 231 | "target_name": "i2" 232 | }, 233 | { 234 | "source_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 235 | "source_name": "value", 236 | "target_nodeId": "818233c9-9d7f-4286-9638-6ef055659eef", 237 | "target_name": "i1" 238 | }, 239 | { 240 | "source_nodeId": "1b5aa89f-a18b-4c63-8dbc-8963afc5eecb", 241 | "source_name": "value", 242 | "target_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 243 | "target_name": "y" 244 | }, 245 | { 246 | "source_nodeId": "320a2a28-79d8-4741-aded-9a8dc1c1d07f", 247 | "source_name": "value", 248 | "target_nodeId": "c86ba08e-ba9d-43c6-bf8b-6b2d27f188bb", 249 | "target_name": "x" 250 | } 251 | ] 252 | } 253 | -------------------------------------------------------------------------------- /examples/startup.json: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": [ 3 | { 4 | "y": 0.0, 5 | "x": 0.0, 6 | "class": "Integer", 7 | "uuid": "f5b94493-dfbd-4def-941d-f8727cfa8533" 8 | }, 9 | { 10 | "y": 250.0, 11 | "x": 100.0, 12 | "class": "Integer", 13 | "uuid": "fa6e28de-f5c5-4809-8feb-724f99cd88ff" 14 | }, 15 | { 16 | "y": 100.0, 17 | "x": 200.0, 18 | "class": "Multiply", 19 | "uuid": "cd63607b-a6ba-4ed4-80ac-2914c56b7151" 20 | }, 21 | { 22 | "y": 150.0, 23 | "x": 400.0, 24 | "class": "Output", 25 | "uuid": "31272aef-3b21-4b20-b81b-85297dc13fea" 26 | }, 27 | { 28 | "y": 50.0, 29 | "x": 300.0, 30 | "class": "BigNode", 31 | "uuid": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16" 32 | } 33 | ], 34 | "edges": [ 35 | { 36 | "source_nodeId": "f5b94493-dfbd-4def-941d-f8727cfa8533", 37 | "source_name": "value", 38 | "target_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 39 | "target_name": "x" 40 | }, 41 | { 42 | "source_nodeId": "fa6e28de-f5c5-4809-8feb-724f99cd88ff", 43 | "source_name": "value", 44 | "target_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 45 | "target_name": "y" 46 | }, 47 | { 48 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 49 | "source_name": "value", 50 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 51 | "target_name": "i1" 52 | }, 53 | { 54 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 55 | "source_name": "value", 56 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 57 | "target_name": "i2" 58 | }, 59 | { 60 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 61 | "source_name": "value", 62 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 63 | "target_name": "i3" 64 | }, 65 | { 66 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 67 | "source_name": "value", 68 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 69 | "target_name": "i4" 70 | }, 71 | { 72 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 73 | "source_name": "value", 74 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 75 | "target_name": "i5" 76 | }, 77 | { 78 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 79 | "source_name": "value", 80 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 81 | "target_name": "i6" 82 | }, 83 | { 84 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 85 | "source_name": "value", 86 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 87 | "target_name": "i7" 88 | }, 89 | { 90 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 91 | "source_name": "value", 92 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 93 | "target_name": "i8" 94 | }, 95 | { 96 | "source_nodeId": "cd63607b-a6ba-4ed4-80ac-2914c56b7151", 97 | "source_name": "value", 98 | "target_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 99 | "target_name": "i9" 100 | }, 101 | { 102 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 103 | "source_name": "o1", 104 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 105 | "target_name": "output" 106 | }, 107 | { 108 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 109 | "source_name": "o2", 110 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 111 | "target_name": "output" 112 | }, 113 | { 114 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 115 | "source_name": "o3", 116 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 117 | "target_name": "output" 118 | }, 119 | { 120 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 121 | "source_name": "o4", 122 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 123 | "target_name": "output" 124 | }, 125 | { 126 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 127 | "source_name": "o5", 128 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 129 | "target_name": "output" 130 | }, 131 | { 132 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 133 | "source_name": "o6", 134 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 135 | "target_name": "output" 136 | }, 137 | { 138 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 139 | "source_name": "o7", 140 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 141 | "target_name": "output" 142 | }, 143 | { 144 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 145 | "source_name": "o8", 146 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 147 | "target_name": "output" 148 | }, 149 | { 150 | "source_nodeId": "e04b58d6-925d-43e4-81dc-c1bfd54d1a16", 151 | "source_name": "o9", 152 | "target_nodeId": "31272aef-3b21-4b20-b81b-85297dc13fea", 153 | "target_name": "output" 154 | } 155 | ] 156 | } 157 | -------------------------------------------------------------------------------- /inspiration/3dsmax_mcg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/3dsmax_mcg.jpg -------------------------------------------------------------------------------- /inspiration/3dsmax_particle_view.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/3dsmax_particle_view.jpg -------------------------------------------------------------------------------- /inspiration/3dsmax_slate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/3dsmax_slate.jpg -------------------------------------------------------------------------------- /inspiration/audulus.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/audulus.jpg -------------------------------------------------------------------------------- /inspiration/blender.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/blender.png -------------------------------------------------------------------------------- /inspiration/blender_tex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/blender_tex.jpg -------------------------------------------------------------------------------- /inspiration/bloodhound.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/bloodhound.png -------------------------------------------------------------------------------- /inspiration/coral.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/coral.jpg -------------------------------------------------------------------------------- /inspiration/countdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/countdown.png -------------------------------------------------------------------------------- /inspiration/dreammaker.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/dreammaker.jpg -------------------------------------------------------------------------------- /inspiration/dynamo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/dynamo.png -------------------------------------------------------------------------------- /inspiration/flexmonkey.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/flexmonkey.PNG -------------------------------------------------------------------------------- /inspiration/flowlab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/flowlab.jpg -------------------------------------------------------------------------------- /inspiration/flowlab_noflo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/flowlab_noflo.jpg -------------------------------------------------------------------------------- /inspiration/fusion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/fusion.jpg -------------------------------------------------------------------------------- /inspiration/generative_components.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/generative_components.png -------------------------------------------------------------------------------- /inspiration/houdini.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/houdini.jpg -------------------------------------------------------------------------------- /inspiration/l.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/l.jpg -------------------------------------------------------------------------------- /inspiration/lightwave.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/lightwave.jpg -------------------------------------------------------------------------------- /inspiration/mamba_fx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/mamba_fx.png -------------------------------------------------------------------------------- /inspiration/maya_hypergraph.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/maya_hypergraph.jpg -------------------------------------------------------------------------------- /inspiration/minko_shaderlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/minko_shaderlab.png -------------------------------------------------------------------------------- /inspiration/modkit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/modkit.png -------------------------------------------------------------------------------- /inspiration/modo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/modo.jpg -------------------------------------------------------------------------------- /inspiration/natron.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/natron.jpg -------------------------------------------------------------------------------- /inspiration/neuromorphic_robotics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/neuromorphic_robotics.png -------------------------------------------------------------------------------- /inspiration/nodebox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/nodebox.png -------------------------------------------------------------------------------- /inspiration/noflo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/noflo.jpg -------------------------------------------------------------------------------- /inspiration/nuke.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/nuke.png -------------------------------------------------------------------------------- /inspiration/num3sis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/num3sis.png -------------------------------------------------------------------------------- /inspiration/pipes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/pipes.png -------------------------------------------------------------------------------- /inspiration/pypes.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/pypes.jpg -------------------------------------------------------------------------------- /inspiration/quadrigam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/quadrigam.png -------------------------------------------------------------------------------- /inspiration/quartz_composer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/quartz_composer.jpg -------------------------------------------------------------------------------- /inspiration/reactable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/reactable.png -------------------------------------------------------------------------------- /inspiration/scheme_bricks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/scheme_bricks.png -------------------------------------------------------------------------------- /inspiration/shake.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/shake.jpg -------------------------------------------------------------------------------- /inspiration/softimage_ice.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/softimage_ice.jpg -------------------------------------------------------------------------------- /inspiration/sql_server_integration.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/sql_server_integration.png -------------------------------------------------------------------------------- /inspiration/streamtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/streamtools.png -------------------------------------------------------------------------------- /inspiration/substance_designer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/substance_designer.jpg -------------------------------------------------------------------------------- /inspiration/thinkbox_magma.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/thinkbox_magma.png -------------------------------------------------------------------------------- /inspiration/touch_designer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/touch_designer.jpg -------------------------------------------------------------------------------- /inspiration/udk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/udk.png -------------------------------------------------------------------------------- /inspiration/udk_kismet.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/udk_kismet.jpg -------------------------------------------------------------------------------- /inspiration/unix_tools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/unix_tools.png -------------------------------------------------------------------------------- /inspiration/vis_trails.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/vis_trails.png -------------------------------------------------------------------------------- /inspiration/visual_jforex.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/visual_jforex.jpg -------------------------------------------------------------------------------- /inspiration/vrl_studio.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/vrl_studio.png -------------------------------------------------------------------------------- /inspiration/vuo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/vuo.png -------------------------------------------------------------------------------- /inspiration/vvvv.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/vvvv.png -------------------------------------------------------------------------------- /inspiration/world_machine.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/world_machine.jpg -------------------------------------------------------------------------------- /inspiration/xpresso.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cb109/qtnodes/63fd3e817d0f8942d9a0e1a11ac94055bb5ec444/inspiration/xpresso.jpg -------------------------------------------------------------------------------- /qtnodes/__init__.py: -------------------------------------------------------------------------------- 1 | """Users should import from here.""" 2 | 3 | from qtnodes.header import Header 4 | from qtnodes.node import Node 5 | from qtnodes.knob import InputKnob, OutputKnob 6 | 7 | from qtnodes.widget import NodeGraphWidget 8 | -------------------------------------------------------------------------------- /qtnodes/__main__.py: -------------------------------------------------------------------------------- 1 | """Manual tests.""" 2 | 3 | from PySide import QtGui 4 | 5 | from .knob import InputKnob, OutputKnob 6 | from .header import Header 7 | from .node import Node 8 | from .widget import NodeGraphWidget 9 | 10 | 11 | class Integer(Node): 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(Integer, self).__init__(*args, **kwargs) 15 | self.addHeader(Header(node=self, text="Int")) 16 | self.addKnob(OutputKnob(name="value")) 17 | # self.header.fillColor = QtGui.QColor(36, 128, 18) 18 | 19 | 20 | class Float(Node): 21 | 22 | def __init__(self, *args, **kwargs): 23 | super(Float, self).__init__(*args, **kwargs) 24 | self.addHeader(Header(node=self, text="Float")) 25 | self.addKnob(OutputKnob(name="value")) 26 | # self.header.fillColor = QtGui.QColor(24, 129, 163) 27 | 28 | 29 | class Multiply(Node): 30 | 31 | def __init__(self, *args, **kwargs): 32 | super(Multiply, self).__init__(*args, **kwargs) 33 | self.addHeader(Header(node=self, text=self.__class__.__name__)) 34 | self.addKnob(InputKnob(name="x")) 35 | self.addKnob(InputKnob(name="y")) 36 | self.addKnob(OutputKnob(name="value")) 37 | # self.header.fillColor = QtGui.QColor(163, 26, 159) 38 | 39 | 40 | class Divide(Multiply): 41 | 42 | def __init__(self, *args, **kwargs): 43 | super(Divide, self).__init__(*args, **kwargs) 44 | # self.header.fillColor = QtGui.QColor(26, 163, 159) 45 | 46 | 47 | class Add(Multiply): 48 | 49 | def __init__(self, *args, **kwargs): 50 | super(Add, self).__init__(*args, **kwargs) 51 | # self.header.fillColor = QtGui.QColor(105, 128, 23) 52 | 53 | 54 | class Subtract(Multiply): 55 | 56 | def __init__(self, *args, **kwargs): 57 | super(Subtract, self).__init__(*args, **kwargs) 58 | # self.header.fillColor = QtGui.QColor(23, 51, 128) 59 | 60 | 61 | class Output(Node): 62 | 63 | def __init__(self, *args, **kwargs): 64 | super(Output, self).__init__(*args, **kwargs) 65 | self.addHeader(Header(node=self, text="Output")) 66 | self.addKnob(InputKnob(name="output")) 67 | # self.header.fillColor = self.fillColor 68 | # self.header.textColor = QtGui.QColor(10, 10, 10) 69 | 70 | 71 | class BigNode(Node): 72 | 73 | def __init__(self, *args, **kwargs): 74 | super(BigNode, self).__init__(*args, **kwargs) 75 | self.addHeader(Header(node=self, text="BigNode")) 76 | self.addKnob(InputKnob(name="i1")) 77 | self.addKnob(OutputKnob(name="o1")) 78 | self.addKnob(InputKnob(name="i2")) 79 | self.addKnob(OutputKnob(name="o2")) 80 | self.addKnob(InputKnob(name="i3")) 81 | self.addKnob(OutputKnob(name="o3")) 82 | self.addKnob(InputKnob(name="i4")) 83 | self.addKnob(OutputKnob(name="o4")) 84 | self.addKnob(InputKnob(name="i5")) 85 | self.addKnob(OutputKnob(name="o5")) 86 | self.addKnob(InputKnob(name="i6")) 87 | self.addKnob(OutputKnob(name="o6")) 88 | self.addKnob(InputKnob(name="i7")) 89 | self.addKnob(OutputKnob(name="o7")) 90 | self.addKnob(InputKnob(name="i8")) 91 | self.addKnob(OutputKnob(name="o8")) 92 | self.addKnob(InputKnob(name="i9")) 93 | self.addKnob(OutputKnob(name="o9")) 94 | 95 | 96 | class Directory(Node): 97 | 98 | def __init__(self, *args, **kwargs): 99 | super(Directory, self).__init__(*args, **kwargs) 100 | self.addHeader(Header(node=self, text="Directory")) 101 | self.addKnob(InputKnob(name="parent")) 102 | self.addKnob(OutputKnob(name="children")) 103 | 104 | 105 | class File(Node): 106 | 107 | def __init__(self, *args, **kwargs): 108 | super(File, self).__init__(*args, **kwargs) 109 | self.addHeader(Header(node=self, text="File")) 110 | self.addKnob(InputKnob(name="parent")) 111 | 112 | 113 | class MaxObject(Node): 114 | 115 | def __init__(self): 116 | super(MaxObject, self).__init__() 117 | self.addHeader(Header(node=self, text=self.__class__.__name__)) 118 | self.addKnob(InputKnob(name="children")) 119 | self.addKnob(OutputKnob(name="parent")) 120 | 121 | 122 | def test(): 123 | app = QtGui.QApplication([]) 124 | 125 | graph = NodeGraphWidget() 126 | graph.setGeometry(100, 100, 800, 600) 127 | graph.show() 128 | 129 | graph.registerNodeClass(Integer) 130 | graph.registerNodeClass(Float) 131 | graph.registerNodeClass(Multiply) 132 | graph.registerNodeClass(Divide) 133 | graph.registerNodeClass(Add) 134 | graph.registerNodeClass(Subtract) 135 | graph.registerNodeClass(Output) 136 | graph.registerNodeClass(BigNode) 137 | graph.registerNodeClass(File) 138 | graph.registerNodeClass(Directory) 139 | graph.registerNodeClass(MaxObject) 140 | 141 | nodeInt1 = Integer(scene=graph.scene) 142 | nodeInt2 = Integer(scene=graph.scene) 143 | nodeMult = Multiply(scene=graph.scene) 144 | nodeOut = Output(scene=graph.scene) 145 | nodeBig = BigNode(scene=graph.scene) 146 | 147 | nodeInt2.moveBy(100, 250) 148 | nodeMult.moveBy(200, 100) 149 | nodeBig.moveBy(300, 50) 150 | nodeOut.moveBy(400, 150) 151 | 152 | nodeInt1.knob("value").connectTo(nodeMult.knob("x")) 153 | nodeInt2.knob("value").connectTo(nodeMult.knob("y")) 154 | 155 | nodeMult.knob("value").connectTo(nodeBig.knob("i1")) 156 | nodeMult.knob("value").connectTo(nodeBig.knob("i2")) 157 | nodeMult.knob("value").connectTo(nodeBig.knob("i3")) 158 | nodeMult.knob("value").connectTo(nodeBig.knob("i4")) 159 | nodeMult.knob("value").connectTo(nodeBig.knob("i5")) 160 | nodeMult.knob("value").connectTo(nodeBig.knob("i6")) 161 | nodeMult.knob("value").connectTo(nodeBig.knob("i7")) 162 | nodeMult.knob("value").connectTo(nodeBig.knob("i8")) 163 | nodeMult.knob("value").connectTo(nodeBig.knob("i9")) 164 | 165 | nodeBig.knob("o1").connectTo(nodeOut.knob("output")) 166 | nodeBig.knob("o2").connectTo(nodeOut.knob("output")) 167 | nodeBig.knob("o3").connectTo(nodeOut.knob("output")) 168 | nodeBig.knob("o4").connectTo(nodeOut.knob("output")) 169 | nodeBig.knob("o5").connectTo(nodeOut.knob("output")) 170 | nodeBig.knob("o6").connectTo(nodeOut.knob("output")) 171 | nodeBig.knob("o7").connectTo(nodeOut.knob("output")) 172 | nodeBig.knob("o8").connectTo(nodeOut.knob("output")) 173 | nodeBig.knob("o9").connectTo(nodeOut.knob("output")) 174 | 175 | app.exec_() 176 | 177 | 178 | if __name__ == '__main__': 179 | test() 180 | 181 | 182 | """ 183 | todos 184 | 185 | - bug: sometimes a node's background rectangle is not cleared when deleting nodes 186 | 187 | - make graphviz layouting aware of actual node width and height 188 | - decouple identifier and display name in all items, so it can be changed 189 | - edit nodes: possibly like in nuke, with an extra floating widget or a sidebar 190 | - attach data to nodes and let them modify it: callbacks? custom signals? 191 | - evaluate the graph, graph traversal, show some values in the ui? 192 | - global settings, like 'restrict user from editing Edges' 193 | - tests! 194 | 195 | """ 196 | -------------------------------------------------------------------------------- /qtnodes/edge.py: -------------------------------------------------------------------------------- 1 | """Connections between Knobs.""" 2 | 3 | import os 4 | 5 | from PySide import QtGui 6 | from PySide import QtCore 7 | 8 | 9 | windows = os.name == "nt" 10 | 11 | DELETE_MODIFIER_KEY = QtCore.Qt.AltModifier if windows else QtCore.Qt.ControlModifier 12 | 13 | 14 | class Edge(QtGui.QGraphicsPathItem): 15 | """A connection between two Knobs.""" 16 | 17 | def __init__(self, **kwargs): 18 | super(Edge, self).__init__(**kwargs) 19 | 20 | self.lineColor = QtGui.QColor(10, 10, 10) 21 | self.removalColor = QtCore.Qt.red 22 | self.thickness = 1 23 | 24 | self.source = None # A Knob. 25 | self.target = None # A Knob. 26 | 27 | self.sourcePos = QtCore.QPointF(0, 0) 28 | self.targetPos = QtCore.QPointF(0, 0) 29 | 30 | self.curv1 = 0.6 31 | self.curv3 = 0.4 32 | 33 | self.curv2 = 0.2 34 | self.curv4 = 0.8 35 | 36 | self.setAcceptHoverEvents(True) 37 | 38 | def mousePressEvent(self, event): 39 | """Delete Edge if icon is clicked with DELETE_MODIFIER_KEY pressed.""" 40 | leftMouse = event.button() == QtCore.Qt.MouseButton.LeftButton 41 | mod = event.modifiers() == DELETE_MODIFIER_KEY 42 | if leftMouse and mod: 43 | self.destroy() 44 | 45 | def updatePath(self): 46 | """Adjust current shape based on Knobs and curvature settings.""" 47 | if self.source: 48 | self.sourcePos = self.source.mapToScene( 49 | self.source.boundingRect().center()) 50 | 51 | if self.target: 52 | self.targetPos = self.target.mapToScene( 53 | self.target.boundingRect().center()) 54 | 55 | path = QtGui.QPainterPath() 56 | path.moveTo(self.sourcePos) 57 | 58 | dx = self.targetPos.x() - self.sourcePos.x() 59 | dy = self.targetPos.y() - self.sourcePos.y() 60 | 61 | ctrl1 = QtCore.QPointF(self.sourcePos.x() + dx * self.curv1, 62 | self.sourcePos.y() + dy * self.curv2) 63 | ctrl2 = QtCore.QPointF(self.sourcePos.x() + dx * self.curv3, 64 | self.sourcePos.y() + dy * self.curv4) 65 | path.cubicTo(ctrl1, ctrl2, self.targetPos) 66 | self.setPath(path) 67 | 68 | def paint(self, painter, option, widget): 69 | """Paint Edge color depending on modifier key pressed or not.""" 70 | mod = QtGui.QApplication.keyboardModifiers() == DELETE_MODIFIER_KEY 71 | if mod: 72 | self.setPen(QtGui.QPen(self.removalColor, self.thickness)) 73 | else: 74 | self.setPen(QtGui.QPen(self.lineColor, self.thickness)) 75 | 76 | self.setBrush(QtCore.Qt.NoBrush) 77 | self.setZValue(-1) 78 | super(Edge, self).paint(painter, option, widget) 79 | 80 | def destroy(self): 81 | """Remove this Edge and its reference in other objects.""" 82 | print("destroy edge:", self) 83 | if self.source: 84 | self.source.removeEdge(self) 85 | if self.target: 86 | self.target.removeEdge(self) 87 | del self 88 | -------------------------------------------------------------------------------- /qtnodes/exceptions.py: -------------------------------------------------------------------------------- 1 | """Custom exceptions.""" 2 | 3 | 4 | class QtNodesError(Exception): 5 | """Base custom exception.""" 6 | 7 | 8 | class UnregisteredNodeClassError(QtNodesError): 9 | """The Node class is not registered.""" 10 | 11 | 12 | class UnknownFlowError(QtNodesError): 13 | """The flow style can not be recognized.""" 14 | 15 | 16 | class KnobConnectionError(QtNodesError): 17 | """Something went wrong while trying to connect two Knobs.""" 18 | 19 | 20 | class DuplicateKnobNameError(QtNodesError): 21 | """A Node's Knobs must have unique names.""" -------------------------------------------------------------------------------- /qtnodes/header.py: -------------------------------------------------------------------------------- 1 | """Node header.""" 2 | 3 | from PySide import QtGui 4 | from PySide import QtCore 5 | 6 | from .helpers import getTextSize 7 | 8 | 9 | class Header(QtGui.QGraphicsItem): 10 | """A Header is a child of a Node and gives it a title. 11 | 12 | Its width resizes automatically to match the Node's width. 13 | """ 14 | def __init__(self, node, text, **kwargs): 15 | super(Header, self).__init__(**kwargs) 16 | self.node = node 17 | self.text = text 18 | self.h = 20 19 | self.fillColor = QtGui.QColor(90, 90, 90) 20 | self.textColor = QtGui.QColor(240, 240, 240) 21 | 22 | def boundingRect(self): 23 | nodebox = self.node.boundingRect() 24 | rect = QtCore.QRect(self.x(), 25 | self.y(), 26 | nodebox.width(), 27 | self.h) 28 | return rect 29 | 30 | def paint(self, painter, option, widget): 31 | # Draw background rectangle. 32 | bbox = self.boundingRect() 33 | 34 | painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) 35 | painter.setBrush(self.fillColor) 36 | painter.drawRoundedRect(bbox, 37 | self.node.roundness, 38 | self.node.roundness) 39 | 40 | # Draw header label. 41 | if self.node.isSelected(): 42 | painter.setPen(QtGui.QPen(QtGui.QColor(255, 255, 0))) 43 | else: 44 | painter.setPen(QtGui.QPen(self.textColor)) 45 | 46 | # # centered text 47 | # textSize = getTextSize(painter, self.text) 48 | # painter.drawText(self.x() + (self.node.w - textSize.width()) / 2, 49 | # self.y() + (self.h + textSize.height() / 2) / 2, 50 | # self.text) 51 | 52 | # left aligned text 53 | textSize = getTextSize(self.text, painter=painter) 54 | painter.drawText(self.x() + self.node.margin, 55 | self.y() + (self.h + textSize.height() / 2) / 2, 56 | self.text) 57 | 58 | def destroy(self): 59 | """Remove this object from the scene and delete it.""" 60 | print("destroy header:", self) 61 | scene = self.node.scene() 62 | scene.removeItem(self) 63 | del self 64 | -------------------------------------------------------------------------------- /qtnodes/helpers.py: -------------------------------------------------------------------------------- 1 | """Various helper functions.""" 2 | 3 | import json 4 | 5 | from PySide import QtGui 6 | from PySide import QtCore 7 | 8 | 9 | def readFileContent(filePath): 10 | """Return the content of the file.""" 11 | with open(filePath) as f: 12 | return f.read() 13 | 14 | 15 | def toJson(serialized): 16 | """Return JSON string from given native Python datatypes.""" 17 | return json.dumps(serialized, encoding="utf-8", indent=4) 18 | 19 | 20 | def fromJson(jsonString): 21 | """Return native Python datatypes from JSON string.""" 22 | return json.loads(jsonString, encoding="utf-8") 23 | 24 | 25 | def getTextSize(text, painter=None): 26 | """Return a QSize based on given string. 27 | 28 | If no painter is supplied, the font metrics are based on a default 29 | QPainter, which may be off depending on the font und text size used. 30 | """ 31 | if not painter: 32 | metrics = QtGui.QFontMetrics(QtGui.QFont()) 33 | else: 34 | metrics = painter.fontMetrics() 35 | size = metrics.size(QtCore.Qt.TextSingleLine, text) 36 | return size 37 | -------------------------------------------------------------------------------- /qtnodes/knob.py: -------------------------------------------------------------------------------- 1 | """Knob classes.""" 2 | 3 | from PySide import QtGui 4 | from PySide import QtCore 5 | 6 | from .helpers import getTextSize 7 | from .exceptions import KnobConnectionError, UnknownFlowError 8 | from .edge import Edge 9 | 10 | 11 | # Currently only affects Knob label placement. 12 | FLOW_LEFT_TO_RIGHT = "flow_left_to_right" 13 | FLOW_RIGHT_TO_LEFT = "flow_right_to_left" 14 | 15 | 16 | class Knob(QtGui.QGraphicsItem): 17 | """A Knob is a socket of a Node and can be connected to other Knobs.""" 18 | 19 | def __init__(self, **kwargs): 20 | super(Knob, self).__init__(**kwargs) 21 | self.x = 0 22 | self.y = 0 23 | self.w = 10 24 | self.h = 10 25 | 26 | self.margin = 5 27 | self.flow = FLOW_LEFT_TO_RIGHT 28 | 29 | self.maxConnections = -1 # A negative value means 'unlimited'. 30 | 31 | self.name = "value" 32 | self.displayName = self.name 33 | 34 | self.labelColor = QtGui.QColor(10, 10, 10) 35 | self.fillColor = QtGui.QColor(130, 130, 130) 36 | self.highlightColor = QtGui.QColor(255, 255, 0) 37 | 38 | # Temp store for Edge currently being created. 39 | self.newEdge = None 40 | 41 | self.edges = [] 42 | 43 | self.setAcceptHoverEvents(True) 44 | 45 | def node(self): 46 | """The Node that this Knob belongs to is its parent item.""" 47 | return self.parentItem() 48 | 49 | def connectTo(self, knob): 50 | """Convenience method to connect this to another Knob. 51 | 52 | This creates an Edge and directly connects it, in contrast to the mouse 53 | events that first create an Edge temporarily and only connect if the 54 | user releases on a valid target Knob. 55 | """ 56 | if knob is self: 57 | return 58 | 59 | self.checkMaxConnections(knob) 60 | 61 | edge = Edge() 62 | edge.source = self 63 | edge.target = knob 64 | 65 | self.addEdge(edge) 66 | knob.addEdge(edge) 67 | 68 | edge.updatePath() 69 | 70 | def addEdge(self, edge): 71 | """Add the given Edge to the internal tracking list. 72 | 73 | This is only one part of the Knob connection procedure. It enables us to 74 | later traverse the whole graph and to see how many connections there 75 | currently are. 76 | 77 | Also make sure it is added to the QGraphicsScene, if not yet done. 78 | """ 79 | self.edges.append(edge) 80 | scene = self.scene() 81 | if edge not in scene.items(): 82 | scene.addItem(edge) 83 | 84 | def removeEdge(self, edge): 85 | """Remove th given Edge from the internal tracking list. 86 | 87 | If it is unknown, do nothing. Also remove it from the QGraphicsScene. 88 | """ 89 | self.edges.remove(edge) 90 | scene = self.scene() 91 | if edge in scene.items(): 92 | scene.removeItem(edge) 93 | 94 | def boundingRect(self): 95 | """Return the bounding box of this Knob.""" 96 | rect = QtCore.QRect(self.x, 97 | self.y, 98 | self.w, 99 | self.h) 100 | return rect 101 | 102 | def highlight(self, toggle): 103 | """Toggle the highlight color on/off. 104 | 105 | Store the old color in a new attribute, so it can be restored. 106 | """ 107 | if toggle: 108 | self._oldFillColor = self.fillColor 109 | self.fillColor = self.highlightColor 110 | else: 111 | self.fillColor = self._oldFillColor 112 | 113 | def paint(self, painter, option, widget): 114 | """Draw the Knob's shape and label.""" 115 | bbox = self.boundingRect() 116 | 117 | # Draw a filled rectangle. 118 | painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) 119 | painter.setBrush(QtGui.QBrush(self.fillColor)) 120 | painter.drawRect(bbox) 121 | 122 | # Draw a text label next to it. Position depends on the flow. 123 | textSize = getTextSize(self.displayName, painter=painter) 124 | if self.flow == FLOW_LEFT_TO_RIGHT: 125 | x = bbox.right() + self.margin 126 | elif self.flow == FLOW_RIGHT_TO_LEFT: 127 | x = bbox.left() - self.margin - textSize.width() 128 | else: 129 | raise UnknownFlowError( 130 | "Flow not recognized: {0}".format(self.flow)) 131 | y = bbox.bottom() 132 | 133 | painter.setPen(QtGui.QPen(self.labelColor)) 134 | painter.drawText(x, y, self.displayName) 135 | 136 | def hoverEnterEvent(self, event): 137 | """Change the Knob's rectangle color.""" 138 | self.highlight(True) 139 | super(Knob, self).hoverEnterEvent(event) 140 | 141 | def hoverLeaveEvent(self, event): 142 | """Change the Knob's rectangle color.""" 143 | self.highlight(False) 144 | super(Knob, self).hoverLeaveEvent(event) 145 | 146 | def mousePressEvent(self, event): 147 | """Handle Edge creation.""" 148 | if event.button() == QtCore.Qt.MouseButton.LeftButton: 149 | print("create edge") 150 | 151 | self.newEdge = Edge() 152 | self.newEdge.source = self 153 | self.newEdge.targetPos = event.scenePos() 154 | self.newEdge.updatePath() 155 | 156 | # Make sure this is removed if the user cancels. 157 | self.addEdge(self.newEdge) 158 | return 159 | 160 | def mouseMoveEvent(self, event): 161 | """Update Edge position when currently creating one.""" 162 | if self.newEdge: 163 | print("update edge") 164 | self.newEdge.targetPos = event.scenePos() 165 | self.newEdge.updatePath() 166 | 167 | def mouseReleaseEvent(self, event): 168 | """Finish Edge creation (if validations are passed). 169 | 170 | TODO: This currently implements some constraints regarding the Knob 171 | connection logic, for which we should probably have a more 172 | flexible approach. 173 | """ 174 | if event.button() == QtCore.Qt.MouseButton.LeftButton: 175 | 176 | node = self.parentItem() 177 | scene = node.scene() 178 | target = scene.itemAt(event.scenePos()) 179 | 180 | try: 181 | if self.newEdge and target: 182 | 183 | if self.newEdge.source is target: 184 | raise KnobConnectionError( 185 | "Can't connect a Knob to itself.") 186 | 187 | if isinstance(target, Knob): 188 | 189 | if type(self) == type(target): 190 | raise KnobConnectionError( 191 | "Can't connect Knobs of same type.") 192 | 193 | newConn = set([self, target]) 194 | for edge in self.edges: 195 | existingConn = set([edge.source, edge.target]) 196 | diff = existingConn.difference(newConn) 197 | if not diff: 198 | raise KnobConnectionError( 199 | "Connection already exists.") 200 | return 201 | 202 | self.checkMaxConnections(target) 203 | 204 | print("finish edge") 205 | target.addEdge(self.newEdge) 206 | self.newEdge.target = target 207 | self.newEdge.updatePath() 208 | self.finalizeEdge(self.newEdge) 209 | self.newEdge = None 210 | return 211 | 212 | raise KnobConnectionError( 213 | "Edge creation cancelled by user.") 214 | 215 | except KnobConnectionError as err: 216 | print(err) 217 | # Abort Edge creation and do some cleanup. 218 | self.removeEdge(self.newEdge) 219 | self.newEdge = None 220 | 221 | def checkMaxConnections(self, knob): 222 | """Check if both this and the target Knob do accept another connection. 223 | 224 | Raise a KnobConnectionError if not. 225 | """ 226 | noLimits = self.maxConnections < 0 and knob.maxConnections < 0 227 | if noLimits: 228 | return 229 | 230 | numSourceConnections = len(self.edges) # Edge already added. 231 | numTargetConnections = len(knob.edges) + 1 232 | 233 | print(numSourceConnections, numTargetConnections) 234 | 235 | sourceMaxReached = numSourceConnections > self.maxConnections 236 | targetMaxReached = numTargetConnections > knob.maxConnections 237 | 238 | if sourceMaxReached or targetMaxReached: 239 | raise KnobConnectionError( 240 | "Maximum number of connections reached.") 241 | 242 | def finalizeEdge(self, edge): 243 | """This intentionally is a NoOp on the Knob baseclass. 244 | 245 | It is meant for subclass Knobs to implement special behaviour 246 | that needs to be considered when connecting two Knobs. 247 | """ 248 | pass 249 | 250 | def destroy(self): 251 | """Remove this Knob, its Edges and associations.""" 252 | print("destroy knob:", self) 253 | edgesToDelete = self.edges[::] # Avoid shrinking during deletion. 254 | for edge in edgesToDelete: 255 | edge.destroy() 256 | node = self.parentItem() 257 | if node: 258 | node.removeKnob(self) 259 | 260 | self.scene().removeItem(self) 261 | del self 262 | 263 | 264 | def ensureEdgeDirection(edge): 265 | """Make sure the Edge direction is as described below. 266 | 267 | .source --> .target 268 | OutputKnob --> InputKnob 269 | 270 | Which basically translates to: 271 | 272 | 'The Node with the OutputKnob is the child of the Node with the InputKnob.' 273 | 274 | This may seem the exact opposite way as expected, but makes sense 275 | when seen as a hierarchy: A Node which output depends on some other 276 | Node's input can be seen as a *child* of the other Node. We need 277 | that information to build a directed graph. 278 | 279 | We assume here that there always is an InputKnob and an OutputKnob 280 | in the given Edge, just their order may be wrong. Since the 281 | serialization relies on that order, it is enforced here. 282 | """ 283 | print("ensure edge direction") 284 | if isinstance(edge.target, OutputKnob): 285 | assert isinstance(edge.source, InputKnob) 286 | actualTarget = edge.source 287 | edge.source = edge.target 288 | edge.target = actualTarget 289 | else: 290 | assert isinstance(edge.source, OutputKnob) 291 | assert isinstance(edge.target, InputKnob) 292 | 293 | print("src:", edge.source.__class__.__name__, 294 | "trg:", edge.target.__class__.__name__) 295 | 296 | 297 | class InputKnob(Knob): 298 | """A Knob that represents an input value for its Node.""" 299 | 300 | def __init__(self, *args, **kwargs): 301 | super(InputKnob, self).__init__(*args, **kwargs) 302 | self.name = kwargs.get("name", "input") 303 | self.displayName = kwargs.get("displayName", self.name) 304 | self.fillColor = kwargs.get("fillColor", QtGui.QColor(130, 230, 130)) 305 | 306 | def finalizeEdge(self, edge): 307 | ensureEdgeDirection(edge) 308 | 309 | 310 | class OutputKnob(Knob): 311 | """A Knob that represents an output value for its Node.""" 312 | 313 | def __init__(self, *args, **kwargs): 314 | super(OutputKnob, self).__init__(*args, **kwargs) 315 | self.name = kwargs.get("name", "output") 316 | self.displayName = kwargs.get("displayName", self.name) 317 | self.fillColor = kwargs.get("fillColor", QtGui.QColor(230, 130, 130)) 318 | self.flow = kwargs.get("flow", FLOW_RIGHT_TO_LEFT) 319 | 320 | def finalizeEdge(self, edge): 321 | ensureEdgeDirection(edge) 322 | -------------------------------------------------------------------------------- /qtnodes/layout.py: -------------------------------------------------------------------------------- 1 | """Automatic tree layouting.""" 2 | import os 3 | import re 4 | 5 | from .node import Node 6 | 7 | # Need to have graphviz installed (its bin/ must be on PATH). 8 | import pydot 9 | import appdirs 10 | 11 | 12 | class Tree(object): 13 | """A wrapper for each Node, solely for layouting purposes.""" 14 | 15 | def __init__(self, node): 16 | self.node = node 17 | self.parents = [] 18 | self.children = [] 19 | 20 | 21 | def _getNodesFromScene(scene): 22 | """Return all Node items of the given QGraphicsScene.""" 23 | nodes = [i for i in scene.items() if isinstance(i, Node)] 24 | return nodes 25 | 26 | 27 | def _makeTree(nodes): 28 | """Return a list of Trees that represent the Node hierarchy.""" 29 | node2tree = {} 30 | for node in nodes: 31 | node2tree[node] = Tree(node) 32 | 33 | for node, tree in node2tree.items(): 34 | for knob in node.knobs(): 35 | for edge in knob.edges: 36 | sourceNode = edge.source.node() 37 | targetNode = edge.target.node() 38 | 39 | if node == sourceNode: 40 | targetTree = node2tree[targetNode] 41 | if targetTree not in tree.parents: 42 | tree.parents.append(targetTree) 43 | 44 | if node == targetNode: 45 | sourceTree = node2tree[sourceNode] 46 | if sourceTree not in tree.children: 47 | tree.children.append(sourceTree) 48 | 49 | trees = node2tree.values() 50 | return trees 51 | 52 | 53 | def autoLayout(scene): 54 | """Tree layout using graphviz. 55 | 56 | Code based on this example: https://gist.github.com/dbr/1255776 57 | """ 58 | print("auto layout") 59 | 60 | nodes = _getNodesFromScene(scene) 61 | if not nodes: 62 | return 63 | 64 | trees = _makeTree(nodes) 65 | 66 | class Dotter(object): 67 | """Walk the Tree hierarchy and write it to a .dot file.""" 68 | 69 | delim = "_" 70 | 71 | def __init__(self): 72 | self.graph = pydot.Dot(graph_type="digraph", rankdir="LR") 73 | self.checked = [] 74 | self.counter = 0 75 | 76 | def nodeToName(self, node): 77 | # FIXME: Using _ has a chance 78 | # of name clashes which gets higher the more nodes we 79 | # have. We should use the full uuid as identifier, but 80 | # make sure graphviz does not use it as the node width 81 | # when doing its layouting. Right now that would result 82 | # in graphs that are very far spaced out. 83 | return (node.header.text + self.delim + node.uuid[:4]) 84 | 85 | def recursiveGrapher(self, tree, level=0): 86 | self.counter += 1 87 | 88 | if tree in self.checked: 89 | # Don't redo parts of tree already checked. 90 | return 91 | 92 | self.checked.append(tree) 93 | 94 | for childTree in tree.children: 95 | childName = self.nodeToName(childTree.node) 96 | parentName = self.nodeToName(tree.node) 97 | edge = pydot.Edge(childName, parentName) 98 | self.graph.add_edge(edge) 99 | print ("{0} Recursing into {1}".format( 100 | level, childName)) 101 | self.recursiveGrapher(childTree, level=level + 1) 102 | 103 | def save(self, filePath): 104 | """Writing the graph to file will apply graphviz' layouting.""" 105 | # TODO: We can use 'dot' or 'neato', however 'neato' 106 | # currently produces pretty bad results (probably related 107 | # to .Dot() settings above, may be worth looking into it.) 108 | self.graph.write_dot(filePath, prog="dot") 109 | 110 | def assignDotResultToNodes(dotFile, nodes): 111 | """Read positions from file and apply them.""" 112 | 113 | # TODO: Use pydot's pydot_parser.py instead. 114 | # Extract the node name and its x and y position. 115 | pattern = r"(?P[\"]{0,1}[a-zA-Z0-9_-]+[\"]{0,1})\s*\[\w+\=(?:\d+(?:\.\d+){0,1})\,\s*pos\=\"(?P\d+(?:\.\d+){0,1})\,(?P\d+(?:\.\d+){0,1})" # noqa 116 | 117 | with open(dotFile) as f: 118 | text = f.read() 119 | 120 | name2node = {} 121 | for node in nodes: 122 | name = dotter.nodeToName(node) 123 | name2node[name] = node 124 | 125 | matches = re.findall(pattern, text) 126 | for name, x, y in matches: 127 | node = name2node[name] 128 | node.setPos(float(x), float(y)) 129 | 130 | dataDir = os.path.join(appdirs.user_data_dir(), "qtnodes") 131 | try: 132 | os.makedirs(dataDir) 133 | except OSError: 134 | if not os.path.isdir(dataDir): 135 | raise 136 | dotFile = os.path.join(dataDir, "last_layout.dot") 137 | print("writing layout to", dotFile) 138 | 139 | dotter = Dotter() 140 | for tree in trees: 141 | dotter.recursiveGrapher(tree) 142 | 143 | dotter.save(dotFile) 144 | assignDotResultToNodes(dotFile, nodes) 145 | -------------------------------------------------------------------------------- /qtnodes/node.py: -------------------------------------------------------------------------------- 1 | """Node classes.""" 2 | 3 | import uuid 4 | 5 | from PySide import QtGui 6 | from PySide import QtCore 7 | 8 | from .helpers import getTextSize 9 | from .knob import Knob, InputKnob, OutputKnob 10 | from .exceptions import DuplicateKnobNameError 11 | 12 | 13 | class Node(QtGui.QGraphicsItem): 14 | """A Node is a container for a header and 0-n Knobs. 15 | 16 | It can be created, removed and modified by the user in the UI. 17 | """ 18 | def __init__(self, **kwargs): 19 | super(Node, self).__init__(**kwargs) 20 | 21 | # This unique id is useful for serialization/reconstruction. 22 | self.uuid = str(uuid.uuid4()) 23 | 24 | self.header = None 25 | 26 | self.x = 0 27 | self.y = 0 28 | self.w = 10 29 | self.h = 10 30 | 31 | self.margin = 6 32 | self.roundness = 0 33 | 34 | self.fillColor = QtGui.QColor(220, 220, 220) 35 | 36 | # General configuration. 37 | self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable) 38 | self.setFlag(QtGui.QGraphicsItem.ItemIsMovable) 39 | 40 | self.setCursor(QtCore.Qt.SizeAllCursor) 41 | 42 | self.setAcceptHoverEvents(True) 43 | self.setAcceptTouchEvents(True) 44 | self.setAcceptDrops(True) 45 | 46 | def knobs(self, cls=None): 47 | """Return a list of childItems that are Knob objects. 48 | 49 | If the optional `cls` is specified, return only Knobs of that class. 50 | This is useful e.g. to get all InputKnobs or OutputKnobs. 51 | """ 52 | knobs = [] 53 | for child in self.childItems(): 54 | if isinstance(child, Knob): 55 | knobs.append(child) 56 | 57 | if cls: 58 | knobs = filter(knobs, lambda k: k.__class__ is cls) 59 | 60 | return knobs 61 | 62 | def knob(self, name): 63 | """Return matching Knob by its name, None otherwise.""" 64 | for knob in self.knobs(): 65 | if knob.name == name: 66 | return knob 67 | return None 68 | 69 | def boundingRect(self): 70 | """Return the bounding box of the Node, limited in height to its Header. 71 | 72 | This is so that the drag & drop sensitive area for the Node is only 73 | active when hovering its Header, as otherwise there would be conflicts 74 | with the hover events for the Node's Knobs. 75 | """ 76 | rect = QtCore.QRect(self.x, 77 | self.y, 78 | self.w, 79 | self.header.h) 80 | return rect 81 | 82 | def updateSizeForChildren(self): 83 | """Adjust width and height as needed for header and knobs.""" 84 | 85 | def adjustHeight(): 86 | """Adjust height to fit header and all knobs.""" 87 | knobs = [c for c in self.childItems() if isinstance(c, Knob)] 88 | knobsHeight = sum([k.h + self.margin for k in knobs]) 89 | heightNeeded = self.header.h + knobsHeight + self.margin 90 | self.h = heightNeeded 91 | 92 | def adjustWidth(): 93 | """Adjust width as needed for the widest child item.""" 94 | headerWidth = (self.margin + getTextSize(self.header.text).width()) 95 | 96 | knobs = [c for c in self.childItems() if isinstance(c, Knob)] 97 | knobWidths = [k.w + self.margin + getTextSize(k.displayName).width() 98 | for k in knobs] 99 | maxWidth = max([headerWidth] + knobWidths) 100 | self.w = maxWidth + self.margin 101 | 102 | adjustWidth() 103 | adjustHeight() 104 | 105 | def addHeader(self, header): 106 | """Assign the given header and adjust the Node's size for it.""" 107 | self.header = header 108 | header.setPos(self.pos()) 109 | header.setParentItem(self) 110 | self.updateSizeForChildren() 111 | 112 | def addKnob(self, knob): 113 | """Add the given Knob to this Node. 114 | 115 | A Knob must have a unique name, meaning there can be no duplicates within 116 | a Node (the displayNames are not constrained though). 117 | 118 | Assign ourselves as the Knob's parent item (which also will put it onto 119 | the current scene, if not yet done) and adjust or size for it. 120 | 121 | The position of the Knob is set relative to this Node and depends on it 122 | either being an Input- or OutputKnob. 123 | """ 124 | knobNames = [k.name for k in self.knobs()] 125 | if knob.name in knobNames: 126 | raise DuplicateKnobNameError( 127 | "Knob names must be unique, but {0} already exists." 128 | .format(knob.name)) 129 | 130 | children = [c for c in self.childItems()] 131 | yOffset = sum([c.h + self.margin for c in children]) 132 | xOffset = self.margin / 2 133 | 134 | knob.setParentItem(self) 135 | knob.margin = self.margin 136 | self.updateSizeForChildren() 137 | 138 | bbox = self.boundingRect() 139 | if isinstance(knob, OutputKnob): 140 | knob.setPos(bbox.right() - knob.w + xOffset, yOffset) 141 | elif isinstance(knob, InputKnob): 142 | knob.setPos(bbox.left() - xOffset, yOffset) 143 | 144 | def removeKnob(self, knob): 145 | """Remove the Knob reference to this node and resize.""" 146 | knob.setParentItem(None) 147 | self.updateSizeForChildren() 148 | 149 | def paint(self, painter, option, widget): 150 | """Draw the Node's container rectangle.""" 151 | painter.setBrush(QtGui.QBrush(self.fillColor)) 152 | painter.setPen(QtGui.QPen(QtCore.Qt.NoPen)) 153 | 154 | # The bounding box is only as high as the header (we do this 155 | # to limit the area that is drag-enabled). Accommodate for that. 156 | bbox = self.boundingRect() 157 | painter.drawRoundedRect(self.x, 158 | self.y, 159 | bbox.width(), 160 | self.h, 161 | self.roundness, 162 | self.roundness) 163 | 164 | def mouseMoveEvent(self, event): 165 | """Update selected item's (and children's) positions as needed. 166 | 167 | We assume here that only Nodes can be selected. 168 | 169 | We cannot just update our own childItems, since we are using 170 | RubberBandDrag, and that would lead to otherwise e.g. Edges 171 | visually lose their connection until an attached Node is moved 172 | individually. 173 | """ 174 | nodes = self.scene().selectedItems() 175 | for node in nodes: 176 | for knob in node.knobs(): 177 | for edge in knob.edges: 178 | edge.updatePath() 179 | super(Node, self).mouseMoveEvent(event) 180 | 181 | def destroy(self): 182 | """Remove this Node, its Header, Knobs and connected Edges.""" 183 | print("destroy node:", self) 184 | self.header.destroy() 185 | for knob in self.knobs(): 186 | knob.destroy() 187 | 188 | scene = self.scene() 189 | scene.removeItem(self) 190 | del self 191 | -------------------------------------------------------------------------------- /qtnodes/serializer.py: -------------------------------------------------------------------------------- 1 | """Serialization and deserialization of the graph.""" 2 | 3 | import uuid 4 | import re 5 | 6 | from .node import Node 7 | from .edge import Edge 8 | from .helpers import fromJson, toJson, readFileContent 9 | from .exceptions import UnregisteredNodeClassError 10 | 11 | 12 | def serializeEdge(edge): 13 | """Return the Edge as native Python datatypes, fit for json. 14 | 15 | Edges only reference Knobs, so we have to store their Node id here 16 | to be able to correctly associate it once reconstructed. 17 | """ 18 | return { 19 | "source_nodeId": edge.source.node().uuid, 20 | "source_name": edge.source.name, 21 | 22 | "target_nodeId": edge.target.node().uuid, 23 | "target_name": edge.target.name, 24 | } 25 | 26 | 27 | def serializeNode(node): 28 | """Return the Node as native Python datatypes, fit for json.""" 29 | return { 30 | "class": node.__class__.__name__, 31 | "uuid": node.uuid, 32 | "x": node.scenePos().x(), 33 | "y": node.scenePos().y(), 34 | } 35 | 36 | 37 | def serializeScene(scene): 38 | """Very naive scene serialization. 39 | 40 | Returns the scene as native Python datatypes, fit for json. 41 | 42 | We only store what Nodes there are, their positions and how they are 43 | connected by Edges. This should be enough to reconstruct the graph 44 | by relying on the other default values in each Node class. 45 | 46 | I repeat: If specific node/knob/header settings like flow, colors or 47 | labels have been changed, that is not (yet) stored here! 48 | """ 49 | nodes = [i for i in scene.items() if isinstance(i, Node)] 50 | edges = [i for i in scene.items() if isinstance(i, Edge)] 51 | data = { 52 | "nodes": [serializeNode(node) for node in nodes], 53 | "edges": [serializeEdge(edge) for edge in edges] 54 | } 55 | return data 56 | 57 | 58 | def reconstructScene(graphWidget, sceneData): 59 | """Rebuild a scene based on the serialized data. 60 | 61 | This reconstructs only type, position and connections right now. 62 | """ 63 | # Node classes need to be registered beforehand. 64 | classMap = {} 65 | for cls in graphWidget.nodeClasses: 66 | classMap[cls.__name__] = cls 67 | 68 | # Reconstruct Nodes. 69 | for nodeData in sceneData["nodes"]: 70 | try: 71 | cls = classMap[nodeData["class"]] 72 | except KeyError as err: 73 | raise UnregisteredNodeClassError(err) 74 | 75 | node = cls() 76 | node.uuid = nodeData["uuid"] # Enforce 'original' uuid. 77 | node.setPos(nodeData["x"], nodeData["y"]) 78 | 79 | graphWidget.addNode(node) 80 | 81 | # Reconstruct their connections. 82 | for edgeData in sceneData["edges"]: 83 | sourceNode = graphWidget.getNodeById(edgeData["source_nodeId"]) 84 | sourceKnob = sourceNode.knob(edgeData["source_name"]) 85 | 86 | targetNode = graphWidget.getNodeById(edgeData["target_nodeId"]) 87 | targetKnob = targetNode.knob(edgeData["target_name"]) 88 | 89 | sourceKnob.connectTo(targetKnob) 90 | 91 | 92 | def saveSceneToFile(sceneData, jsonFile): 93 | """Store the serialized scene as .json file.""" 94 | with open(jsonFile, "w") as f: 95 | f.write(toJson(sceneData) + "\n") 96 | 97 | 98 | def _renewUUIDs(jsonString): 99 | """Renew uuids in given text, by doing complete string replacements 100 | with newly generated uuids. 101 | 102 | Return the resulting text. 103 | """ 104 | RE_UUID = r"(?P\w{8}-\w{4}-\w{4}-\w{4}-\w{12})" 105 | matches = re.findall(RE_UUID, jsonString) 106 | uuids = list(set(matches)) 107 | for original in uuids: 108 | replacement = str(uuid.uuid4()) 109 | jsonString = jsonString.replace(original, replacement) 110 | return jsonString 111 | 112 | 113 | def loadSceneFromFile(jsonFile, refreshIds=False): 114 | """Read the serialized scene data from a .json file. 115 | 116 | refreshIds: If True, ensures all uuids are renewed (on a text 117 | basis), meaning they will not overwrite existing scene content. 118 | This can be used to 'merge' a file into an existing scene. 119 | """ 120 | text = readFileContent(jsonFile) 121 | if refreshIds: 122 | text = _renewUUIDs(text) 123 | sceneData = fromJson(text) 124 | return sceneData 125 | 126 | 127 | def mergeSceneFromFile(jsonFile): 128 | """Like loading, but with new uuids, so it can be merged.""" 129 | return loadSceneFromFile(jsonFile, refreshIds=True) 130 | -------------------------------------------------------------------------------- /qtnodes/view.py: -------------------------------------------------------------------------------- 1 | """Custom QGraphicsView.""" 2 | 3 | from PySide import QtGui 4 | from PySide import QtCore 5 | 6 | from .node import Node 7 | from .edge import Edge 8 | 9 | 10 | CURRENT_ZOOM = 1.0 11 | ALTERNATE_MODE_KEY = QtCore.Qt.Key.Key_Alt 12 | 13 | 14 | class GridView(QtGui.QGraphicsView): 15 | """This view will draw a grid in its background.""" 16 | 17 | def __init__(self, *args, **kwargs): 18 | super(GridView, self).__init__(*args, **kwargs) 19 | 20 | self.fillColor = QtGui.QColor(250, 250, 250) 21 | self.lineColor = QtGui.QColor(230, 230, 230) 22 | 23 | self.xStep = 20 24 | self.yStep = 20 25 | 26 | self.panningMult = 2.0 * CURRENT_ZOOM 27 | self.panning = False 28 | self.zoomStep = 1.1 29 | 30 | # Since we implement custom panning, we don't need the scrollbars. 31 | self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 32 | self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) 33 | 34 | self.setDragMode(QtGui.QGraphicsView.RubberBandDrag) 35 | self.setTransformationAnchor(QtGui.QGraphicsView.AnchorUnderMouse) 36 | 37 | def nodes(self): 38 | """Return all Nodes in the scene.""" 39 | return [i for i in self.scene().items() if isinstance(i, Node)] 40 | 41 | def edges(self): 42 | """Return all Edges in the scene.""" 43 | return [i for i in self.scene().items() if isinstance(i, Edge)] 44 | 45 | def redrawEdges(self): 46 | """Trigger a repaint of all Edges in the scene.""" 47 | for edge in self.edges(): 48 | edge.updatePath() 49 | 50 | def keyPressEvent(self, event): 51 | """Trigger a redraw of Edges to update their color.""" 52 | if event.key() == ALTERNATE_MODE_KEY: 53 | self.redrawEdges() 54 | super(GridView, self).keyPressEvent(event) 55 | 56 | def keyReleaseEvent(self, event): 57 | """Trigger a redraw of Edges to update their color.""" 58 | if event.key() == ALTERNATE_MODE_KEY: 59 | self.redrawEdges() 60 | super(GridView, self).keyReleaseEvent(event) 61 | 62 | def mousePressEvent(self, event): 63 | """Initiate custom panning using middle mouse button.""" 64 | if event.button() == QtCore.Qt.MiddleButton: 65 | self.setDragMode(QtGui.QGraphicsView.NoDrag) 66 | self.panning = True 67 | self.prevPos = event.pos() 68 | self.setCursor(QtCore.Qt.SizeAllCursor) 69 | elif event.button() == QtCore.Qt.LeftButton: 70 | self.setDragMode(QtGui.QGraphicsView.RubberBandDrag) 71 | super(GridView, self).mousePressEvent(event) 72 | 73 | def mouseMoveEvent(self, event): 74 | if self.panning: 75 | delta = (self.mapToScene(event.pos()) * self.panningMult - 76 | self.mapToScene(self.prevPos) * self.panningMult) * -1.0 77 | center = QtCore.QPoint(self.viewport().width() / 2 + delta.x(), 78 | self.viewport().height() / 2 + delta.y()) 79 | newCenter = self.mapToScene(center) 80 | self.centerOn(newCenter) 81 | self.prevPos = event.pos() 82 | return 83 | super(GridView, self).mouseMoveEvent(event) 84 | 85 | def mouseReleaseEvent(self, event): 86 | if self.panning: 87 | self.panning = False 88 | self.setCursor(QtCore.Qt.ArrowCursor) 89 | super(GridView, self).mouseReleaseEvent(event) 90 | 91 | def wheelEvent(self, event): 92 | positive = event.delta() >= 0 93 | zoom = self.zoomStep if positive else 1.0 / self.zoomStep 94 | self.scale(zoom, zoom) 95 | 96 | # Assuming we always scale x and y proportionally, expose the 97 | # current horizontal scaling factor so other items can use it. 98 | global CURRENT_ZOOM 99 | CURRENT_ZOOM = self.transform().m11() 100 | 101 | def drawBackground(self, painter, rect): 102 | painter.setBrush(QtGui.QBrush(self.fillColor)) 103 | painter.setPen(QtGui.QPen(self.lineColor)) 104 | 105 | top = rect.top() 106 | bottom = rect.bottom() 107 | left = rect.left() 108 | right = rect.right() 109 | 110 | lines = [] 111 | 112 | currentXPos = left 113 | while currentXPos <= right: 114 | line = QtCore.QLine(currentXPos, top, currentXPos, bottom) 115 | lines.append(line) 116 | currentXPos += self.xStep 117 | 118 | currentYPos = top 119 | while currentYPos <= bottom: 120 | line = QtCore.QLine(left, currentYPos, right, currentYPos) 121 | lines.append(line) 122 | currentYPos += self.yStep 123 | 124 | painter.drawRect(rect) 125 | painter.drawLines(lines) 126 | -------------------------------------------------------------------------------- /qtnodes/widget.py: -------------------------------------------------------------------------------- 1 | """Editor widget. 2 | 3 | Regarding the slightly weird signal connection syntax refer to: 4 | 5 | http://stackoverflow.com/questions/20390323/ pyqt-dynamic-generate-qmenu-action-and-connect # noqa 6 | """ 7 | import os 8 | 9 | from PySide import QtGui 10 | from PySide import QtCore 11 | 12 | from .node import Node 13 | from .view import GridView 14 | from .layout import autoLayout 15 | from . import serializer 16 | 17 | 18 | class NodeGraphWidget(QtGui.QWidget): 19 | """Display the node graph and offer editing functionality.""" 20 | 21 | def __init__(self, parent=None): 22 | super(NodeGraphWidget, self).__init__(parent=parent) 23 | 24 | self.scene = QtGui.QGraphicsScene() 25 | self.view = GridView() 26 | self.view.setScene(self.scene) 27 | 28 | self.view.setRenderHint(QtGui.QPainter.Antialiasing) 29 | self.view.setViewportUpdateMode( 30 | QtGui.QGraphicsView.FullViewportUpdate) 31 | 32 | layout = QtGui.QVBoxLayout() 33 | layout.addWidget(self.view) 34 | self.setLayout(layout) 35 | 36 | self.nodeClasses = [] 37 | 38 | # A cache for storing a representation of the current scene. 39 | # This is used for the Hold scene / Fetch scene feature. 40 | self.lastStoredSceneData = None 41 | 42 | def clearScene(self): 43 | """Remove everything in the current scene. 44 | 45 | FIXME: The GC does all the work here, which is probably not the 46 | finest solution, but works for now. 47 | """ 48 | self.scene = QtGui.QGraphicsScene() 49 | self.view.setScene(self.scene) 50 | 51 | def keyPressEvent(self, event): 52 | """React on various keys regarding Nodes.""" 53 | 54 | # Delete selected nodes. 55 | if event.key() == QtCore.Qt.Key.Key_Delete: 56 | selectedNodes = [i for i in self.scene.selectedItems() 57 | if isinstance(i, Node)] 58 | for node in selectedNodes: 59 | node.destroy() 60 | 61 | super(NodeGraphWidget, self).keyPressEvent(event) 62 | 63 | def addSceneMenuActions(self, menu): 64 | """Add scene specific actions like hold/fetch/save/load.""" 65 | subMenu = menu.addMenu("Scene") 66 | 67 | def _saveSceneAs(): 68 | filePath, _ = QtGui.QFileDialog.getSaveFileName( 69 | self, 70 | "Save Scene to JSON", 71 | os.path.join(QtCore.QDir.currentPath(), "scene.json"), 72 | "JSON File (*.json)" 73 | ) 74 | if filePath: 75 | sceneData = serializer.serializeScene(self.scene) 76 | serializer.saveSceneToFile(sceneData, filePath) 77 | 78 | saveToAction = subMenu.addAction("Save As...") 79 | saveToAction.triggered.connect(_saveSceneAs) 80 | 81 | def _loadSceneFrom(): 82 | filePath, _ = QtGui.QFileDialog.getOpenFileName( 83 | self, 84 | "Open Scene JSON File", 85 | os.path.join(QtCore.QDir.currentPath(), "scene.json"), 86 | "JSON File (*.json)" 87 | ) 88 | if filePath: 89 | sceneData = serializer.loadSceneFromFile(filePath) 90 | if sceneData: 91 | self.clearScene() 92 | serializer.reconstructScene(self, sceneData) 93 | 94 | loadFromAction = subMenu.addAction("Open File...") 95 | loadFromAction.triggered.connect(_loadSceneFrom) 96 | 97 | def _mergeSceneFrom(): 98 | filePath, _ = QtGui.QFileDialog.getOpenFileName( 99 | self, 100 | "Open Scene JSON File", 101 | os.path.join(QtCore.QDir.currentPath(), "scene.json"), 102 | "JSON File (*.json)" 103 | ) 104 | if filePath: 105 | sceneData = serializer.mergeSceneFromFile(filePath) 106 | if sceneData: 107 | # Select only new nodes so they can be moved. 108 | oldNodes = set(self.view.nodes()) 109 | serializer.reconstructScene(self, sceneData) 110 | allNodes = set(self.view.nodes()) 111 | mergedNodes = allNodes - oldNodes 112 | for node in mergedNodes: 113 | node.setSelected(True) 114 | 115 | mergeFromAction = subMenu.addAction("Merge File...") 116 | mergeFromAction.triggered.connect(_mergeSceneFrom) 117 | 118 | subMenu.addSeparator() 119 | 120 | def _storeCurrentScene(): 121 | self.lastStoredSceneData = serializer.serializeScene(self.scene) 122 | QtGui.QMessageBox.information(self, "Hold", 123 | "Scene state holded.") 124 | 125 | holdAction = subMenu.addAction("Hold") 126 | holdAction.triggered.connect(_storeCurrentScene) 127 | 128 | def _loadLastStoredScene(): 129 | if not self.lastStoredSceneData: 130 | print("scene data is empty, nothing to load") 131 | return 132 | self.clearScene() 133 | serializer.reconstructScene(self, self.lastStoredSceneData) 134 | QtGui.QMessageBox.information(self, "Fetch", 135 | "Scene state fetched.") 136 | 137 | fetchAction = subMenu.addAction("Fetch") 138 | fetchAction.triggered.connect(_loadLastStoredScene) 139 | 140 | subMenu.addSeparator() 141 | 142 | clearSceneAction = subMenu.addAction("Clear") 143 | clearSceneAction.triggered.connect(self.clearScene) 144 | 145 | subMenu.addSeparator() 146 | 147 | def _layoutScene(): 148 | autoLayout(self.scene) 149 | self.view.redrawEdges() 150 | 151 | layoutSceneAction = subMenu.addAction("Auto Layout") 152 | layoutSceneAction.triggered.connect(_layoutScene) 153 | 154 | def addNodesMenuActions(self, menu): 155 | subMenu = menu.addMenu("Nodes") 156 | for cls in self.nodeClasses: 157 | className = cls.__name__ 158 | action = subMenu.addAction(className) 159 | action.triggered[()].connect( 160 | lambda cls=cls: self._createNode(cls)) 161 | 162 | def contextMenuEvent(self, event): 163 | """Show a menu to create registered Nodes.""" 164 | menu = QtGui.QMenu(self) 165 | self.addNodesMenuActions(menu) 166 | self.addSceneMenuActions(menu) 167 | menu.exec_(event.globalPos()) 168 | 169 | super(NodeGraphWidget, self).contextMenuEvent(event) 170 | 171 | def _createNode(self, cls, atMousePos=True, center=True): 172 | """The class must provide defaults in its constructor. 173 | 174 | We ensure the scene immediately has the Node added, otherwise 175 | the GC could snack it up right away. 176 | """ 177 | node = cls() 178 | self.addNode(node) 179 | 180 | if atMousePos: 181 | mousePos = self.view.mapToScene( 182 | self.mapFromGlobal(QtGui.QCursor.pos())) 183 | node.setPos(mousePos) 184 | if center: 185 | self.view.centerOn(node.pos()) 186 | 187 | def registerNodeClass(self, cls): 188 | if cls not in self.nodeClasses: 189 | self.nodeClasses.append(cls) 190 | 191 | def unregisterNodeClass(self, cls): 192 | if cls in self.nodeClasses: 193 | self.nodeClasses.remove(cls) 194 | 195 | def addNode(self, node): 196 | """Add a Node to the current scene. 197 | 198 | This is only necessary when the scene has not been passed on 199 | creation, e.g. when you create a Node programmatically. 200 | """ 201 | if node not in self.scene.items(): 202 | self.scene.addItem(node) 203 | 204 | def getNodeById(self, uuid): 205 | """Return Node that matches the given uuid string.""" 206 | nodes = [i for i in self.scene.items() if isinstance(i, Node)] 207 | for node in nodes: 208 | if node.uuid == uuid: 209 | return node 210 | return None 211 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.0 2 | pydot==1.0.2 # For tree layouting. Needs graphviz. 3 | PySide==1.2.4 4 | --------------------------------------------------------------------------------