Skip to content

feat: added option panel and save directory #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,18 @@
import importlib
importlib.reload(materials)
importlib.reload(geo_nodes)
importlib.reload(options)
else:
from . import materials
from . import geo_nodes
from . import options

import bpy

class NodeToPythonMenu(bpy.types.Menu):
bl_idname = "NODE_MT_node_to_python"
bl_label = "Node To Python"

@classmethod
def poll(cls, context):
return True
Expand All @@ -30,21 +32,30 @@ def draw(self, context):
layout = self.layout.column_flow(columns=1)
layout.operator_context = 'INVOKE_DEFAULT'




classes = [NodeToPythonMenu,
options.NTPOptions,
geo_nodes.GeoNodesToPython,
geo_nodes.SelectGeoNodesMenu,
geo_nodes.GeoNodesToPythonPanel,
materials.MaterialToPython,
materials.SelectMaterialMenu,
materials.MaterialToPythonPanel
materials.MaterialToPythonPanel,
options.NTPOptionsPanel
]

def register():
for cls in classes:
bpy.utils.register_class(cls)
scene = bpy.types.Scene
scene.ntp_options = bpy.props.PointerProperty(type=options.NTPOptions)

def unregister():
for cls in classes:
bpy.utils.unregister_class(cls)
del bpy.types.Scene.ntp_options

if __name__ == "__main__":
register()
42 changes: 31 additions & 11 deletions geo_nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class GeoNodesToPython(bpy.types.Operator):
('ADDON', "Addon", "Create a full addon")
]
)

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

def execute(self, context):
Expand All @@ -187,16 +188,17 @@ def execute(self, context):

if self.mode == 'ADDON':
#find base directory to save new addon
base_dir = bpy.path.abspath("//")
if not base_dir or base_dir == "":
dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
if not dir or dir == "":
self.report({'ERROR'},
("NodeToPython: Save your blend file before using "
"NodeToPython!"))
return {'CANCELLED'}

#save in addons/ subdirectory
zip_dir = os.path.join(base_dir, "addons", nt_var)
zip_dir = os.path.join(dir, nt_var)
addon_dir = os.path.join(zip_dir, nt_var)

if not os.path.exists(addon_dir):
os.makedirs(addon_dir)
file = open(f"{addon_dir}/__init__.py", "w")
Expand All @@ -215,8 +217,8 @@ def execute(self, context):
#dictionary to keep track of node->variable name pairs
node_vars = {}

#keeps track of all used variables
used_vars = set()
#dictionary to keep track of variables->usage count pairs
used_vars = {}

def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
nt_var = create_var(node_tree.name, used_vars)
Expand Down Expand Up @@ -248,40 +250,48 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
used_vars)
node_trees.add(node_nt)
elif node.bl_idname == 'NodeGroupInput' and not inputs_set:
group_io_settings(node, file, inner, "input", nt_var, node_tree)
group_io_settings(node, file, inner, "input", nt_var,
node_tree)
inputs_set = True

elif node.bl_idname == 'NodeGroupOutput' and not outputs_set:
group_io_settings(node, file, inner, "output", nt_var, node_tree)
group_io_settings(node, file, inner, "output", nt_var,
node_tree)
outputs_set = True

#create node
node_var = create_node(node, file, inner, nt_var,
node_vars, used_vars)
node_vars, used_vars)
set_settings_defaults(node, geo_node_settings, file, inner,
node_var)
node_var)
hide_sockets(node, file, inner, node_var)

if node.bl_idname == 'GeometryNodeGroup':
if node.node_tree is not None:
file.write((f"{inner}{node_var}.node_tree = "
f"bpy.data.node_groups"
f"[{str_to_py_str(node.node_tree.name)}]\n"))

elif node.bl_idname == 'ShaderNodeValToRGB':
color_ramp_settings(node, file, inner, node_var)

elif node.bl_idname in curve_nodes:
curve_node_settings(node, file, inner, node_var)

elif node.bl_idname in image_nodes and self.mode == 'ADDON':
img = node.image
if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}:
save_image(img, addon_dir)
load_image(img, file, inner, f"{node_var}.image")

elif node.bl_idname == 'GeometryNodeSimulationInput':
sim_inputs.append(node)

elif node.bl_idname == 'GeometryNodeSimulationOutput':
file.write(f"{inner}#remove generated sim state items\n")
file.write(f"{inner}for item in {node_var}.state_items:\n")
file.write(f"{inner}\t{node_var}.state_items.remove(item)\n")

for i, si in enumerate(node.state_items):
socket_type = enum_to_py_str(si.socket_type)
name = str_to_py_str(si.name)
Expand Down Expand Up @@ -314,10 +324,12 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars):
set_input_defaults(node, file, inner, node_var)
set_output_defaults(sim_input, file, inner, sim_input_var)

#set look of nodes
set_parents(node_tree, file, inner, node_vars)
set_locations(node_tree, file, inner, node_vars)
set_dimensions(node_tree, file, inner, node_vars)

#create connections
init_links(node_tree, file, inner, nt_var, node_vars)

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

