Skip to content

Commit 242f9fe

Browse files
Merge pull request #55 from BrendanParmer/save_location
feat: added option panel and save directory
2 parents c75d3df + 7ac4da5 commit 242f9fe

File tree

5 files changed

+91
-30
lines changed

5 files changed

+91
-30
lines changed

__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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,21 +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
4047
]
4148

4249
def register():
4350
for cls in classes:
4451
bpy.utils.register_class(cls)
52+
scene = bpy.types.Scene
53+
scene.ntp_options = bpy.props.PointerProperty(type=options.NTPOptions)
54+
4555
def unregister():
4656
for cls in classes:
4757
bpy.utils.unregister_class(cls)
58+
del bpy.types.Scene.ntp_options
4859

4960
if __name__ == "__main__":
5061
register()

geo_nodes.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ class GeoNodesToPython(bpy.types.Operator):
176176
('ADDON', "Addon", "Create a full addon")
177177
]
178178
)
179+
179180
geo_nodes_group_name: bpy.props.StringProperty(name="Node Group")
180181

181182
def execute(self, context):
@@ -187,16 +188,17 @@ def execute(self, context):
187188

188189
if self.mode == 'ADDON':
189190
#find base directory to save new addon
190-
base_dir = bpy.path.abspath("//")
191-
if not base_dir or base_dir == "":
191+
dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
192+
if not dir or dir == "":
192193
self.report({'ERROR'},
193194
("NodeToPython: Save your blend file before using "
194195
"NodeToPython!"))
195196
return {'CANCELLED'}
196197

197198
#save in addons/ subdirectory
198-
zip_dir = os.path.join(base_dir, "addons", nt_var)
199+
zip_dir = os.path.join(dir, nt_var)
199200
addon_dir = os.path.join(zip_dir, nt_var)
201+
200202
if not os.path.exists(addon_dir):
201203
os.makedirs(addon_dir)
202204
file = open(f"{addon_dir}/__init__.py", "w")
@@ -215,8 +217,8 @@ def execute(self, context):
215217
#dictionary to keep track of node->variable name pairs
216218
node_vars = {}
217219

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

221223
def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
222224
nt_var = create_var(node_tree.name, used_vars)
@@ -248,40 +250,48 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
248250
used_vars)
249251
node_trees.add(node_nt)
250252
elif node.bl_idname == 'NodeGroupInput' and not inputs_set:
251-
group_io_settings(node, file, inner, "input", nt_var, node_tree)
253+
group_io_settings(node, file, inner, "input", nt_var,
254+
node_tree)
252255
inputs_set = True
253256

254257
elif node.bl_idname == 'NodeGroupOutput' and not outputs_set:
255-
group_io_settings(node, file, inner, "output", nt_var, node_tree)
258+
group_io_settings(node, file, inner, "output", nt_var,
259+
node_tree)
256260
outputs_set = True
257261

258262
#create node
259263
node_var = create_node(node, file, inner, nt_var,
260-
node_vars, used_vars)
264+
node_vars, used_vars)
261265
set_settings_defaults(node, geo_node_settings, file, inner,
262-
node_var)
266+
node_var)
263267
hide_sockets(node, file, inner, node_var)
264268

265269
if node.bl_idname == 'GeometryNodeGroup':
266270
if node.node_tree is not None:
267271
file.write((f"{inner}{node_var}.node_tree = "
268272
f"bpy.data.node_groups"
269273
f"[{str_to_py_str(node.node_tree.name)}]\n"))
274+
270275
elif node.bl_idname == 'ShaderNodeValToRGB':
271276
color_ramp_settings(node, file, inner, node_var)
277+
272278
elif node.bl_idname in curve_nodes:
273279
curve_node_settings(node, file, inner, node_var)
280+
274281
elif node.bl_idname in image_nodes and self.mode == 'ADDON':
275282
img = node.image
276283
if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}:
277284
save_image(img, addon_dir)
278285
load_image(img, file, inner, f"{node_var}.image")
286+
279287
elif node.bl_idname == 'GeometryNodeSimulationInput':
280288
sim_inputs.append(node)
289+
281290
elif node.bl_idname == 'GeometryNodeSimulationOutput':
282291
file.write(f"{inner}#remove generated sim state items\n")
283292
file.write(f"{inner}for item in {node_var}.state_items:\n")
284293
file.write(f"{inner}\t{node_var}.state_items.remove(item)\n")
294+
285295
for i, si in enumerate(node.state_items):
286296
socket_type = enum_to_py_str(si.socket_type)
287297
name = str_to_py_str(si.name)
@@ -314,10 +324,12 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
314324
set_input_defaults(node, file, inner, node_var)
315325
set_output_defaults(sim_input, file, inner, sim_input_var)
316326

327+
#set look of nodes
317328
set_parents(node_tree, file, inner, node_vars)
318329
set_locations(node_tree, file, inner, node_vars)
319330
set_dimensions(node_tree, file, inner, node_vars)
320331

332+
#create connections
321333
init_links(node_tree, file, inner, nt_var, node_vars)
322334

323335
file.write(f"{inner}return {nt_var}\n")
@@ -358,7 +370,14 @@ def apply_modifier():
358370

359371
if self.mode == 'ADDON':
360372
zip_addon(zip_dir)
361-
self.report({'INFO'}, "NodeToPython: Saved geometry nodes group")
373+
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}")
362381
return {'FINISHED'}
363382

364383
def invoke(self, context, event):
@@ -379,7 +398,8 @@ def draw(self, context):
379398
layout = self.layout.column_flow(columns=1)
380399
layout.operator_context = 'INVOKE_DEFAULT'
381400

