Skip to content

Commit f966929

Browse files
Merge pull request #110 from BrendanParmer/OptionsExpansion
Options expansion
2 parents ae8ea9d + e97aad7 commit f966929

File tree

6 files changed

+248
-68
lines changed

6 files changed

+248
-68
lines changed

blender_manifest.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ tagline = "Turn node groups into Python code"
77
maintainer = "Brendan Parmer <[email protected]>"
88
type = "add-on"
99

10-
# In add-on mode, NodeToPython will create and write to files at a specified directory
11-
permissions = ["files"]
12-
1310
website = "https://github.com/BrendanParmer/NodeToPython"
1411

1512
tags = ["Development", "Compositing", "Geometry Nodes", "Material", "Node"]
@@ -19,4 +16,7 @@ blender_version_max = "4.3.0"
1916

2017
license = [
2118
"SPDX:MIT",
22-
]
19+
]
20+
21+
[permissions]
22+
files = "In add-on mode, NodeToPython will create and write to files in a specified directory"

compositor/operator.py

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@ class NTPCompositorOperator(NTP_Operator):
2020
bl_idname = "node.ntp_compositor"
2121
bl_label = "Compositor to Python"
2222
bl_options = {'REGISTER', 'UNDO'}
23-
24-
mode : bpy.props.EnumProperty(
25-
name = "Mode",
26-
items = [
27-
('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
28-
('ADDON', "Addon", "Create a full addon")
29-
]
30-
)
3123

3224
compositor_name: bpy.props.StringProperty(name="Node Group")
3325
is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups")
@@ -193,6 +185,9 @@ def _process_node_tree(self, node_tree: CompositorNodeTree):
193185
self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer)
194186

195187
def execute(self, context):
188+
if not self._setup_options(context.scene.ntp_options):
189+
return {'CANCELLED'}
190+
196191
#find node group to replicate
197192
if self.is_scene:
198193
self._base_node_tree = bpy.data.scenes[self.compositor_name].node_tree
@@ -209,7 +204,7 @@ def execute(self, context):
209204
#set up names to use in generated addon
210205
comp_var = clean_string(self.compositor_name)
211206

212-
if self.mode == 'ADDON':
207+
if self._mode == 'ADDON':
213208
self._outer = "\t\t"
214209
self._inner = "\t\t\t"
215210

@@ -225,19 +220,21 @@ def execute(self, context):
225220
self._write("def execute(self, context):", "\t")
226221
else:
227222
self._file = StringIO("")
223+
if self._include_imports:
224+
self._file.write("import bpy, mathutils\n\n")
228225

229226
if self.is_scene:
230-
if self.mode == 'ADDON':
227+
if self._mode == 'ADDON':
231228
self._create_scene("\t\t")
232-
elif self.mode == 'SCRIPT':
229+
elif self._mode == 'SCRIPT':
233230
self._create_scene("")
234231

235232
node_trees_to_process = self._topological_sort(self._base_node_tree)
236233

237234
for node_tree in node_trees_to_process:
238235
self._process_node_tree(node_tree)
239236

240-
if self.mode == 'ADDON':
237+
if self._mode == 'ADDON':
241238
self._write("return {'FINISHED'}\n", self._outer)
242239

243240
self._create_menu_func()
@@ -249,7 +246,7 @@ def execute(self, context):
249246

250247
self._file.close()
251248

252-
if self.mode == 'ADDON':
249+
if self._mode == 'ADDON':
253250
self._zip_addon()
254251

255252
self._report_finished("compositor nodes")

geometry/operator.py

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,6 @@ class NTPGeoNodesOperator(NTP_Operator):
2020
bl_idname = "node.ntp_geo_nodes"
2121
bl_label = "Geo Nodes to Python"
2222
bl_options = {'REGISTER', 'UNDO'}
23-
24-
mode: bpy.props.EnumProperty(
25-
name = "Mode",
26-
items = [
27-
('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
28-
('ADDON', "Addon", "Create a full addon")
29-
]
30-
)
3123

3224
geo_nodes_group_name: bpy.props.StringProperty(name="Node Group")
3325

@@ -179,13 +171,16 @@ def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str):
179171

