Skip to content

Commit 7bd92df

Browse files
Merge pull request #57 from BrendanParmer/v2.2
v2.2.0
2 parents 94ad28a + 7452ba3 commit 7bd92df

File tree

6 files changed

+254
-121
lines changed

6 files changed

+254
-121
lines changed

__init__.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "Node to Python",
33
"description": "Convert Blender node groups to a Python add-on!",
44
"author": "Brendan Parmer",
5-
"version": (2, 1, 0),
5+
"version": (2, 2, 0),
66
"blender": (3, 0, 0),
77
"location": "Node",
88
"category": "Node",
@@ -12,16 +12,18 @@
1212
import importlib
1313
importlib.reload(materials)
1414
importlib.reload(geo_nodes)
15+
importlib.reload(options)
1516
else:
1617
from . import materials
1718
from . import geo_nodes
19+
from . import options
1820

1921
import bpy
2022

2123
class NodeToPythonMenu(bpy.types.Menu):
2224
bl_idname = "NODE_MT_node_to_python"
2325
bl_label = "Node To Python"
24-
26+
2527
@classmethod
2628
def poll(cls, context):
2729
return True
@@ -30,20 +32,30 @@ def draw(self, context):
3032
layout = self.layout.column_flow(columns=1)
3133
layout.operator_context = 'INVOKE_DEFAULT'
3234

35+
36+
37+
3338
classes = [NodeToPythonMenu,
39+
options.NTPOptions,
3440
geo_nodes.GeoNodesToPython,
3541
geo_nodes.SelectGeoNodesMenu,
3642
geo_nodes.GeoNodesToPythonPanel,
3743
materials.MaterialToPython,
3844
materials.SelectMaterialMenu,
39-
materials.MaterialToPythonPanel]
45+
materials.MaterialToPythonPanel,
46+
options.NTPOptionsPanel
47+
]
4048

4149
def register():
4250
for cls in classes:
4351
bpy.utils.register_class(cls)
52+
scene = bpy.types.Scene
53+
scene.ntp_options = bpy.props.PointerProperty(type=options.NTPOptions)
54+
4455
def unregister():
4556
for cls in classes:
4657
bpy.utils.unregister_class(cls)
58+
del bpy.types.Scene.ntp_options
4759

4860
if __name__ == "__main__":
4961
register()

docs/README.md

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython)
66

77
## About
8-
A Blender add-on to create add-ons! This add-on will take your Geometry Nodes or Materials and convert them into legible Python add-ons!
8+
A Blender add-on to create scripts and add-ons! This add-on will take your Geometry Nodes or Materials and convert them into legible Python code.
99

1010
Node To Python automatically handles node layout, default values, subgroups, naming, colors, and more!
1111

@@ -15,11 +15,11 @@ Blender's node-based editors are powerful, yet accessible tools, and I wanted to
1515
* interfacing with other parts of the software or properties of an object
1616

1717
## Supported Versions
18-
NodeToPython v2.1 is compatible with Blender 3.0 - 3.6 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update.
18+
NodeToPython v2.2 is compatible with Blender 3.0 - 3.6 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update.
1919

