├── .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 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/profiles_settings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | 
5 | 
6 | 
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 |
--------------------------------------------------------------------------------