180172

181173
def execute(self, context):
174+
if not self._setup_options(context.scene.ntp_options):
175+
return {'CANCELLED'}
176+
182177
#find node group to replicate
183178
nt = bpy.data.node_groups[self.geo_nodes_group_name]
184179

185180
#set up names to use in generated addon
186181
nt_var = clean_string(nt.name)
187182

188-
if self.mode == 'ADDON':
183+
if self._mode == 'ADDON':
189184
self._outer = "\t\t"
190185
self._inner = "\t\t\t"
191186

@@ -200,13 +195,16 @@ def execute(self, context):
200195
self._write("def execute(self, context):", "\t")
201196
else:
202197
self._file = StringIO("")
198+
if self._include_imports:
199+
self._file.write("import bpy, mathutils\n\n")
200+
203201

204202
node_trees_to_process = self._topological_sort(nt)
205203

206204
for node_tree in node_trees_to_process:
207205
self._process_node_tree(node_tree)
208206

209-
if self.mode == 'ADDON':
207+
if self._mode == 'ADDON':
210208
self._apply_modifier(nt, nt_var)
211209
self._write("return {'FINISHED'}\n", self._outer)
212210
self._create_menu_func()
@@ -217,7 +215,7 @@ def execute(self, context):
217215
context.window_manager.clipboard = self._file.getvalue()
218216
self._file.close()
219217

220-
if self.mode == 'ADDON':
218+
if self._mode == 'ADDON':
221219
self._zip_addon()
222220

223221
self._report_finished("geometry node group")

ntp_operator.py

Lines changed: 78 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import shutil
1515

1616
from .ntp_node_tree import NTP_NodeTree
17+
from .options import NTPOptions
1718
from .node_settings import NodeInfo, ST
1819
from .utils import *
1920

@@ -46,14 +47,6 @@ class NTP_Operator(Operator):
4647
bl_idname = ""
4748
bl_label = ""
4849

