├── .gitignore ├── .idea ├── .gitignore ├── blender-python-nodes.iml ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml └── vcs.xml ├── LICENSE ├── README.md ├── batman.png ├── example.blend ├── install-pynodes.py ├── prepare-blender-addon.sh ├── pynodes ├── __init__.py ├── helpers │ ├── __init__.py │ ├── array.py │ ├── dict.py │ ├── file.py │ └── list.py ├── nodes │ ├── AutoNodeTypeAdder.py │ ├── PythonBaseNode.py │ ├── PythonEvalStatementNode.py │ ├── PythonLoadImageNode.py │ ├── PythonNode.py │ ├── PythonNodeGroupNodes.py │ ├── PythonPrintResultBaseNode.py │ ├── PythonRequestURLNode.py │ ├── PythonSaveImageBaseNode.py │ ├── PythonShowArrayShapeBaseNode.py │ ├── PythonTestNode.py │ ├── PythonWaitSecondsNode.py │ └── __init__.py └── registry.py ├── readme-assets ├── a_python_node.png ├── a_python_node_tree.png ├── another_python_node_concatenate.png ├── another_python_node_lists.png └── another_python_node_tree_shape_example.png └── test-blender-pynodes.sh /.gitignore: -------------------------------------------------------------------------------- 1 | pynodes.zip 2 | *.blend1 3 | 4 | .DS_Store 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # SageMath parsed files 107 | *.sage.py 108 | 109 | # Environments 110 | .env 111 | .venv 112 | env/ 113 | venv/ 114 | ENV/ 115 | env.bak/ 116 | venv.bak/ 117 | 118 | # Spyder project settings 119 | .spyderproject 120 | .spyproject 121 | 122 | # Rope project settings 123 | .ropeproject 124 | 125 | # mkdocs documentation 126 | /site 127 | 128 | # mypy 129 | .mypy_cache/ 130 | .dmypy.json 131 | dmypy.json 132 | 133 | # Pyre type checker 134 | .pyre/ 135 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Editor-based HTTP Client requests 5 | /httpRequests/ 6 | # Datasource local storage ignored files 7 | /dataSources/ 8 | /dataSources.local.xml 9 | -------------------------------------------------------------------------------- /.idea/blender-python-nodes.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 All rights reserved 2 | Charles Strauss 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender Python Nodes 2 | This is a blender addon that enables python programming using only blender nodes! Help wanted! 3 | 4 | ![a python node tree](/readme-assets/a_python_node_tree.png) 5 | ![another python node tree](/readme-assets/another_python_node_tree_shape_example.png) 6 | ![a python node](/readme-assets/a_python_node.png) 7 | 8 | # How to Install into blender: 9 | 1. run `./prepare-blender-addon.sh` in this directory (this is a convenience script) 10 | 2. load blender, install the `pynodes.zip` file created by the previous line. Click [here](https://www.youtube.com/watch?v=vYh1qh9y1MI) to see how to install addons in blender. 11 | 12 | # How to develop this addon: 13 | 1. use `which blender` to find the location of your blender executable installation 14 | 2. modify BLENDER_EXECUTABLE in blender-nodes.py to the absolut path of your executable (see blender-nodes.py for example) (this is a convenience script which automatically installs the addon into blender upon loading up) 15 | 3. make some changes... (copy one of the files in pynodes/nodes/ and modify the `init` function of the node to add/remove input and output sockets, change up the `run` function to access your own sockets, and execute your desired code.). See PythonPrintResultBaseNode.py for a very basic base node behavior (it prints out the output to the terminal). 16 | 17 | # Terminology: 18 | * Base node - node which executes the tree it is connected to return the result (could be saving contents to a file, printing result to terminal, displaying the result somewhere in blender, uploading something to the cloud, or sending an email) 19 | * Node - has inputs and outputs, but will never be executed unless attached to a base node 20 | 21 | # How do we get the python functions, and parameters? 22 | We use introspection to put all global modules into the add menu, and recursibly discover all modules. This means if you import ANY python library into the environment used by blender, you can access its functions. We try to use python to get the parameters, and when we cant, we use some clever regex. This approach does not always work unfortunatley. At the moment, many python builtins (such as exec, print, and hundreds more basic python functions) don't work welle because python can't tell us their exact parameter signature. See `pynodes/nodes/AutoNodeTypeAdder.py` for the functions used to do this upon addon registration. 23 | 24 | # Visions: 25 | 1. an open source deep fake creation tool 26 | 2. an ide for arbitrary python logic 27 | 3. a way for incorporating all the functionality of gmic, and ffmpeg into the blender compositor 28 | 4. a more descriptive, easily interpretable way for high-level programming 29 | 30 | # TODO: 31 | ~~1. make currently-running node glow/change color to green~~ 32 | 33 | ~~2. make errors turn node red,~~ and be traceable 34 | 35 | ~~3. auto convert python functions into nodes~~ (has some issues such as converting builtin funcitons which have no signature) 36 | 37 | ~~4. auto convert python modules into nodes in categories~~ (perhaps wasn't a particularly useful feature, now its just hard to find anything 38 | 39 | - Run all functions and interpret python in seperate python interepreter module? So it is scoped together and doesn't clash with scope of run method in node class? 40 | - nodes could be "compiled" into a "file" (or just large string) for running, for speed and seperation 41 | 42 | 5. implement blender utility nodes: 43 | 44 | 1. mix node, compositiong nodes, color ramp, noise generation... 45 | 2. input from rendered scenes, compositor nodes, texture nodes 46 | 3. input from motion tracking, drivers... 47 | ~~4. bpy commands...~~ 48 | 49 | 6. impelemnt lots of NNs into python modules and nodes to be used artstically by blender users with little-no code expierence 50 | 51 | 1. cyclegan x -> y, person to tom cruz or tom hanks for example. (floor -> beach, person -> celebrity, stone -> wood, greenscreen -> mask, ...) 52 | 2. pose transfer for dancing (https://aliaksandrsiarohin.github.io/first-order-model-website/) 53 | 3. up-resolution, infilling, image-generation, frame-interpolation 54 | 4. material-manipulation in still frames (change glossyness/other attributes of masked object) 55 | 5. insert smoke simulation, fluid simulation, fire 56 | 6. automatic vegtation insert: trees, plants, plants from picture, bark, rocks, rotateable, clouds 57 | 7. automatic time of day, re-lighting gan 58 | 8. realism filter. go from cg environments to real-looking environment 59 | 9. automatic depth inference from single image, and multiple images 60 | 10. gmic qt filters node 61 | 11. glsl, osl filters node 62 | 12. nft-generator gan 63 | 13. expieremental: object re-orientation gan 64 | 65 | 7. dictionary creation node (fill in sockets as values, and use text input as key) 66 | 67 | 8. special functions like if, while, for, try, def, class 68 | 69 | 9. implement async 70 | 71 | # Goals: 72 | 73 | 1. be able to make crazy tiktoks of people without them knowing using only dfs 74 | 75 | 2. make ai-augmented art directly in blender 76 | 77 | 3. enable anyone to create high-quality visuals with little-to no monetary investment of their own 78 | -------------------------------------------------------------------------------- /batman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/batman.png -------------------------------------------------------------------------------- /example.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/example.blend -------------------------------------------------------------------------------- /install-pynodes.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.split(__file__)[0]) 4 | 5 | # print('installing environment into blender') 6 | 7 | # import env_managment.envs.pytorch_blender as pytorch_blender 8 | # pytorch_blender.register() 9 | # import env_managment.envs.tensorflow_blender as tensorflow_blender 10 | # tensorflow_blender.register() 11 | 12 | # print('installed environment into blender') 13 | 14 | # import tensorflow 15 | # import tensorflow_datasets 16 | 17 | # print('imported environment packages') 18 | 19 | import pynodes 20 | import pynodes.nodes 21 | 22 | print('registering pynodes') 23 | 24 | pynodes.register() 25 | 26 | print('pynodes was registered') 27 | 28 | # import bpy 29 | # bpy.context.area.ui_type = 'PythonCompositorTreeType' 30 | -------------------------------------------------------------------------------- /prepare-blender-addon.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This prepares a production-ready file for distributing pynodes, in the form of pynodes.zip 4 | # pynodes.zip can be added to blender by the consumer. 5 | 6 | rm pynodes.zip 7 | zip -r pynodes.zip pynodes --exclude *__pycache__* 8 | -------------------------------------------------------------------------------- /pynodes/__init__.py: -------------------------------------------------------------------------------- 1 | bl_info = { 2 | "name": "Python Nodes", 3 | "description": "The power of python, brought to you through blender nodes.", 4 | "author": "Charles M. S. Strauss, and Robert R. Strauss", 5 | "version": (1, 0), 6 | "blender": (2, 92, 0), 7 | "location": "Console", 8 | "warning": "Will run arbitrary code.", 9 | "support": "COMMUNITY", 10 | "category": "Nodes", 11 | } 12 | 13 | import bpy 14 | from bpy.types import NodeTree, Node, NodeSocket 15 | import numpy as np 16 | 17 | # TODO ideas: 18 | # 1. autorename sockets based off of return type, numpy array could show dtype, shape 19 | 20 | # Derived from the NodeTree base type, similar to Menu, Operator, Panel, etc. 21 | class PythonCompositorTree(NodeTree): 22 | # Description string 23 | '''A custom node tree type that will show up in the editor type list''' 24 | # Optional identifier string. If not explicitly defined, the python class name is used. 25 | bl_idname = 'PythonCompositorTreeType' 26 | # Label for nice name display 27 | bl_label = "Python Compositor Tree" 28 | # Icon identifier 29 | bl_icon = 'NODETREE' 30 | 31 | # def get_node_execution_scope(): 32 | # ''' 33 | # Returns the scope to be used when evaluating python node inputs. 34 | # Quickly get a bunch of constants, can be extracted and modified to 35 | # have more constants or packages later. Note that many python 36 | # builtins are already in the scope by default, such as int or str. 37 | # ''' 38 | # import numpy as np 39 | # import bpy 40 | # import os 41 | # import sys 42 | # # tensorflow, ffmpeg, gmic qt, osl, PIL... 43 | # return { 44 | # 'pi':np.pi, 45 | # 'tau':np.pi*2, 46 | # 'e':np.e, 47 | # 'np':np, 48 | # 'bpy':bpy, 49 | # 'os':os, 50 | # 'sys':sys 51 | # } 52 | # 53 | # node_execution_scope = get_node_execution_scope() 54 | 55 | # Custom socket type 56 | class AbstractPyObjectSocket(NodeSocket): 57 | # Description string 58 | '''General python node socket type''' 59 | 60 | 61 | # for storing the inputs and outputs of nodes without overriding default_value 62 | # we can't change value of _value_, but we can set what it points to if its a list 63 | _value_ = [{},]#: bpy.props.PointerProperty(type=bpy.types.Object, name='value', description='Pointer to object contained by the socket') 64 | 65 | # blender properties have to be wrapped in this so they are inherited in a way that blender properties can access them 66 | class Properties: 67 | 68 | argvalue : bpy.props.StringProperty( 69 | name = 'argvalue', 70 | description = 'manually input value to argument for function', 71 | default = '', 72 | maxlen= 1024, 73 | update = lambda s,c: s.argvalue_updated() 74 | ) 75 | 76 | argvalue_hidden : bpy.props.BoolProperty( 77 | default=False 78 | ) 79 | 80 | def argvalue_updated(self): 81 | pass 82 | 83 | def node_updated(self): 84 | pass 85 | 86 | def get_value(self): 87 | if (self.is_linked or self.is_output) and self.identifier in self._value_[0]: 88 | return self._value_[0][self.identifier] 89 | else: 90 | if not self.argvalue is None and not self.argvalue=='': 91 | return eval(self.argvalue) 92 | 93 | def set_value(self, value): 94 | self._value_[0][self.identifier] = value 95 | 96 | def hide_text_input(self): 97 | self.argvalue_hidden = True 98 | 99 | def show_text_input(self): 100 | self.argvalue_hidden = False 101 | 102 | def is_empty(self): 103 | return (not self.is_linked) and (self.argvalue=='' or self.argvalue==None) 104 | 105 | # Socket color 106 | def draw_color(self, context, node): 107 | return (0.7, 0.7, 0.7, 0.5) 108 | 109 | def draw(self, context, layout, node, text): 110 | if text!='': 111 | layout.label(text=text) 112 | # give default value selector when not linked 113 | if not self.argvalue_hidden and not self.is_linked and not self.is_output: 114 | layout.prop(self, 'argvalue', text='') 115 | 116 | class PyObjectSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): 117 | ''' Python node socket type for normal function arguments and outputs ''' 118 | # Optional identifier string. If not explicitly defined, the python class name is used. 119 | bl_idname = 'PyObjectSocketType' 120 | # Label for nice name display 121 | bl_label = "Python Object Socket" 122 | 123 | class AbstractPyObjectVarArgSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): 124 | # Description string 125 | '''PyNodes node socket type for variable arguments (varargs, *args)''' 126 | 127 | class Properties: 128 | socket_index : bpy.props.IntProperty( 129 | name = 'socket_index', 130 | default = -1 131 | ) 132 | 133 | socket_index_valid : bpy.props.BoolProperty( 134 | name = 'socket_index_valid', 135 | default = True 136 | ) 137 | 138 | def __init__(self): 139 | super().__init__() 140 | self.name = self.identifier 141 | if not self.socket_index_valid: 142 | self.node.inputs.move(self.node.inputs.find(self.identifier), self.socket_index) 143 | self.socket_index_valid = True 144 | 145 | # var args nodes automatically remove or add more of themselves as they are used 146 | def node_updated(self): 147 | self.update() 148 | 149 | def argvalue_updated(self): 150 | self.update() 151 | 152 | def socket_init(self): 153 | pass 154 | 155 | def update(self): 156 | 157 | if self.is_output: 158 | socket_collection = self.node.outputs 159 | else: 160 | socket_collection = self.node.inputs 161 | 162 | emptypins = 0 163 | # count the number of non-linked, empty sibling vararg pins 164 | for i in socket_collection: 165 | if i.bl_idname == self.bl_idname: 166 | if i.is_empty(): 167 | emptypins+=1 168 | last_empty_socket = i 169 | last_socket = i 170 | 171 | # there is at least one other empty non-linked one (other than self) 172 | if emptypins > 1: 173 | # remove self if empty and not linked 174 | self.node.unsubscribe_to_update(last_empty_socket.node_updated) 175 | socket_collection.remove(last_empty_socket) 176 | # create new pin if there is not enough 177 | elif emptypins < 1: 178 | new_socket = socket_collection.new( 179 | self.bl_idname, 180 | '', 181 | identifier=self.identifier 182 | ) 183 | 184 | if last_socket.socket_index==-1: 185 | new_socket.socket_index = socket_collection.find(last_socket.identifier)+1 186 | else: 187 | new_socket.socket_index = last_socket.socket_index+1 188 | new_socket.socket_index_valid = False 189 | 190 | new_socket.socket_init() 191 | 192 | class PyObjectVarArgSocket(AbstractPyObjectVarArgSocket.Properties, AbstractPyObjectVarArgSocket): 193 | # Description string 194 | '''PyNodes node socket type for variable arguments (varargs, *args)''' 195 | # Optional identifier string. If not explicitly defined, the python class name is used. 196 | bl_idname = 'PyObjectVarArgSocketType' 197 | # Label for nice name display 198 | bl_label = 'Expanding Socket' 199 | 200 | def __init__(self): 201 | super().__init__() 202 | # pin shape 203 | self.display_shape = 'DIAMOND' 204 | 205 | # class PyObjectVarArgSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): 206 | # # Description string 207 | # '''Python node socket type for variable arguments (varargs, *args)''' 208 | # # Optional identifier string. If not explicitly defined, the python class name is used. 209 | # bl_idname = 'PyObjectVarArgSocketType' 210 | # # Label for nice name display 211 | # bl_label = 'Python *args Object Socket' 212 | # 213 | # def __init__(self): 214 | # super().__init__() 215 | # # pin shape 216 | # self.display_shape = 'DIAMOND' 217 | # self.name = self.identifier 218 | # # socket must be indexible using name. 219 | # # therefore force name to be unique like identifier 220 | # self.node.subscribe_to_update(self.node_updated) 221 | # # subscribe socket to node update events 222 | # 223 | # # var args nodes automatically remove or add more of themselves as they are used 224 | # def node_updated(self): 225 | # # print('node_updated', self.node, self) 226 | # self.update() 227 | # 228 | # def argvalue_updated(self): 229 | # # print('argvalue_updated', self.node, self) 230 | # self.update() 231 | # 232 | # def update(self): 233 | # emptyvarargpins = 0 234 | # # count the number of non-linked, empty sibling vararg pins 235 | # for input in self.node.inputs: 236 | # if input.bl_idname == PyObjectVarArgSocket.bl_idname: 237 | # if input.is_empty(): 238 | # emptyvarargpins+=1 239 | # 240 | # # there is at least one other empty non-linked one (other than self) 241 | # if emptyvarargpins > 1: 242 | # # remove self if empty and not linked 243 | # self.node.unsubscribe_to_update(self) 244 | # self.node.inputs.remove(self) 245 | # # create new pin if there is not enough 246 | # elif emptyvarargpins < 1: 247 | # self.node.inputs.new(PyObjectVarArgSocket.bl_idname, '*arg') 248 | 249 | # class PyObjectKwArgSocket(AbstractPyObjectVarArgSocket.Properties, AbstractPyObjectVarArgSocket): 250 | # # Description string 251 | # '''PyNodes socket type for variable arguments (varargs, *args)''' 252 | # # Optional identifier string. If not explicitly defined, the python class name is used. 253 | # bl_idname = 'PyObjectKwArgSocket' 254 | # # Label for nice name display 255 | # bl_label = 'Expanding Socket for Optional Attributes' 256 | # 257 | # attribute : bpy.props.StringProperty( 258 | # name='Attribute', 259 | # description="An attribute to set.", 260 | # update=lambda s,c:s.node.update() 261 | # ) 262 | # 263 | # attribute_collection : bpy.props.CollectionProperty( 264 | # name='Attribute Selector', 265 | # type=bpy.types.PropertyGroup 266 | # ) 267 | # 268 | # def update_attribute_collection(self): 269 | # self.attribute_collection.clear() 270 | # for a in self.get_display_attributes(): 271 | # self.attribute_collection.add().name = a 272 | # 273 | # def node_updated(self): 274 | # super().node_updated() 275 | # self.update_attribute_collection() 276 | # 277 | # def socket_init(self): 278 | # super().socket_init() 279 | # self.update_attribute_collection() 280 | # 281 | # def get_display_attributes(self): 282 | # unused_attributes = self.node.unused_attributes() 283 | # if not self.attribute is None: 284 | # unused_attributes+=[self.attribute,] 285 | # unused_attributes = sorted(unused_attributes) 286 | # return unused_attributes 287 | # 288 | # def draw(self, context, layout, node, text): 289 | # if text!='': 290 | # layout.label(text=text) 291 | # layout.prop_search(self, 'attribute', self, 'attribute_collection', text='') 292 | # if not self.is_linked and not self.is_output: 293 | # layout.prop(self, 'argvalue', text='') 294 | # 295 | # def __init__(self): 296 | # super().__init__() 297 | # # pin shape 298 | # self.display_shape = 'SQUARE' 299 | 300 | class PyObjectKwArgSocket(AbstractPyObjectSocket.Properties, AbstractPyObjectSocket): 301 | # Description string 302 | '''Python node socket type for keyword argumnets''' 303 | # Optional identifier string. If not explicitly defined, the python class name is used. 304 | bl_idname = 'PyObjectKWArgSocketType' 305 | # Label for nice name display 306 | bl_label = 'Python *kwargs Object Socket' 307 | 308 | def __init__(self): 309 | super().__init__() 310 | # pin shape 311 | self.display_shape = 'SQUARE' 312 | 313 | # method to set the default value defined by the kwargs 314 | def set_default(self, value): 315 | self.argvalue = value 316 | 317 | ### Node Categories ### 318 | # Node categories are a python system for automatically 319 | # extending the Add menu, toolbar panels and search operator. 320 | # For more examples see release/scripts/startup/nodeitems_builtins.py 321 | 322 | import nodeitems_utils 323 | from nodeitems_utils import NodeCategory, NodeItem 324 | 325 | # our own base class with an appropriate poll function, 326 | # so the categories only show up in our own tree type 327 | 328 | class PythonCompositorNodeCategory(NodeCategory): 329 | @classmethod 330 | def poll(cls, context): 331 | return context.space_data.tree_type == PythonCompositorTree.bl_idname 332 | 333 | class PythonCompositorOperator(bpy.types.Operator): 334 | @classmethod 335 | def poll(cls, context): 336 | return context.space_data.tree_type == PythonCompositorTree.bl_idname 337 | 338 | # Have to add grouping behavior manually: 339 | 340 | def group_make(self, new_group_name): 341 | self.node_tree = bpy.data.node_groups.new(new_group_name, PythonCompositorTree.bl_idname) 342 | self.group_name = self.node_tree.name 343 | 344 | nodes = self.node_tree.nodes 345 | inputnode = nodes.new('PyNodesGroupInputsNode') 346 | outputnode = nodes.new('PyNodesGroupOutputsNode') 347 | inputnode.location = (-300, 0) 348 | outputnode.location = (300, 0) 349 | return self.node_tree 350 | 351 | class PyNodesGroupEdit(PythonCompositorOperator): 352 | bl_idname = "node.pynodes_group_edit" 353 | bl_label = "edits an pynodes node group" 354 | 355 | group_name : bpy.props.StringProperty(default='Node Group') 356 | 357 | def execute(self, context): 358 | node = context.active_node 359 | ng = bpy.data.node_groups 360 | 361 | # print(self.group_name) 362 | 363 | group_node = ng.get(self.group_name) 364 | if not group_node: 365 | group_node = group_make(node, new_group_name=self.group_name) 366 | 367 | bpy.ops.node.pynodes_switch_layout(layout_name=self.group_name) 368 | # print(context.space_data, context.space_data.node_tree) 369 | # context.space_data.node_tree = ng[self.group_name] # does the same 370 | 371 | # by switching, space_data is now different 372 | parent_tree_name = node.id_data.name 373 | path = context.space_data.path 374 | path.clear() 375 | path.append(ng[parent_tree_name]) # below the green opacity layer 376 | path.append(ng[self.group_name]) # top level 377 | 378 | return {"FINISHED"} 379 | 380 | class PyNodesTreePathParent(PythonCompositorOperator): 381 | '''Go to parent node tree''' 382 | bl_idname = "node.pynodes_tree_path_parent" 383 | bl_label = "Parent PyNodes Node Tree" 384 | bl_options = {'REGISTER', 'UNDO'} 385 | 386 | @classmethod 387 | def poll(cls, context): 388 | space = context.space_data 389 | return super() and len(space.path) > 1 390 | 391 | def execute(self, context): 392 | space = context.space_data 393 | space.path.pop() 394 | context.space_data.node_tree = space.path[0].node_tree 395 | return {'FINISHED'} 396 | 397 | def pynodes_group_edit(self, context): 398 | self.layout.separator() 399 | self.layout.operator( 400 | PyNodesGroupEdit.bl_idname, 401 | text="Edit Group (pynodes)") 402 | self.layout.operator( 403 | PyNodesTreePathParent.bl_idname, 404 | text="Exit/Enter Group (pynodes)") 405 | 406 | class PyNodesSwitchToLayout(bpy.types.Operator): 407 | """Switch to exact layout, user friendly way""" 408 | bl_idname = "node.pynodes_switch_layout" 409 | bl_label = "switch layouts" 410 | bl_options = {'REGISTER', 'UNDO'} 411 | 412 | layout_name: bpy.props.StringProperty( 413 | default='', name='layout_name', 414 | description='layout name to change layout by button') 415 | 416 | @classmethod 417 | def poll(cls, context): 418 | if context.space_data.type == 'NODE_EDITOR': 419 | if bpy.context.space_data.tree_type == PythonCompositorTree.bl_idname: 420 | return True 421 | else: 422 | return False 423 | 424 | def execute(self, context): 425 | ng = bpy.data.node_groups.get(self.layout_name) 426 | if ng: 427 | context.space_data.path.start(ng) 428 | else: 429 | return {'CANCELLED'} 430 | return {'FINISHED'} 431 | 432 | class NODE_MT_add_test_node_tree(PythonCompositorOperator): 433 | """Programmatically create node tree for testing, if it dosen't already exist.""" 434 | bl_idname = "node.add_test_node_tree" 435 | bl_label = "Add test node tree." 436 | bl_options = {'REGISTER', 'UNDO'} 437 | 438 | def execute(self, context): 439 | 440 | if bpy.data.node_groups.get('Python Node Tree Test', False)==False: 441 | bpy.ops.node.new_node_tree( 442 | type='PythonCompositorTreeType', 443 | name='Python Node Tree Test' 444 | ) 445 | test_node_tree = bpy.data.node_groups.get('Python Node Tree Test', False) 446 | load_image = test_node_tree.nodes.new('PythonLoadImageNode') 447 | save_image = test_node_tree.nodes.new('PythonSaveImageBaseNode') 448 | test_node_tree.links.new( 449 | input=save_image.inputs[0], 450 | output=load_image.outputs[0] 451 | ) 452 | return {'FINISHED'} 453 | 454 | def add_test_node_tree(self, context): 455 | self.layout.separator() 456 | self.layout.operator( 457 | NODE_MT_add_test_node_tree.bl_idname, 458 | text="Add Test Node Tree") 459 | 460 | 461 | from pynodes import registry 462 | from pynodes import helpers 463 | 464 | def register(): 465 | 466 | registry.registerAll() 467 | 468 | from bpy.utils import register_class 469 | # register the essentials to building a PythonNode 470 | register_class(PythonCompositorTree) 471 | register_class(PyObjectSocket) 472 | register_class(PyObjectVarArgSocket) 473 | register_class(PyObjectKwArgSocket) 474 | 475 | register_class(PyNodesGroupEdit) 476 | register_class(PyNodesTreePathParent) 477 | register_class(PyNodesSwitchToLayout) 478 | bpy.types.NODE_MT_node.append(pynodes_group_edit) 479 | 480 | register_class(NODE_MT_add_test_node_tree) 481 | bpy.types.NODE_MT_node.append(add_test_node_tree) 482 | 483 | def unregister(): 484 | 485 | from bpy.utils import unregister_class 486 | 487 | registry.unregisterAll() 488 | 489 | unregister_class(PythonCompositorTree) 490 | unregister_class(PyObjectSocket) 491 | unregister_class(PyObjectVarArgSocket) 492 | unregister_class(PyObjectKwArgSocket) 493 | 494 | unregister_class(PyNodesGroupEdit) 495 | unregister_class(PyNodesTreePathParent) 496 | unregister_class(PyNodesSwitchToLayout) 497 | -------------------------------------------------------------------------------- /pynodes/helpers/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | import pynodes.helpers.list 3 | import pynodes.helpers.dict 4 | import pynodes.helpers.array 5 | import pynodes.helpers.file 6 | -------------------------------------------------------------------------------- /pynodes/helpers/array.py: -------------------------------------------------------------------------------- 1 | 2 | def get(array, index_or_slice): 3 | return array[index_or_slice] 4 | -------------------------------------------------------------------------------- /pynodes/helpers/dict.py: -------------------------------------------------------------------------------- 1 | 2 | def create(**kwargs): 3 | return kwargs 4 | 5 | def set(dict, k, v): 6 | lst = dict.copy() 7 | lst[k] = v 8 | return lst 9 | 10 | def get(dict, k): 11 | return dict[k] 12 | 13 | def keys(dict): 14 | return dict.keys() 15 | -------------------------------------------------------------------------------- /pynodes/helpers/file.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from pathlib import Path 3 | 4 | def write(path, content:str, overwrite=False): 5 | ''' 6 | Simple functional-oriented string-based-file writer. 7 | Raises exception if an exception is encountered. 8 | Output is the given path otherwise. 9 | ''' 10 | 11 | try: 12 | 13 | if overwrite==False: 14 | assert not Path(path).exists(), f'Overwrite prevented! {str(path)}' 15 | assert not Path(path).is_dir(), f'Path is a folder. {str(path)}' 16 | 17 | with open(str(path), 'w') as f: 18 | f.write(content) 19 | 20 | except Exception as e: 21 | traceback.print_exc() 22 | raise e 23 | 24 | return path 25 | 26 | def read(path): 27 | with open(path, 'r') as f: 28 | return f.read() -------------------------------------------------------------------------------- /pynodes/helpers/list.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | 3 | def create(*args): 4 | return [*args] 5 | 6 | def append(list, *args): 7 | lst = list.copy() 8 | for e in args: 9 | lst.append(e) 10 | return lst 11 | 12 | def insert(list, index, object): 13 | lst = list.copy() 14 | lst.insert(index, object) 15 | return lst 16 | 17 | def pop(list, index): 18 | lst = list.copy() 19 | return lst.pop(index) 20 | 21 | def push(list, object): 22 | ''' 23 | Pushes element onto front (0th position) of list. 24 | ''' 25 | return insert(list, 0, object) 26 | -------------------------------------------------------------------------------- /pynodes/nodes/AutoNodeTypeAdder.py: -------------------------------------------------------------------------------- 1 | import types 2 | 3 | import inspect 4 | 5 | import urllib.parse 6 | 7 | import bpy 8 | 9 | import numpy as np 10 | 11 | import pynodes 12 | from pynodes import registry 13 | from pynodes.nodes import PythonNode 14 | 15 | import re 16 | 17 | def add_node_type(func): 18 | try: 19 | # Doc string 20 | docstr = str(func.__doc__) 21 | 22 | # get the module and function name, for the category and node name 23 | if hasattr(func, '__module__') and hasattr(func, '__name__'): 24 | mod = str(func.__module__) # module name 25 | qname = str(func.__name__) # function name 26 | else: # its an object 27 | mod = str(func.__class__.__module__) # module name 28 | qname = str(func.__class__.__name__ + ' object') # function name 29 | 30 | # getting the arguments to the function 31 | try: 32 | # use inspect to get call signature 33 | argstr = inspect.signature(func).__str__()[1:-1] 34 | 35 | except ValueError as ve: 36 | # parse docstring with regex to find call signature 37 | try: 38 | # basically find a word with a (, and if there is a matching () inside of it, ignore that ) and find the next ) 39 | sigmatch = re.findall(r'(\w+)\((.*?\(.*?\).*?|.*?)\)', docstr)[0] 40 | qname = sigmatch[0] 41 | i = 0 42 | def count_repl(_): 43 | nonlocal i 44 | i+=1 45 | return f'tuple_arg_{int(i):03d}' 46 | argstr = re.sub(r'\(.*?\)', count_repl, sigmatch[1]) 47 | except Exception as e: 48 | argstr = '...' 49 | # r'(\w+)\((.*?)(\)\\n)', docstr) # worked for test cases 50 | # r'(\w+)\(((?:[^)(]+|(?P))*)\)', docstr) 51 | # r'(\w+)\((.*)\)', docstr) 52 | # r'(\w+)\((.*)\)\\n', docstr) 53 | # r'(\w+)\((.*)\)', docstr) # original regex 54 | 55 | arglist = argstr.split(',') 56 | 57 | class nodeType(PythonNode): 58 | docstr 59 | 60 | # module 61 | mmod = mod 62 | 63 | # Optional identifier string. If not explicitly defined, the python class name is used. 64 | bl_idname = (mmod + '.' + qname)[:64] 65 | # Label for nice name display 66 | bl_label = mmod + '.' + qname 67 | 68 | def init(self, context): 69 | super().init(context) 70 | 71 | # add each of the function arguments as pins 72 | for i, arg in enumerate(arglist): 73 | if '=' in arg: 74 | key, value = arg.split('=') 75 | self.inputs.new(pynodes.PyObjectKwArgSocket.bl_idname, key.strip()) 76 | self.inputs[key.strip()].set_default(value.strip()) 77 | elif arg.strip() == '...' or arg.strip().startswith('*'): 78 | self.inputs.new(pynodes.PyObjectVarArgSocket.bl_idname, '*arg') 79 | else: 80 | self.inputs.new(pynodes.PyObjectSocket.bl_idname, arg.strip()) 81 | 82 | # add the output pin 83 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, qname) 84 | 85 | def draw_buttons_ext(self, context, layout): 86 | # open a link to documentation for this command on google (best I could hack together) 87 | row = layout.column() 88 | 89 | row.label(text='Documentation') 90 | 91 | google_search = f'documentation for {self.bl_label} (python)' 92 | 93 | row.operator( 94 | "wm.url_open", 95 | text="Google Documentation" 96 | ).url = f'http://www.google.com?q={urllib.parse.quote_plus(google_search)}' 97 | 98 | chatgpt_prompt = f'Explain how to use {self.bl_label} in python.' 99 | 100 | row.operator( 101 | "wm.url_open", 102 | text="ChatGPT Help" 103 | ).url = f'http://chat.openai.com?q={urllib.parse.quote_plus(chatgpt_prompt)}' 104 | 105 | def run(self): 106 | # collect the inputs 107 | posargs = [ 108 | input 109 | for input in self.inputs if 110 | input.bl_idname == pynodes.PyObjectVarArgSocket.bl_idname or 111 | input.bl_idname == pynodes.PyObjectSocket.bl_idname 112 | ] 113 | kwargs = [ 114 | input 115 | for input in self.inputs if input.bl_idname == pynodes.PyObjectKwArgSocket.bl_idname 116 | ] 117 | 118 | # get the values 119 | posvals = [ 120 | self.get_input(input.name) 121 | for input in posargs if not input.is_empty() 122 | ] 123 | 124 | kwdict = dict({ 125 | input.name: self.get_input(input.name) 126 | for input in kwargs if not input.is_empty() 127 | }) 128 | 129 | # pass inputs to the function and run it 130 | # print(posvals) 131 | # print(kwdict) 132 | output = func(*posvals, **kwdict) # TODO: should this be ran in a scope somehow? 133 | 134 | # send the output of the function to the output socket 135 | self.set_output(qname, output) 136 | 137 | # register this node function 138 | registry.registerNodeType(nodeType) 139 | except Exception as e: 140 | print(f'Could not register {func} as a node, skipping: ') 141 | print(e) 142 | 143 | added = [] 144 | 145 | def add_scope(scope): 146 | for key, obj in scope.copy().items(): 147 | if any([obj is elem for elem in added]): 148 | pass 149 | else: 150 | added.append(obj) 151 | 152 | if callable(obj): 153 | add_node_type(obj) 154 | elif isinstance(obj, types.ModuleType): 155 | add_scope(vars(obj)) 156 | else: 157 | pass # TODO: do something about constants or non-module non-callables? 158 | # make nodes that are just for a modules constants, no inputs just outputs 159 | 160 | def add_basic_nodes(): # adds recursivley, getting into all globals 161 | import numpy as np 162 | import bpy 163 | import os 164 | import sys 165 | import requests 166 | import pynodes.helpers 167 | scope = { 168 | 'np':np, 169 | 'bpy':bpy, 170 | 'os':os, 171 | 'sys':sys, 172 | 'helpers':pynodes.helpers, 173 | 'requests': requests 174 | } 175 | print(scope) 176 | add_scope(scope) 177 | 178 | # def add_all_globals(): 179 | # import sys 180 | # add_scope(sys.modules) 181 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonBaseNode.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import traceback 3 | from pynodes import nodes 4 | # from pynodes.nodes import PythonNode 5 | 6 | class EvaluateNodesOperator(bpy.types.Operator): 7 | """Cause active python node tree to evaluate results.""" 8 | bl_idname = "node.evaluate_python_node_tree" 9 | bl_label = "" 10 | bl_icon = "PLAY" 11 | 12 | @classmethod 13 | def poll(cls, context): 14 | space = context.space_data 15 | return space.type == 'NODE_EDITOR' 16 | 17 | def execute(self, context): 18 | # context.node.compute_output() 19 | context.node.execute_python_node_tree() 20 | return {'FINISHED'} 21 | 22 | from pynodes import registry 23 | registry.registerOperator(EvaluateNodesOperator) 24 | 25 | class PythonBaseNode(nodes.PythonNode): 26 | # === Basics === 27 | # Description string 28 | '''Abstract python base node. Connected nodes will be run. Unconnected nodes are not run.''' 29 | # Optional identifier string. If not explicitly defined, the python class name is used. 30 | bl_idname = 'PythonBaseNode' 31 | # Label for nice name display 32 | bl_label = "Python Base Node" 33 | 34 | class Properties: 35 | pass 36 | 37 | def mark_dirty(self): 38 | self.is_current = False 39 | super().mark_dirty() 40 | 41 | # def compute_output(self): 42 | # print('compute_output run') 43 | # try: 44 | # super().compute_output() 45 | # except nodes.PythonNode.PythonNodeRunError as e: 46 | # print('Python Nodes caught the following error:') 47 | # traceback.print_exc() 48 | 49 | def draw_buttons(self, context, layout): 50 | layout.operator('node.evaluate_python_node_tree', icon='PLAY') 51 | 52 | # Detail buttons in the sidebar. 53 | # If this function is not defined, the draw_buttons function is used instead 54 | def draw_buttons_ext(self, context, layout): 55 | layout.operator('node.evaluate_python_node_tree', icon='PLAY') 56 | 57 | def is_connected_to_base(self): 58 | return True 59 | 60 | def execute_python_node_tree(self): 61 | 62 | not_explored = [self] 63 | explored = [] 64 | 65 | # compute order to run nodes in (reverse BFS order) 66 | 67 | while len(not_explored)>0: 68 | n = not_explored.pop() 69 | 70 | # explore n 71 | for k in n.inputs.keys(): 72 | inp = n.inputs[k] 73 | if inp.is_linked: 74 | for link in inp.links: 75 | if link.from_socket.node in explored: 76 | continue 77 | 78 | not_explored.append(link.from_socket.node) 79 | 80 | if n not in explored: 81 | explored.append(n) 82 | 83 | explored.reverse() 84 | for n in explored: 85 | if n.get_dirty(): 86 | n.compute_output() -------------------------------------------------------------------------------- /pynodes/nodes/PythonEvalStatementNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | import numpy as np 4 | import bpy 5 | 6 | class PythonEvalStatementNode(nodes.PythonNode): 7 | # === Basics === 8 | # Description string 9 | '''Evaluates a Python statement (one-liner), returns object from return''' 10 | # Optional identifier string. If not explicitly defined, the python class name is used. 11 | bl_idname = 'PythonEvalFunctionNode' 12 | # Label for nice name display 13 | bl_label = "Eval Statement" 14 | 15 | arbitrary_code : bpy.props.StringProperty( 16 | name='',#'Code to Evaluate', 17 | description='Evaluates string as python, and returns object.', 18 | default="f'Hello World'", 19 | update=lambda s,c:s.update() 20 | ) 21 | 22 | # Additional buttons displayed on the node. 23 | def draw_buttons(self, context, layout): 24 | layout.prop(self, "arbitrary_code") 25 | 26 | # Detail buttons in the sidebar. 27 | # If this function is not defined, the draw_buttons function is used instead 28 | def draw_buttons_ext(self, context, layout): 29 | layout.prop(self, "arbitrary_code") 30 | 31 | def init(self, context): 32 | super().init(context) 33 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Python Object") 34 | self.width *= 3 # make this thing big so you can type in big statements. 35 | 36 | def run(self): 37 | self.set_output("Python Object", eval(self.arbitrary_code)) 38 | 39 | 40 | 41 | from pynodes import registry 42 | registry.registerNodeType(PythonEvalStatementNode) 43 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonLoadImageNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | # from pynodes.nodes import PythonNode 4 | import numpy as np 5 | import bpy 6 | 7 | class PythonLoadImageNode(nodes.PythonNode): 8 | # === Basics === 9 | # Description string 10 | '''Load Image Data''' 11 | # Optional identifier string. If not explicitly defined, the python class name is used. 12 | bl_idname = 'PythonLoadImageNode' 13 | # Label for nice name display 14 | bl_label = "Python Image Loader" 15 | 16 | # filename : bpy.props.StringProperty( 17 | # name='Filename', 18 | # description="Filepath of image.", 19 | # default='/home/cs/Resources/textures/000001_1k_color.png', 20 | # update=lambda s,c:s.update()#update_filename 21 | # ) 22 | # 23 | # # Additional buttons displayed on the node. 24 | # def draw_buttons(self, context, layout): 25 | # layout.prop(self, "filename") 26 | # 27 | # # Detail buttons in the sidebar. 28 | # # If this function is not defined, the draw_buttons function is used instead 29 | # def draw_buttons_ext(self, context, layout): 30 | # layout.prop(self, "filename") 31 | 32 | def init(self, context): 33 | super().init(context) 34 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Image") 35 | self.inputs.new('NodeSocketImage', "Image") 36 | bpy.data.images.load('/home/cs/Resources/textures/000001_1k_color.png', check_existing=True) 37 | 38 | def run(self): 39 | img = self.inputs["Image"].default_value 40 | img_arr = np.array(img.pixels).reshape([*img.size, img.channels]) 41 | self.set_output("Image", img_arr) 42 | 43 | 44 | 45 | from pynodes import registry 46 | registry.registerNodeType(PythonLoadImageNode) 47 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonNode.py: -------------------------------------------------------------------------------- 1 | from typing import overload 2 | 3 | from setuptools.sandbox import override_temp 4 | 5 | import pynodes 6 | import traceback 7 | import bpy 8 | import numpy as np 9 | 10 | # Mix-in class for all custom nodes in this tree type. 11 | # Defines a poll function to enable instantiation. 12 | class PythonCompositorTreeNode: 13 | @classmethod 14 | def poll(cls, ntree): 15 | # classes extending this will only appear in the python node tree 16 | return ntree.bl_idname == pynodes.PythonCompositorTree.bl_idname 17 | 18 | class ColorfulNode(bpy.types.Node): 19 | # === Basics === 20 | # Description string 21 | '''Abstract node that can change colors.''' 22 | 23 | def init(self, context): 24 | pass 25 | 26 | def set_color(self, color): 27 | self.use_custom_color = True 28 | self.color = color 29 | # bpy.ops.wm.redraw_timer(type='DRAW', iterations=1) 30 | 31 | def set_no_color(self): 32 | self.use_custom_color = False 33 | # bpy.ops.wm.redraw_timer(type='DRAW', iterations=1) 34 | 35 | # Derived from the Node base type. 36 | # Defines functionality of python node to only require that call is overloaded 37 | class PythonNode(ColorfulNode, PythonCompositorTreeNode): 38 | # === Basics === 39 | # Description string 40 | '''Abstract python node.''' 41 | # Optional identifier string. If not explicitly defined, the python class name is used. 42 | bl_idname = 'PythonNode' 43 | # Label for nice name display 44 | bl_label = "Python Node" 45 | # Icon identifier 46 | bl_icon = 'SCRIPT' 47 | 48 | # class Properties: 49 | is_dirty = True 50 | # bpy.props.BoolProperty( 51 | # name='dirty', 52 | # default=True 53 | # ) 54 | 55 | class PythonNodeRunError(Exception): 56 | ''' 57 | raised when a node has an error. 58 | ''' 59 | def __init__(self, node, e): 60 | self.node = node 61 | self.ex = e 62 | super().__init__( 63 | f'Exception raised by node {node.bl_idname}:\n{e}' 64 | ) 65 | 66 | def mark_dirty(self): 67 | ''' 68 | Propogate to all downstream nodes that this nodes is not up to date. 69 | ''' 70 | self.is_dirty = True 71 | self.set_no_color() 72 | for k in self.outputs.keys(): 73 | out = self.outputs[k] 74 | if out.is_linked: 75 | for o in out.links: 76 | node = o.to_socket.node 77 | if not node is self and o.is_valid and not node.get_dirty(): 78 | node.mark_dirty() 79 | 80 | 81 | def get_dirty(self): 82 | return self.is_dirty 83 | 84 | def is_connected_to_base(self): 85 | ''' 86 | Return true if this node is connected to a node which is a "base" node. 87 | ''' 88 | # BFS in direction of output nodes; find the first base node 89 | 90 | if isinstance(self, pynodes.nodes.PythonBaseNode): 91 | return True 92 | 93 | not_explored = [self] 94 | explored = set() 95 | 96 | while len(not_explored)>0: 97 | n = not_explored.pop() 98 | 99 | # explore n 100 | for k in n.outputs.keys(): 101 | out = n.outputs[k] 102 | if out.is_linked: 103 | for link in out.links: 104 | if link.to_socket.node in explored: 105 | continue 106 | 107 | if link.is_valid and isinstance(link, pynodes.nodes.PythonBaseNode): 108 | return True 109 | 110 | not_explored.append(link.to_socket.node) 111 | 112 | explored.add(n) 113 | 114 | return False 115 | 116 | def run(self): 117 | ''' 118 | Do whatever calculations this node does. Take input from self.get_input 119 | and set outputs using self.set_output. 120 | ''' 121 | pass 122 | 123 | def get_input(self, k, default_func=lambda:None): 124 | ''' 125 | Called by run to get value stored behind socket. 126 | ''' 127 | v = self.inputs[k] 128 | if v.is_linked and len(v.links)>0 and v.links[0].is_valid: 129 | o = v.links[0].from_socket 130 | # from sockets are always the first link; don't ever want to have more than one for simplicity 131 | value = o.get_value() 132 | return value 133 | 134 | return v.get_value() # gets value from argval input box 135 | 136 | def set_output(self, k, v): 137 | self.outputs[k].set_value(v) 138 | 139 | update_subscribers = [] 140 | 141 | def subscribe_to_update(self, callback): 142 | self.update_subscribers.append(callback) 143 | 144 | def unsubscribe_to_update(self, callback): 145 | if callback in self.update_subscribers: 146 | self.update_subscribers.remove(callback) 147 | 148 | def update(self): 149 | ''' 150 | Called when the node tree changes. 151 | ''' 152 | if not self.get_dirty(): 153 | self.mark_dirty() 154 | # mark node, and downstream nodes as dirty/not current 155 | 156 | for socket in self.inputs: 157 | if isinstance(socket, pynodes.AbstractPyObjectSocket): 158 | socket.node_updated() 159 | # call all socket callbacks and other subscribers 160 | for callback in self.update_subscribers: 161 | try: 162 | callback() 163 | except Exception as e: 164 | print('Encountered error while executing node update callback.') 165 | traceback.print_exc() 166 | # self.unsubscribe_to_update(callback) 167 | 168 | def interrupt_execution(self, e): 169 | raise PythonNode.PythonNodeRunError(self, e) 170 | 171 | def compute_output(self): 172 | # print(self, 'compute_output') 173 | try: 174 | if self.get_dirty(): 175 | self.set_color([0.0, 0.0, 0.5]) 176 | self.run() 177 | self.is_dirty = False 178 | # self.propagate() 179 | self.set_color([0.0, 0.5, 0.0]) 180 | except Exception as e: 181 | self.mark_dirty() 182 | self.set_color([0.5, 0.0, 0.0]) 183 | traceback.print_exc() 184 | self.interrupt_execution(e) 185 | 186 | # def propagate(self): 187 | # ''' 188 | # Pass this nodes outputs to nodes linked to outputs. 189 | # ''' 190 | # for out in self.outputs: 191 | # if out.is_linked: 192 | # for link in out.links: 193 | # if link.is_valid: 194 | # link.to_socket.set_value(out.get_value()) -------------------------------------------------------------------------------- /pynodes/nodes/PythonNodeGroupNodes.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | import time 4 | import bpy 5 | import numpy as np 6 | 7 | from pynodes import registry 8 | from bpy.utils import register_class 9 | 10 | class PythonNodeGroupIOSocket(pynodes.AbstractPyObjectVarArgSocket.Properties, pynodes.AbstractPyObjectVarArgSocket): 11 | ''' PyNodes node socket type for group input and output nodes''' 12 | # Optional identifier string. If not explicitly defined, the python class name is used. 13 | bl_idname = 'PythonNodeGroupIOSocket' 14 | # Label for nice name display 15 | bl_label = "PyNodes Node Group Input/Output Object Socket" 16 | 17 | # def __init__(self): 18 | # super().__init__() 19 | # # pin shape 20 | # self.display_shape = 'CIRCLE' 21 | 22 | def argvalue_updated(self): 23 | super().argvalue_updated() 24 | print('updated') 25 | 26 | def get_value(self): 27 | if self.identifier in self._value_[0]: 28 | return self._value_[0][self.identifier] 29 | else: 30 | return None 31 | 32 | def draw(self, context, layout, node, text): 33 | layout.prop(self, 'argvalue', text='') 34 | 35 | register_class(PythonNodeGroupIOSocket) 36 | 37 | class AbstractPyNodesGroupIONode(nodes.PythonNode): 38 | '''A socket that is matched to sockets on an input or output node, and has a name associated with it''' 39 | bl_category = 'Groups' 40 | 41 | def run(self): 42 | pass 43 | 44 | def update(self): 45 | super().update() 46 | ng = self.id_data.nodes.id_data 47 | for ang in bpy.data.node_groups: 48 | if ang!=ng: 49 | for n in ang.nodes: 50 | if n!=self: # might not be required 51 | if n.bl_idname==PythonNodeGroupNode.bl_idname and n.node_group==ng.name: 52 | n.setup_sockets() 53 | 54 | class PyNodesGroupInputsNode(AbstractPyNodesGroupIONode): 55 | # === Basics === 56 | # Description string 57 | '''Input to node group. Exposes inputs from a group node using this node group.''' 58 | # Optional identifier string. If not explicitly defined, the python class name is used. 59 | bl_idname = 'PyNodesGroupInputsNode' 60 | # Label for nice name display 61 | bl_label = "Group Input" 62 | 63 | def init(self, context): 64 | super().init(context) 65 | self.outputs.new(PythonNodeGroupIOSocket.bl_idname, "Inputs") 66 | 67 | registry.registerNodeType(PyNodesGroupInputsNode) 68 | 69 | class PyNodesGroupOutputsNode(AbstractPyNodesGroupIONode): 70 | # === Basics === 71 | # Description string 72 | '''Output to node group. Exposes outputs to a group node using this node group.''' 73 | # Optional identifier string. If not explicitly defined, the python class name is used. 74 | bl_idname = 'PyNodesGroupOutputsNode' 75 | # Label for nice name display 76 | bl_label = "Group Output" 77 | 78 | def init(self, context): 79 | super().init(context) 80 | self.inputs.new(PythonNodeGroupIOSocket.bl_idname, "Outputs") 81 | 82 | registry.registerNodeType(PyNodesGroupOutputsNode) 83 | 84 | class PythonNodeGroupNode(nodes.PythonNode): 85 | # === Basics === 86 | # Description string 87 | '''A group node, which executes nodes in a node group.''' 88 | # Optional identifier string. If not explicitly defined, the python class name is used. 89 | bl_idname = 'PythonNodeGroupNode' 90 | # Label for nice name display 91 | bl_label = "Node Group" 92 | bl_category = 'Groups' 93 | 94 | node_group : bpy.props.StringProperty( 95 | name='Node Group', 96 | description="Stores the node group to execute.", 97 | update=lambda s,c:s.node_group_changed() 98 | ) 99 | 100 | node_group_collection : bpy.props.CollectionProperty( 101 | name='Node Group Selector', 102 | type=bpy.types.PropertyGroup 103 | ) 104 | 105 | def update(self): 106 | super().update() 107 | self.update_node_group_collection() 108 | 109 | def node_group_changed(self): 110 | self.setup_sockets() 111 | self.update() 112 | 113 | def update_node_group_collection(self): 114 | self.node_group_collection.clear() 115 | for a in bpy.data.node_groups: 116 | self.node_group_collection.add().name = a.name 117 | 118 | def get_input_node_sockets(self): 119 | ng = bpy.data.node_groups[self.node_group] 120 | input_nodes = [ 121 | n 122 | for n in ng.nodes 123 | if n.bl_idname==PyNodesGroupInputsNode.bl_idname 124 | ] 125 | if len(input_nodes)>=1: 126 | return input_nodes[0].outputs 127 | else: 128 | return [] 129 | 130 | def get_output_node_sockets(self): 131 | ng = bpy.data.node_groups[self.node_group] 132 | output_nodes = [ 133 | n 134 | for n in ng.nodes 135 | if n.bl_idname==PyNodesGroupOutputsNode.bl_idname 136 | ] 137 | if len(output_nodes)>=1: 138 | return output_nodes[0].inputs 139 | else: 140 | return [] 141 | 142 | def setup_sockets(self): 143 | 144 | if self.node_group=='': 145 | # for the case where the node group selector was just cleared 146 | self.label = 'Node Group' 147 | self.inputs.clear() 148 | self.outputs.clear() 149 | return 150 | 151 | self.label = self.node_group 152 | 153 | ng = bpy.data.node_groups[self.node_group] 154 | 155 | input_node_outputs = self.get_input_node_sockets() 156 | # needs a behavior when there is no input node. should be own function 157 | output_node_inputs = self.get_output_node_sockets() 158 | # needs a behavior when there is no output node. should be own function 159 | 160 | # remove the sockets that are not going to be re-used from inputs 161 | input_keys = [s.argvalue for s in input_node_outputs] 162 | for in_socket in self.inputs: 163 | if in_socket.identifier not in input_keys: 164 | self.inputs.remove(in_socket) 165 | 166 | # add sockets we don't already have to self.inputs 167 | for i, in_socket in enumerate(input_node_outputs): 168 | if in_socket.argvalue in self.inputs.keys(): # skip all the ones we already have 169 | print('moving', in_socket.argvalue, i) 170 | self.inputs.move(self.inputs.find(in_socket.identifier), i) 171 | continue 172 | if not in_socket.is_empty(): 173 | self.inputs.new(pynodes.PyObjectSocket.bl_idname, in_socket.argvalue) 174 | 175 | # remove the sockets that are not going to be re-used from outputs 176 | output_keys = [s.argvalue for s in output_node_inputs] 177 | for out_socket in self.outputs: 178 | if out_socket.identifier not in output_keys: 179 | self.outputs.remove(out_socket) 180 | 181 | # add sockets we don't already have to self.outputs 182 | for i, out_socket in enumerate(output_node_inputs): 183 | if out_socket.argvalue in self.outputs.keys(): # skip all the ones we already have 184 | print('moving', out_socket.argvalue, i) 185 | self.outputs.move(self.outputs.find(out_socket.identifier), i) 186 | continue 187 | if not out_socket.is_empty(): 188 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, out_socket.argvalue) 189 | 190 | def draw_buttons(self, context, layout): 191 | layout.prop_search(self, 'node_group', self, 'node_group_collection', text='') 192 | 193 | def init(self, context): 194 | super().init(context) 195 | self.update_node_group_collection() 196 | 197 | def run(self): 198 | 199 | ng = bpy.data.node_groups[self.node_group] 200 | 201 | input_node = [n for n in ng.nodes if n.bl_idname==PyNodesGroupInputsNode.bl_idname ][0] 202 | output_node= [n for n in ng.nodes if n.bl_idname==PyNodesGroupOutputsNode.bl_idname][0] 203 | 204 | for in_socket in self.inputs: 205 | if not in_socket.is_empty(): 206 | for out_socket in input_node.outputs: 207 | if out_socket.argvalue==in_socket.identifier: 208 | input_node.set_output(out_socket.identifier, in_socket.get_value()) 209 | break 210 | 211 | # run computations between input and output nodes for given inputs 212 | 213 | output_node.compute_output() 214 | 215 | for out_socket in output_node.inputs: 216 | if not out_socket.is_empty(): 217 | self.set_output(out_socket.argvalue, output_node.get_input(out_socket.identifier)) 218 | 219 | registry.registerNodeType(PythonNodeGroupNode) 220 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonPrintResultBaseNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | # from pynodes.nodes.PythonBaseNode import PythonBaseNodeProperties 4 | import numpy as np 5 | import bpy 6 | 7 | class PythonPrintResultBaseNode(nodes.PythonBaseNode.Properties, nodes.PythonBaseNode): 8 | # === Basics === 9 | # Description string 10 | '''Print Result of Connected Nodes''' 11 | # Optional identifier string. If not explicitly defined, the python class name is used. 12 | bl_idname = 'PythonPrintResultBaseNode' 13 | # Label for nice name display 14 | bl_label = "Print Result of Connected Nodes" 15 | 16 | def init(self, context): 17 | super().init(context) 18 | self.inputs.new(pynodes.PyObjectSocket.bl_idname, "Result") 19 | 20 | def run(self): 21 | a = self.get_input("Result", lambda:self.empty) 22 | print('Result:\n', a) 23 | 24 | from pynodes import registry 25 | registry.registerNodeType(PythonPrintResultBaseNode) 26 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonRequestURLNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | import time 4 | import bpy 5 | import requests 6 | 7 | class PythonRequestURLNode(nodes.PythonNode): 8 | # === Basics === 9 | # Description string 10 | '''Hold thread of execution on this node for specified number of seconds.''' 11 | # Optional identifier string. If not explicitly defined, the python class name is used. 12 | bl_idname = 'PythonRequestURLNode' 13 | # Label for nice name display 14 | bl_label = "Get URL / Download" 15 | 16 | url : bpy.props.StringProperty( 17 | name='URL', 18 | default='https://1000logos.net/wp-content/uploads/2021/10/Batman-Logo.png', 19 | update=lambda s,c:s.update() 20 | ) 21 | 22 | # Additional buttons displayed on the node. 23 | def draw_buttons(self, context, layout): 24 | layout.prop(self, "url") 25 | 26 | # Detail buttons in the sidebar. 27 | # If this function is not defined, the draw_buttons function is used instead 28 | def draw_buttons_ext(self, context, layout): 29 | layout.prop(self, "url") 30 | 31 | def init(self, context): 32 | super().init(context) 33 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Request") 34 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Content") 35 | 36 | def run(self): 37 | 38 | r = requests.get(self.url) 39 | self.set_output("Request", r) 40 | self.set_output('Content', r.content) 41 | 42 | from pynodes import registry 43 | registry.registerNodeType(PythonRequestURLNode) 44 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonSaveImageBaseNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | # from pynodes.nodes.PythonBaseNode import PythonBaseNodeProperties 4 | import numpy as np 5 | import bpy 6 | 7 | class PythonSaveImageBaseNode(nodes.PythonBaseNode.Properties, nodes.PythonBaseNode): 8 | # === Basics === 9 | # Description string 10 | '''Save image to image viewer.''' 11 | # Optional identifier string. If not explicitly defined, the python class name is used. 12 | bl_idname = 'PythonSaveImageBaseNode' 13 | # Label for nice name display 14 | bl_label = "Python Image Result" 15 | 16 | empty = np.zeros([6, 9, 4], dtype=np.float32) 17 | 18 | def init(self, context): 19 | super().init(context) 20 | self.inputs.new(pynodes.PyObjectSocket.bl_idname, "Image") 21 | 22 | def run(self): 23 | array = self.get_input("Image") 24 | alpha = array.shape[2]==4 25 | if self.bl_label in bpy.data.images.keys(): 26 | bpy.data.images.remove(bpy.data.images[self.bl_label]) 27 | image = bpy.data.images.new( 28 | self.bl_label, 29 | alpha=alpha, 30 | width=array.shape[1], 31 | height=array.shape[0] 32 | ) 33 | image.pixels = array.ravel() 34 | 35 | from pynodes import registry 36 | registry.registerNodeType(PythonSaveImageBaseNode) 37 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonShowArrayShapeBaseNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | # from pynodes.nodes.PythonBaseNode import PythonBaseNodeProperties 4 | import numpy as np 5 | import bpy 6 | 7 | class PythonShowArrayShapeBaseNode(nodes.PythonBaseNode.Properties, nodes.PythonBaseNode): 8 | # === Basics === 9 | # Description string 10 | '''Print the shape of given array to the nodes text''' 11 | # Optional identifier string. If not explicitly defined, the python class name is used. 12 | bl_idname = 'PythonShowArrayShapeBaseNode' 13 | # Label for nice name display 14 | bl_label = "Show Array Shape" 15 | 16 | def init(self, context): 17 | super().init(context) 18 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Shape") 19 | self.inputs.new(pynodes.PyObjectSocket.bl_idname, "Array") 20 | 21 | def draw_buttons(self, context, layout): 22 | layout.operator('node.evaluate_python_node_tree', icon='PLAY') 23 | row = layout.row() 24 | row.label(text=self.outputs["Shape"].get_value().__str__()) 25 | 26 | def run(self): 27 | a = self.get_input("Array", lambda:self.empty) 28 | output = np.shape(a) 29 | self.set_output("Shape", output) 30 | print('Result:\n', np.shape(a)) 31 | 32 | from pynodes import registry 33 | registry.registerNodeType(PythonShowArrayShapeBaseNode) 34 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonTestNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | import numpy as np 4 | import bpy 5 | 6 | class PythonTestNode(nodes.PythonNode): 7 | # === Basics === 8 | # Description string 9 | '''Load Image Data''' 10 | # Optional identifier string. If not explicitly defined, the python class name is used. 11 | bl_idname = 'PythonTestNode' 12 | # Label for nice name display 13 | bl_label = "Python Test Node" 14 | 15 | def init(self, context): 16 | super().init(context) 17 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Random Matrix") 18 | 19 | def run(self): 20 | r = np.random.uniform(size=(50,2), low=-5, high=5) 21 | r[r>1] = 0 22 | self.set_output("Random Matrix", r) 23 | 24 | 25 | 26 | from pynodes import registry 27 | registry.registerNodeType(PythonTestNode) 28 | -------------------------------------------------------------------------------- /pynodes/nodes/PythonWaitSecondsNode.py: -------------------------------------------------------------------------------- 1 | import pynodes 2 | from pynodes import nodes 3 | import time 4 | import bpy 5 | 6 | class PythonWaitSecondsNode(nodes.PythonNode): 7 | # === Basics === 8 | # Description string 9 | '''Hold thread of execution on this node for specified number of seconds.''' 10 | # Optional identifier string. If not explicitly defined, the python class name is used. 11 | bl_idname = 'PythonWaitSecondsNode' 12 | # Label for nice name display 13 | bl_label = "Pause" 14 | 15 | seconds : bpy.props.FloatProperty( 16 | name='Seconds', 17 | description="Seconds to wait", 18 | default=6.9, 19 | update=lambda s,c:s.update() 20 | ) 21 | 22 | # Additional buttons displayed on the node. 23 | def draw_buttons(self, context, layout): 24 | layout.prop(self, "seconds") 25 | 26 | # Detail buttons in the sidebar. 27 | # If this function is not defined, the draw_buttons function is used instead 28 | def draw_buttons_ext(self, context, layout): 29 | layout.prop(self, "seconds") 30 | 31 | def init(self, context): 32 | super().init(context) 33 | self.inputs.new(pynodes.PyObjectSocket.bl_idname, "Anything") 34 | self.outputs.new(pynodes.PyObjectSocket.bl_idname, "Anything") 35 | 36 | def run(self): 37 | inp = self.get_input("Anything", lambda:None) 38 | time.sleep(self.seconds) 39 | self.set_output("Anything", inp) 40 | 41 | from pynodes import registry 42 | registry.registerNodeType(PythonWaitSecondsNode) 43 | -------------------------------------------------------------------------------- /pynodes/nodes/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | from pynodes.nodes.PythonNode import PythonNode 3 | from pynodes.nodes.PythonBaseNode import PythonBaseNode 4 | 5 | from pynodes.nodes.PythonLoadImageNode import PythonLoadImageNode 6 | from pynodes.nodes.PythonSaveImageBaseNode import PythonSaveImageBaseNode 7 | from pynodes.nodes.PythonPrintResultBaseNode import PythonPrintResultBaseNode 8 | from pynodes.nodes.PythonWaitSecondsNode import PythonWaitSecondsNode 9 | from pynodes.nodes.PythonShowArrayShapeBaseNode import PythonShowArrayShapeBaseNode 10 | from pynodes.nodes.PythonNodeGroupNodes import * 11 | 12 | from pynodes.nodes.PythonEvalStatementNode import PythonEvalStatementNode 13 | from pynodes.nodes.PythonRequestURLNode import PythonRequestURLNode 14 | from pynodes.nodes.PythonTestNode import PythonTestNode 15 | 16 | from pynodes.nodes.AutoNodeTypeAdder import add_basic_nodes 17 | 18 | add_basic_nodes() 19 | -------------------------------------------------------------------------------- /pynodes/registry.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Global list of registered classes with search names. 3 | ''' 4 | 5 | import bpy 6 | 7 | import nodeitems_utils 8 | from nodeitems_utils import NodeCategory, NodeItem 9 | 10 | import pynodes 11 | 12 | node_registry_dict = {} 13 | 14 | node_classes = [] 15 | 16 | operator_classes = [] 17 | 18 | def registerNodeType(clazz): 19 | 20 | try: 21 | category = clazz.mmod 22 | except AttributeError as ae: 23 | category = 'Custom' 24 | 25 | if category.startswith('_'): 26 | return 27 | 28 | if not category in node_registry_dict.keys(): 29 | node_registry_dict[category] = [] 30 | node_registry_dict[category].append(clazz) 31 | 32 | if not clazz in node_classes: 33 | node_classes.append(clazz) 34 | 35 | def registerOperator(clazz): 36 | 37 | if clazz not in operator_classes: 38 | operator_classes.append(clazz) 39 | 40 | def unregisterAll(): 41 | try: 42 | nodeitems_utils.unregister_node_categories('pythonGlobalFuncs') 43 | for cls in reversed(node_classes): 44 | bpy.utils.unregister_class(cls) 45 | for cls in reversed(operator_classes): 46 | bpy.utils.unregister_class(cls) 47 | except RuntimeError as re: 48 | print('re', re, 'caught') 49 | 50 | def registerAll(): 51 | node_categories = [] 52 | 53 | for cls in operator_classes: 54 | bpy.utils.register_class(cls) 55 | 56 | i = 0 57 | for category, clazzes in sorted(node_registry_dict.items()): 58 | cat = pynodes.PythonCompositorNodeCategory( 59 | f'PythonNode_{i:09d}',# category.replace('.', '_'), 60 | category, 61 | items= 62 | [ 63 | NodeItem(clazz.bl_idname) 64 | for clazz in clazzes if not clazz.bl_idname.split('.')[-1].startswith('_') 65 | ] 66 | ) 67 | 68 | node_categories.append(cat) 69 | i+=1 70 | 71 | for cls in node_classes: 72 | bpy.utils.register_class(cls) 73 | 74 | nodeitems_utils.register_node_categories('pythonGlobalFuncs', node_categories) 75 | -------------------------------------------------------------------------------- /readme-assets/a_python_node.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/readme-assets/a_python_node.png -------------------------------------------------------------------------------- /readme-assets/a_python_node_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/readme-assets/a_python_node_tree.png -------------------------------------------------------------------------------- /readme-assets/another_python_node_concatenate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/readme-assets/another_python_node_concatenate.png -------------------------------------------------------------------------------- /readme-assets/another_python_node_lists.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/readme-assets/another_python_node_lists.png -------------------------------------------------------------------------------- /readme-assets/another_python_node_tree_shape_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CharlesSS07/blender-python-nodes/e4cd410ca8c6717fb2fca98fba95749e68a851ef/readme-assets/another_python_node_tree_shape_example.png -------------------------------------------------------------------------------- /test-blender-pynodes.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # This is not necessary for production; This script simply automates the process of starting blender and installing the newest version of the addon. 4 | # In production, the user would install this addon from pynodes.zip and never need to resinstall it, whereas development requires reinstalling constantly. 5 | 6 | BLENDER_EXECUTABLE='/Applications/Blender.app/Contents/MacOS/Blender' 7 | 8 | $BLENDER_EXECUTABLE $PWD/example.blend -P $PWD/install-pynodes.py 9 | --------------------------------------------------------------------------------