2020
## Installation
2121
1. Download the `NodeToPython.zip` file from the [latest release](https://github.com/BrendanParmer/NodeToPython/releases)
22-
* If you clone the repository or download other options, you'll need to rename the zip and the first folder to "NodeToPython" so Blender can properly import the add-on
22+
* If you download other options, you'll need to rename the zip and the first folder to "NodeToPython" so Blender can properly import the add-on
2323
2. In Blender, navigate to `Edit > Preferences > Add-ons`
2424
3. Click Install, and find where you downloaded the zip file. Then hit the `Install Add-on` button, and you're done!
2525

@@ -30,26 +30,25 @@ In the tab, there's panels to create add-ons for Geometry Nodes and Materials, e
3030

3131
![Add-on Location](./img/location.png "Add-on Location")
3232

33-
Just select the one you want, and soon a zip file will be created in an `addons` folder located in the folder where your blend file is.
34-
35-
From here, you can install it like a regular add-on.
33+
Select the node group you want code for, and you'll be prompted with a **Script** or **Add-on** option.
34+
* **Script** mode creates a function that generates the node tree and copies it to your Blender clipboard.
35+
* Doesn't include `import bpy` line
36+
* To keep NodeToPython cross-platform and independent of third-party libraries, to get it into your system clipboard you need to paste into the Blender text editor and recopy it currently
37+
* **Add-on** mode generates a zip file for you in the save directory specified in the NodeToPython menu. From here, you can install it like a regular add-on. The generated add-on comes complete with operator registration and creating a modifier/material for the node tree to be used in.
3638

3739
## Future
38-
### v2.2
39-
* A "copy" mode, where just the functionality to build the node group is just copied to the clipbaord
40-
* Choose the location where to save the add-on
41-
42-
### v2.3
40+
### v3.x
4341
* Expansion to Compositing nodes
44-
* Add all referenced assets to the Asset Library for use outside of the original blend file
42+
* New Blender 4.0 nodes and changes
4543

4644
### Later
45+
* Better asset handling
4746
* Auto-set handle movies and image sequences
4847
* Automatically format code to be PEP8 compliant
4948
* Automatically detect the minimum version of Blender needed to run the add-on
5049

5150
## Potential Issues
52-
* As of version 2.1, the add-on will not set default values for
51+
* As of version 2.2, the add-on will not set default values for
5352
* Scripts
5453
* IES files
5554
* Filepaths
@@ -62,6 +61,7 @@ From here, you can install it like a regular add-on.
6261
* Textures
6362

6463
* In a future version, I plan on having the add-on adding all of the above to the Asset Library for reference
64+
* You may run into naming conflicts if your add-on shares a name with another Blender add-on or operator (see [#56](https://github.com/BrendanParmer/NodeToPython/issues/56))
6565

6666
## Bug Reports and Suggestions
6767

@@ -72,4 +72,4 @@ When submitting an issue, please include
7272
* A short description of what you were trying to accomplish, or steps to reproduce the issue.
7373
* Sample blend files are more than welcome!
7474

75-
Got suggestions? Create an issue, happy to hear what features people want.
75+
Got suggestions? Create an issue, happy to hear what features people want.

geo_nodes.py

Lines changed: 93 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import os
33

44
from .utils import *
5+
from io import StringIO
56

67
geo_node_settings = {
78
# Attribute nodes
@@ -168,6 +169,14 @@ class GeoNodesToPython(bpy.types.Operator):
168169
bl_label = "Geo Nodes to Python"
169170
bl_options = {'REGISTER', 'UNDO'}
170171

172+
mode : bpy.props.EnumProperty(
173+
name = "Mode",
174+
items = [
175+
('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
176+
('ADDON', "Addon", "Create a full addon")
177+
]
178+
)
179+
171180
geo_nodes_group_name: bpy.props.StringProperty(name="Node Group")
172181

173182
def execute(self, context):
@@ -176,37 +185,40 @@ def execute(self, context):
176185

177186
#set up names to use in generated addon
178187
nt_var = clean_string(nt.name)
179-
class_name = clean_string(nt.name.replace(" ", "").replace('.', ""),
180-
lower = False)
181-
182-
#find base directory to save new addon
183-
base_dir = bpy.path.abspath("//")
184-
if not base_dir or base_dir == "":
185-
self.report({'ERROR'},
186-
("NodeToPython: Save your blend file before using "
187-
"NodeToPython!"))
188-
return {'CANCELLED'}
189-
190-
#save in /addons/ subdirectory
191-
zip_dir = os.path.join(base_dir, "addons", nt_var)
192-
addon_dir = os.path.join(zip_dir, nt_var)
193-
if not os.path.exists(addon_dir):
194-
os.makedirs(addon_dir)
195-
file = open(f"{addon_dir}/__init__.py", "w")
196-
197-
create_header(file, nt.name)
198-
init_operator(file, class_name, nt_var, nt.name)
199188

200-
file.write("\tdef execute(self, context):\n")
189+
if self.mode == 'ADDON':
190+
#find base directory to save new addon
191+
dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
192+
if not dir or dir == "":
193+
self.report({'ERROR'},
194+
("NodeToPython: Save your blend file before using "
195+
"NodeToPython!"))
196+
return {'CANCELLED'}
197+
198+
#save in addons/ subdirectory
199+
zip_dir = os.path.join(dir, nt_var)
200+
addon_dir = os.path.join(zip_dir, nt_var)
201+
202+
if not os.path.exists(addon_dir):
203+
os.makedirs(addon_dir)
204+
file = open(f"{addon_dir}/__init__.py", "w")
205+
206+
create_header(file, nt.name)
207+
class_name = clean_string(nt.name.replace(" ", "").replace('.', ""),
208+
lower = False)
209+
init_operator(file, class_name, nt_var, nt.name)
210+
file.write("\tdef execute(self, context):\n")
211+
else:
212+
file = StringIO("")
201213

202214
#set to keep track of already created node trees
203215
node_trees = set()
204216

205217
#dictionary to keep track of node->variable name pairs
206218
node_vars = {}
207219

208-
#keeps track of all used variables
209-
used_vars = set()
220+
#dictionary to keep track of variables->usage count pairs
221+
used_vars = {}
210222

211223
def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
212224
nt_var = create_var(node_tree.name, used_vars)
@@ -218,8 +230,8 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
218230
file.write(f"{outer}def {nt_var}_node_group():\n")
219231
file.write((f"{inner}{nt_var}"
220232
f"= bpy.data.node_groups.new("
221-
f"type = \"GeometryNodeTree\", "
222-
f"name = \"{node_tree.name}\")\n"))
233+
f"type = \'GeometryNodeTree\', "
234+
f"name = {str_to_py_str(node_tree.name)})\n"))
223235
file.write("\n")
224236

225237
inputs_set = False
@@ -238,40 +250,48 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
238250
used_vars)
239251
node_trees.add(node_nt)
240252
elif node.bl_idname == 'NodeGroupInput' and not inputs_set:
241-
group_io_settings(node, file, inner, "input", nt_var, node_tree)
253+
group_io_settings(node, file, inner, "input", nt_var,
254+
node_tree)
242255
inputs_set = True
243256

244257
elif node.bl_idname == 'NodeGroupOutput' and not outputs_set:
245-
group_io_settings(node, file, inner, "output", nt_var, node_tree)
258+
group_io_settings(node, file, inner, "output", nt_var,
259+
node_tree)
246260
outputs_set = True
247261

248262
#create node
249263
node_var = create_node(node, file, inner, nt_var,
250-
node_vars, used_vars)
264+
node_vars, used_vars)
251265
set_settings_defaults(node, geo_node_settings, file, inner,
252-
node_var)
266+
node_var)
253267
hide_sockets(node, file, inner, node_var)
254268