49-
mode: bpy.props.EnumProperty(
50-
name="Mode",
51-
items=[
52-
('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"),
53-
('ADDON', "Addon", "Create a full addon")
54-
]
55-
)
56-
5750
# node tree input sockets that have default properties
5851
if bpy.app.version < (4, 0, 0):
5952
default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'}
@@ -74,9 +67,6 @@ def __init__(self):
7467
# File (TextIO) or string (StringIO) the add-on/script is generated into
7568
self._file: TextIO = None
7669

77-
# Path to the current directory
78-
self._dir: str = None
79-
8070
# Path to the directory of the zip file
8171
self._zip_dir: str = None
8272

@@ -108,11 +98,49 @@ def __init__(self):
10898
for name in RESERVED_NAMES:
10999
self._used_vars[name] = 0
110100

101+
# Generate socket default, min, and max values
102+
self._include_group_socket_values = True
103+
104+
# Set dimensions of generated nodes
105+
self._should_set_dimensions = True
106+
107+
if bpy.app.version >= (3, 4, 0):
108+
# Set default values for hidden sockets
109+
self._set_unavailable_defaults = False
110+
111111
def _write(self, string: str, indent: str = None):
112112
if indent is None:
113113
indent = self._inner
114114
self._file.write(f"{indent}{string}\n")
115115

116+
def _setup_options(self, options: NTPOptions) -> bool:
117+
# General
118+
self._mode = options.mode
119+
self._include_group_socket_values = options.include_group_socket_values
120+
self._should_set_dimensions = options.set_dimensions
121+
if bpy.app.version >= (3, 4, 0):
122+
self._set_unavailable_defaults = options.set_unavailable_defaults
123+
124+
#Script
125+
if options.mode == 'SCRIPT':
126+
self._include_imports = options.include_imports
127+
#Addon
128+
elif options.mode == 'ADDON':
129+
self._dir_path = bpy.path.abspath(options.dir_path)
130+
self._name_override = options.name_override
131+
self._description = options.description
132+
self._author_name = options.author_name
133+
self._version = options.version
134+
self._location = options.location
135+
self._category = options.category
136+
self._custom_category = options.custom_category
137+
if options.menu_id in dir(bpy.types):
138+
self._menu_id = options.menu_id
139+
else:
140+
self.report({'ERROR'}, f"{options.menu_id} is not a valid menu")
141+
return False
142+
return True
143+
116144
def _setup_addon_directories(self, context: Context, nt_var: str) -> bool:
117145
"""
118146
Finds/creates directories to save add-on to
@@ -124,15 +152,13 @@ def _setup_addon_directories(self, context: Context, nt_var: str) -> bool:
124152
Returns:
125153
(bool): success of addon directory setup
126154
"""
127-
# find base directory to save new addon
128-
self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
129-
if not self._dir or self._dir == "":
155+
if not self._dir_path or self._dir_path == "":
130156
self.report({'ERROR'},
131157
("NodeToPython: No save location found. Please select "
132158
"one in the NodeToPython Options panel"))
133159
return False
134160

135-
self._zip_dir = os.path.join(self._dir, nt_var)
161+
self._zip_dir = os.path.join(self._dir_path, nt_var)
136162
self._addon_dir = os.path.join(self._zip_dir, nt_var)
137163

138164
if not os.path.exists(self._addon_dir):
@@ -150,12 +176,19 @@ def _create_header(self, name: str) -> None:
150176
"""
151177

152178
self._write("bl_info = {", "")
153-
self._write(f"\t\"name\" : \"{name}\",", "")
154-
self._write("\t\"author\" : \"Node To Python\",", "")
155-
self._write("\t\"version\" : (1, 0, 0),", "")
179+
if self._name_override and self._name_override != "":
180+
name = self._name_override
181+
self._write(f"\t\"name\" : {str_to_py_str(name)},", "")
182+
if self._description and self._description != "":
183+
self.write(f"\t\"description\" : {str_to_py_str(self._description)}," "")
184+
self._write(f"\t\"author\" : {str_to_py_str(self._author_name)},", "")
185+
self._write(f"\t\"version\" : {vec3_to_py_str(self._version)},", "")
156186
self._write(f"\t\"blender\" : {bpy.app.version},", "")
157-
self._write("\t\"location\" : \"Object\",", "") # TODO
158-
self._write("\t\"category\" : \"Node\"", "")
187+
self._write(f"\t\"location\" : {str_to_py_str(self._location)},", "")
188+
category = self._category
189+
if category == "Custom":
190+
category = self._custom_category
191+
self._write(f"\t\"category\" : {str_to_py_str(category)},", "")
159192
self._write("}\n", "")
160193
self._write("import bpy", "")
161194
self._write("import mathutils", "")
@@ -172,8 +205,8 @@ def _init_operator(self, idname: str, label: str) -> None:
172205
label (str): appearence inside Blender
173206
"""
174207
self._write(f"class {self._class_name}(bpy.types.Operator):", "")
175-
self._write(f"\tbl_idname = \"object.{idname}\"", "")
176-
self._write(f"\tbl_label = \"{label}\"", "")
208+
self._write(f"\tbl_idname = \"node.{idname}\"", "")
209+
self._write(f"\tbl_label = {str_to_py_str(label)}", "")
177210
self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}", "")
178211
self._write("")
179212