382-
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']
383403

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

materials.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,15 +103,14 @@ def execute(self, context):
103103
mat_var = clean_string(self.material_name)
104104

105105
if self.mode == 'ADDON':
106-
dir = bpy.path.abspath("//")
106+
dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
107107
if not dir or dir == "":
108108
self.report({'ERROR'},
109109
("NodeToPython: Save your blender file before using "
110110
"NodeToPython!"))
111111
return {'CANCELLED'}
112112

113-
#save in addons/ subdirectory
114-
zip_dir = os.path.join(dir, "addons", mat_var)
113+
zip_dir = os.path.join(dir, mat_var)
115114
addon_dir = os.path.join(zip_dir, mat_var)
116115
if not os.path.exists(addon_dir):
117116
os.makedirs(addon_dir)
@@ -129,6 +128,7 @@ def create_material(indent: str):
129128
file.write((f"{indent}mat = bpy.data.materials.new("
130129
f"name = {str_to_py_str(self.material_name)})\n"))
131130
file.write(f"{indent}mat.use_nodes = True\n")
131+
132132
if self.mode == 'ADDON':
133133
create_material("\t\t")
134134
elif self.mode == 'SCRIPT':
@@ -141,7 +141,7 @@ def create_material(indent: str):
141141
node_vars = {}
142142

143143
#keeps track of all used variables
144-
used_vars = set()
144+
used_vars = {}
145145

146146
def is_outermost_node_group(level: int) -> bool:
147147
if self.mode == 'ADDON' and level == 2:
@@ -189,7 +189,8 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars):
189189
if node.bl_idname == 'ShaderNodeGroup':
190190
node_nt = node.node_tree
191191
if node_nt is not None and node_nt not in node_trees:
192-
process_mat_node_group(node_nt, level + 1, node_vars, used_vars)
192+
process_mat_node_group(node_nt, level + 1, node_vars,
193+
used_vars)
193194
node_trees.add(node_nt)
194195

195196
node_var = create_node(node, file, inner, nt_var, node_vars,
@@ -258,7 +259,11 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars):
258259

259260
if self.mode == 'ADDON':
260261
zip_addon(zip_dir)
261-
self.report({'INFO'}, "NodeToPython: Saved material")
262+
if self.mode == 'SCRIPT':
263+
location = "clipboard"
264+
else:
265+
location = dir
266+
self.report({'INFO'}, f"NodeToPython: Saved material to {location}")
262267
return {'FINISHED'}
263268

264269
def invoke(self, context, event):

options.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import bpy
2+
3+
class NTPOptions(bpy.types.PropertyGroup):
4+
"""
5+
Property group used during conversion of node group to python
6+
"""
7+
dir_path : bpy.props.StringProperty(
8+
name = "Save Location",
9+
subtype='DIR_PATH',
10+
description="Save location if generating an add-on",
11+
default = "//"
12+
)
13+
14+
class NTPOptionsPanel(bpy.types.Panel):
15+
bl_label = "Options"
16+
bl_idname = "NODE_PT_ntp_options"
17+
bl_space_type = 'NODE_EDITOR'
18+
bl_region_type = 'UI'
19+
bl_context = ''
20+
bl_category = "NodeToPython"
21+
22+
@classmethod
23+
def poll(cls, context):
24+
return True
25+
def draw(self, context):
26+
layout = self.layout
27+
layout.operator_context = 'INVOKE_DEFAULT'
28+
layout.prop(context.scene.ntp_options, "dir_path")

utils.py

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,13 @@ def init_operator(file: TextIO, name: str, idname: str, label: str):
132132
file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n")
133133
file.write("\n")
134134

135-
def create_var(name: str, used_vars: set) -> str:
135+
def create_var(name: str, used_vars: dict) -> str:
136136
"""
137137
Creates a unique variable name for a node tree
138138
139139
Parameters:
140140
name (str): basic string we'd like to create the variable name out of
141-
used_vars (set): set containing all used variable names so far
141+
used_vars (dict): dictionary containing variable names and usage counts
142142
143143
Returns:
144144
clean_name (str): variable name for the node tree
@@ -147,13 +147,12 @@ def create_var(name: str, used_vars: set) -> str:
147147
name = "unnamed"
148148
clean_name = clean_string(name)
149149
var = clean_name
150-
i = 0
151-
while var in used_vars:
152-
i += 1
153-
var = f"{clean_name}_{i}"
154-
155-
used_vars.add(var)
156-
return var
150+
if var in used_vars:
151+
used_vars[var] += 1
152+
return f"{clean_name}_{used_vars[var]}"
153+
else:
154+
used_vars[var] = 0
155+
return clean_name
157156

158157
def make_indents(level: int) -> Tuple[str, str]:
159158
"""
@@ -474,7 +473,6 @@ def set_input_defaults(node, file: TextIO, inner: str, node_var: str,
474473

475474
#images
476475
elif input.bl_idname == 'NodeSocketImage':
477-
print("Input is linked: ", input.is_linked)
478476
img = input.default_value
479477
if img is not None and addon_dir != "": #write in a better way
480478
save_image(img, addon_dir)
@@ -719,7 +717,6 @@ def save_image(img, addon_dir: str):
719717
#save the image
720718
img_str = img_to_py_str(img)
721719
img_path = f"{img_dir}/{img_str}"
722-
print("Image Path: ", img_path)
723720
if not os.path.exists(img_path):
724721
img.save_render(img_path)
725722

0 commit comments

Comments
 (0)