if self.mode == 'ADDON':
zip_addon(zip_dir)
self.report({'INFO'}, "NodeToPython: Saved geometry nodes group")

#alert user that NTP is finished
if self.mode == 'SCRIPT':
location = "clipboard"
else:
location = dir
self.report({'INFO'},
f"NodeToPython: Saved geometry nodes group to {location}")
return {'FINISHED'}

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

geo_node_groups = [node for node in bpy.data.node_groups if node.type == 'GEOMETRY']
geo_node_groups = [node for node in bpy.data.node_groups
if node.type == 'GEOMETRY']

for geo_ng in geo_node_groups:
op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name)
Expand Down
17 changes: 11 additions & 6 deletions materials.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,15 +103,14 @@ def execute(self, context):
mat_var = clean_string(self.material_name)

if self.mode == 'ADDON':
dir = bpy.path.abspath("//")
dir = bpy.path.abspath(context.scene.ntp_options.dir_path)
if not dir or dir == "":
self.report({'ERROR'},
("NodeToPython: Save your blender file before using "
"NodeToPython!"))
return {'CANCELLED'}

#save in addons/ subdirectory
zip_dir = os.path.join(dir, "addons", mat_var)
zip_dir = os.path.join(dir, mat_var)
addon_dir = os.path.join(zip_dir, mat_var)
if not os.path.exists(addon_dir):
os.makedirs(addon_dir)
Expand All @@ -129,6 +128,7 @@ def create_material(indent: str):
file.write((f"{indent}mat = bpy.data.materials.new("
f"name = {str_to_py_str(self.material_name)})\n"))
file.write(f"{indent}mat.use_nodes = True\n")

if self.mode == 'ADDON':
create_material("\t\t")
elif self.mode == 'SCRIPT':
Expand All @@ -141,7 +141,7 @@ def create_material(indent: str):
node_vars = {}

#keeps track of all used variables
used_vars = set()
used_vars = {}

def is_outermost_node_group(level: int) -> bool:
if self.mode == 'ADDON' and level == 2:
Expand Down Expand Up @@ -189,7 +189,8 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars):
if node.bl_idname == 'ShaderNodeGroup':
node_nt = node.node_tree
if node_nt is not None and node_nt not in node_trees:
process_mat_node_group(node_nt, level + 1, node_vars, used_vars)
process_mat_node_group(node_nt, level + 1, node_vars,
used_vars)
node_trees.add(node_nt)

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

if self.mode == 'ADDON':
zip_addon(zip_dir)
self.report({'INFO'}, "NodeToPython: Saved material")
if self.mode == 'SCRIPT':
location = "clipboard"
else:
location = dir
self.report({'INFO'}, f"NodeToPython: Saved material to {location}")
return {'FINISHED'}

def invoke(self, context, event):
Expand Down
28 changes: 28 additions & 0 deletions options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import bpy

class NTPOptions(bpy.types.PropertyGroup):
"""
Property group used during conversion of node group to python
"""
dir_path : bpy.props.StringProperty(
name = "Save Location",
subtype='DIR_PATH',
description="Save location if generating an add-on",
default = "//"
)

class NTPOptionsPanel(bpy.types.Panel):
bl_label = "Options"
bl_idname = "NODE_PT_ntp_options"
bl_space_type = 'NODE_EDITOR'
bl_region_type = 'UI'
bl_context = ''
bl_category = "NodeToPython"

@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
layout.operator_context = 'INVOKE_DEFAULT'
layout.prop(context.scene.ntp_options, "dir_path")
19 changes: 8 additions & 11 deletions utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,13 @@ def init_operator(file: TextIO, name: str, idname: str, label: str):
file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n")
file.write("\n")

def create_var(name: str, used_vars: set) -> str:
def create_var(name: str, used_vars: dict) -> str:
"""
Creates a unique variable name for a node tree

Parameters:
name (str): basic string we'd like to create the variable name out of
used_vars (set): set containing all used variable names so far
used_vars (dict): dictionary containing variable names and usage counts

Returns:
clean_name (str): variable name for the node tree
Expand All @@ -147,13 +147,12 @@ def create_var(name: str, used_vars: set) -> str:
name = "unnamed"
clean_name = clean_string(name)
var = clean_name
i = 0
while var in used_vars:
i += 1
var = f"{clean_name}_{i}"

used_vars.add(var)
return var
if var in used_vars:
used_vars[var] += 1
return f"{clean_name}_{used_vars[var]}"
else:
used_vars[var] = 0
return clean_name

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

#images
elif input.bl_idname == 'NodeSocketImage':
print("Input is linked: ", input.is_linked)
img = input.default_value
if img is not None and addon_dir != "": #write in a better way
save_image(img, addon_dir)
Expand Down Expand Up @@ -719,7 +717,6 @@ def save_image(img, addon_dir: str):
#save the image
img_str = img_to_py_str(img)
img_path = f"{img_dir}/{img_str}"
print("Image Path: ", img_path)
if not os.path.exists(img_path):
img.save_render(img_path)

Expand Down