@@ -389,6 +422,9 @@ def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface,
389422
with the input/output
390423
socket_var (str): variable name for the socket
391424
"""
425+
if not self._include_group_socket_values:
426+
return
427+
392428
if socket_interface.type not in self.default_sockets_v3:
393429
return
394430

@@ -485,6 +521,8 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket,
485521
with the input/output
486522
socket_var (str): variable name for the socket
487523
"""
524+
if not self._include_group_socket_values:
525+
return
488526
if type(socket_interface) in self.nondefault_sockets_v4:
489527
return
490528

@@ -710,6 +748,10 @@ def _set_input_defaults(self, node: Node) -> None:
710748

711749
for i, input in enumerate(node.inputs):
712750
if input.bl_idname not in DONT_SET_DEFAULTS and not input.is_linked:
751+
if bpy.app.version >= (3, 4, 0):
752+
if (not self._set_unavailable_defaults) and input.is_unavailable:
753+
continue
754+
713755
# TODO: this could be cleaner
714756
socket_var = f"{node_var}.inputs[{i}]"
715757

@@ -1006,13 +1048,19 @@ def _save_image(self, img: bpy.types.Image) -> None:
10061048
if img is None:
10071049
return
10081050

1051+
img_str = img_to_py_str(img)
1052+
1053+
if not img.has_data:
1054+
self.report({'WARNING'}, f"{img_str} has no data")
1055+
return
1056+
10091057
# create image dir if one doesn't exist
10101058
img_dir = os.path.join(self._addon_dir, IMAGE_DIR_NAME)
10111059
if not os.path.exists(img_dir):
10121060
os.mkdir(img_dir)
10131061

10141062
# save the image
1015-
img_str = img_to_py_str(img)
1063+
10161064
img_path = f"{img_dir}/{img_str}"
10171065
if not os.path.exists(img_path):
10181066
img.save_render(img_path)
@@ -1212,6 +1260,9 @@ def _set_dimensions(self, node_tree: NodeTree) -> None:
12121260
Parameters:
12131261
node_tree (NodeTree): node tree we're obtaining nodes from
12141262
"""
1263+
if not self._should_set_dimensions:
1264+
return
1265+
12151266
self._write(f"#Set dimensions")
12161267
for node in node_tree.nodes:
12171268
node_var = self._node_vars[node]
@@ -1305,7 +1356,7 @@ def _create_register_func(self) -> None:
13051356
"""
13061357
self._write("def register():", "")
13071358
self._write(f"bpy.utils.register_class({self._class_name})", "\t")
1308-
self._write("bpy.types.VIEW3D_MT_object.append(menu_func)", "\t")
1359+
self._write(f"bpy.types.{self._menu_id}.append(menu_func)", "\t")
13091360
self._write("")
13101361

13111362
def _create_unregister_func(self) -> None:
@@ -1314,7 +1365,7 @@ def _create_unregister_func(self) -> None:
13141365
"""
13151366
self._write("def unregister():", "")
13161367
self._write(f"bpy.utils.unregister_class({self._class_name})", "\t")
1317-
self._write("bpy.types.VIEW3D_MT_object.remove(menu_func)", "\t")
1368+
self._write(f"bpy.types.{self._menu_id}.remove(menu_func)", "\t")
13181369
self._write("")
13191370

13201371
def _create_main_func(self) -> None:
@@ -1347,18 +1398,12 @@ def _report_finished(self, object: str):
13471398
object (str): the copied node tree or encapsulating structure
13481399
(geometry node modifier, material, scene, etc.)
13491400
"""
1350-
if self.mode == 'SCRIPT':
1401+
if self._mode == 'SCRIPT':
13511402
location = "clipboard"
13521403
else:
1353-
location = self._dir
1404+
location = self._dir_path
13541405
self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}")
13551406

13561407
# ABSTRACT
13571408
def execute(self):
13581409
return {'FINISHED'}
1359-
1360-
def invoke(self, context, event):
1361-
return context.window_manager.invoke_props_dialog(self)
1362-
1363-
def draw(self, context):
1364-
self.layout.prop(self, "mode")

0 commit comments

Comments
 (0)