255269
if node.bl_idname == 'GeometryNodeGroup':
256270
if node.node_tree is not None:
257271
file.write((f"{inner}{node_var}.node_tree = "
258272
f"bpy.data.node_groups"
259-
f"[\"{node.node_tree.name}\"]\n"))
273+
f"[{str_to_py_str(node.node_tree.name)}]\n"))
274+
260275
elif node.bl_idname == 'ShaderNodeValToRGB':
261276
color_ramp_settings(node, file, inner, node_var)
277+
262278
elif node.bl_idname in curve_nodes:
263279
curve_node_settings(node, file, inner, node_var)
264-
elif node.bl_idname in image_nodes:
280+
281+
elif node.bl_idname in image_nodes and self.mode == 'ADDON':
265282
img = node.image
266283
if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}:
267284
save_image(img, addon_dir)
268285
load_image(img, file, inner, f"{node_var}.image")
286+
269287
elif node.bl_idname == 'GeometryNodeSimulationInput':
270288
sim_inputs.append(node)
289+
271290
elif node.bl_idname == 'GeometryNodeSimulationOutput':
272291
file.write(f"{inner}#remove generated sim state items\n")
273292
file.write(f"{inner}for item in {node_var}.state_items:\n")
274293
file.write(f"{inner}\t{node_var}.state_items.remove(item)\n")
294+
275295
for i, si in enumerate(node.state_items):
276296
socket_type = enum_to_py_str(si.socket_type)
277297
name = str_to_py_str(si.name)
@@ -284,7 +304,10 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
284304
f"{attr_domain}\n"))
285305

286306
if node.bl_idname != 'GeometryNodeSimulationInput':
287-
set_input_defaults(node, file, inner, node_var, addon_dir)
307+
if self.mode == 'ADDON':
308+
set_input_defaults(node, file, inner, node_var, addon_dir)
309+
else:
310+
set_input_defaults(node, file, inner, node_var)
288311
set_output_defaults(node, file, inner, node_var)
289312

290313
#create simulation zones
@@ -295,13 +318,18 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
295318
f"({sim_output_var})\n"))
296319

297320
#must set defaults after paired with output
298-
set_input_defaults(sim_input, file, inner, sim_input_var, addon_dir)
321+
if self.mode == 'ADDON':
322+
set_input_defaults(node, file, inner, node_var, addon_dir)
323+
else:
324+
set_input_defaults(node, file, inner, node_var)
299325
set_output_defaults(sim_input, file, inner, sim_input_var)
300326

327+
#set look of nodes
301328
set_parents(node_tree, file, inner, node_vars)
302329
set_locations(node_tree, file, inner, node_vars)
303330
set_dimensions(node_tree, file, inner, node_vars)
304331

332+
#create connections
305333
init_links(node_tree, file, inner, nt_var, node_vars)
306334

307335
file.write(f"{inner}return {nt_var}\n")
@@ -311,7 +339,11 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
311339
f"{nt_var}_node_group()\n\n"))
312340
return used_vars
313341

314-
process_geo_nodes_group(nt, 2, node_vars, used_vars)
342+
if self.mode == 'ADDON':
343+
level = 2
344+
else:
345+
level = 0
346+
process_geo_nodes_group(nt, level, node_vars, used_vars)
315347

316348
def apply_modifier():
317349
#get object
@@ -323,21 +355,37 @@ def apply_modifier():
323355
file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, "
324356
f"type = 'NODES')\n"))
325357
file.write(f"\t\tmod.node_group = {nt_var}\n")
326-
apply_modifier()
327-
328-
file.write("\t\treturn {'FINISHED'}\n\n")
329-
330-
create_menu_func(file, class_name)
331-
create_register_func(file, class_name)
332-
create_unregister_func(file, class_name)
333-
create_main_func(file)
358+
if self.mode == 'ADDON':
359+
apply_modifier()
334360

361+
file.write("\t\treturn {'FINISHED'}\n\n")
362+
363+
create_menu_func(file, class_name)
364+
create_register_func(file, class_name)
365+
create_unregister_func(file, class_name)
366+
create_main_func(file)
367+
else:
368+
context.window_manager.clipboard = file.getvalue()
335369
file.close()
336370

337-
zip_addon(zip_dir)
371+
if self.mode == 'ADDON':
372+
zip_addon(zip_dir)
338373

374+
#alert user that NTP is finished
375+
if self.mode == 'SCRIPT':
376+
location = "clipboard"
377+
else:
378+
location = dir
379+
self.report({'INFO'},
380+
f"NodeToPython: Saved geometry nodes group to {location}")
339381
return {'FINISHED'}
382+
383+
def invoke(self, context, event):
384+
return context.window_manager.invoke_props_dialog(self)
340385

386+
def draw(self, context):
387+
self.layout.prop(self, "mode")
388+
341389
class SelectGeoNodesMenu(bpy.types.Menu):
342390
bl_idname = "NODE_MT_ntp_geo_nodes_selection"
343391
bl_label = "Select Geo Nodes"
@@ -350,7 +398,8 @@ def draw(self, context):
350398
layout = self.layout.column_flow(columns=1)
351399
layout.operator_context = 'INVOKE_DEFAULT'
352400

353-
geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY']
401+
geo_node_groups = [node for node in bpy.data.node_groups
402+
if node.type == 'GEOMETRY']
354403

355404
for geo_ng in geo_node_groups:
356405
op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name)

0 commit comments

Comments
 (0)