From 6e079d9f5cf65d45666f315152e1cf355b31d5d2 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 4 Aug 2023 23:16:15 -0500 Subject: [PATCH 01/72] typo: fixed spelling of SelectMaterialMenu.bl_idname --- materials.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/materials.py b/materials.py index 466bee5..101cdd6 100644 --- a/materials.py +++ b/materials.py @@ -272,7 +272,7 @@ def draw(self, context): self.layout.prop(self, "mode") class SelectMaterialMenu(bpy.types.Menu): - bl_idname = "NODE_MT_npt_mat_selection" + bl_idname = "NODE_MT_ntp_mat_selection" bl_label = "Select Material" @classmethod @@ -312,4 +312,4 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_npt_mat_selection", text="Materials") \ No newline at end of file + row.menu("NODE_MT_ntp_mat_selection", text="Materials") \ No newline at end of file From 80d7a138c74d7517d8d694abddef567c8b423cc9 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 4 Aug 2023 23:47:59 -0500 Subject: [PATCH 02/72] refactor: class renaming --- __init__.py | 24 +++++++++++++++--------- geo_nodes.py | 16 ++++++++-------- materials.py | 14 +++++++------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/__init__.py b/__init__.py index 201080e..fc987da 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ "name": "Node to Python", "description": "Convert Blender node groups to a Python add-on!", "author": "Brendan Parmer", - "version": (2, 2, 0), + "version": (3, 0, 0), "blender": (3, 0, 0), "location": "Node", "category": "Node", @@ -10,12 +10,14 @@ if "bpy" in locals(): import importlib - importlib.reload(materials) + importlib.reload(compositor) importlib.reload(geo_nodes) + importlib.reload(materials) importlib.reload(options) else: - from . import materials + from . import compositor from . import geo_nodes + from . import materials from . import options import bpy @@ -37,12 +39,16 @@ def draw(self, context): classes = [NodeToPythonMenu, options.NTPOptions, - geo_nodes.GeoNodesToPython, - geo_nodes.SelectGeoNodesMenu, - geo_nodes.GeoNodesToPythonPanel, - materials.MaterialToPython, - materials.SelectMaterialMenu, - materials.MaterialToPythonPanel, + compositor.NTPCompositorOperator, + compositor.NTPCompositorScenesMenu, + compositor.NTPCompositorGroupsMenu, + compositor.NTPCompositingPanel, + geo_nodes.NTPGeoNodesOperator, + geo_nodes.NTPGeoNodesMenu, + geo_nodes.NTPGeoNodesPanel, + materials.NTPMaterialOperator, + materials.NTPMaterialMenu, + materials.NTPMaterialPanel, options.NTPOptionsPanel ] diff --git a/geo_nodes.py b/geo_nodes.py index c70f219..f5f6de1 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -164,8 +164,8 @@ image_nodes = {'GeometryNodeInputImage'} -class GeoNodesToPython(bpy.types.Operator): - bl_idname = "node.geo_nodes_to_python" +class NTPGeoNodesOperator(bpy.types.Operator): + bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} @@ -386,8 +386,8 @@ def invoke(self, context, event): def draw(self, context): self.layout.prop(self, "mode") -class SelectGeoNodesMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_geo_nodes_selection" +class NTPGeoNodesMenu(bpy.types.Menu): + bl_idname = "NODE_MT_ntp_geo_nodes" bl_label = "Select Geo Nodes" @classmethod @@ -402,12 +402,12 @@ def draw(self, context): if node.type == 'GEOMETRY'] for geo_ng in geo_node_groups: - op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name) + op = layout.operator(NTPGeoNodesOperator.bl_idname, text=geo_ng.name) op.geo_nodes_group_name = geo_ng.name -class GeoNodesToPythonPanel(bpy.types.Panel): +class NTPGeoNodesPanel(bpy.types.Panel): bl_label = "Geometry Nodes to Python" - bl_idname = "NODE_PT_geo_nodes_to_python" + bl_idname = "NODE_PT_geo_nodes" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_context = '' @@ -434,4 +434,4 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_geo_nodes_selection", text="Geometry Nodes") \ No newline at end of file + row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") \ No newline at end of file diff --git a/materials.py b/materials.py index 101cdd6..66c0d00 100644 --- a/materials.py +++ b/materials.py @@ -77,8 +77,8 @@ image_nodes = {'ShaderNodeTexEnvironment', 'ShaderNodeTexImage'} -class MaterialToPython(bpy.types.Operator): - bl_idname = "node.material_to_python" +class NTPMaterialOperator(bpy.types.Operator): + bl_idname = "node.ntp_material" bl_label = "Material to Python" bl_options = {'REGISTER', 'UNDO'} @@ -271,8 +271,8 @@ def invoke(self, context, event): def draw(self, context): self.layout.prop(self, "mode") -class SelectMaterialMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_mat_selection" +class NTPMaterialMenu(bpy.types.Menu): + bl_idname = "NODE_MT_ntp_material" bl_label = "Select Material" @classmethod @@ -283,10 +283,10 @@ def draw(self, context): layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' for mat in bpy.data.materials: - op = layout.operator(MaterialToPython.bl_idname, text=mat.name) + op = layout.operator(NTPMaterialOperator.bl_idname, text=mat.name) op.material_name = mat.name -class MaterialToPythonPanel(bpy.types.Panel): +class NTPMaterialPanel(bpy.types.Panel): bl_label = "Material to Python" bl_idname = "NODE_PT_mat_to_python" bl_space_type = 'NODE_EDITOR' @@ -312,4 +312,4 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_mat_selection", text="Materials") \ No newline at end of file + row.menu("NODE_MT_ntp_material", text="Materials") \ No newline at end of file From 04099612f1718bd85d15fabd32ce1155f27ab9d2 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Fri, 4 Aug 2023 23:55:18 -0500 Subject: [PATCH 03/72] feat: added UI for compositor nodes --- compositor.py | 289 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 compositor.py diff --git a/compositor.py b/compositor.py new file mode 100644 index 0000000..f8dbbf4 --- /dev/null +++ b/compositor.py @@ -0,0 +1,289 @@ +import bpy +import os + +from .utils import * +from io import StringIO + +node_settings = { +} + +curve_nodes = {'ShaderNodeFloatCurve', + 'ShaderNodeVectorCurve', + 'ShaderNodeRGBCurve'} + +image_nodes = {'ShaderNodeTexEnvironment', + 'ShaderNodeTexImage'} + +class NTPCompositorOperator(bpy.types.Operator): + bl_idname = "node.compositor_to_python" + bl_label = "Compositor to Python" + bl_options = {'REGISTER', 'UNDO'} + + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + compositor_name: bpy.props.StringProperty(name="Node Group") + is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") + + def execute(self, context): + """ + #find node group to replicate + nt = bpy.data.materials[self.material_name].node_tree + if nt is None: + self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " + "valid material. Is Use Nodes selected?")) + return {'CANCELLED'} + + #set up names to use in generated addon + mat_var = clean_string(self.material_name) + + if self.mode == 'ADDON': + 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'} + + 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) + file = open(f"{addon_dir}/__init__.py", "w") + + create_header(file, self.material_name) + class_name = clean_string(self.material_name, lower=False) + init_operator(file, class_name, mat_var, self.material_name) + + file.write("\tdef execute(self, context):\n") + else: + file = StringIO("") + + 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': + create_material("") + + #set to keep track of already created node trees + node_trees = set() + + #dictionary to keep track of node->variable name pairs + node_vars = {} + + #keeps track of all used variables + used_vars = {} + + def is_outermost_node_group(level: int) -> bool: + if self.mode == 'ADDON' and level == 2: + return True + elif self.mode == 'SCRIPT' and level == 0: + return True + return False + + def process_mat_node_group(node_tree, level, node_vars, used_vars): + if is_outermost_node_group(level): + nt_var = create_var(self.material_name, used_vars) + nt_name = self.material_name + else: + nt_var = create_var(node_tree.name, used_vars) + nt_name = node_tree.name + + outer, inner = make_indents(level) + + #initialize node group + file.write(f"{outer}#initialize {nt_var} node group\n") + file.write(f"{outer}def {nt_var}_node_group():\n") + + if is_outermost_node_group(level): #outermost node group + file.write(f"{inner}{nt_var} = mat.node_tree\n") + file.write(f"{inner}#start with a clean node tree\n") + file.write(f"{inner}for node in {nt_var}.nodes:\n") + file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + else: + file.write((f"{inner}{nt_var}" + f"= bpy.data.node_groups.new(" + f"type = \'ShaderNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + file.write("\n") + + inputs_set = False + outputs_set = False + + #initialize nodes + file.write(f"{inner}#initialize {nt_var} nodes\n") + + #dictionary to keep track of node->variable name pairs + node_vars = {} + + for node in node_tree.nodes: + 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) + node_trees.add(node_nt) + + node_var = create_node(node, file, inner, nt_var, node_vars, + used_vars) + + set_settings_defaults(node, node_settings, file, inner, node_var) + hide_sockets(node, file, inner, node_var) + + if node.bl_idname == 'ShaderNodeGroup': + if node.node_tree is not None: + file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + elif node.bl_idname == 'NodeGroupInput' and not inputs_set: + 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) + outputs_set = True + + 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") + image_user_settings(node, file, inner, node_var) + + 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) + + if self.mode == 'ADDON': + set_input_defaults(node, file, inner, node_var, addon_dir) + else: + set_input_defaults(node, file, inner, node_var) + set_output_defaults(node, file, inner, node_var) + + set_parents(node_tree, file, inner, node_vars) + set_locations(node_tree, file, inner, node_vars) + set_dimensions(node_tree, file, inner, node_vars) + + init_links(node_tree, file, inner, nt_var, node_vars) + + file.write(f"\n{outer}{nt_var}_node_group()\n\n") + + if self.mode == 'ADDON': + level = 2 + else: + level = 0 + process_mat_node_group(nt, level, node_vars, used_vars) + + if self.mode == 'ADDON': + file.write("\t\treturn {'FINISHED'}\n\n") + + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) + else: + context.window_manager.clipboard = file.getvalue() + + file.close() + + if self.mode == 'ADDON': + zip_addon(zip_dir) + """ + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = dir + self.report({'INFO'}, f"NodeToPython: Saved compositor nodes to {location}") + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + def draw(self, context): + self.layout.prop(self, "mode") + +class NTPCompositorScenesMenu(bpy.types.Menu): + bl_idname = "NODE_MT_ntp_comp_scenes" + bl_label = "Select " + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for scene in bpy.data.scenes: + if scene.node_tree: + op = layout.operator(NTPCompositorOperator.bl_idname, text=scene.name) + op.compositor_name = scene.name + op.is_scene = True + print(scene.node_tree.name) + +class NTPCompositorGroupsMenu(bpy.types.Menu): + bl_idname = "NODE_MT_ntp_comp_groups" + bl_label = "Select " + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for node_group in bpy.data.node_groups: + if isinstance(node_group, bpy.types.CompositorNodeTree): + op = layout.operator(NTPCompositorOperator.bl_idname, text=node_group.name) + op.compositor_name = node_group.name + op.is_scene = False + +class NTPCompositingPanel(bpy.types.Panel): + bl_label = "Compositor to Python" + bl_idname = "NODE_PT_ntp_compositor" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + scenes_row = layout.row() + + # Disables menu when there are no materials + scenes = [scene for scene in bpy.data.scenes + if scene.node_tree is not None] + scenes_exist = len(scenes) > 0 + scenes_row.enabled = scenes_exist + + scenes_row.alignment = 'EXPAND' + scenes_row.operator_context = 'INVOKE_DEFAULT' + scenes_row.menu("NODE_MT_ntp_comp_scenes", + text="Scene Compositor Nodes") + + groups_row = layout.row() + groups = [ng for ng in bpy.data.node_groups + if isinstance(ng, bpy.types.CompositorNodeTree)] + groups_exist = len(groups) > 0 + groups_row.enabled = groups_exist + + groups_row.alignment = 'EXPAND' + groups_row.operator_context = 'INVOKE_DEFAULT' + groups_row.menu("NODE_MT_ntp_comp_groups", + text="Group Compositor Nodes") From cffd9aed8140613d9f2cb8d39f48e65628a97071 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 6 Aug 2023 16:06:57 -0500 Subject: [PATCH 04/72] feat: add most compositor node settings --- compositor.py | 194 ++++++++++++++++++++++++++++++++++++++++++-------- materials.py | 6 +- 2 files changed, 166 insertions(+), 34 deletions(-) diff --git a/compositor.py b/compositor.py index f8dbbf4..ef978b2 100644 --- a/compositor.py +++ b/compositor.py @@ -5,14 +5,140 @@ from io import StringIO node_settings = { + #Input + 'CompositorNodeBokehImage' : ["flaps", "angle", "rounding", "catadioptric", + "shift"], + 'CompositorNodeImage' : [], #TODO: handle image selection + 'CompositorNodeMask' : ["use_feather", "size_source", "size_x", "size_y", + "use_motion_blur", + "motion_blur_samples", "motion_blur_shutter"], #TODO: handle mask selection + 'CompositorNodeMovieClip' : [], #TODO: handle movie clip selection + 'CompositorNodeRLayers' : ["name", "layer"], + 'CompositorNodeRGB' : [], #should be handled by outputs + 'CompositorNodeSceneTime' : [], #should be good + 'CompositorNodeTexture' : [], #TODO: handle texture selection + 'CompositorNodeTime' : ["frame_start", "frame_end"], + 'CompositorNodeTrackPos' : [], #TODO: handle movie selection + 'CompositorNodeValue' : [], #should be handled by outputs (why is this a separate class??) + + #Output + 'CompositorNodeComposite' : ["use_alpha"], + 'CompositorNodeOutputFile' : ["base_path"], #TODO: doesn't seem portable + 'CompositorNodeLevels' : ["channel"], + 'CompositorNodeSplitViewer' : ["axis", "factor"], + 'CompositorNodeViewer' : ["use_alpha"], + + #Color + 'CompositorNodeAlphaOver' : ["use_premultiply", "premul"], + 'CompositorNodeBrightContrast' : ["use_premultiply"], + 'CompositorNodeColorBalance' : ["correction_method", "lift", "gamma", + "gain", "offset", "power", "slope", + "offset_basis"], + 'CompositorNodeColorCorrection' : ["red", "green", "blue", + "master_saturation", "master_contrast", + "master_gamma", "master_gain", + "master_lift", + "highlights_saturation", "highlights_contrast", + "highlights_gamma", "highlights_gain", + "highlights_lift", + "midtones_saturation", "midtones_contrast", + "midtones_gamma", "midtones_gain", + "midtones_lift", + "shadows_saturation", "shadows_contrast", + "shadows_gamma", "shadows_gain", + "shadows_lift", + "midtones_start", "midtones_end"], + 'CompositorNodeExposure' : [], + 'CompositorNodeGamma' : [], + 'CompositorNodeHueCorrect' : [], + 'CompositorNodeHueSat' : [], + 'CompositorNodeInvert' : ["invert_rgb", "invert_alpha"], + 'CompositorNodeMixRGB' : ["blend_type", "use_alpha", "use_clamp"], #TODO: has an update() method, may need to figure out why... + 'CompositorNodePosterize' : [], + 'CompositorNodeCurveRGB' : [], + 'CompositorNodeTonemap' : ["tonemap_type", "intensity", "contrast", "adaptation", "correction", "key", "offset", "gamma"], + 'CompositorNodeZcombine' : ["use_alpha", "use_antialias_z"], + + #Converter + 'CompositorNodePremulKey' : ["mapping"], + 'CompositorNodeValToRGB' : [], #TODO: check to see if this'll work out of the box + 'CompositorNodeConvertColorSpace' : ["from_color_space", "to_color_space"], + 'CompositorNodeCombineColor' : ["mode", "ycc_mode"], #why isn't this standardized across blender? + 'CompositorNodeCombineXYZ' : [], + 'CompositorNodeIDMask' : ["index", "use_antialiasing"], + 'CompositorNodeMath' : ["operation", "use_clamp"], + 'CompositorNodeRGBToBW' : [], + 'CompositorNodeSeparateColor' : ["mode", "ycc_mode"], + 'CompositorNodeSeparateXYZ' : [], + 'CompositorNodeSetAlpha' : ["mode"], + 'CompositorNodeSwitchView' : [], + + #Filter + 'CompositorNodeAntiAliasing' : ["threshold", "contrast_limit", "corner_rounding"], + 'CompositorNodeBilateralblur' : ["iterations", "sigma_color", "sigma_space"], + 'CompositorNodeBlur' : ["filter_type", "use_variable_size", "use_gamma_correction", "use_relative", "aspect_correction", "factor", "factor_x", "factor_y", "use_extended_bounds"], + 'CompositorNodeBokehBlur' : ["use_variable_size", "blur_max", "use_extended_bounds"], + 'CompositorNodeDefocus' : ["bokeh", "angle", "use_gamma_correction", "f_stop", "blur_max", "threshold", "use_preview", "use_zbuffer", "z_scale"], + 'CompositorNodeDespeckle' : ["threshold", "threshold_neighbor"], + 'CompositorNodeDilateErode' : ["mode", "distance", "edge", "falloff"], + 'CompositorNodeDBlur' : ["iterations", "center_x", "center_y", "distance", "angle", "spin", "zoom"], + 'CompositorNodeFilter' : ["filter_type"], + 'CompositorNodeGlare' : ["glare_type", "quality", "iterations", "color_modulation", "mix", "threshold", "streaks", "angle_offset", "fade", "size", "use_rotate_45"], + 'CompositorNodeInpaint' : ["distance"], + 'CompositorNodePixelate' : [], + 'CompositorNodeSunBeams' : ["source", "ray_length"], #TODO: check that source doesn't freak out + 'CompositorNodeVecBlur' : ["samples", "factor", "speed_min", "speed_max", "use_curved"], + + #Vector + 'CompositorNodeMapRange' : ["use_clamp"], + 'CompositorNodeMapValue' : ["offset", "size", "use_min", "min", "use_max", "max"], #why are all these vectors?? TODO: check to make sure it doesn't flip + 'CompositorNodeNormal' : [], #should be handled with io system + 'CompositorNodeNormalize' : [], + 'CompositorNodeCurveVec' : [], + + #Matte + 'CompositorNodeBoxMask' : ["x", "y", "width", "height", "rotation", "mask_type"], + 'CompositorNodeChannelMatte' : ["color_space", "matte_channel", "limit_method", "limit_channel", "limit_max", "limit_min"], + 'CompositorNodeChromaMatte' : ["tolerance", "threshold", "gain"], + 'CompositorNodeColorMatte' : ["color_hue", "color_saturation", "color_value"], + 'CompositorNodeColorSpill' : ["channel", "limit_method", "ratio", "use_unspill", "unspill_red", "unspill_green", "unspill_blue"], + 'CompositorNodeCryptomatteV2' : ["source"], #TODO: will need a lot of special handling + 'CompositorNodeCryptomatte' : [], #TODO: will likely need same handling as above + 'CompositorNodeDiffMatte' : ["tolerance", "falloff"], + 'CompositorNodeDistanceMatte' : ["tolerance", "falloff", "channel"], + 'CompositorNodeDoubleEdgeMask' : ["inner_mode", "edge_mode"], + 'CompositorNodeEllipseMask' : ["x", "y", "width", "height", "rotation", "mask_type"], + 'CompositorNodeKeying' : ["blur_pre", "screen_balance", "despill_factor", "despill_balance", "edge_kernel_radius", "edge_kernel_tolerance", "clip_black", "clip_white", "dilate_distance", "feather_falloff", "feather_distance", "blur_post"], + 'CompositorNodeKeyingScreen' : [], #TODO: movie stuff + 'CompositorNodeLumaMatte' : ["limit_max", "limit_min"], + + #Distort + 'CompositorNodeCornerPin' : [], + 'CompositorNodeCrop' : ["use_crop_size", "relative", "min_x", "max_x", "min_y", "max_y", "rel_min_x", "rel_max_x", "rel_min_y", "rel_max_y"], + 'CompositorNodeDisplace' : [], + 'CompositorNodeFlip' : ["axis"], + 'CompositorNodeLensdist' : ["use_projector", "use_jitter", "use_fit"], + 'CompositorNodeMapUV' : ["alpha"], + 'CompositorNodeMovieDistortion' : [], #TODO: movie stuff + 'CompositorNodePlaneTrackDeform' : ["use_motion_blur", "motion_blur_samples", "motion_blur_shutter"], #TODO: movie stuff + 'CompositorNodeRotate' : ["filter_type"], + 'CompositorNodeScale' : ["space", "frame_method", "offset_x", "offset_y"], + 'CompositorNodeStablize' : [], #TODO: movie stuff + 'CompositorNodeTransform' : ["filter_type"], + 'CompositorNodeTranslate' : ["use_relative", "wrapping"], + + #Layout + 'CompositorNodeSwitch' : ["check"] } -curve_nodes = {'ShaderNodeFloatCurve', - 'ShaderNodeVectorCurve', - 'ShaderNodeRGBCurve'} +curve_nodes = { + 'CompositorNodeTime', #TODO: check this works + 'CompositorNodeHueCorrect', #TODO: probbably will need custom work + 'CompositorNodeCurveRGB', #may just work out of the box + 'CompositorNodeCurveVec', #may just work out of the box +} -image_nodes = {'ShaderNodeTexEnvironment', - 'ShaderNodeTexImage'} +image_nodes = {'CompositorNodeImage',} class NTPCompositorOperator(bpy.types.Operator): bl_idname = "node.compositor_to_python" @@ -33,14 +159,19 @@ class NTPCompositorOperator(bpy.types.Operator): def execute(self, context): """ #find node group to replicate - nt = bpy.data.materials[self.material_name].node_tree + if self.is_scene: + nt = bpy.data.scenes[self.compositor_name].node_tree + else: + nt = bpy.data.node_groups[self.compositor_name] if nt is None: + #shouldn't happen self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " - "valid material. Is Use Nodes selected?")) + "valid compositor node tree. Is Use Nodes " + "selected?")) return {'CANCELLED'} #set up names to use in generated addon - mat_var = clean_string(self.material_name) + comp_var = clean_string(self.compositor_name) if self.mode == 'ADDON': dir = bpy.path.abspath(context.scene.ntp_options.dir_path) @@ -50,29 +181,30 @@ def execute(self, context): "NodeToPython!")) return {'CANCELLED'} - zip_dir = os.path.join(dir, mat_var) - addon_dir = os.path.join(zip_dir, mat_var) + zip_dir = os.path.join(dir, comp_var) + addon_dir = os.path.join(zip_dir, comp_var) if not os.path.exists(addon_dir): os.makedirs(addon_dir) file = open(f"{addon_dir}/__init__.py", "w") - create_header(file, self.material_name) - class_name = clean_string(self.material_name, lower=False) - init_operator(file, class_name, mat_var, self.material_name) + create_header(file, self.compositor_name) + class_name = clean_string(self.compositor_name, lower=False) + init_operator(file, class_name, comp_var, self.compositor_name) file.write("\tdef execute(self, context):\n") else: file = StringIO("") - 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': - create_material("") + if self.is_scene: + def create_scene(indent: str): + file.write((f"{indent}scene = bpy.data.scenes.new(" #TODO: see if using scene as name effects nodes named scene + f"name = {str_to_py_str(self.compositor_name)})\n")) + file.write(f"{indent}scene.use_nodes = True\n") + + if self.mode == 'ADDON': + create_scene("\t\t") + elif self.mode == 'SCRIPT': + create_scene("") #set to keep track of already created node trees node_trees = set() @@ -90,10 +222,10 @@ def is_outermost_node_group(level: int) -> bool: return True return False - def process_mat_node_group(node_tree, level, node_vars, used_vars): + def process_comp_node_group(node_tree, level, node_vars, used_vars): if is_outermost_node_group(level): - nt_var = create_var(self.material_name, used_vars) - nt_name = self.material_name + nt_var = create_var(self.compositor_name, used_vars) + nt_name = self.compositor_name else: nt_var = create_var(node_tree.name, used_vars) nt_name = node_tree.name @@ -105,14 +237,14 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): file.write(f"{outer}def {nt_var}_node_group():\n") if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = mat.node_tree\n") + file.write(f"{inner}{nt_var} = scene.node_tree\n") file.write(f"{inner}#start with a clean node tree\n") file.write(f"{inner}for node in {nt_var}.nodes:\n") file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") else: file.write((f"{inner}{nt_var}" f"= bpy.data.node_groups.new(" - f"type = \'ShaderNodeTree\', " + f"type = \'CompositorNodeTree\', " f"name = {str_to_py_str(nt_name)})\n")) file.write("\n") @@ -126,10 +258,10 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): node_vars = {} for node in node_tree.nodes: - if node.bl_idname == 'ShaderNodeGroup': + if node.bl_idname == 'CompositorNodeGroup': 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, + process_comp_node_group(node_nt, level + 1, node_vars, used_vars) node_trees.add(node_nt) @@ -139,7 +271,7 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): set_settings_defaults(node, node_settings, file, inner, node_var) hide_sockets(node, file, inner, node_var) - if node.bl_idname == 'ShaderNodeGroup': + if node.bl_idname == 'CompositorNodeGroup': if node.node_tree is not None: file.write((f"{inner}{node_var}.node_tree = " f"bpy.data.node_groups" @@ -183,7 +315,7 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): level = 2 else: level = 0 - process_mat_node_group(nt, level, node_vars, used_vars) + process_comp_node_group(nt, level, node_vars, used_vars) if self.mode == 'ADDON': file.write("\t\treturn {'FINISHED'}\n\n") diff --git a/materials.py b/materials.py index 66c0d00..e65210f 100644 --- a/materials.py +++ b/materials.py @@ -125,7 +125,7 @@ def execute(self, context): file = StringIO("") def create_material(indent: str): - file.write((f"{indent}mat = bpy.data.materials.new(" + file.write((f"{indent}mat = bpy.data.materials.new(" #TODO: see if using mat effects nodes named mat f"name = {str_to_py_str(self.material_name)})\n")) file.write(f"{indent}mat.use_nodes = True\n") @@ -282,7 +282,7 @@ def poll(cls, context): def draw(self, context): layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' - for mat in bpy.data.materials: + for mat in bpy.data.materials: #TODO: filter by node tree exists op = layout.operator(NTPMaterialOperator.bl_idname, text=mat.name) op.material_name = mat.name @@ -306,7 +306,7 @@ def draw(self, context): row = layout.row() # Disables menu when there are no materials - materials = bpy.data.materials + materials = bpy.data.materials #TODO: filter by node tree exist materials_exist = len(materials) > 0 row.enabled = materials_exist From 486872db3ab11783e62f5b448079c2d2cd8cbdad Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 13 Aug 2023 16:48:38 -0500 Subject: [PATCH 05/72] feat: scene copying and setup --- compositor.py | 37 ++++++++++++++++++++++++++++--------- materials.py | 2 +- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/compositor.py b/compositor.py index ef978b2..0b21453 100644 --- a/compositor.py +++ b/compositor.py @@ -4,6 +4,14 @@ from .utils import * from io import StringIO +SCENE_VAR = "scene" +BASE_NAME_VAR = "base_name" +END_NAME_VAR = "end_name" + +ntp_vars = {SCENE_VAR, BASE_NAME_VAR, END_NAME_VAR} +#TODO: do something similar for geo nodes and materials, should be useful for +# possible conflicts between ntp_vars and node vars + node_settings = { #Input 'CompositorNodeBokehImage' : ["flaps", "angle", "rounding", "catadioptric", @@ -157,7 +165,6 @@ class NTPCompositorOperator(bpy.types.Operator): is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") def execute(self, context): - """ #find node group to replicate if self.is_scene: nt = bpy.data.scenes[self.compositor_name].node_tree @@ -194,12 +201,24 @@ def execute(self, context): file.write("\tdef execute(self, context):\n") else: file = StringIO("") - if self.is_scene: def create_scene(indent: str): - file.write((f"{indent}scene = bpy.data.scenes.new(" #TODO: see if using scene as name effects nodes named scene - f"name = {str_to_py_str(self.compositor_name)})\n")) - file.write(f"{indent}scene.use_nodes = True\n") + file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") #TODO: see if using scene as name effects nodes named scene + + #TODO: wrap in more general unique name util function + file.write(f"{indent}# Generate unique scene name\n") + file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") + file.write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") + file.write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + file.write(f"{indent}\ti = 1\n") + file.write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + file.write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + file.write(f"{indent}\t\ti += 1\n\n") + + file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") + file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") + file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") if self.mode == 'ADDON': create_scene("\t\t") @@ -237,7 +256,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): file.write(f"{outer}def {nt_var}_node_group():\n") if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = scene.node_tree\n") + file.write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") file.write(f"{inner}#start with a clean node tree\n") file.write(f"{inner}for node in {nt_var}.nodes:\n") file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") @@ -247,7 +266,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): f"type = \'CompositorNodeTree\', " f"name = {str_to_py_str(nt_name)})\n")) file.write("\n") - + """ inputs_set = False outputs_set = False @@ -310,7 +329,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): init_links(node_tree, file, inner, nt_var, node_vars) file.write(f"\n{outer}{nt_var}_node_group()\n\n") - + """ if self.mode == 'ADDON': level = 2 else: @@ -331,7 +350,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): if self.mode == 'ADDON': zip_addon(zip_dir) - """ + if self.mode == 'SCRIPT': location = "clipboard" else: diff --git a/materials.py b/materials.py index e65210f..c0ee1ba 100644 --- a/materials.py +++ b/materials.py @@ -131,7 +131,7 @@ def create_material(indent: str): if self.mode == 'ADDON': create_material("\t\t") - elif self.mode == 'SCRIPT': + elif self.mode == 'SCRIPT': #TODO: should add option for just creating the node group create_material("") #set to keep track of already created node trees From 3e9ebc9161683288b7cac49d2706dd0629dbdeab Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Thu, 24 Aug 2023 23:47:15 -0500 Subject: [PATCH 06/72] refactor: add types to node settings --- compositor.py | 478 +++++++++++++++++++++++++++++++++++--------------- geo_nodes.py | 409 ++++++++++++++++++++++++++++++------------ materials.py | 248 +++++++++++++++++++------- utils.py | 32 ++-- 4 files changed, 835 insertions(+), 332 deletions(-) diff --git a/compositor.py b/compositor.py index 0b21453..7df8437 100644 --- a/compositor.py +++ b/compositor.py @@ -12,142 +12,336 @@ #TODO: do something similar for geo nodes and materials, should be useful for # possible conflicts between ntp_vars and node vars -node_settings = { - #Input - 'CompositorNodeBokehImage' : ["flaps", "angle", "rounding", "catadioptric", - "shift"], - 'CompositorNodeImage' : [], #TODO: handle image selection - 'CompositorNodeMask' : ["use_feather", "size_source", "size_x", "size_y", - "use_motion_blur", - "motion_blur_samples", "motion_blur_shutter"], #TODO: handle mask selection - 'CompositorNodeMovieClip' : [], #TODO: handle movie clip selection - 'CompositorNodeRLayers' : ["name", "layer"], - 'CompositorNodeRGB' : [], #should be handled by outputs - 'CompositorNodeSceneTime' : [], #should be good - 'CompositorNodeTexture' : [], #TODO: handle texture selection - 'CompositorNodeTime' : ["frame_start", "frame_end"], - 'CompositorNodeTrackPos' : [], #TODO: handle movie selection - 'CompositorNodeValue' : [], #should be handled by outputs (why is this a separate class??) - - #Output - 'CompositorNodeComposite' : ["use_alpha"], - 'CompositorNodeOutputFile' : ["base_path"], #TODO: doesn't seem portable - 'CompositorNodeLevels' : ["channel"], - 'CompositorNodeSplitViewer' : ["axis", "factor"], - 'CompositorNodeViewer' : ["use_alpha"], - - #Color - 'CompositorNodeAlphaOver' : ["use_premultiply", "premul"], - 'CompositorNodeBrightContrast' : ["use_premultiply"], - 'CompositorNodeColorBalance' : ["correction_method", "lift", "gamma", - "gain", "offset", "power", "slope", - "offset_basis"], - 'CompositorNodeColorCorrection' : ["red", "green", "blue", - "master_saturation", "master_contrast", - "master_gamma", "master_gain", - "master_lift", - "highlights_saturation", "highlights_contrast", - "highlights_gamma", "highlights_gain", - "highlights_lift", - "midtones_saturation", "midtones_contrast", - "midtones_gamma", "midtones_gain", - "midtones_lift", - "shadows_saturation", "shadows_contrast", - "shadows_gamma", "shadows_gain", - "shadows_lift", - "midtones_start", "midtones_end"], - 'CompositorNodeExposure' : [], - 'CompositorNodeGamma' : [], - 'CompositorNodeHueCorrect' : [], - 'CompositorNodeHueSat' : [], - 'CompositorNodeInvert' : ["invert_rgb", "invert_alpha"], - 'CompositorNodeMixRGB' : ["blend_type", "use_alpha", "use_clamp"], #TODO: has an update() method, may need to figure out why... - 'CompositorNodePosterize' : [], - 'CompositorNodeCurveRGB' : [], - 'CompositorNodeTonemap' : ["tonemap_type", "intensity", "contrast", "adaptation", "correction", "key", "offset", "gamma"], - 'CompositorNodeZcombine' : ["use_alpha", "use_antialias_z"], - - #Converter - 'CompositorNodePremulKey' : ["mapping"], - 'CompositorNodeValToRGB' : [], #TODO: check to see if this'll work out of the box - 'CompositorNodeConvertColorSpace' : ["from_color_space", "to_color_space"], - 'CompositorNodeCombineColor' : ["mode", "ycc_mode"], #why isn't this standardized across blender? - 'CompositorNodeCombineXYZ' : [], - 'CompositorNodeIDMask' : ["index", "use_antialiasing"], - 'CompositorNodeMath' : ["operation", "use_clamp"], - 'CompositorNodeRGBToBW' : [], - 'CompositorNodeSeparateColor' : ["mode", "ycc_mode"], - 'CompositorNodeSeparateXYZ' : [], - 'CompositorNodeSetAlpha' : ["mode"], - 'CompositorNodeSwitchView' : [], - - #Filter - 'CompositorNodeAntiAliasing' : ["threshold", "contrast_limit", "corner_rounding"], - 'CompositorNodeBilateralblur' : ["iterations", "sigma_color", "sigma_space"], - 'CompositorNodeBlur' : ["filter_type", "use_variable_size", "use_gamma_correction", "use_relative", "aspect_correction", "factor", "factor_x", "factor_y", "use_extended_bounds"], - 'CompositorNodeBokehBlur' : ["use_variable_size", "blur_max", "use_extended_bounds"], - 'CompositorNodeDefocus' : ["bokeh", "angle", "use_gamma_correction", "f_stop", "blur_max", "threshold", "use_preview", "use_zbuffer", "z_scale"], - 'CompositorNodeDespeckle' : ["threshold", "threshold_neighbor"], - 'CompositorNodeDilateErode' : ["mode", "distance", "edge", "falloff"], - 'CompositorNodeDBlur' : ["iterations", "center_x", "center_y", "distance", "angle", "spin", "zoom"], - 'CompositorNodeFilter' : ["filter_type"], - 'CompositorNodeGlare' : ["glare_type", "quality", "iterations", "color_modulation", "mix", "threshold", "streaks", "angle_offset", "fade", "size", "use_rotate_45"], - 'CompositorNodeInpaint' : ["distance"], - 'CompositorNodePixelate' : [], - 'CompositorNodeSunBeams' : ["source", "ray_length"], #TODO: check that source doesn't freak out - 'CompositorNodeVecBlur' : ["samples", "factor", "speed_min", "speed_max", "use_curved"], - - #Vector - 'CompositorNodeMapRange' : ["use_clamp"], - 'CompositorNodeMapValue' : ["offset", "size", "use_min", "min", "use_max", "max"], #why are all these vectors?? TODO: check to make sure it doesn't flip - 'CompositorNodeNormal' : [], #should be handled with io system +compositor_node_settings : dict[str, list[(str, str)]] = { + # INPUT + 'CompositorNodeBokehImage' : [("angle", "float"), + ("catadioptric", "float"), + ("flaps", "int"), + ("rounding", "float"), + ("shift", "float")], + 'CompositorNodeImage' : [("frame_duration", "int"), + ("frame_offset", "int"), + ("frame_start", "int"), + ("image", "Image"), #TODO: handle image selection + ("layer", "enum"), + ("use_auto_refresh", "bool"), + ("use_cyclic", "bool"), + ("use_straight_alpha_output", "bool"), + ("view", "enum")], + 'CompositorNodeMask' : [("mask", "Mask"), #TODO + ("motion_blur_samples", "int"), + ("motion_blur_shutter", "float"), + ("size_source", "enum"), + ("size_x", "int"), + ("size_y", "int"), + ("use_feather", "bool"), + ("use_motion_blur", "bool")], + 'CompositorNodeMovieClip' : [("clip", "MovieClip")], #TODO: handle movie clip selection + 'CompositorNodeRLayers' : [("layer", "enum"), + ("scene", "Scene")], #TODO + 'CompositorNodeRGB' : [], + 'CompositorNodeSceneTime' : [], + 'CompositorNodeTexture' : [("node_output", "int"), #TODO: ?? + ("texture", "Texture")], #TODO: handle texture selection + 'CompositorNodeTime' : [("curve", "CurveMapping"), + ("frame_end", "int"), + ("frame_start", "int")], + 'CompositorNodeTrackPos' : [("clip", "MovieClip"), #TODO: this is probably wrong + ("frame_relative", "int") + ("position", "enum"), + ("track_name", "str"), + ("tracking_object", "str")], + 'CompositorNodeValue' : [], #should be handled by outputs (why is this a separate class??) + + + # OUTPUT + 'CompositorNodeComposite' : [("use_alpha", "bool")], + 'CompositorNodeOutputFile' : [("active_input_index", "int"), #TODO: probably not right at all + ("base_path", "str"), + ("file_slots", "CompositorNodeOutputFileFileSlots"), + ("format", "ImageFormatSettings"), + ("layer_slots", "CompositorNodeOutputFileLayerSlots")], + 'CompositorNodeLevels' : [("channel", "enum")], + 'CompositorNodeSplitViewer' : [("axis", "enum"), + ("factor", "int")], + 'CompositorNodeViewer' : [("center_x", "float"), + ("center_y", "float"), + ("tile_order", "enum"), + ("use_alpha", "bool")], + + + # COLOR + 'CompositorNodeAlphaOver' : [("premul", "float"), + ("use_premultiply", "bool")], + 'CompositorNodeBrightContrast' : [("use_premultiply", "bool")], + 'CompositorNodeColorBalance' : [("correction_method", "enum"), + ("gain", "Vec3"), + ("gamma", "Vec3"), + ("lift", "Vec3"), + ("offset", "Vec3"), + ("offset_basis", "float"), + ("power", "Vec3"), + ("slope", "Vec3")], + 'CompositorNodeColorCorrection' : [("blue", "bool"), + ("green", "bool"), + ("highlights_contrast", "float"), + ("highlights_gain", "float"), + CurveMapp ("midtones_lift", "float"), + ("midtones_saturation", "float"), + ("midtones_start", "float"), + ("red", "bool"), + ("shadows_contrast", "float"), + ("shadows_gain", "float"), + ("shadows_gamma", "float"), + ("shadows_lift", "float"), + ("shadows_saturation", "float")], + 'CompositorNodeExposure' : [], + 'CompositorNodeGamma' : [], + 'CompositorNodeHueCorrect' : [("mapping", "CurveMapping")], + 'CompositorNodeHueSat' : [], + 'CompositorNodeInvert' : [("invert_alpha", "bool"), + ("invert_rgb", "bool")], + 'CompositorNodeMixRGB' : [("blend_type", "enum"), + ("use_alpha", "bool"), + ("use_clamp", "bool")], #TODO: has an update() method, may need to figure out why... + 'CompositorNodePosterize' : [], + 'CompositorNodeCurveRGB' : [("mapping", "CurveMapping")], + 'CompositorNodeTonemap' : [("adaptation", "float"), + ("contrast", "float"), + ("correction", "float"), + ("gamma", "float"), + ("intensity", "float"), + ("key", "float"), + ("offset", "float"), + ("tonemap_type", "enum")], + 'CompositorNodeZcombine' : [("use_alpha", "bool"), + ("use_antialias_z", "bool")], + + + # CONVERTER + 'CompositorNodePremulKey' : [("mapping", "enum")], + 'CompositorNodeValToRGB' : [("color_ramp", "ColorRamp")], #TODO: check to see if this'll work out of the box + 'CompositorNodeConvertColorSpace' : [("from_color_space", "enum"), + ("to_color_space", "enum")], + 'CompositorNodeCombineColor' : [("mode", "enum"), + ("ycc_mode", "enum")], #why isn't this standardized across blender? + 'CompositorNodeCombineXYZ' : [], + 'CompositorNodeIDMask' : [("index", "int"), + ("use_antialiasing", "bool")], + 'CompositorNodeMath' : [("operation", "enum"), + ("use_clamp", "bool")], + 'CompositorNodeRGBToBW' : [], + 'CompositorNodeSeparateColor' : [("mode", "enum"), + ("ycc_mode", "enum")], + 'CompositorNodeSeparateXYZ' : [], + 'CompositorNodeSetAlpha' : [("mode", "enum")], + 'CompositorNodeSwitchView' : [], + + + # FILTER + 'CompositorNodeAntiAliasing' : [("contrast_limit", "float"), + ("corner_rounding", "float"), + ("threshold", "float")], + 'CompositorNodeBilateralblur' : [("iterations", "int"), + ("sigma_color", "float"), + ("sigma_space", "float")], + 'CompositorNodeBlur' : [("aspect_correction", "enum"), + ("factor", "float"), + ("factor_x", "float"), + ("factor_y", "float"), + ("filter_type", "enum"), + ("size_x", "int"), + ("size_y", "int"), + ("use_bokeh", "bool"), + ("use_extended_bounds", "bool"), + ("use_gamma_correction", "bool"), + ("use_relative", "bool"), + ("use_variable_size", "bool")], + 'CompositorNodeBokehBlur' : [("blur_max", "float"), + ("use_extended_bounds", "bool"), + ("use_variable_size", "bool")], + 'CompositorNodeDefocus' : [("angle", "float"), + ("blur_max", "float"), + ("bokeh", "enum"), + ("f_stop", "float"), + ("scene", "Scene"), #TODO + ("threshold", "float"), + ("use_gamma_correction", "bool"), + ("use_preview", "bool"), + ("use_zbuffer", "bool"), + ("z_scale", "float")], + 'CompositorNodeDespeckle' : [("threshold", "float"), + ("threshold_neighbor", "float")], + 'CompositorNodeDilateErode' : [("distance", "int"), + ("edge", "float"), + ("falloff", "enum"), + ("mode", "enum")], + 'CompositorNodeDBlur' : [("angle", "float"), + ("center_x", "float"), + ("center_y", "float"), + ("distance", "float"), + ("iterations", "int"), + ("spin", "float"), + ("zoom", "float")], + 'CompositorNodeFilter' : [("filter_type", "enum")], + 'CompositorNodeGlare' : [("angle_offset", "float"), + ("color_modulation", "float"), + ("fade", "float"), + ("glare_type", "enum"), + ("iterations", "int"), + ("mix", "float"), + ("quality", "enum"), + ("size", "int"), + ("streaks", "int"), + ("threshold", "float"), + ("use_rotate_45", "bool")], + 'CompositorNodeInpaint' : [("distance", "int")], + 'CompositorNodePixelate' : [], + 'CompositorNodeSunBeams' : [("ray_length", "float"), + ("source", "Vec2")], + 'CompositorNodeVecBlur' : [("factor", "float"), + ("samples", "int"), + ("speed_max", "int"), + ("speed_min", "int"), + ("use_curved", "bool")], + + + # VECTOR + 'CompositorNodeMapRange' : [("use_clamp", "bool")], + 'CompositorNodeMapValue' : [("max", "Vec1"), + ("min", "Vec1"), + ("offset", "Vec1"), + ("size", "Vec1"), + ("use_max", "bool"), + ("use_min", "bool")], #why are all these vectors?? TODO: check to make sure it doesn't flip + 'CompositorNodeNormal' : [], 'CompositorNodeNormalize' : [], - 'CompositorNodeCurveVec' : [], - - #Matte - 'CompositorNodeBoxMask' : ["x", "y", "width", "height", "rotation", "mask_type"], - 'CompositorNodeChannelMatte' : ["color_space", "matte_channel", "limit_method", "limit_channel", "limit_max", "limit_min"], - 'CompositorNodeChromaMatte' : ["tolerance", "threshold", "gain"], - 'CompositorNodeColorMatte' : ["color_hue", "color_saturation", "color_value"], - 'CompositorNodeColorSpill' : ["channel", "limit_method", "ratio", "use_unspill", "unspill_red", "unspill_green", "unspill_blue"], - 'CompositorNodeCryptomatteV2' : ["source"], #TODO: will need a lot of special handling - 'CompositorNodeCryptomatte' : [], #TODO: will likely need same handling as above - 'CompositorNodeDiffMatte' : ["tolerance", "falloff"], - 'CompositorNodeDistanceMatte' : ["tolerance", "falloff", "channel"], - 'CompositorNodeDoubleEdgeMask' : ["inner_mode", "edge_mode"], - 'CompositorNodeEllipseMask' : ["x", "y", "width", "height", "rotation", "mask_type"], - 'CompositorNodeKeying' : ["blur_pre", "screen_balance", "despill_factor", "despill_balance", "edge_kernel_radius", "edge_kernel_tolerance", "clip_black", "clip_white", "dilate_distance", "feather_falloff", "feather_distance", "blur_post"], - 'CompositorNodeKeyingScreen' : [], #TODO: movie stuff - 'CompositorNodeLumaMatte' : ["limit_max", "limit_min"], - - #Distort - 'CompositorNodeCornerPin' : [], - 'CompositorNodeCrop' : ["use_crop_size", "relative", "min_x", "max_x", "min_y", "max_y", "rel_min_x", "rel_max_x", "rel_min_y", "rel_max_y"], - 'CompositorNodeDisplace' : [], - 'CompositorNodeFlip' : ["axis"], - 'CompositorNodeLensdist' : ["use_projector", "use_jitter", "use_fit"], - 'CompositorNodeMapUV' : ["alpha"], - 'CompositorNodeMovieDistortion' : [], #TODO: movie stuff - 'CompositorNodePlaneTrackDeform' : ["use_motion_blur", "motion_blur_samples", "motion_blur_shutter"], #TODO: movie stuff - 'CompositorNodeRotate' : ["filter_type"], - 'CompositorNodeScale' : ["space", "frame_method", "offset_x", "offset_y"], - 'CompositorNodeStablize' : [], #TODO: movie stuff - 'CompositorNodeTransform' : ["filter_type"], - 'CompositorNodeTranslate' : ["use_relative", "wrapping"], - - #Layout + 'CompositorNodeCurveVec' : [("mapping", "CurveMapping")], + + + # MATTE + 'CompositorNodeBoxMask' : [("height", "float"), + ("mask_type", "enum"), + ("rotation", "float"), + ("width", "float"), + ("x", "float"), + ("y", "float")], + 'CompositorNodeChannelMatte' : [("color_space", "enum"), + ("limit_channel", "enum"), + ("limit_max", "float"), + ("limit_method", "enum"), + ("limit_min", "float"), + ("matte_channel", "enum")], + 'CompositorNodeChromaMatte' : [("gain", "float"), + ("lift", "float"), + ("shadow_adjust", "float"), + ("threshold", "float"), + ("tolerance", "float")], + 'CompositorNodeColorMatte' : [("color_hue", "float"), + ("color_saturation", "float"), + ("color_value", "float")], + 'CompositorNodeColorSpill' : [("channel", "enum"), + ("limit_channel", "enum"), + ("limit_method", "enum"), + ("ratio", "float"), + ("unspill_blue", "float"), + ("unspill_green", "float"), + ("unspill_red", "float"), + ("use_unspill", "bool")], + 'CompositorNodeCryptomatteV2' : [("add", "Vec3"), #TODO: will need a lot of special handling + ("entries", "CryptomatteEntry"), #TODO: (readonly?) + ("frame_duration", "int"), + ("frame_offset", "int"), + ("frame_start", "int"), + ("has_layers", "bool"), #TODO: readonly? + ("has_views", "bool"), #TODO: readonly? + ("image", "Image"), + ("layer", "enum"), + ("layer_name", "enum"), + ("matte_id", "str"), + ("remove", "Vec3"), + ("scene", "Scene"), + ("source", "enum"), + ("use_auto_refresh", "bool"), + ("use_cyclic", "bool"), + ("view", "enum")], + 'CompositorNodeCryptomatte' : [("add", "Vec3"), #TODO: will need a lot of special handling + ("matte_id", "str"), + ("remove", "Vec3")], + 'CompositorNodeDiffMatte' : [("falloff", "float"), + ("tolerance", "float")], + 'CompositorNodeDistanceMatte' : [("channel", "enum"), + ("falloff", "float"), + ("tolerance", "float")], + 'CompositorNodeDoubleEdgeMask' : [("edge_mode", "enum"), + ("inner_mode", "enum")], + 'CompositorNodeEllipseMask' : [("height", "float"), + ("mask_type", "enum"), + ("rotation", "float"), + ("width", "float"), + ("x", "float"), + ("y", "float")], + 'CompositorNodeKeying' : [("blur_post", "int"), + ("blur_pre", "int"), + ("clip_black", "float"), + ("clip_white", "float"), + ("despill_balance", "float"), + ("despill_factor", "float"), + ("dilate_distance", "int"), + ("edge_kernel_radius", "int"), + ("edge_kernel_tolerance", "float"), + ("feather_distance", "int"), + ("feather_falloff", "enum"), + ("screen_balance", "float")], + 'CompositorNodeKeyingScreen' : [("clip", "MovieClip"), + ("tracing_object", "str")], #TODO: movie stuff + 'CompositorNodeLumaMatte' : [("limit_max", "float"), + ("limit_min", "float")], + + + # DISTORT + 'CompositorNodeCornerPin' : [], + 'CompositorNodeCrop' : [("max_x", "int"), + ("max_y", "int"), + ("min_x", "int"), + ("min_y", "int"), + ("rel_max_x", "float"), + ("rel_max_y", "float"), + ("rel_min_x", "float"), + ("rel_min_y", "float"), + ("relative", "bool"), + ("use_crop_size", "bool")], + 'CompositorNodeDisplace' : [], + 'CompositorNodeFlip' : [("axis", "enum")], + 'CompositorNodeLensdist' : [("use_fit", "bool"), + ("use_jitter", "bool"), + ("use_projector", "bool")], + 'CompositorNodeMapUV' : [("alpha", "int")], + 'CompositorNodeMovieDistortion' : [("clip", "MovieClip"), + ("distortion_type", "enum")], #TODO: movie stuff + 'CompositorNodePlaneTrackDeform' : [("clip", "MovieClip"), + ("motion_blur_samples", "int"), + ("motion_blur_shutter", "float"), + ("plane_track_name", "str"), + ("tracking_object", "str"), + ("use_motion_blur", "bool")], #TODO: movie stuff + 'CompositorNodeRotate' : [("filter_type", "enum")], + 'CompositorNodeScale' : [("frame_method", "enum"), + ("offset_x", "float"), + ("offset_y", "float"), + ("space", "enum")], + 'CompositorNodeStablize' : [("clip", "MovieClip"), + ("filter_type", "enum"), + ("invert", "bool")], #TODO: movie stuff + 'CompositorNodeTransform' : [("filter_type", "enum")], + 'CompositorNodeTranslate' : [("use_relative", "bool"), + ("wrap_axis", "enum")], + + + # LAYOUT 'CompositorNodeSwitch' : ["check"] } -curve_nodes = { - 'CompositorNodeTime', #TODO: check this works - 'CompositorNodeHueCorrect', #TODO: probbably will need custom work - 'CompositorNodeCurveRGB', #may just work out of the box - 'CompositorNodeCurveVec', #may just work out of the box -} - -image_nodes = {'CompositorNodeImage',} - class NTPCompositorOperator(bpy.types.Operator): bl_idname = "node.compositor_to_python" bl_label = "Compositor to Python" @@ -266,7 +460,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): f"type = \'CompositorNodeTree\', " f"name = {str_to_py_str(nt_name)})\n")) file.write("\n") - """ + inputs_set = False outputs_set = False @@ -303,14 +497,14 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): group_io_settings(node, file, inner, "output", nt_var, node_tree) outputs_set = True - 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") - image_user_settings(node, file, inner, node_var) - - elif node.bl_idname == 'ShaderNodeValToRGB': + # 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") + # image_user_settings(node, file, inner, node_var) + + elif node.bl_idname == 'CompositorNodeValToRGB': color_ramp_settings(node, file, inner, node_var) elif node.bl_idname in curve_nodes: @@ -329,7 +523,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): init_links(node_tree, file, inner, nt_var, node_vars) file.write(f"\n{outer}{nt_var}_node_group()\n\n") - """ + if self.mode == 'ADDON': level = 2 else: diff --git a/geo_nodes.py b/geo_nodes.py index f5f6de1..b83d058 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -4,166 +4,341 @@ from .utils import * from io import StringIO -geo_node_settings = { - # Attribute nodes - "GeometryNodeAttributeStatistic" : ["data_type", "domain"], - "GeometryNodeAttributeDomainSize" : ["component"], +geo_node_settings : dict[str, list[(str, str)]] = { + # ATTRIBUTE + 'GeometryNodeAttributeStatistic' : [("data_type", "enum"), + ("domain", "enum")], + 'GeometryNodeAttributeDomainSize' : [("component", "enum")], + 'GeometryNodeBlurAttribute' : [("data_type", "enum")], + 'GeometryNodeCaptureAttribute' : [("data_type", "enum"), + ("domain", "enum")], + 'GeometryNodeRemoveAttribute' : [], + 'GeometryNodeStoreNamedAttribute' : [("data_type", "enum"), + ("domain", "enum")], + 'GeometryNodeAttributeTransfer' : [("data_type", "enum"), + ("domain", "enum"), + ("mapping", "enum")], + + # INPUT + # Input > Constant + 'FunctionNodeInputBool' : [("boolean", "bool")], + 'FunctionNodeInputColor' : [("color", "Vec4")], + 'GeometryNodeInputImage' : [("image", "Image")], + 'FunctionNodeInputInt' : [("integer", "int")], + 'GeometryNodeInputMaterial' : [("material", "Material")], + 'FunctionNodeInputString' : [("string", "str")], + 'ShaderNodeValue' : [], + 'FunctionNodeInputVector' : [("vector", "Vec3")], + + #Input > Group + 'NodeGroupInput' : [], + + # Input > Scene + 'GeometryNodeCollectionInfo' : [("transform_space", "enum")], + 'GeometryNodeImageInfo' : [], + 'GeometryNodeIsViewport' : [], + 'GeometryNodeObjectInfo' : [("transform_space", "enum")], + 'GeometryNodeSelfObject' : [], + 'GeometryNodeInputSceneTime' : [], - "GeometryNodeBlurAttribute" : ["data_type"], - "GeometryNodeCaptureAttribute" : ["data_type", "domain"], - "GeometryNodeStoreNamedAttribute" : ["data_type", "domain"], - "GeometryNodeAttributeTransfer" : ["data_type", "mapping"], - # Input Nodes - # Input > Constant - "FunctionNodeInputBool" : ["boolean"], - "FunctionNodeInputColor" : ["color"], - "FunctionNodeInputInt" : ["integer"], - "GeometryNodeInputMaterial" : ["material"], - "FunctionNodeInputString" : ["string"], - "FunctionNodeInputVector" : ["vector"], + # OUTPUT + 'GeometryNodeViewer' : [("data_type", "enum"), + ("domain", "enum")], - # Input > Scene - "GeometryNodeCollectionInfo" : ["transform_space"], - "GeometryNodeObjectInfo" : ["transform_space"], - # Output Nodes - "GeometryNodeViewer" : ["domain"], + # GEOMETRY + 'GeometryNodeJoinGeometry' : [], + 'GeometryNodeGeometryToInstance' : [], - # Geometry Nodes # Geometry > Read - "GeometryNodeInputNamedAttribute" : ["data_type"], + 'GeometryNodeInputID' : [], + 'GeometryNodeInputIndex' : [], + 'GeometryNodeInputNamedAttribute' : [("data_type", "enum")], + 'GeometryNodeInputNormal' : [], + 'GeometryNodeInputPosition' : [], + 'GeometryNodeInputRadius' : [], # Geometry > Sample - "GeometryNodeProximity" : ["target_element"], - "GeometryNodeRaycast" : ["data_type", "mapping"], - "GeometryNodeSampleIndex" : ["data_type", "domain", "clamp"], - "GeometryNodeSampleNearest" : ["domain"], + 'GeometryNodeProximity' : [("target_element", "enum")], + 'GeometryNodeIndexOfNearest' : [], + 'GeometryNodeRaycast' : [("data_type", "enum"), + ("mapping", "enum")], + 'GeometryNodeSampleIndex' : [("clamp", "bool"), + ("data_type", "enum"), + ("domain", "enum")], + 'GeometryNodeSampleNearest' : [("domain", "enum")], + + # Geometry > Write + 'GeometryNodeSetID' : [], + 'GeometryNodeSetPosition' : [], # Geometry > Operations - "GeometryNodeDeleteGeometry" : ["domain", "mode"], - "GeometryNodeDuplicateElements" : ["domain"], - "GeometryNodeMergeByDistance" : ["mode"], - "GeometryNodeSeparateGeometry" : ["domain"], - - - # Curve + 'GeometryNodeBoundBox' : [], + 'GeometryNodeConvexHull' : [], + 'GeometryNodeDeleteGeometry' : [("domain", "enum"), + ("mode", "enum")], + 'GeometryNodeDuplicateElements' : [("domain", "enum")], + 'GeometryNodeMergeByDistance' : [("mode", "enum")], + 'GeometryNodeTransform' : [], + 'GeometryNodeSeparateComponents' : [], + 'GeometryNodeSeparateGeometry' : [("domain", "enum")], + + + # CURVE # Curve > Read - "GeometryNodeCurveHandleTypeSelection" : ["mode", "handle_type"], + 'GeometryNodeInputCurveHandlePositions' : [], + 'GeometryNodeCurveLength' : [], + 'GeometryNodeInputTangent' : [], + 'GeometryNodeInputCurveTilt' : [], + 'GeometryNodeCurveEndpointSelection' : [], + 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", "enum"), + ("mode", "enum")], + 'GeometryNodeInputSplineCyclic' : [], + 'GeometryNodeSplineLength' : [], + 'GeometryNodeSplineParameter' : [], + 'GeometryNodeInputSplineResolution' : [], # Curve > Sample - "GeometryNodeSampleCurve" : ["data_type", "mode", "use_all_curves"], + 'GeometryNodeSampleCurve' : [("data_type", "enum"), + ("mode", "enum"), + ("use_all_curves", "bool")], # Curve > Write - "GeometryNodeSetCurveNormal" : ["mode"], - "GeometryNodeSetCurveHandlePositions" : ["mode"], - "GeometryNodeCurveSetHandles" : ["mode", "handle_type"], - "GeometryNodeCurveSplineType" : ["spline_type"], + 'GeometryNodeSetCurveNormal' : [("mode", "enum")], + 'GeometryNodeSetCurveRadius' : [], + 'GeometryNodeSetCurveTilt' : [], + 'GeometryNodeSetCurveHandlePositions' : [("mode", "enum")], + 'GeometryNodeCurveSetHandles' : [("handle_type", "enum"), + ("mode", "enum")], + 'GeometryNodeSetSplineCyclic' : [], + 'GeometryNodeSetSplineResolution' : [], + 'GeometryNodeCurveSplineType' : [("spline_type", "enum")], # Curve > Operations - "GeometryNodeCurveToPoints" : ["mode"], - "GeometryNodeFillCurve" : ["mode"], - "GeometryNodeFilletCurve" : ["mode"], - "GeometryNodeResampleCurve" : ["mode"], - "GeometryNodeTrimCurve" : ["mode"], + 'GeometryNodeCurveToMesh' : [], + 'GeometryNodeCurveToPoints' : [("mode", "enum")], + 'GeometryNodeDeformCurvesOnSurface' : [], + 'GeometryNodeFillCurve' : [("mode", "enum")], + 'GeometryNodeFilletCurve' : [("mode", "enum")], + 'GeometryNodeInterpolateCurves' : [], + 'GeometryNodeResampleCurve' : [("mode", "enum")], + 'GeometryNodeReverseCurve' : [], + 'GeometryNodeSubdivideCurve' : [], + 'GeometryNodeTrimCurve' : [("mode", "enum")], # Curve > Primitives - "GeometryNodeCurveArc" : ["mode"], - "GeometryNodeCurvePrimitiveBezierSegment" : ["mode"], - "GeometryNodeCurvePrimitiveCircle" : ["mode"], - "GeometryNodeCurvePrimitiveLine" : ["mode"], - "GeometryNodeCurvePrimitiveQuadrilateral" : ["mode"], - + 'GeometryNodeCurveArc' : [("mode", "enum")], + 'GeometryNodeCurvePrimitiveBezierSegment' : [("mode", "enum")], + 'GeometryNodeCurvePrimitiveCircle' : [("mode", "enum")], + 'GeometryNodeCurvePrimitiveLine' : [("mode", "enum")], + 'GeometryNodeCurveSpiral' : [], + 'GeometryNodeCurveQuadraticBezier' : [], + 'GeometryNodeCurvePrimitiveQuadrilateral' : [("mode", "enum")], + 'GeometryNodeCurveStar' : [], + + # Curve > Topology + 'GeometryNodeOffsetPointInCurve' : [], + 'GeometryNodeCurveOfPoint' : [], + 'GeometryNodePointsOfCurve' : [], + + + # INSTANCES + 'GeometryNodeInstanceOnPoints' : [], + 'GeometryNodeInstancesToPoints' : [], + 'GeometryNodeRealizeInstances' : [("legacy_behavior", "bool")], + 'GeometryNodeRotateInstances' : [], + 'GeometryNodeScaleInstances' : [], + 'GeometryNodeTranslateInstances' : [], + 'GeometryNodeInputInstanceRotation' : [], + 'GeometryNodeInputInstanceScale' : [], + + + # MESH + # Mesh > Read + 'GeometryNodeInputMeshEdgeAngle' : [], + 'GeometryNodeInputMeshEdgeNeighbors' : [], + 'GeometryNodeInputMeshEdgeVertices' : [], + 'GeometryNodeEdgesToFaceGroups' : [], + 'GeometryNodeInputMeshFaceArea' : [], + 'GeometryNodeInputMeshFaceNeighbors' : [], + 'GeometryNodeMeshFaceSetBoundaries' : [], + 'GeometryNodeInputMeshFaceIsPlanar' : [], + 'GeometryNodeInputShadeSmooth' : [], + 'GeometryNodeInputMeshIsland' : [], + 'GeometryNodeInputShortestEdgePaths' : [], + 'GeometryNodeInputMeshVertexNeighbors' : [], - # Mesh Nodes # Mesh > Sample - "GeometryNodeSampleNearestSurface" : ["data_type"], - "GeometryNodeSampleUVSurface" : ["data_type"], + 'GeometryNodeSampleNearestSurface' : [("data_type", "enum")], + 'GeometryNodeSampleUVSurface' : [("data_type", "enum")], + + # Mesh > Write + 'GeometryNodeSetShadeSmooth' : [], # Mesh > Operations - "GeometryNodeExtrudeMesh" : ["mode"], - "GeometryNodeMeshBoolean" : ["operation"], - "GeometryNodeMeshToPoints" : ["mode"], - "GeometryNodeMeshToVolume" : ["resolution_mode"], - "GeometryNodeScaleElements" : ["domain", "scale_mode"], - "GeometryNodeSubdivisionSurface" : ["uv_smooth", "boundary_smooth"], - "GeometryNodeTriangulate" : ["quad_method", "ngon_method"], + 'GeometryNodeDualMesh' : [], + 'GeometryNodeEdgePathsToCurves' : [], + 'GeometryNodeEdgePathsToSelection' : [], + 'GeometryNodeExtrudeMesh' : [("mode", "enum")], + 'GeometryNodeFlipFaces' : [], + 'GeometryNodeMeshBoolean' : [("operation", "enum")], + 'GeometryNodeMeshToCurve' : [], + 'GeometryNodeMeshToPoints' : [("mode", "enum")], + 'GeometryNodeMeshToVolume' : [("resolution_mode", "enum")], + 'GeometryNodeScaleElements' : [("domain", "enum"), + ("scale_mode", "enum")], + 'GeometryNodeSplitEdges' : [], + 'GeometryNodeSubdivideMesh' : [], + 'GeometryNodeSubdivisionSurface' : [("boundary_smooth", "enum"), + ("uv_smooth", "enum")], + 'GeometryNodeTriangulate' : [("ngon_method", "enum"), + ("quad_method", "enum")], # Mesh > Primitives - "GeometryNodeMeshCone" : ["fill_type"], - "GeometryNodeMeshCylinder" : ["fill_type"], - "GeometryNodeMeshCircle" : ["fill_type"], - "GeometryNodeMeshLine" : ["mode"], + 'GeometryNodeMeshCone' : [("fill_type", "enum")], + 'GeometryNodeMeshCube' : [], + 'GeometryNodeMeshCylinder' : [("fill_type", "enum")], + 'GeometryNodeMeshGrid' : [], + 'GeometryNodeMeshIcoSphere' : [], + 'GeometryNodeMeshCircle' : [("fill_type", "enum")], + 'GeometryNodeMeshLine' : [("count_mode", "enum"), + ("mode", "enum")], + 'GeometryNodeMeshUVSphere' : [], + + # Mesh > Topology + 'GeometryNodeCornersOfFace' : [], + 'GeometryNodeCornersOfVertex' : [], + 'GeometryNodeEdgesOfCorner' : [], + 'GeometryNodeEdgesOfVertex' : [], + 'GeometryNodeFaceOfCorner' : [], + 'GeometryNodeOffsetCornerInFace' : [], + 'GeometryNodeVertexOfCorner' : [], # Mesh > UV - "GeometryNodeUVUnwrap" : ["method"], - + 'GeometryNodeUVPackIslands' : [], + 'GeometryNodeUVUnwrap' : [("method", "enum")], - # Point Nodes - "GeometryNodeDistributePointsInVolume" : ["mode"], - "GeometryNodeDistributePointsOnFaces" : ["distribute_method"], - "GeometryNodePointsToVolume" : ["resolution_mode"], - # Volume Nodes - "GeometryNodeVolumeToMesh" : ["resolution_mode"], + # POINT + 'GeometryNodeDistributePointsInVolume' : [("mode", "enum")], + 'GeometryNodeDistributePointsOnFaces' : [("distribute_method", "enum"), + ("use_legacy_normal", "bool")], + 'GeometryNodePoints' : [], + 'GeometryNodePointsToVertices' : [], + 'GeometryNodePointsToVolume' : [("resolution_mode", "enum")], + 'GeometryNodeSetPointRadius' : [], - # Texture Nodes - "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", - "squash_frequency"], - "ShaderNodeTexGradient" : ["gradient_type"], - "GeometryNodeImageTexture" : ["interpolation", "extension"], - "ShaderNodeTexMagic" : ["turbulence_depth"], - "ShaderNodeTexNoise" : ["noise_dimensions"], - "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], - "ShaderNodeTexWave" : ["wave_type", "bands_direction", "wave_profile"], - "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], + # VOLUME + 'GeometryNodeVolumeCube' : [], + 'GeometryNodeVolumeToMesh' : [("resolution_mode", "enum")], - - # Utilities + + # SIMULATION + 'GeometryNodeSimulationInput' : [], + 'GeometryNodeSimulationOutput' : [], + + + # MATERIAL + 'GeometryNodeReplaceMaterial' : [], + 'GeometryNodeInputMaterialIndex' : [], + 'GeometryNodeMaterialSelection' : [], + 'GeometryNodeSetMaterial' : [], + 'GeometryNodeSetMaterialIndex' : [], + + + # TEXTURE + 'ShaderNodeTexBrick' : [("offset", "float"), + ("offset_frequency", "int"), + ("squash", "float"), + ("squash_frequency", "int")], + 'ShaderNodeTexChecker' : [], + 'ShaderNodeTexGradient' : [("gradient_type", "enum")], + 'GeometryNodeImageTexture' : [("extension", "enum"), + ("interpolation", "enum")], + 'ShaderNodeTexMagic' : [("turbulence_depth", "int")], + 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", "enum"), + ("musgrave_type", "enum")], + 'ShaderNodeTexNoise' : [("noise_dimensions", "enum")], + 'ShaderNodeTexVoronoi' : [("distance", "enum"), + ("feature", "enum"), + ("voronoi_dimensions", "enum")], + 'ShaderNodeTexWave' : [("bands_direction", "enum"), + ("rings_direction", "enum"), + ("wave_profile", "enum"), + ("wave_type", "enum")], + 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", "enum")], + + + # UTILITIES + 'ShaderNodeMix' : [("blend_type", "enum"), + ("clamp_factor", "bool"), + ("clamp_result", "bool"), + ("data_type", "enum"), + ("factor_mode", "enum")], + 'FunctionNodeRandomValue' : [("data_type", "enum")], + 'GeometryNodeSwitch' : [("input_type", "enum")], + # Utilities > Color - "FunctionNodeCombineColor" : ["mode"], - "ShaderNodeMixRGB" : ["blend_type", "use_clamp"], #legacy - "FunctionNodeSeparateColor" : ["mode"], + 'ShaderNodeValToRGB' : [("color_ramp", "ColorRamp")], + 'ShaderNodeRGBCurve' : [("mapping", "CurveMapping")], + 'FunctionNodeCombineColor' : [("mode", "enum")], + 'ShaderNodeMixRGB' : [("blend_type", "enum"), + ("use_alpha", "bool"), + ("use_clamp", "bool")], #legacy + 'FunctionNodeSeparateColor' : [("mode", "enum")], # Utilities > Text - "GeometryNodeStringToCurves" : ["overflow", "align_x", "align_y", - "pivot_mode"], + 'GeometryNodeStringJoin' : [], + 'FunctionNodeReplaceString' : [], + 'FunctionNodeSliceString' : [], + 'FunctionNodeStringLength' : [], + 'GeometryNodeStringToCurves' : [("align_x", "enum"), + ("align_y", "enum"), + ("font", "Font"), #TODO: font + ("overflow", "enum"), + ("pivot_mode", "enum")], + 'FunctionNodeValueToString' : [], + 'FunctionNodeInputSpecialCharacters' : [], # Utilities > Vector - "ShaderNodeVectorMath" : ["operation"], - "ShaderNodeVectorRotate" : ["rotation_type", "invert"], + 'ShaderNodeVectorCurve' : [("mapping", "CurveMapping")], + 'ShaderNodeVectorMath' : [("operation", "enum")], + 'ShaderNodeVectorRotate' : [("invert", "bool"), + ("rotation_type", "enum")], + 'ShaderNodeCombineXYZ' : [], + 'ShaderNodeSeparateXYZ' : [], # Utilities > Field - "GeometryNodeAccumulateField" : ["data_type", "domain"], - "GeometryNodeFieldAtIndex" : ["data_type", "domain"], - "GeometryNodeFieldOnDomain" : ["data_type", "domain" ], + 'GeometryNodeAccumulateField' : [("data_type", "enum"), + ("domain", "enum")], + 'GeometryNodeFieldAtIndex' : [("data_type", "enum"), + ("domain", "enum")], + 'GeometryNodeFieldOnDomain' : [("data_type", "enum"), + ("domain", "enum")], # Utilities > Math - "FunctionNodeBooleanMath" : ["operation"], - "ShaderNodeClamp" : ["clamp_type"], - "FunctionNodeCompare" : ["data_type", "operation", "mode"], - "FunctionNodeFloatToInt" : ["rounding_mode"], - "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], - "ShaderNodeMath" : ["operation", "use_clamp"], - - # Utilities > Rotate - "FunctionNodeAlignEulerToVector" : ["axis", "pivot_axis"], - "FunctionNodeRotateEuler" : ["type", "space"], - - # Utilities > General - "ShaderNodeMix" : ["data_type", "blend_type", "clamp_result", - "clamp_factor", "factor_mode"], - "FunctionNodeRandomValue" : ["data_type"], - "GeometryNodeSwitch" : ["input_type"] + 'FunctionNodeBooleanMath' : [("operation", "enum")], + 'ShaderNodeClamp' : [("clamp_type", "enum")], + 'FunctionNodeCompare' : [("data_type", "enum"), + ("mode", "enum"), + ("operation", "enum")], + 'ShaderNodeFloatCurve' : [("mapping", "CurveMapping")], + 'FunctionNodeFloatToInt' : [("rounding_mode", "enum")], + 'ShaderNodeMapRange' : [("clamp", "bool"), + ("data_type", "enum"), + ("interpolation_type", "enum")], + 'ShaderNodeMath' : [("operation", "enum"), + ("use_clamp", "bool")], + + # Utilities > Rotation + 'FunctionNodeAlignEulerToVector' : [("axis", "enum"), + ("pivot_axis", "enum")], + 'FunctionNodeRotateEuler' : [("space", "enum"), + ("type", "enum")] } -curve_nodes = {'ShaderNodeFloatCurve', - 'ShaderNodeVectorCurve', - 'ShaderNodeRGBCurve'} - -image_nodes = {'GeometryNodeInputImage'} - class NTPGeoNodesOperator(bpy.types.Operator): bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" diff --git a/materials.py b/materials.py index c0ee1ba..a0d37f0 100644 --- a/materials.py +++ b/materials.py @@ -4,70 +4,192 @@ from .utils import * from io import StringIO -node_settings = { - #input - "ShaderNodeAmbientOcclusion" : ["samples", "inside", "only_local"], - "ShaderNodeAttribute" : ["attribute_type", "attribute_name"], - "ShaderNodeBevel" : ["samples"], - "ShaderNodeVertexColor" : ["layer_name"], - "ShaderNodeTangent" : ["direction_type", "axis"], - "ShaderNodeTexCoord" : ["object", "from_instancer"], - "ShaderNodeUVMap" : ["from_instancer", "uv_map"], - "ShaderNodeWireframe" : ["use_pixel_size"], - - #output - "ShaderNodeOutputAOV" : ["name"], - "ShaderNodeOutputMaterial" : ["target"], - - #shader - "ShaderNodeBsdfGlass" : ["distribution"], - "ShaderNodeBsdfGlossy" : ["distribution"], - "ShaderNodeBsdfPrincipled" : ["distribution", "subsurface_method"], - "ShaderNodeBsdfRefraction" : ["distribution"], - "ShaderNodeSubsurfaceScattering" : ["falloff"], - - #texture - "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", "squash_frequency"], - "ShaderNodeTexEnvironment" : ["interpolation", "projection", "image_user.frame_duration", "image_user.frame_start", "image_user.frame_offset", "image_user.use_cyclic", "image_user.use_auto_refresh"], - "ShaderNodeTexGradient" : ["gradient_type"], - "ShaderNodeTexIES" : ["mode"], - "ShaderNodeTexImage" : ["interpolation", "projection", "projection_blend", - "extension"], - "ShaderNodeTexMagic" : ["turbulence_depth"], - "ShaderNodeTexMusgrave" : ["musgrave_dimensions", "musgrave_type"], - "ShaderNodeTexNoise" : ["noise_dimensions"], - "ShaderNodeTexPointDensity" : ["point_source", "object", "space", "radius", - "interpolation", "resolution", - "vertex_color_source"], - "ShaderNodeTexSky" : ["sky_type", "sun_direction", "turbidity", - "ground_albedo", "sun_disc", "sun_size", - "sun_intensity", "sun_elevation", - "sun_rotation", "altitude", "air_density", - "dust_density", "ozone_density"], - "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], - "ShaderNodeTexWave" : ["wave_type", "rings_direction", "wave_profile"], - "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], - - #color - "ShaderNodeMix" : ["data_type", "clamp_factor", "factor_mode", "blend_type", - "clamp_result"], - - #vector - "ShaderNodeBump" : ["invert"], - "ShaderNodeDisplacement" : ["space"], - "ShaderNodeMapping" : ["vector_type"], - "ShaderNodeNormalMap" : ["space", "uv_map"], - "ShaderNodeVectorDisplacement" : ["space"], - "ShaderNodeVectorRotate" : ["rotation_type", "invert"], - "ShaderNodeVectorTransform" : ["vector_type", "convert_from", "convert_to"], +shader_node_settings : dict[str, list[(str, str)]] = { + # INPUT + 'ShaderNodeAmbientOcclusion' : [("inside", "bool"), + ("only_local", "bool"), + ("samples", "int")], + 'ShaderNodeAttribute' : [("attribute_name", "str"), + ("attribute_type", "enum")], + 'ShaderNodeBevel' : [("samples", "int")], + 'ShaderNodeCameraData' : [], + 'ShaderNodeVertexColor' : [("layer_name", "str")], + 'ShaderNodeHairInfo' : [], + 'ShaderNodeFresnel' : [], + 'ShaderNodeNewGeometry' : [], + 'ShaderNodeLayerWeight' : [], + 'ShaderNodeLightPath' : [], + 'ShaderNodeObjectInfo' : [], + 'ShaderNodeParticleInfo' : [], + 'ShaderNodePointInfo' : [], + 'ShaderNodeRGB' : [], + 'ShaderNodeTangent' : [("axis", "enum"), + ("direction_type", "enum"), + ("uv_map", "str")], #TODO: makes sense? maybe make special type + 'ShaderNodeTexCoord' : [("from_instancer", "bool"), + ("object", "Object")], + 'ShaderNodeUVAlongStroke' : [("use_tips", "bool")], + 'ShaderNodeUVMap' : [("from_instancer", "bool"), + ("uv_map", "str")], #TODO: see ShaderNodeTangent + 'ShaderNodeValue' : [], + 'ShaderNodeVolumeInfo' : [], + 'ShaderNodeWireframe' : [("use_pixel_size", "bool")], + + + # OUTPUT + 'ShaderNodeOutputAOV' : [("name", "str")], + 'ShaderNodeOutputLight' : [("is_active_output", "bool"), + ("target", "enum")], + 'ShaderNodeOutputLineStyle' : [("blend_type", "enum"), + ("is_active_output", "bool"), + ("target", "enum"), + ("use_alpha", "bool"), + ("use_clamp", "bool")], + 'ShaderNodeOutputMaterial' : [("is_active_output", "bool"), + ("target", "enum")], + 'ShaderNodeOutputWorld' : [("is_active_output", "bool"), + ("target", "enum")], + + + # SHADER + 'ShaderNodeAddShader' : [], + 'ShaderNodeBsdfAnisotropic' : [("distribution", "enum")], + 'ShaderNodeBackground' : [], + 'ShaderNodeBsdfDiffuse' : [], + 'ShaderNodeEmission' : [], + 'ShaderNodeBsdfGlass' : [("distribution", "enum")], + 'ShaderNodeBsdfGlossy' : [("distribution", "enum")], + 'ShaderNodeBsdfHair' : [("component", "enum")], + 'ShaderNodeHoldout' : [], + 'ShaderNodeMixShader' : [], + 'ShaderNodeBsdfPrincipled' : [("distribution", "enum"), + ("subsurface_method", "enum")], + 'ShaderNodeBsdfHairPrincipled' : [("parametrization", "enum")], + 'ShaderNodeVolumePrincipled' : [], + 'ShaderNodeBsdfRefraction' : [("distribution", "enum")], + 'ShaderNodeEeveeSpecular' : [], + 'ShaderNodeSubsurfaceScattering' : [("falloff", "enum")], + 'ShaderNodeBsdfToon' : [("component", "enum")], + 'ShaderNodeBsdfTranslucent' : [], + 'ShaderNodeBsdfTransparent' : [], + 'ShaderNodeBsdfVelvet' : [], + 'ShaderNodeVolumeAbsorption' : [], + 'ShaderNodeVolumeScatter' : [], + + + # TEXTURE + 'ShaderNodeTexBrick' : [("offset", "float"), + ("offset_frequency", "int"), + ("squash", "float"), + ("squash_frequency", "int")], + 'ShaderNodeTexChecker' : [], + 'ShaderNodeTexEnvironment' : [("image", "Image"), + ("image_user", "ImageUser"), + ("interpolation", "enum"), + ("projection", "enum")], + 'ShaderNodeTexGradient' : [("gradient_type", "enum")], + 'ShaderNodeTexIES' : [("filepath", "str"), #TODO + ("ies", "Text"), + ("mode", "enum")], + 'ShaderNodeTexImage' : [("extension", "enum"), + ("image", "Image"), + ("image_user", "ImageUser"), + ("interpolation", "enum"), + ("projection", "enum"), + ("projection_blend", "float")], + 'ShaderNodeTexMagic' : [("turbulence_depth", "int")], + 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", "enum"), + ("musgrave_type", "enum")], + 'ShaderNodeTexNoise' : [("noise_dimensions", "enum")], + 'ShaderNodeTexPointDensity' : [("interpolation", "enum"), + ("object", "Object"), + ("particle_color_source", "enum"), + ("particle_system", "ParticleSystem"), + ("point_source", "enum"), + ("radius", "float"), + ("resolution", "int"), + ("space", "enum"), + ("vertex_attribute_name", "str"), #TODO + ("vertex_color_source", "enum")], + 'ShaderNodeTexSky' : [("air_density", "float"), + ("altitude", "float"), + ("dust_density", "float"), + ("ground_albedo", "float"), + ("ozone_density", "float"), + ("sky_type", "enum"), + ("sun_direction", "Vec3"), + ("sun_disc", "bool"), + ("sun_elevation", "float"), + ("sun_intensity", "float"), + ("sun_rotation", "float"), + ("sun_size", "float") + ("turbidity", "float")], + 'ShaderNodeTexVoronoi' : [("distance", "enum"), + ("feature", "enum"), + ("voronoi_dimensions", "enum")], + 'ShaderNodeTexWave' : [("bands_direction", "enum"), + ("rings_direction", "enum"), + ("wave_profile", "enum"), + ("wave_type", "enum")], + 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", "enum")], + + + # COLOR + 'ShaderNodeBrightContrast' : [], + 'ShaderNodeGamma' : [], + 'ShaderNodeHueSaturation' : [], + 'ShaderNodeInvert' : [], + 'ShaderNodeLightFalloff' : [], + 'ShaderNodeMix' : [("blend_type", "enum"), + ("clamp_factor", "bool"), + ("clamp_result", "bool"), + ("data_type", "enum"), + ("factor_mode", "enum")], + 'ShaderNodeRGBCurve' : [("mapping", "CurveMapping")], + + + # VECTOR + 'ShaderNodeBump' : [("invert", "bool")], + 'ShaderNodeDisplacement' : [("space", "enum")], + 'ShaderNodeMapping' : [("vector_type", "enum")], + 'ShaderNodeNormalMap' : [("space", "enum"), + ("uv_map", "str")], #TODO + 'ShaderNodeVectorCurve' : [("mapping", "CurveMapping")], + 'ShaderNodeVectorDisplacement' : [("space", "enum")], + 'ShaderNodeVectorRotate' : [("invert", "bool"), + ("rotation_type", "enum")], + 'ShaderNodeVectorTransform' : [("convert_from", "enum"), + ("convert_to", "enum"), + ("vector_type", "enum")], - #converter - "ShaderNodeClamp" : ["clamp_type"], - "ShaderNodeCombineColor" : ["mode"], - "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], - "ShaderNodeMath" : ["operation", "use_clamp"], - "ShaderNodeSeparateColor" : ["mode"], - "ShaderNodeVectorMath" : ["operation"] + + # CONVERTER + 'ShaderNodeBlackbody' : [], + 'ShaderNodeClamp' : [("clamp_type", "enum")], + 'ShaderNodeValToRGB' : [("color_ramp", "ColorRamp")], + 'ShaderNodeCombineColor' : [("mode", "enum")], + 'ShaderNodeCombineXYZ' : [], + 'ShaderNodeFloatCurve' : [("mapping", "CurveMapping")], + 'ShaderNodeMapRange' : [("clamp", "bool"), + ("data_type", "enum"), + ("interpolation_type", "enum")], + 'ShaderNodeMath' : [("operation", "enum"), + ("use_clamp", "bool")], + 'ShaderNodeRGBToBW' : [], + 'ShaderNodeSeparateColor' : [("mode", "enum")], + 'ShaderNodeSeparateXYZ' : [], + 'ShaderNodeShaderToRGB' : [], + 'ShaderNodeVectorMath' : [("operation", "enum")], + 'ShaderNodeWavelength' : [], + + + # SCRIPT + 'ShaderNodeScript' : [("bytecode", "str"), #TODO: test all that + ("bytecode_hash", "str"), + ("filepath", "str"), + ("mode", "enum"), + ("script", "text"), + ("use_auto_update", "bool")] } curve_nodes = {'ShaderNodeFloatCurve', diff --git a/utils.py b/utils.py index d66632b..6d09ec0 100644 --- a/utils.py +++ b/utils.py @@ -94,6 +94,13 @@ def img_to_py_str(img) -> str: format = img.file_format.lower() return f"{name}.{format}" +type_to_py_str : dict[str, function] = { + "enum" : enum_to_py_str, + "str" : str_to_py_str, + "vec3" : vec3_to_py_str, + "vec4" : vec4_to_py_str +} + def create_header(file: TextIO, name: str): """ Sets up the bl_info and imports the Blender API @@ -384,40 +391,45 @@ def curve_node_settings(node, file: TextIO, inner: str, node_var: str): node_var (str): variable name for the add-on's curve node """ + if node.bl_idname == 'CompositorNodeTime': + mapping = node.curve #TODO: ask for consistency here? + else: + mapping = node.mapping + #mapping settings file.write(f"{inner}#mapping settings\n") mapping_var = f"{inner}{node_var}.mapping" #extend - extend = enum_to_py_str(node.mapping.extend) + extend = enum_to_py_str(mapping.extend) file.write(f"{mapping_var}.extend = {extend}\n") #tone - tone = enum_to_py_str(node.mapping.tone) + tone = enum_to_py_str(mapping.tone) file.write(f"{mapping_var}.tone = {tone}\n") #black level - b_lvl_str = vec3_to_py_str(node.mapping.black_level) + b_lvl_str = vec3_to_py_str(mapping.black_level) file.write((f"{mapping_var}.black_level = {b_lvl_str}\n")) #white level - w_lvl_str = vec3_to_py_str(node.mapping.white_level) + w_lvl_str = vec3_to_py_str(mapping.white_level) file.write((f"{mapping_var}.white_level = {w_lvl_str}\n")) #minima and maxima - min_x = node.mapping.clip_min_x + min_x = mapping.clip_min_x file.write(f"{mapping_var}.clip_min_x = {min_x}\n") - min_y = node.mapping.clip_min_y + min_y = mapping.clip_min_y file.write(f"{mapping_var}.clip_min_y = {min_y}\n") - max_x = node.mapping.clip_max_x + max_x = mapping.clip_max_x file.write(f"{mapping_var}.clip_max_x = {max_x}\n") - max_y = node.mapping.clip_max_y + max_y = mapping.clip_max_y file.write(f"{mapping_var}.clip_max_y = {max_y}\n") #use_clip - use_clip = node.mapping.use_clip + use_clip = mapping.use_clip file.write(f"{mapping_var}.use_clip = {use_clip}\n") #create curves - for i, curve in enumerate(node.mapping.curves): + for i, curve in enumerate(mapping.curves): file.write(f"{inner}#curve {i}\n") curve_i = f"{node_var}_curve_{i}" file.write((f"{inner}{curve_i} = {node_var}.mapping.curves[{i}]\n")) From 7805bcf4618cbacecc3471fe864a809484603bf1 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 26 Aug 2023 16:14:54 -0500 Subject: [PATCH 07/72] refactor: switch dictionary format to use type enums instead of strings --- compositor.py | 664 ++++++++++++++++++++++++++++---------------------- geo_nodes.py | 450 +++++++++++++++++++++++----------- materials.py | 339 ++++++++++++++++---------- utils.py | 93 +++++-- 4 files changed, 966 insertions(+), 580 deletions(-) diff --git a/compositor.py b/compositor.py index 7df8437..9384b1b 100644 --- a/compositor.py +++ b/compositor.py @@ -12,334 +12,415 @@ #TODO: do something similar for geo nodes and materials, should be useful for # possible conflicts between ntp_vars and node vars -compositor_node_settings : dict[str, list[(str, str)]] = { +compositor_node_settings : dict[str, list[(str, ST)]] = { # INPUT - 'CompositorNodeBokehImage' : [("angle", "float"), - ("catadioptric", "float"), - ("flaps", "int"), - ("rounding", "float"), - ("shift", "float")], - 'CompositorNodeImage' : [("frame_duration", "int"), - ("frame_offset", "int"), - ("frame_start", "int"), - ("image", "Image"), #TODO: handle image selection - ("layer", "enum"), - ("use_auto_refresh", "bool"), - ("use_cyclic", "bool"), - ("use_straight_alpha_output", "bool"), - ("view", "enum")], - 'CompositorNodeMask' : [("mask", "Mask"), #TODO - ("motion_blur_samples", "int"), - ("motion_blur_shutter", "float"), - ("size_source", "enum"), - ("size_x", "int"), - ("size_y", "int"), - ("use_feather", "bool"), - ("use_motion_blur", "bool")], - 'CompositorNodeMovieClip' : [("clip", "MovieClip")], #TODO: handle movie clip selection - 'CompositorNodeRLayers' : [("layer", "enum"), - ("scene", "Scene")], #TODO + 'CompositorNodeBokehImage' : [("angle", ST.FLOAT), + ("catadioptric", ST.FLOAT), + ("flaps", ST.INT), + ("rounding", ST.FLOAT), + ("shift", ST.FLOAT)], + + 'CompositorNodeImage' : [("frame_duration", ST.INT), + ("frame_offset", ST.INT), + ("frame_start", ST.INT), + ("image", ST.IMAGE), + ("layer", ST.ENUM), + ("use_auto_refresh", ST.BOOL), + ("use_cyclic", ST.BOOL), + ("use_straight_alpha_output", ST.BOOL), + ("view", ST.ENUM)], + + 'CompositorNodeMask' : [("mask", ST.MASK), + ("motion_blur_samples", ST.INT), + ("motion_blur_shutter", ST.FLOAT), + ("size_source", ST.ENUM), + ("size_x", ST.INT), + ("size_y", ST.INT), + ("use_feather", ST.BOOL), + ("use_motion_blur", ST.BOOL)], + + 'CompositorNodeMovieClip' : [("clip", ST.MOVIE_CLIP)], + + 'CompositorNodeRLayers' : [("layer", ST.ENUM), + ("scene", ST.SCENE)], + 'CompositorNodeRGB' : [], + 'CompositorNodeSceneTime' : [], - 'CompositorNodeTexture' : [("node_output", "int"), #TODO: ?? - ("texture", "Texture")], #TODO: handle texture selection - 'CompositorNodeTime' : [("curve", "CurveMapping"), - ("frame_end", "int"), - ("frame_start", "int")], - 'CompositorNodeTrackPos' : [("clip", "MovieClip"), #TODO: this is probably wrong - ("frame_relative", "int") - ("position", "enum"), - ("track_name", "str"), - ("tracking_object", "str")], - 'CompositorNodeValue' : [], #should be handled by outputs (why is this a separate class??) + + 'CompositorNodeTexture' : [("node_output", ST.INT), #TODO: ?? + ("texture", ST.TEXTURE)], + + 'CompositorNodeTime' : [("curve", ST.CURVE_MAPPING), + ("frame_end", ST.INT), + ("frame_start", ST.INT)], + + 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), + ("frame_relative", ST.INT) + ("position", ST.ENUM), + ("track_name", ST.STRING), #TODO: probably not right + ("tracking_object", ST.STRING)], + + 'CompositorNodeValue' : [], #TODO: double check that outputs set here # OUTPUT - 'CompositorNodeComposite' : [("use_alpha", "bool")], - 'CompositorNodeOutputFile' : [("active_input_index", "int"), #TODO: probably not right at all - ("base_path", "str"), - ("file_slots", "CompositorNodeOutputFileFileSlots"), - ("format", "ImageFormatSettings"), - ("layer_slots", "CompositorNodeOutputFileLayerSlots")], - 'CompositorNodeLevels' : [("channel", "enum")], - 'CompositorNodeSplitViewer' : [("axis", "enum"), - ("factor", "int")], - 'CompositorNodeViewer' : [("center_x", "float"), - ("center_y", "float"), - ("tile_order", "enum"), - ("use_alpha", "bool")], + 'CompositorNodeComposite' : [("use_alpha", ST.BOOL)], + + 'CompositorNodeOutputFile' : [("active_input_index", ST.INT), #TODO: probably not right at all + + ("base_path", ST.STRING), + ("file_slots", ST.FILE_SLOTS), + ("format", ST.IMAGE_FORMAT_SETTINGS), + ("layer_slots", ST.LAYER_SLOTS)], + + 'CompositorNodeLevels' : [("channel", ST.ENUM)], + + 'CompositorNodeSplitViewer' : [("axis", ST.ENUM), + ("factor", ST.INT)], + + 'CompositorNodeViewer' : [("center_x", ST.FLOAT), + ("center_y", ST.FLOAT), + ("tile_order", ST.ENUM), + ("use_alpha", ST.BOOL)], # COLOR - 'CompositorNodeAlphaOver' : [("premul", "float"), - ("use_premultiply", "bool")], - 'CompositorNodeBrightContrast' : [("use_premultiply", "bool")], - 'CompositorNodeColorBalance' : [("correction_method", "enum"), - ("gain", "Vec3"), - ("gamma", "Vec3"), - ("lift", "Vec3"), - ("offset", "Vec3"), - ("offset_basis", "float"), - ("power", "Vec3"), - ("slope", "Vec3")], - 'CompositorNodeColorCorrection' : [("blue", "bool"), - ("green", "bool"), - ("highlights_contrast", "float"), - ("highlights_gain", "float"), - CurveMapp ("midtones_lift", "float"), - ("midtones_saturation", "float"), - ("midtones_start", "float"), - ("red", "bool"), - ("shadows_contrast", "float"), - ("shadows_gain", "float"), - ("shadows_gamma", "float"), - ("shadows_lift", "float"), - ("shadows_saturation", "float")], + 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), + ("use_premultiply", ST.BOOL)], + + 'CompositorNodeBrightContrast' : [("use_premultiply", ST.BOOL)], + + 'CompositorNodeColorBalance' : [("correction_method", ST.ENUM), + ("gain", ST.VEC3), + ("gamma", ST.VEC3), + ("lift", ST.VEC3), + ("offset", ST.VEC3), + ("offset_basis", ST.FLOAT), + ("power", ST.VEC3), + ("slope", ST.VEC3)], + + 'CompositorNodeColorCorrection' : [("blue", ST.BOOL), + ("green", ST.BOOL), + ("highlights_contrast", ST.FLOAT), + ("highlights_gain", ST.FLOAT), + ("midtones_lift", ST.FLOAT), + ("midtones_saturation", ST.FLOAT), + ("midtones_start", ST.FLOAT), + ("red", ST.BOOL), + ("shadows_contrast", ST.FLOAT), + ("shadows_gain", ST.FLOAT), + ("shadows_gamma", ST.FLOAT), + ("shadows_lift", ST.FLOAT), + ("shadows_saturation", ST.FLOAT)], + 'CompositorNodeExposure' : [], + 'CompositorNodeGamma' : [], - 'CompositorNodeHueCorrect' : [("mapping", "CurveMapping")], + + 'CompositorNodeHueCorrect' : [("mapping", ST.CURVE_MAPPING)], + 'CompositorNodeHueSat' : [], - 'CompositorNodeInvert' : [("invert_alpha", "bool"), - ("invert_rgb", "bool")], - 'CompositorNodeMixRGB' : [("blend_type", "enum"), - ("use_alpha", "bool"), - ("use_clamp", "bool")], #TODO: has an update() method, may need to figure out why... + + 'CompositorNodeInvert' : [("invert_alpha", ST.BOOL), + ("invert_rgb", ST.BOOL)], + + 'CompositorNodeMixRGB' : [("blend_type", ST.ENUM), + ("use_alpha", ST.BOOL), + ("use_clamp", ST.BOOL)], #TODO: what is update() method for? + 'CompositorNodePosterize' : [], - 'CompositorNodeCurveRGB' : [("mapping", "CurveMapping")], - 'CompositorNodeTonemap' : [("adaptation", "float"), - ("contrast", "float"), - ("correction", "float"), - ("gamma", "float"), - ("intensity", "float"), - ("key", "float"), - ("offset", "float"), - ("tonemap_type", "enum")], - 'CompositorNodeZcombine' : [("use_alpha", "bool"), - ("use_antialias_z", "bool")], + + 'CompositorNodeCurveRGB' : [("mapping", ST.CURVE_MAPPING)], + + 'CompositorNodeTonemap' : [("adaptation", ST.FLOAT), + ("contrast", ST.FLOAT), + ("correction", ST.FLOAT), + ("gamma", ST.FLOAT), + ("intensity", ST.FLOAT), + ("key", ST.FLOAT), + ("offset", ST.FLOAT), + ("tonemap_type", ST.ENUM)], + + 'CompositorNodeZcombine' : [("use_alpha", ST.BOOL), + ("use_antialias_z", ST.BOOL)], # CONVERTER - 'CompositorNodePremulKey' : [("mapping", "enum")], - 'CompositorNodeValToRGB' : [("color_ramp", "ColorRamp")], #TODO: check to see if this'll work out of the box - 'CompositorNodeConvertColorSpace' : [("from_color_space", "enum"), - ("to_color_space", "enum")], - 'CompositorNodeCombineColor' : [("mode", "enum"), - ("ycc_mode", "enum")], #why isn't this standardized across blender? + 'CompositorNodePremulKey' : [("mapping", ST.ENUM)], + + 'CompositorNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + + 'CompositorNodeConvertColorSpace' : [("from_color_space", ST.ENUM), + ("to_color_space", ST.ENUM)], + + 'CompositorNodeCombineColor' : [("mode", ST.ENUM), + ("ycc_mode", ST.ENUM)], + 'CompositorNodeCombineXYZ' : [], - 'CompositorNodeIDMask' : [("index", "int"), - ("use_antialiasing", "bool")], - 'CompositorNodeMath' : [("operation", "enum"), - ("use_clamp", "bool")], + + 'CompositorNodeIDMask' : [("index", ST.INT), + ("use_antialiasing", ST.BOOL)], + + 'CompositorNodeMath' : [("operation", ST.ENUM), + ("use_clamp", ST.BOOL)], + 'CompositorNodeRGBToBW' : [], - 'CompositorNodeSeparateColor' : [("mode", "enum"), - ("ycc_mode", "enum")], + + 'CompositorNodeSeparateColor' : [("mode", ST.ENUM), + ("ycc_mode", ST.ENUM)], + 'CompositorNodeSeparateXYZ' : [], - 'CompositorNodeSetAlpha' : [("mode", "enum")], + + 'CompositorNodeSetAlpha' : [("mode", ST.ENUM)], + 'CompositorNodeSwitchView' : [], # FILTER - 'CompositorNodeAntiAliasing' : [("contrast_limit", "float"), - ("corner_rounding", "float"), - ("threshold", "float")], - 'CompositorNodeBilateralblur' : [("iterations", "int"), - ("sigma_color", "float"), - ("sigma_space", "float")], - 'CompositorNodeBlur' : [("aspect_correction", "enum"), - ("factor", "float"), - ("factor_x", "float"), - ("factor_y", "float"), - ("filter_type", "enum"), - ("size_x", "int"), - ("size_y", "int"), - ("use_bokeh", "bool"), - ("use_extended_bounds", "bool"), - ("use_gamma_correction", "bool"), - ("use_relative", "bool"), - ("use_variable_size", "bool")], - 'CompositorNodeBokehBlur' : [("blur_max", "float"), - ("use_extended_bounds", "bool"), - ("use_variable_size", "bool")], - 'CompositorNodeDefocus' : [("angle", "float"), - ("blur_max", "float"), - ("bokeh", "enum"), - ("f_stop", "float"), - ("scene", "Scene"), #TODO - ("threshold", "float"), - ("use_gamma_correction", "bool"), - ("use_preview", "bool"), - ("use_zbuffer", "bool"), - ("z_scale", "float")], - 'CompositorNodeDespeckle' : [("threshold", "float"), - ("threshold_neighbor", "float")], - 'CompositorNodeDilateErode' : [("distance", "int"), - ("edge", "float"), - ("falloff", "enum"), - ("mode", "enum")], - 'CompositorNodeDBlur' : [("angle", "float"), - ("center_x", "float"), - ("center_y", "float"), - ("distance", "float"), - ("iterations", "int"), - ("spin", "float"), - ("zoom", "float")], - 'CompositorNodeFilter' : [("filter_type", "enum")], - 'CompositorNodeGlare' : [("angle_offset", "float"), - ("color_modulation", "float"), - ("fade", "float"), - ("glare_type", "enum"), - ("iterations", "int"), - ("mix", "float"), - ("quality", "enum"), - ("size", "int"), - ("streaks", "int"), - ("threshold", "float"), - ("use_rotate_45", "bool")], - 'CompositorNodeInpaint' : [("distance", "int")], + 'CompositorNodeAntiAliasing' : [("contrast_limit", ST.FLOAT), + ("corner_rounding", ST.FLOAT), + ("threshold", ST.FLOAT)], + + 'CompositorNodeBilateralblur' : [("iterations", ST.INT), + ("sigma_color", ST.FLOAT), + ("sigma_space", ST.FLOAT)], + + 'CompositorNodeBlur' : [("aspect_correction", ST.ENUM), + ("factor", ST.FLOAT), + ("factor_x", ST.FLOAT), + ("factor_y", ST.FLOAT), + ("filter_type", ST.ENUM), + ("size_x", ST.INT), + ("size_y", ST.INT), + ("use_bokeh", ST.BOOL), + ("use_extended_bounds", ST.BOOL), + ("use_gamma_correction", ST.BOOL), + ("use_relative", ST.BOOL), + ("use_variable_size", ST.BOOL)], + + 'CompositorNodeBokehBlur' : [("blur_max", ST.FLOAT), + ("use_extended_bounds", ST.BOOL), + ("use_variable_size", ST.BOOL)], + + 'CompositorNodeDefocus' : [("angle", ST.FLOAT), + ("blur_max", ST.FLOAT), + ("bokeh", ST.ENUM), + ("f_stop", ST.FLOAT), + ("scene", ST.SCENE), + ("threshold", ST.FLOAT), + ("use_gamma_correction", ST.BOOL), + ("use_preview", ST.BOOL), + ("use_zbuffer", ST.BOOL), + ("z_scale", ST.FLOAT)], + + 'CompositorNodeDespeckle' : [("threshold", ST.FLOAT), + ("threshold_neighbor", ST.FLOAT)], + + 'CompositorNodeDilateErode' : [("distance", ST.INT), + ("edge", ST.FLOAT), + ("falloff", ST.ENUM), + ("mode", ST.ENUM)], + + 'CompositorNodeDBlur' : [("angle", ST.FLOAT), + ("center_x", ST.FLOAT), + ("center_y", ST.FLOAT), + ("distance", ST.FLOAT), + ("iterations", ST.INT), + ("spin", ST.FLOAT), + ("zoom", ST.FLOAT)], + + 'CompositorNodeFilter' : [("filter_type", ST.ENUM)], + + 'CompositorNodeGlare' : [("angle_offset", ST.FLOAT), + ("color_modulation", ST.FLOAT), + ("fade", ST.FLOAT), + ("glare_type", ST.ENUM), + ("iterations", ST.INT), + ("mix", ST.FLOAT), + ("quality", ST.ENUM), + ("size", ST.INT), + ("streaks", ST.INT), + ("threshold", ST.FLOAT), + ("use_rotate_45", ST.BOOL)], + + 'CompositorNodeInpaint' : [("distance", ST.INT)], + 'CompositorNodePixelate' : [], - 'CompositorNodeSunBeams' : [("ray_length", "float"), - ("source", "Vec2")], - 'CompositorNodeVecBlur' : [("factor", "float"), - ("samples", "int"), - ("speed_max", "int"), - ("speed_min", "int"), - ("use_curved", "bool")], + + 'CompositorNodeSunBeams' : [("ray_length", ST.FLOAT), + ("source", ST.VEC2)], + + 'CompositorNodeVecBlur' : [("factor", ST.FLOAT), + ("samples", ST.INT), + ("speed_max", ST.INT), + ("speed_min", ST.INT), + ("use_curved", ST.BOOL)], # VECTOR - 'CompositorNodeMapRange' : [("use_clamp", "bool")], - 'CompositorNodeMapValue' : [("max", "Vec1"), - ("min", "Vec1"), - ("offset", "Vec1"), - ("size", "Vec1"), - ("use_max", "bool"), - ("use_min", "bool")], #why are all these vectors?? TODO: check to make sure it doesn't flip + 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], + + 'CompositorNodeMapValue' : [("max", ST.VEC1), + ("min", ST.VEC1), + ("offset", ST.VEC1), + ("size", ST.VEC1), + ("use_max", ST.BOOL), + ("use_min", ST.BOOL)], #why are all these vectors?? TODO: check to make sure it doesn't flip + 'CompositorNodeNormal' : [], + 'CompositorNodeNormalize' : [], - 'CompositorNodeCurveVec' : [("mapping", "CurveMapping")], + + 'CompositorNodeCurveVec' : [("mapping", ST.CURVE_MAPPING)], # MATTE - 'CompositorNodeBoxMask' : [("height", "float"), - ("mask_type", "enum"), - ("rotation", "float"), - ("width", "float"), - ("x", "float"), - ("y", "float")], - 'CompositorNodeChannelMatte' : [("color_space", "enum"), - ("limit_channel", "enum"), - ("limit_max", "float"), - ("limit_method", "enum"), - ("limit_min", "float"), - ("matte_channel", "enum")], - 'CompositorNodeChromaMatte' : [("gain", "float"), - ("lift", "float"), - ("shadow_adjust", "float"), - ("threshold", "float"), - ("tolerance", "float")], - 'CompositorNodeColorMatte' : [("color_hue", "float"), - ("color_saturation", "float"), - ("color_value", "float")], - 'CompositorNodeColorSpill' : [("channel", "enum"), - ("limit_channel", "enum"), - ("limit_method", "enum"), - ("ratio", "float"), - ("unspill_blue", "float"), - ("unspill_green", "float"), - ("unspill_red", "float"), - ("use_unspill", "bool")], - 'CompositorNodeCryptomatteV2' : [("add", "Vec3"), #TODO: will need a lot of special handling - ("entries", "CryptomatteEntry"), #TODO: (readonly?) - ("frame_duration", "int"), - ("frame_offset", "int"), - ("frame_start", "int"), - ("has_layers", "bool"), #TODO: readonly? - ("has_views", "bool"), #TODO: readonly? - ("image", "Image"), - ("layer", "enum"), - ("layer_name", "enum"), - ("matte_id", "str"), - ("remove", "Vec3"), - ("scene", "Scene"), - ("source", "enum"), - ("use_auto_refresh", "bool"), - ("use_cyclic", "bool"), - ("view", "enum")], - 'CompositorNodeCryptomatte' : [("add", "Vec3"), #TODO: will need a lot of special handling - ("matte_id", "str"), - ("remove", "Vec3")], - 'CompositorNodeDiffMatte' : [("falloff", "float"), - ("tolerance", "float")], - 'CompositorNodeDistanceMatte' : [("channel", "enum"), - ("falloff", "float"), - ("tolerance", "float")], - 'CompositorNodeDoubleEdgeMask' : [("edge_mode", "enum"), - ("inner_mode", "enum")], - 'CompositorNodeEllipseMask' : [("height", "float"), - ("mask_type", "enum"), - ("rotation", "float"), - ("width", "float"), - ("x", "float"), - ("y", "float")], - 'CompositorNodeKeying' : [("blur_post", "int"), - ("blur_pre", "int"), - ("clip_black", "float"), - ("clip_white", "float"), - ("despill_balance", "float"), - ("despill_factor", "float"), - ("dilate_distance", "int"), - ("edge_kernel_radius", "int"), - ("edge_kernel_tolerance", "float"), - ("feather_distance", "int"), - ("feather_falloff", "enum"), - ("screen_balance", "float")], - 'CompositorNodeKeyingScreen' : [("clip", "MovieClip"), - ("tracing_object", "str")], #TODO: movie stuff - 'CompositorNodeLumaMatte' : [("limit_max", "float"), - ("limit_min", "float")], + 'CompositorNodeBoxMask' : [("height", ST.FLOAT), + ("mask_type", ST.ENUM), + ("rotation", ST.FLOAT), + ("width", ST.FLOAT), + ("x", ST.FLOAT), + ("y", ST.FLOAT)], + + 'CompositorNodeChannelMatte' : [("color_space", ST.ENUM), + ("limit_channel", ST.ENUM), + ("limit_max", ST.FLOAT), + ("limit_method", ST.ENUM), + ("limit_min", ST.FLOAT), + ("matte_channel", ST.ENUM)], + + 'CompositorNodeChromaMatte' : [("gain", ST.FLOAT), + ("lift", ST.FLOAT), + ("shadow_adjust", ST.FLOAT), + ("threshold", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeColorMatte' : [("color_hue", ST.FLOAT), + ("color_saturation", ST.FLOAT), + ("color_value", ST.FLOAT)], + + 'CompositorNodeColorSpill' : [("channel", ST.ENUM), + ("limit_channel", ST.ENUM), + ("limit_method", ST.ENUM), + ("ratio", ST.FLOAT), + ("unspill_blue", ST.FLOAT), + ("unspill_green", ST.FLOAT), + ("unspill_red", ST.FLOAT), + ("use_unspill", ST.BOOL)], + + 'CompositorNodeCryptomatteV2' : [("add", ST.VEC3), + ("entries", ST.CRYPTOMATTE_ENTRIES), + ("frame_duration", ST.INT), + ("frame_offset", ST.INT), + ("frame_start", ST.INT), + ("has_layers", ST.BOOL), #TODO: readonly? + ("has_views", ST.BOOL), #TODO: readonly? + ("image", ST.IMAGE), + ("layer", ST.ENUM), + ("layer_name", ST.ENUM), + ("matte_id", ST.STRING), + ("remove", ST.VEC3), + ("scene", ST.SCENE), + ("source", ST.ENUM), + ("use_auto_refresh", ST.BOOL), + ("use_cyclic", ST.BOOL), + ("view", ST.ENUM)], + + 'CompositorNodeCryptomatte' : [("add", ST.VEC3), #TODO: may need special handling + ("matte_id", ST.STRING), + ("remove", ST.VEC3)], + + 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeDistanceMatte' : [("channel", ST.ENUM), + ("falloff", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeDoubleEdgeMask' : [("edge_mode", ST.ENUM), + ("inner_mode", ST.ENUM)], + + 'CompositorNodeEllipseMask' : [("height", ST.FLOAT), + ("mask_type", ST.ENUM), + ("rotation", ST.FLOAT), + ("width", ST.FLOAT), + ("x", ST.FLOAT), + ("y", ST.FLOAT)], + + 'CompositorNodeKeying' : [("blur_post", ST.INT), + ("blur_pre", ST.INT), + ("clip_black", ST.FLOAT), + ("clip_white", ST.FLOAT), + ("despill_balance", ST.FLOAT), + ("despill_factor", ST.FLOAT), + ("dilate_distance", ST.INT), + ("edge_kernel_radius", ST.INT), + ("edge_kernel_tolerance", ST.FLOAT), + ("feather_distance", ST.INT), + ("feather_falloff", ST.ENUM), + ("screen_balance", ST.FLOAT)], + + 'CompositorNodeKeyingScreen' : [("clip", ST.MOVIE_CLIP), + ("tracing_object", ST.STRING)], + + 'CompositorNodeLumaMatte' : [("limit_max", ST.FLOAT), + ("limit_min", ST.FLOAT)], # DISTORT 'CompositorNodeCornerPin' : [], - 'CompositorNodeCrop' : [("max_x", "int"), - ("max_y", "int"), - ("min_x", "int"), - ("min_y", "int"), - ("rel_max_x", "float"), - ("rel_max_y", "float"), - ("rel_min_x", "float"), - ("rel_min_y", "float"), - ("relative", "bool"), - ("use_crop_size", "bool")], + + 'CompositorNodeCrop' : [("max_x", ST.INT), + ("max_y", ST.INT), + ("min_x", ST.INT), + ("min_y", ST.INT), + ("rel_max_x", ST.FLOAT), + ("rel_max_y", ST.FLOAT), + ("rel_min_x", ST.FLOAT), + ("rel_min_y", ST.FLOAT), + ("relative", ST.BOOL), + ("use_crop_size", ST.BOOL)], + 'CompositorNodeDisplace' : [], - 'CompositorNodeFlip' : [("axis", "enum")], - 'CompositorNodeLensdist' : [("use_fit", "bool"), - ("use_jitter", "bool"), - ("use_projector", "bool")], - 'CompositorNodeMapUV' : [("alpha", "int")], - 'CompositorNodeMovieDistortion' : [("clip", "MovieClip"), - ("distortion_type", "enum")], #TODO: movie stuff - 'CompositorNodePlaneTrackDeform' : [("clip", "MovieClip"), - ("motion_blur_samples", "int"), - ("motion_blur_shutter", "float"), - ("plane_track_name", "str"), - ("tracking_object", "str"), - ("use_motion_blur", "bool")], #TODO: movie stuff - 'CompositorNodeRotate' : [("filter_type", "enum")], - 'CompositorNodeScale' : [("frame_method", "enum"), - ("offset_x", "float"), - ("offset_y", "float"), - ("space", "enum")], - 'CompositorNodeStablize' : [("clip", "MovieClip"), - ("filter_type", "enum"), - ("invert", "bool")], #TODO: movie stuff - 'CompositorNodeTransform' : [("filter_type", "enum")], - 'CompositorNodeTranslate' : [("use_relative", "bool"), - ("wrap_axis", "enum")], + + 'CompositorNodeFlip' : [("axis", ST.ENUM)], + + 'CompositorNodeLensdist' : [("use_fit", ST.BOOL), + ("use_jitter", ST.BOOL), + ("use_projector", ST.BOOL)], + + 'CompositorNodeMapUV' : [("alpha", ST.INT)], + + 'CompositorNodeMovieDistortion' : [("clip", ST.MOVIE_CLIP), + ("distortion_type", ST.ENUM)], + + 'CompositorNodePlaneTrackDeform' : [("clip", ST.MOVIE_CLIP), + ("motion_blur_samples", ST.INT), + ("motion_blur_shutter", ST.FLOAT), + ("plane_track_name", ST.STRING), + ("tracking_object", ST.STRING), + ("use_motion_blur", ST.BOOL)], + + 'CompositorNodeRotate' : [("filter_type", ST.ENUM)], + + 'CompositorNodeScale' : [("frame_method", ST.ENUM), + ("offset_x", ST.FLOAT), + ("offset_y", ST.FLOAT), + ("space", ST.ENUM)], + + 'CompositorNodeStablize' : [("clip", ST.MOVIE_CLIP), + ("filter_type", ST.ENUM), + ("invert", ST.BOOL)], + + 'CompositorNodeTransform' : [("filter_type", ST.ENUM)], + + 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), + ("wrap_axis", ST.ENUM)], # LAYOUT - 'CompositorNodeSwitch' : ["check"] + 'CompositorNodeSwitch' : [("check", ST.BOOL)] } class NTPCompositorOperator(bpy.types.Operator): @@ -434,8 +515,9 @@ def is_outermost_node_group(level: int) -> bool: elif self.mode == 'SCRIPT' and level == 0: return True return False - + """ def process_comp_node_group(node_tree, level, node_vars, used_vars): + if is_outermost_node_group(level): nt_var = create_var(self.compositor_name, used_vars) nt_name = self.compositor_name @@ -481,7 +563,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): node_var = create_node(node, file, inner, nt_var, node_vars, used_vars) - set_settings_defaults(node, node_settings, file, inner, node_var) + set_settings_defaults(node, compositor_node_settings, file, inner, node_var) hide_sockets(node, file, inner, node_var) if node.bl_idname == 'CompositorNodeGroup': @@ -544,7 +626,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): if self.mode == 'ADDON': zip_addon(zip_dir) - + """ if self.mode == 'SCRIPT': location = "clipboard" else: diff --git a/geo_nodes.py b/geo_nodes.py index b83d058..ee8c03a 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -4,339 +4,501 @@ from .utils import * from io import StringIO -geo_node_settings : dict[str, list[(str, str)]] = { +geo_node_settings : dict[str, list[(str, ST)]] = { # ATTRIBUTE - 'GeometryNodeAttributeStatistic' : [("data_type", "enum"), - ("domain", "enum")], - 'GeometryNodeAttributeDomainSize' : [("component", "enum")], - 'GeometryNodeBlurAttribute' : [("data_type", "enum")], - 'GeometryNodeCaptureAttribute' : [("data_type", "enum"), - ("domain", "enum")], + 'GeometryNodeAttributeStatistic' : [("data_type", ST.ENUM), + ("domain", ST.ENUM)], + + 'GeometryNodeAttributeDomainSize' : [("component", ST.ENUM)], + + 'GeometryNodeBlurAttribute' : [("data_type", ST.ENUM)], + + 'GeometryNodeCaptureAttribute' : [("data_type", ST.ENUM), + ("domain", ST.ENUM)], + 'GeometryNodeRemoveAttribute' : [], - 'GeometryNodeStoreNamedAttribute' : [("data_type", "enum"), - ("domain", "enum")], - 'GeometryNodeAttributeTransfer' : [("data_type", "enum"), - ("domain", "enum"), - ("mapping", "enum")], + + 'GeometryNodeStoreNamedAttribute' : [("data_type", ST.ENUM), + ("domain", ST.ENUM)], + + 'GeometryNodeAttributeTransfer' : [("data_type", ST.ENUM), + ("domain", ST.ENUM), + ("mapping", ST.ENUM)], # INPUT # Input > Constant - 'FunctionNodeInputBool' : [("boolean", "bool")], - 'FunctionNodeInputColor' : [("color", "Vec4")], - 'GeometryNodeInputImage' : [("image", "Image")], - 'FunctionNodeInputInt' : [("integer", "int")], - 'GeometryNodeInputMaterial' : [("material", "Material")], - 'FunctionNodeInputString' : [("string", "str")], + 'FunctionNodeInputBool' : [("boolean", ST.BOOL)], + + 'FunctionNodeInputColor' : [("color", ST.VEC4)], + + 'GeometryNodeInputImage' : [("image", ST.IMAGE)], + + 'FunctionNodeInputInt' : [("integer", ST.INT)], + + 'GeometryNodeInputMaterial' : [("material", ST.MATERIAL)], + + 'FunctionNodeInputString' : [("string", ST.STRING)], + 'ShaderNodeValue' : [], - 'FunctionNodeInputVector' : [("vector", "Vec3")], + + 'FunctionNodeInputVector' : [("vector", ST.VEC3)], #Input > Group 'NodeGroupInput' : [], # Input > Scene - 'GeometryNodeCollectionInfo' : [("transform_space", "enum")], + 'GeometryNodeCollectionInfo' : [("transform_space", ST.ENUM)], + 'GeometryNodeImageInfo' : [], + 'GeometryNodeIsViewport' : [], - 'GeometryNodeObjectInfo' : [("transform_space", "enum")], + + 'GeometryNodeObjectInfo' : [("transform_space", ST.ENUM)], + 'GeometryNodeSelfObject' : [], + 'GeometryNodeInputSceneTime' : [], # OUTPUT - 'GeometryNodeViewer' : [("data_type", "enum"), - ("domain", "enum")], + 'GeometryNodeViewer' : [("data_type", ST.ENUM), + + ("domain", ST.ENUM)], # GEOMETRY 'GeometryNodeJoinGeometry' : [], + 'GeometryNodeGeometryToInstance' : [], # Geometry > Read 'GeometryNodeInputID' : [], + 'GeometryNodeInputIndex' : [], - 'GeometryNodeInputNamedAttribute' : [("data_type", "enum")], + + 'GeometryNodeInputNamedAttribute' : [("data_type", ST.ENUM)], + 'GeometryNodeInputNormal' : [], + 'GeometryNodeInputPosition' : [], + 'GeometryNodeInputRadius' : [], # Geometry > Sample - 'GeometryNodeProximity' : [("target_element", "enum")], + 'GeometryNodeProximity' : [("target_element", ST.ENUM)], + 'GeometryNodeIndexOfNearest' : [], - 'GeometryNodeRaycast' : [("data_type", "enum"), - ("mapping", "enum")], - 'GeometryNodeSampleIndex' : [("clamp", "bool"), - ("data_type", "enum"), - ("domain", "enum")], - 'GeometryNodeSampleNearest' : [("domain", "enum")], + + 'GeometryNodeRaycast' : [("data_type", ST.ENUM), + ("mapping", ST.ENUM)], + + 'GeometryNodeSampleIndex' : [("clamp", ST.BOOL), + ("data_type", ST.ENUM), + ("domain", ST.ENUM)], + + 'GeometryNodeSampleNearest' : [("domain", ST.ENUM)], # Geometry > Write 'GeometryNodeSetID' : [], + 'GeometryNodeSetPosition' : [], # Geometry > Operations 'GeometryNodeBoundBox' : [], + 'GeometryNodeConvexHull' : [], - 'GeometryNodeDeleteGeometry' : [("domain", "enum"), - ("mode", "enum")], - 'GeometryNodeDuplicateElements' : [("domain", "enum")], - 'GeometryNodeMergeByDistance' : [("mode", "enum")], + + 'GeometryNodeDeleteGeometry' : [("domain", ST.ENUM), + ("mode", ST.ENUM)], + + 'GeometryNodeDuplicateElements' : [("domain", ST.ENUM)], + + 'GeometryNodeMergeByDistance' : [("mode", ST.ENUM)], + 'GeometryNodeTransform' : [], + 'GeometryNodeSeparateComponents' : [], - 'GeometryNodeSeparateGeometry' : [("domain", "enum")], + + 'GeometryNodeSeparateGeometry' : [("domain", ST.ENUM)], # CURVE # Curve > Read 'GeometryNodeInputCurveHandlePositions' : [], + 'GeometryNodeCurveLength' : [], + 'GeometryNodeInputTangent' : [], + 'GeometryNodeInputCurveTilt' : [], + 'GeometryNodeCurveEndpointSelection' : [], - 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", "enum"), - ("mode", "enum")], + + 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", ST.ENUM), + ("mode", ST.ENUM)], + 'GeometryNodeInputSplineCyclic' : [], + 'GeometryNodeSplineLength' : [], + 'GeometryNodeSplineParameter' : [], + 'GeometryNodeInputSplineResolution' : [], # Curve > Sample - 'GeometryNodeSampleCurve' : [("data_type", "enum"), - ("mode", "enum"), - ("use_all_curves", "bool")], + 'GeometryNodeSampleCurve' : [("data_type", ST.ENUM), + ("mode", ST.ENUM), + ("use_all_curves", ST.BOOL)], # Curve > Write - 'GeometryNodeSetCurveNormal' : [("mode", "enum")], + 'GeometryNodeSetCurveNormal' : [("mode", ST.ENUM)], + 'GeometryNodeSetCurveRadius' : [], + 'GeometryNodeSetCurveTilt' : [], - 'GeometryNodeSetCurveHandlePositions' : [("mode", "enum")], - 'GeometryNodeCurveSetHandles' : [("handle_type", "enum"), - ("mode", "enum")], + + 'GeometryNodeSetCurveHandlePositions' : [("mode", ST.ENUM)], + + 'GeometryNodeCurveSetHandles' : [("handle_type", ST.ENUM), + ("mode", ST.ENUM)], + 'GeometryNodeSetSplineCyclic' : [], + 'GeometryNodeSetSplineResolution' : [], - 'GeometryNodeCurveSplineType' : [("spline_type", "enum")], + + 'GeometryNodeCurveSplineType' : [("spline_type", ST.ENUM)], # Curve > Operations 'GeometryNodeCurveToMesh' : [], - 'GeometryNodeCurveToPoints' : [("mode", "enum")], + + 'GeometryNodeCurveToPoints' : [("mode", ST.ENUM)], + 'GeometryNodeDeformCurvesOnSurface' : [], - 'GeometryNodeFillCurve' : [("mode", "enum")], - 'GeometryNodeFilletCurve' : [("mode", "enum")], + + 'GeometryNodeFillCurve' : [("mode", ST.ENUM)], + + 'GeometryNodeFilletCurve' : [("mode", ST.ENUM)], + 'GeometryNodeInterpolateCurves' : [], - 'GeometryNodeResampleCurve' : [("mode", "enum")], + + 'GeometryNodeResampleCurve' : [("mode", ST.ENUM)], + 'GeometryNodeReverseCurve' : [], + 'GeometryNodeSubdivideCurve' : [], - 'GeometryNodeTrimCurve' : [("mode", "enum")], + + 'GeometryNodeTrimCurve' : [("mode", ST.ENUM)], # Curve > Primitives - 'GeometryNodeCurveArc' : [("mode", "enum")], - 'GeometryNodeCurvePrimitiveBezierSegment' : [("mode", "enum")], - 'GeometryNodeCurvePrimitiveCircle' : [("mode", "enum")], - 'GeometryNodeCurvePrimitiveLine' : [("mode", "enum")], + 'GeometryNodeCurveArc' : [("mode", ST.ENUM)], + + 'GeometryNodeCurvePrimitiveBezierSegment' : [("mode", ST.ENUM)], + + 'GeometryNodeCurvePrimitiveCircle' : [("mode", ST.ENUM)], + + 'GeometryNodeCurvePrimitiveLine' : [("mode", ST.ENUM)], + 'GeometryNodeCurveSpiral' : [], + 'GeometryNodeCurveQuadraticBezier' : [], - 'GeometryNodeCurvePrimitiveQuadrilateral' : [("mode", "enum")], + + 'GeometryNodeCurvePrimitiveQuadrilateral' : [("mode", ST.ENUM)], + 'GeometryNodeCurveStar' : [], # Curve > Topology 'GeometryNodeOffsetPointInCurve' : [], + 'GeometryNodeCurveOfPoint' : [], + 'GeometryNodePointsOfCurve' : [], # INSTANCES 'GeometryNodeInstanceOnPoints' : [], + 'GeometryNodeInstancesToPoints' : [], - 'GeometryNodeRealizeInstances' : [("legacy_behavior", "bool")], + + 'GeometryNodeRealizeInstances' : [("legacy_behavior", ST.BOOL)], + 'GeometryNodeRotateInstances' : [], + 'GeometryNodeScaleInstances' : [], + 'GeometryNodeTranslateInstances' : [], + 'GeometryNodeInputInstanceRotation' : [], + 'GeometryNodeInputInstanceScale' : [], # MESH # Mesh > Read 'GeometryNodeInputMeshEdgeAngle' : [], + 'GeometryNodeInputMeshEdgeNeighbors' : [], + 'GeometryNodeInputMeshEdgeVertices' : [], + 'GeometryNodeEdgesToFaceGroups' : [], + 'GeometryNodeInputMeshFaceArea' : [], + 'GeometryNodeInputMeshFaceNeighbors' : [], + 'GeometryNodeMeshFaceSetBoundaries' : [], + 'GeometryNodeInputMeshFaceIsPlanar' : [], + 'GeometryNodeInputShadeSmooth' : [], + 'GeometryNodeInputMeshIsland' : [], + 'GeometryNodeInputShortestEdgePaths' : [], + 'GeometryNodeInputMeshVertexNeighbors' : [], # Mesh > Sample - 'GeometryNodeSampleNearestSurface' : [("data_type", "enum")], - 'GeometryNodeSampleUVSurface' : [("data_type", "enum")], + 'GeometryNodeSampleNearestSurface' : [("data_type", ST.ENUM)], + + 'GeometryNodeSampleUVSurface' : [("data_type", ST.ENUM)], # Mesh > Write 'GeometryNodeSetShadeSmooth' : [], # Mesh > Operations 'GeometryNodeDualMesh' : [], + 'GeometryNodeEdgePathsToCurves' : [], + 'GeometryNodeEdgePathsToSelection' : [], - 'GeometryNodeExtrudeMesh' : [("mode", "enum")], + + 'GeometryNodeExtrudeMesh' : [("mode", ST.ENUM)], + 'GeometryNodeFlipFaces' : [], - 'GeometryNodeMeshBoolean' : [("operation", "enum")], + + 'GeometryNodeMeshBoolean' : [("operation", ST.ENUM)], + 'GeometryNodeMeshToCurve' : [], - 'GeometryNodeMeshToPoints' : [("mode", "enum")], - 'GeometryNodeMeshToVolume' : [("resolution_mode", "enum")], - 'GeometryNodeScaleElements' : [("domain", "enum"), - ("scale_mode", "enum")], + + 'GeometryNodeMeshToPoints' : [("mode", ST.ENUM)], + + 'GeometryNodeMeshToVolume' : [("resolution_mode", ST.ENUM)], + + 'GeometryNodeScaleElements' : [("domain", ST.ENUM), + ("scale_mode", ST.ENUM)], + 'GeometryNodeSplitEdges' : [], + 'GeometryNodeSubdivideMesh' : [], - 'GeometryNodeSubdivisionSurface' : [("boundary_smooth", "enum"), - ("uv_smooth", "enum")], - 'GeometryNodeTriangulate' : [("ngon_method", "enum"), - ("quad_method", "enum")], + + 'GeometryNodeSubdivisionSurface' : [("boundary_smooth", ST.ENUM), + ("uv_smooth", ST.ENUM)], + + 'GeometryNodeTriangulate' : [("ngon_method", ST.ENUM), + ("quad_method", ST.ENUM)], # Mesh > Primitives - 'GeometryNodeMeshCone' : [("fill_type", "enum")], + 'GeometryNodeMeshCone' : [("fill_type", ST.ENUM)], + 'GeometryNodeMeshCube' : [], - 'GeometryNodeMeshCylinder' : [("fill_type", "enum")], + + 'GeometryNodeMeshCylinder' : [("fill_type", ST.ENUM)], + 'GeometryNodeMeshGrid' : [], + 'GeometryNodeMeshIcoSphere' : [], - 'GeometryNodeMeshCircle' : [("fill_type", "enum")], - 'GeometryNodeMeshLine' : [("count_mode", "enum"), - ("mode", "enum")], + + 'GeometryNodeMeshCircle' : [("fill_type", ST.ENUM)], + + 'GeometryNodeMeshLine' : [("count_mode", ST.ENUM), + ("mode", ST.ENUM)], + 'GeometryNodeMeshUVSphere' : [], # Mesh > Topology 'GeometryNodeCornersOfFace' : [], + 'GeometryNodeCornersOfVertex' : [], + 'GeometryNodeEdgesOfCorner' : [], + 'GeometryNodeEdgesOfVertex' : [], + 'GeometryNodeFaceOfCorner' : [], + 'GeometryNodeOffsetCornerInFace' : [], + 'GeometryNodeVertexOfCorner' : [], # Mesh > UV 'GeometryNodeUVPackIslands' : [], - 'GeometryNodeUVUnwrap' : [("method", "enum")], + + 'GeometryNodeUVUnwrap' : [("method", ST.ENUM)], # POINT - 'GeometryNodeDistributePointsInVolume' : [("mode", "enum")], - 'GeometryNodeDistributePointsOnFaces' : [("distribute_method", "enum"), - ("use_legacy_normal", "bool")], + 'GeometryNodeDistributePointsInVolume' : [("mode", ST.ENUM)], + + 'GeometryNodeDistributePointsOnFaces' : [("distribute_method", ST.ENUM), + ("use_legacy_normal", ST.BOOL)], + 'GeometryNodePoints' : [], + 'GeometryNodePointsToVertices' : [], - 'GeometryNodePointsToVolume' : [("resolution_mode", "enum")], + + 'GeometryNodePointsToVolume' : [("resolution_mode", ST.ENUM)], + 'GeometryNodeSetPointRadius' : [], # VOLUME 'GeometryNodeVolumeCube' : [], - 'GeometryNodeVolumeToMesh' : [("resolution_mode", "enum")], + + 'GeometryNodeVolumeToMesh' : [("resolution_mode", ST.ENUM)], # SIMULATION 'GeometryNodeSimulationInput' : [], + 'GeometryNodeSimulationOutput' : [], # MATERIAL 'GeometryNodeReplaceMaterial' : [], + 'GeometryNodeInputMaterialIndex' : [], + 'GeometryNodeMaterialSelection' : [], + 'GeometryNodeSetMaterial' : [], + 'GeometryNodeSetMaterialIndex' : [], # TEXTURE - 'ShaderNodeTexBrick' : [("offset", "float"), - ("offset_frequency", "int"), - ("squash", "float"), - ("squash_frequency", "int")], + 'ShaderNodeTexBrick' : [("offset", ST.FLOAT), + ("offset_frequency", ST.INT), + ("squash", ST.FLOAT), + ("squash_frequency", ST.INT)], + 'ShaderNodeTexChecker' : [], - 'ShaderNodeTexGradient' : [("gradient_type", "enum")], - 'GeometryNodeImageTexture' : [("extension", "enum"), - ("interpolation", "enum")], - 'ShaderNodeTexMagic' : [("turbulence_depth", "int")], - 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", "enum"), - ("musgrave_type", "enum")], - 'ShaderNodeTexNoise' : [("noise_dimensions", "enum")], - 'ShaderNodeTexVoronoi' : [("distance", "enum"), - ("feature", "enum"), - ("voronoi_dimensions", "enum")], - 'ShaderNodeTexWave' : [("bands_direction", "enum"), - ("rings_direction", "enum"), - ("wave_profile", "enum"), - ("wave_type", "enum")], - 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", "enum")], + + 'ShaderNodeTexGradient' : [("gradient_type", ST.ENUM)], + + 'GeometryNodeImageTexture' : [("extension", ST.ENUM), + ("interpolation", ST.ENUM)], + + 'ShaderNodeTexMagic' : [("turbulence_depth", ST.INT)], + + 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", ST.ENUM), + ("musgrave_type", ST.ENUM)], + + 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM)], + + 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), + ("feature", ST.ENUM), + ("voronoi_dimensions", ST.ENUM)], + + 'ShaderNodeTexWave' : [("bands_direction", ST.ENUM), + ("rings_direction", ST.ENUM), + ("wave_profile", ST.ENUM), + ("wave_type", ST.ENUM)], + + 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", ST.ENUM)], # UTILITIES - 'ShaderNodeMix' : [("blend_type", "enum"), - ("clamp_factor", "bool"), - ("clamp_result", "bool"), - ("data_type", "enum"), - ("factor_mode", "enum")], - 'FunctionNodeRandomValue' : [("data_type", "enum")], - 'GeometryNodeSwitch' : [("input_type", "enum")], + 'ShaderNodeMix' : [("blend_type", ST.ENUM), + ("clamp_factor", ST.BOOL), + ("clamp_result", ST.BOOL), + ("data_type", ST.ENUM), + ("factor_mode", ST.ENUM)], + + 'FunctionNodeRandomValue' : [("data_type", ST.ENUM)], + + 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], # Utilities > Color - 'ShaderNodeValToRGB' : [("color_ramp", "ColorRamp")], - 'ShaderNodeRGBCurve' : [("mapping", "CurveMapping")], - 'FunctionNodeCombineColor' : [("mode", "enum")], - 'ShaderNodeMixRGB' : [("blend_type", "enum"), - ("use_alpha", "bool"), - ("use_clamp", "bool")], #legacy - 'FunctionNodeSeparateColor' : [("mode", "enum")], + 'ShaderNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + + 'ShaderNodeRGBCurve' : [("mapping", ST.CURVE_MAPPING)], + + 'FunctionNodeCombineColor' : [("mode", ST.ENUM)], + + 'ShaderNodeMixRGB' : [("blend_type", ST.ENUM), + ("use_alpha", ST.BOOL), + ("use_clamp", ST.BOOL)], #legacy + + 'FunctionNodeSeparateColor' : [("mode", ST.ENUM)], # Utilities > Text 'GeometryNodeStringJoin' : [], + 'FunctionNodeReplaceString' : [], + 'FunctionNodeSliceString' : [], + 'FunctionNodeStringLength' : [], - 'GeometryNodeStringToCurves' : [("align_x", "enum"), - ("align_y", "enum"), - ("font", "Font"), #TODO: font - ("overflow", "enum"), - ("pivot_mode", "enum")], + + 'GeometryNodeStringToCurves' : [("align_x", ST.ENUM), + ("align_y", ST.ENUM), + ("font", ST.FONT), + ("overflow", ST.ENUM), + ("pivot_mode", ST.ENUM)], + 'FunctionNodeValueToString' : [], + 'FunctionNodeInputSpecialCharacters' : [], # Utilities > Vector - 'ShaderNodeVectorCurve' : [("mapping", "CurveMapping")], - 'ShaderNodeVectorMath' : [("operation", "enum")], - 'ShaderNodeVectorRotate' : [("invert", "bool"), - ("rotation_type", "enum")], + 'ShaderNodeVectorCurve' : [("mapping", ST.CURVE_MAPPING)], + + 'ShaderNodeVectorMath' : [("operation", ST.ENUM)], + + 'ShaderNodeVectorRotate' : [("invert", ST.BOOL), + ("rotation_type", ST.ENUM)], + 'ShaderNodeCombineXYZ' : [], + 'ShaderNodeSeparateXYZ' : [], # Utilities > Field - 'GeometryNodeAccumulateField' : [("data_type", "enum"), - ("domain", "enum")], - 'GeometryNodeFieldAtIndex' : [("data_type", "enum"), - ("domain", "enum")], - 'GeometryNodeFieldOnDomain' : [("data_type", "enum"), - ("domain", "enum")], + 'GeometryNodeAccumulateField' : [("data_type", ST.ENUM), + ("domain", ST.ENUM)], + + 'GeometryNodeFieldAtIndex' : [("data_type", ST.ENUM), + ("domain", ST.ENUM)], + + 'GeometryNodeFieldOnDomain' : [("data_type", ST.ENUM), + ("domain", ST.ENUM)], # Utilities > Math - 'FunctionNodeBooleanMath' : [("operation", "enum")], - 'ShaderNodeClamp' : [("clamp_type", "enum")], - 'FunctionNodeCompare' : [("data_type", "enum"), - ("mode", "enum"), - ("operation", "enum")], - 'ShaderNodeFloatCurve' : [("mapping", "CurveMapping")], - 'FunctionNodeFloatToInt' : [("rounding_mode", "enum")], - 'ShaderNodeMapRange' : [("clamp", "bool"), - ("data_type", "enum"), - ("interpolation_type", "enum")], - 'ShaderNodeMath' : [("operation", "enum"), - ("use_clamp", "bool")], + 'FunctionNodeBooleanMath' : [("operation", ST.ENUM)], + + 'ShaderNodeClamp' : [("clamp_type", ST.ENUM)], + + 'FunctionNodeCompare' : [("data_type", ST.ENUM), + ("mode", ST.ENUM), + ("operation", ST.ENUM)], + + 'ShaderNodeFloatCurve' : [("mapping", ST.CURVE_MAPPING)], + + 'FunctionNodeFloatToInt' : [("rounding_mode", ST.ENUM)], + + 'ShaderNodeMapRange' : [("clamp", ST.BOOL), + ("data_type", ST.ENUM), + ("interpolation_type", ST.ENUM)], + + 'ShaderNodeMath' : [("operation", ST.ENUM), + ("use_clamp", ST.BOOL)], # Utilities > Rotation - 'FunctionNodeAlignEulerToVector' : [("axis", "enum"), - ("pivot_axis", "enum")], - 'FunctionNodeRotateEuler' : [("space", "enum"), - ("type", "enum")] + 'FunctionNodeAlignEulerToVector' : [("axis", ST.ENUM), + ("pivot_axis", ST.ENUM)], + + 'FunctionNodeRotateEuler' : [("space", ST.ENUM), + ("type", ST.ENUM)] } class NTPGeoNodesOperator(bpy.types.Operator): diff --git a/materials.py b/materials.py index a0d37f0..5bbdf2c 100644 --- a/materials.py +++ b/materials.py @@ -4,192 +4,277 @@ from .utils import * from io import StringIO -shader_node_settings : dict[str, list[(str, str)]] = { +#TODO: move to a json, different ones for each blender version? +shader_node_settings : dict[str, list[(str, ST)]] = { # INPUT - 'ShaderNodeAmbientOcclusion' : [("inside", "bool"), - ("only_local", "bool"), - ("samples", "int")], - 'ShaderNodeAttribute' : [("attribute_name", "str"), - ("attribute_type", "enum")], - 'ShaderNodeBevel' : [("samples", "int")], + 'ShaderNodeAmbientOcclusion' : [("inside", ST.BOOL), + ("only_local", ST.BOOL), + ("samples", ST.INT)], + + 'ShaderNodeAttribute' : [("attribute_name", ST.STRING), #TODO: separate attribute type? + ("attribute_type", ST.ENUM)], + + 'ShaderNodeBevel' : [("samples", ST.INT)], + 'ShaderNodeCameraData' : [], - 'ShaderNodeVertexColor' : [("layer_name", "str")], + + 'ShaderNodeVertexColor' : [("layer_name", ST.STRING)], #TODO: separate color attribute type? + 'ShaderNodeHairInfo' : [], + 'ShaderNodeFresnel' : [], + 'ShaderNodeNewGeometry' : [], + 'ShaderNodeLayerWeight' : [], + 'ShaderNodeLightPath' : [], + 'ShaderNodeObjectInfo' : [], + 'ShaderNodeParticleInfo' : [], + 'ShaderNodePointInfo' : [], + 'ShaderNodeRGB' : [], - 'ShaderNodeTangent' : [("axis", "enum"), - ("direction_type", "enum"), - ("uv_map", "str")], #TODO: makes sense? maybe make special type - 'ShaderNodeTexCoord' : [("from_instancer", "bool"), - ("object", "Object")], - 'ShaderNodeUVAlongStroke' : [("use_tips", "bool")], - 'ShaderNodeUVMap' : [("from_instancer", "bool"), - ("uv_map", "str")], #TODO: see ShaderNodeTangent + + 'ShaderNodeTangent' : [("axis", ST.ENUM), + ("direction_type", ST.ENUM), + ("uv_map", ST.STRING)], #TODO: special UV Map type? + + 'ShaderNodeTexCoord' : [("from_instancer", ST.BOOL), + ("object", ST.OBJECT)], + + 'ShaderNodeUVAlongStroke' : [("use_tips", ST.BOOL)], + + 'ShaderNodeUVMap' : [("from_instancer", ST.BOOL), + ("uv_map", ST.STRING)], #TODO: see ShaderNodeTangent + 'ShaderNodeValue' : [], + 'ShaderNodeVolumeInfo' : [], - 'ShaderNodeWireframe' : [("use_pixel_size", "bool")], + + 'ShaderNodeWireframe' : [("use_pixel_size", ST.BOOL)], # OUTPUT - 'ShaderNodeOutputAOV' : [("name", "str")], - 'ShaderNodeOutputLight' : [("is_active_output", "bool"), - ("target", "enum")], - 'ShaderNodeOutputLineStyle' : [("blend_type", "enum"), - ("is_active_output", "bool"), - ("target", "enum"), - ("use_alpha", "bool"), - ("use_clamp", "bool")], - 'ShaderNodeOutputMaterial' : [("is_active_output", "bool"), - ("target", "enum")], - 'ShaderNodeOutputWorld' : [("is_active_output", "bool"), - ("target", "enum")], + 'ShaderNodeOutputAOV' : [("name", ST.STRING)], + + 'ShaderNodeOutputLight' : [("is_active_output", ST.BOOL), + ("target", ST.ENUM)], + + 'ShaderNodeOutputLineStyle' : [("blend_type", ST.ENUM), + ("is_active_output", ST.BOOL), + ("target", ST.ENUM), + ("use_alpha", ST.BOOL), + ("use_clamp", ST.BOOL)], + + 'ShaderNodeOutputMaterial' : [("is_active_output", ST.BOOL), + ("target", ST.ENUM)], + + 'ShaderNodeOutputWorld' : [("is_active_output", ST.BOOL), + ("target", ST.ENUM)], # SHADER 'ShaderNodeAddShader' : [], - 'ShaderNodeBsdfAnisotropic' : [("distribution", "enum")], + + 'ShaderNodeBsdfAnisotropic' : [("distribution", ST.ENUM)], + 'ShaderNodeBackground' : [], + 'ShaderNodeBsdfDiffuse' : [], + 'ShaderNodeEmission' : [], - 'ShaderNodeBsdfGlass' : [("distribution", "enum")], - 'ShaderNodeBsdfGlossy' : [("distribution", "enum")], - 'ShaderNodeBsdfHair' : [("component", "enum")], + + 'ShaderNodeBsdfGlass' : [("distribution", ST.ENUM)], + + 'ShaderNodeBsdfGlossy' : [("distribution", ST.ENUM)], + + 'ShaderNodeBsdfHair' : [("component", ST.ENUM)], + 'ShaderNodeHoldout' : [], + 'ShaderNodeMixShader' : [], - 'ShaderNodeBsdfPrincipled' : [("distribution", "enum"), - ("subsurface_method", "enum")], - 'ShaderNodeBsdfHairPrincipled' : [("parametrization", "enum")], + + 'ShaderNodeBsdfPrincipled' : [("distribution", ST.ENUM), + ("subsurface_method", ST.ENUM)], + + 'ShaderNodeBsdfHairPrincipled' : [("parametrization", ST.ENUM)], + 'ShaderNodeVolumePrincipled' : [], - 'ShaderNodeBsdfRefraction' : [("distribution", "enum")], + + 'ShaderNodeBsdfRefraction' : [("distribution", ST.ENUM)], + 'ShaderNodeEeveeSpecular' : [], - 'ShaderNodeSubsurfaceScattering' : [("falloff", "enum")], - 'ShaderNodeBsdfToon' : [("component", "enum")], + + 'ShaderNodeSubsurfaceScattering' : [("falloff", ST.ENUM)], + + 'ShaderNodeBsdfToon' : [("component", ST.ENUM)], + 'ShaderNodeBsdfTranslucent' : [], + 'ShaderNodeBsdfTransparent' : [], + 'ShaderNodeBsdfVelvet' : [], + 'ShaderNodeVolumeAbsorption' : [], + 'ShaderNodeVolumeScatter' : [], # TEXTURE - 'ShaderNodeTexBrick' : [("offset", "float"), - ("offset_frequency", "int"), - ("squash", "float"), - ("squash_frequency", "int")], + 'ShaderNodeTexBrick' : [("offset", ST.FLOAT), + ("offset_frequency", ST.INT), + ("squash", ST.FLOAT), + ("squash_frequency", ST.INT)], + 'ShaderNodeTexChecker' : [], - 'ShaderNodeTexEnvironment' : [("image", "Image"), - ("image_user", "ImageUser"), - ("interpolation", "enum"), - ("projection", "enum")], - 'ShaderNodeTexGradient' : [("gradient_type", "enum")], - 'ShaderNodeTexIES' : [("filepath", "str"), #TODO - ("ies", "Text"), - ("mode", "enum")], - 'ShaderNodeTexImage' : [("extension", "enum"), - ("image", "Image"), - ("image_user", "ImageUser"), - ("interpolation", "enum"), - ("projection", "enum"), - ("projection_blend", "float")], - 'ShaderNodeTexMagic' : [("turbulence_depth", "int")], - 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", "enum"), - ("musgrave_type", "enum")], - 'ShaderNodeTexNoise' : [("noise_dimensions", "enum")], - 'ShaderNodeTexPointDensity' : [("interpolation", "enum"), - ("object", "Object"), - ("particle_color_source", "enum"), - ("particle_system", "ParticleSystem"), - ("point_source", "enum"), - ("radius", "float"), - ("resolution", "int"), - ("space", "enum"), - ("vertex_attribute_name", "str"), #TODO - ("vertex_color_source", "enum")], - 'ShaderNodeTexSky' : [("air_density", "float"), - ("altitude", "float"), - ("dust_density", "float"), - ("ground_albedo", "float"), - ("ozone_density", "float"), - ("sky_type", "enum"), - ("sun_direction", "Vec3"), - ("sun_disc", "bool"), - ("sun_elevation", "float"), - ("sun_intensity", "float"), - ("sun_rotation", "float"), - ("sun_size", "float") - ("turbidity", "float")], - 'ShaderNodeTexVoronoi' : [("distance", "enum"), - ("feature", "enum"), - ("voronoi_dimensions", "enum")], - 'ShaderNodeTexWave' : [("bands_direction", "enum"), - ("rings_direction", "enum"), - ("wave_profile", "enum"), - ("wave_type", "enum")], - 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", "enum")], + + 'ShaderNodeTexEnvironment' : [("image", ST.IMAGE), + ("image_user", ST.IMAGE_USER), + ("interpolation", ST.ENUM), + ("projection", ST.ENUM)], + + 'ShaderNodeTexGradient' : [("gradient_type", ST.ENUM)], + + 'ShaderNodeTexIES' : [("filepath", ST.STRING), #TODO + ("ies", ST.TEXT), + ("mode", ST.ENUM)], + + 'ShaderNodeTexImage' : [("extension", ST.ENUM), + ("image", ST.IMAGE), + ("image_user", ST.IMAGE_USER), + ("interpolation", ST.ENUM), + ("projection", ST.ENUM), + ("projection_blend", ST.FLOAT)], + + 'ShaderNodeTexMagic' : [("turbulence_depth", ST.INT)], + + 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", ST.ENUM), + ("musgrave_type", ST.ENUM)], + + 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM)], + + 'ShaderNodeTexPointDensity' : [("interpolation", ST.ENUM), + ("object", ST.OBJECT), + ("particle_color_source", ST.ENUM), + ("particle_system", ST.PARTICLE_SYSTEM), + ("point_source", ST.ENUM), + ("radius", ST.FLOAT), + ("resolution", ST.INT), + ("space", ST.ENUM), + ("vertex_attribute_name", ST.STRING), #TODO + ("vertex_color_source", ST.ENUM)], + + 'ShaderNodeTexSky' : [("air_density", ST.FLOAT), + ("altitude", ST.FLOAT), + ("dust_density", ST.FLOAT), + ("ground_albedo", ST.FLOAT), + ("ozone_density", ST.FLOAT), + ("sky_type", ST.ENUM), + ("sun_direction", ST.VEC3), + ("sun_disc", ST.BOOL), + ("sun_elevation", ST.FLOAT), + ("sun_intensity", ST.FLOAT), + ("sun_rotation", ST.FLOAT), + ("sun_size", ST.FLOAT) + ("turbidity", ST.FLOAT)], + + 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), + ("feature", ST.ENUM), + ("voronoi_dimensions", ST.ENUM)], + + 'ShaderNodeTexWave' : [("bands_direction", ST.ENUM), + ("rings_direction", ST.ENUM), + ("wave_profile", ST.ENUM), + ("wave_type", ST.ENUM)], + + 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", ST.ENUM)], # COLOR 'ShaderNodeBrightContrast' : [], + 'ShaderNodeGamma' : [], + 'ShaderNodeHueSaturation' : [], + 'ShaderNodeInvert' : [], + 'ShaderNodeLightFalloff' : [], - 'ShaderNodeMix' : [("blend_type", "enum"), - ("clamp_factor", "bool"), - ("clamp_result", "bool"), - ("data_type", "enum"), - ("factor_mode", "enum")], - 'ShaderNodeRGBCurve' : [("mapping", "CurveMapping")], + + 'ShaderNodeMix' : [("blend_type", ST.ENUM), + ("clamp_factor", ST.BOOL), + ("clamp_result", ST.BOOL), + ("data_type", ST.ENUM), + ("factor_mode", ST.ENUM)], + + 'ShaderNodeRGBCurve' : [("mapping", ST.CURVE_MAPPING)], # VECTOR - 'ShaderNodeBump' : [("invert", "bool")], - 'ShaderNodeDisplacement' : [("space", "enum")], - 'ShaderNodeMapping' : [("vector_type", "enum")], - 'ShaderNodeNormalMap' : [("space", "enum"), - ("uv_map", "str")], #TODO - 'ShaderNodeVectorCurve' : [("mapping", "CurveMapping")], - 'ShaderNodeVectorDisplacement' : [("space", "enum")], - 'ShaderNodeVectorRotate' : [("invert", "bool"), - ("rotation_type", "enum")], - 'ShaderNodeVectorTransform' : [("convert_from", "enum"), - ("convert_to", "enum"), - ("vector_type", "enum")], + 'ShaderNodeBump' : [("invert", ST.BOOL)], + + 'ShaderNodeDisplacement' : [("space", ST.ENUM)], + + 'ShaderNodeMapping' : [("vector_type", ST.ENUM)], + + 'ShaderNodeNormalMap' : [("space", ST.ENUM), + ("uv_map", ST.STRING)], #TODO + + 'ShaderNodeVectorCurve' : [("mapping", ST.CURVE_MAPPING)], + + 'ShaderNodeVectorDisplacement' : [("space", ST.ENUM)], + + 'ShaderNodeVectorRotate' : [("invert", ST.BOOL), + ("rotation_type", ST.ENUM)], + + 'ShaderNodeVectorTransform' : [("convert_from", ST.ENUM), + ("convert_to", ST.ENUM), + ("vector_type", ST.ENUM)], # CONVERTER 'ShaderNodeBlackbody' : [], - 'ShaderNodeClamp' : [("clamp_type", "enum")], - 'ShaderNodeValToRGB' : [("color_ramp", "ColorRamp")], - 'ShaderNodeCombineColor' : [("mode", "enum")], + + 'ShaderNodeClamp' : [("clamp_type", ST.ENUM)], + + 'ShaderNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + + 'ShaderNodeCombineColor' : [("mode", ST.ENUM)], + 'ShaderNodeCombineXYZ' : [], - 'ShaderNodeFloatCurve' : [("mapping", "CurveMapping")], - 'ShaderNodeMapRange' : [("clamp", "bool"), - ("data_type", "enum"), - ("interpolation_type", "enum")], - 'ShaderNodeMath' : [("operation", "enum"), - ("use_clamp", "bool")], + + 'ShaderNodeFloatCurve' : [("mapping", ST.CURVE_MAPPING)], + + 'ShaderNodeMapRange' : [("clamp", ST.BOOL), + ("data_type", ST.ENUM), + ("interpolation_type", ST.ENUM)], + + 'ShaderNodeMath' : [("operation", ST.ENUM), + ("use_clamp", ST.BOOL)], + 'ShaderNodeRGBToBW' : [], - 'ShaderNodeSeparateColor' : [("mode", "enum")], + + 'ShaderNodeSeparateColor' : [("mode", ST.ENUM)], + 'ShaderNodeSeparateXYZ' : [], + 'ShaderNodeShaderToRGB' : [], - 'ShaderNodeVectorMath' : [("operation", "enum")], + + 'ShaderNodeVectorMath' : [("operation", ST.ENUM)], + 'ShaderNodeWavelength' : [], # SCRIPT - 'ShaderNodeScript' : [("bytecode", "str"), #TODO: test all that - ("bytecode_hash", "str"), - ("filepath", "str"), - ("mode", "enum"), - ("script", "text"), - ("use_auto_update", "bool")] + 'ShaderNodeScript' : [("bytecode", ST.STRING), #TODO: test all that + ("bytecode_hash", ST.STRING), + ("filepath", ST.STRING), + ("mode", ST.ENUM), + ("script", ST.TEXT), + ("use_auto_update", ST.BOOL)] } curve_nodes = {'ShaderNodeFloatCurve', diff --git a/utils.py b/utils.py index 6d09ec0..372daae 100644 --- a/utils.py +++ b/utils.py @@ -1,6 +1,7 @@ import bpy import mathutils +from enum import Enum, auto import os import re import shutil @@ -13,6 +14,37 @@ 'NodeSocketShader', 'NodeSocketVirtual'} +class ST(Enum): + """ + Socket and Settings Types + """ + ENUM = auto() + STRING = auto() + BOOL = auto() + INT = auto() + FLOAT = auto() + VEC1 = auto() + VEC2 = auto() + VEC3 = auto() + VEC4 = auto() + MATERIAL = auto() #Could use a look + OBJECT = auto() #Could take a looking at + IMAGE = auto() #needs refactor + IMAGE_USER = auto() #unimplemented + MOVIE_CLIP = auto() #unimplmented + COLOR_RAMP = auto() #needs refactor + CURVE_MAPPING = auto() #needs refactor + TEXTURE = auto() #unimplemented + TEXT = auto() #unimplemented + SCENE = auto() #unimplemented + PARTICLE_SYSTEM = auto() #unimplemented + FONT = auto() #unimplemented + MASK = auto() #unimplemented + CRYPTOMATTE_ENTRIES = auto() #unimplemented + IMAGE_FORMAT_SETTINGS = auto() + FILE_SLOTS = auto() + LAYER_SLOTS = auto() #unimplemented + #node tree input sockets that have default properties default_sockets = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} @@ -56,29 +88,53 @@ def str_to_py_str(string: str) -> str: """ return f"\"{string}\"" -def vec3_to_py_str(vec) -> str: +def vec1_to_py_str(vec1) -> str: + """ + Converts a 1D vector to a string usable by the add-on + + Parameters: + vec1: a 1d vector + + Returns: + (str): string representation of the vector + """ + return f"({vec1[0]})" + +def vec2_to_py_str(vec2) -> str: + """ + Converts a 2D vector to a string usable by the add-on + + Parameters: + vec2: a 2D vector + + Returns: + (str): string representation of the vector + """ + return f"({vec2[0]}, {vec2[1]})" + +def vec3_to_py_str(vec3) -> str: """ Converts a 3D vector to a string usable by the add-on Parameters: - vec (mathutils.Vector): a 3d vector + vec3: a 3d vector Returns: - (str): string version + (str): string representation of the vector """ - return f"({vec[0]}, {vec[1]}, {vec[2]})" + return f"({vec3[0]}, {vec3[1]}, {vec3[2]})" -def vec4_to_py_str(vec) -> str: +def vec4_to_py_str(vec4) -> str: """ Converts a 4D vector to a string usable by the add-on Parameters: - vec (mathutils.Vector): a 4d vector + vec4: a 4d vector Returns: (str): string version """ - return f"({vec[0]}, {vec[1]}, {vec[2]}, {vec[3]})" + return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})" def img_to_py_str(img) -> str: """ @@ -94,13 +150,6 @@ def img_to_py_str(img) -> str: format = img.file_format.lower() return f"{name}.{format}" -type_to_py_str : dict[str, function] = { - "enum" : enum_to_py_str, - "str" : str_to_py_str, - "vec3" : vec3_to_py_str, - "vec4" : vec4_to_py_str -} - def create_header(file: TextIO, name: str): """ Sets up the bl_info and imports the Blender API @@ -220,7 +269,7 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, return node_var -def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, +def set_settings_defaults(node, settings: dict[str, list[(str, str)]], file: TextIO, inner: str, node_var: str): """ Sets the defaults for any settings a node may have @@ -233,9 +282,16 @@ def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname in settings: - for setting in settings[node.bl_idname]: + for (setting, type) in settings[node.bl_idname]: attr = getattr(node, setting, None) - if attr: + if not attr: + continue + if type == "enum": + file.write(f"{inner}{node_var}.{setting} = {enum_to_py_str(attr)}\n") + elif type == "str": + file.write(f"{inner}{node_var}.{setting} = {str_to_py_str(attr)}\n") + elif type == "int": + file.write(f"{inner}{node_var}.{setting} = {attr}\n") if type(attr) == str: attr = enum_to_py_str(attr) if type(attr) == mathutils.Vector: @@ -552,7 +608,7 @@ def set_output_defaults(node, file: TextIO, inner: str, node_var: str): 'ShaderNodeNormal'} if node.bl_idname in output_default_nodes: - dv = node.outputs[0].default_value + dv = node.outputs[0].default_value #TODO: see if this is still the case if node.bl_idname == 'ShaderNodeRGB': dv = vec4_to_py_str(list(dv)) if node.bl_idname == 'ShaderNodeNormal': @@ -641,6 +697,7 @@ def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, gnashing of teeth. This is a quick fix that doesn't run quick """ + #TODO: try using index() method for i, item in enumerate(link.from_node.outputs.items()): if item[1] == input_socket: input_idx = i From f1d85b66d3a6d7c4c8527e10f691e6589a2853a8 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:37:23 -0500 Subject: [PATCH 08/72] refactor: changed set_settings_default() to use new types --- utils.py | 53 ++++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/utils.py b/utils.py index 372daae..8d1d945 100644 --- a/utils.py +++ b/utils.py @@ -269,7 +269,7 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, return node_var -def set_settings_defaults(node, settings: dict[str, list[(str, str)]], file: TextIO, inner: str, +def set_settings_defaults(node, settings: dict[str, list[(str, ST)]], file: TextIO, inner: str, node_var: str): """ Sets the defaults for any settings a node may have @@ -286,32 +286,31 @@ def set_settings_defaults(node, settings: dict[str, list[(str, str)]], file: Tex attr = getattr(node, setting, None) if not attr: continue - if type == "enum": - file.write(f"{inner}{node_var}.{setting} = {enum_to_py_str(attr)}\n") - elif type == "str": - file.write(f"{inner}{node_var}.{setting} = {str_to_py_str(attr)}\n") - elif type == "int": - file.write(f"{inner}{node_var}.{setting} = {attr}\n") - if type(attr) == str: - attr = enum_to_py_str(attr) - if type(attr) == mathutils.Vector: - attr = vec3_to_py_str(attr) - if type(attr) == bpy.types.bpy_prop_array: - attr = vec4_to_py_str(list(attr)) - if type(attr) == bpy.types.Material: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.materials:\n")) - file.write((f"{inner}\t{node_var}.{setting} = " - f"bpy.data.materials[{name}]\n")) - continue - if type(attr) == bpy.types.Object: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.objects:\n")) - file.write((f"{inner}\t{node_var}.{setting} = " - f"bpy.data.objects[{name}]\n")) - continue - file.write((f"{inner}{node_var}.{setting} " - f"= {attr}\n")) + setting_str = f"{inner}{node_var}.{setting}" + if type == ST.ENUM: + file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") + elif type == ST.STRING: + file.write(f"{setting_str} = {str_to_py_str(attr)}\n") + elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: + file.write(f"{setting_str} = {attr}\n") + elif type == ST.VEC1: + file.write(f"{setting_str} = {vec1_to_py_str(attr)}\n") + elif type == ST.VEC2: + file.write(f"{setting_str} = {vec2_to_py_str(attr)}\n") + elif type == ST.VEC3: + file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") + elif type == ST.VEC4: + file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + elif type == ST.MATERIAL: + name = str_to_py_str(attr.name) + file.write((f"{inner}if {name} in bpy.data.materials:\n")) + file.write((f"{inner}\t{node_var}.{setting} = " + f"bpy.data.materials[{name}]\n")) + elif type == ST.OBJECT: + name = str_to_py_str(attr.name) + file.write((f"{inner}if {name} in bpy.data.objects:\n")) + file.write((f"{inner}\t{node_var}.{setting} = " + f"bpy.data.objects[{name}]\n")) def hide_sockets(node, file: TextIO, inner: str, node_var: str): """ From c0eb85dcf70c01e3544679222022d67d5d6009ae Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:58:57 -0500 Subject: [PATCH 09/72] style: better type hints for util functions --- utils.py | 150 ++++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 35 deletions(-) diff --git a/utils.py b/utils.py index 8d1d945..5103827 100644 --- a/utils.py +++ b/utils.py @@ -16,7 +16,7 @@ class ST(Enum): """ - Socket and Settings Types + Settings Types """ ENUM = auto() STRING = auto() @@ -136,7 +136,7 @@ def vec4_to_py_str(vec4) -> str: """ return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})" -def img_to_py_str(img) -> str: +def img_to_py_str(img : bpy.types.Image) -> str: """ Converts a Blender image into its string @@ -150,7 +150,7 @@ def img_to_py_str(img) -> str: format = img.file_format.lower() return f"{name}.{format}" -def create_header(file: TextIO, name: str): +def create_header(file: TextIO, name: str) -> None: """ Sets up the bl_info and imports the Blender API @@ -172,7 +172,7 @@ def create_header(file: TextIO, name: str): file.write("import os\n") file.write("\n") -def init_operator(file: TextIO, name: str, idname: str, label: str): +def init_operator(file: TextIO, name: str, idname: str, label: str) -> None: """ Initializes the add-on's operator @@ -229,8 +229,13 @@ def make_indents(level: int) -> Tuple[str, str]: inner = "\t"*(level + 1) return outer, inner -def create_node(node, file: TextIO, inner: str, node_tree_var: str, - node_vars: dict, used_vars: set) -> str: +def create_node(node: bpy.types.Node, + file: TextIO, + inner: str, + node_tree_var: str, + node_vars: dict[bpy.types.Node, str], + used_vars: set #necessary? + ) -> str: """ Initializes a new node with location, dimension, and label info @@ -239,8 +244,8 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, file (TextIO): file containing the generated add-on inner (str): indentation level for this logic node_tree_var (str): variable name for the node tree - node_vars (dict): dictionary containing (bpy.types.Node, str) - pairs, with a Node and its corresponding variable name + node_vars (dict): dictionary containing Node to corresponding variable name + pairs used_vars (set): set of used variable names Returns: @@ -269,8 +274,12 @@ def create_node(node, file: TextIO, inner: str, node_tree_var: str, return node_var -def set_settings_defaults(node, settings: dict[str, list[(str, ST)]], file: TextIO, inner: str, - node_var: str): +def set_settings_defaults(node: bpy.types.Node, + settings: dict[str, list[(str, ST)]], + file: TextIO, + inner: str, + node_var: str + ) -> None: """ Sets the defaults for any settings a node may have @@ -312,7 +321,11 @@ def set_settings_defaults(node, settings: dict[str, list[(str, ST)]], file: Text file.write((f"{inner}\t{node_var}.{setting} = " f"bpy.data.objects[{name}]\n")) -def hide_sockets(node, file: TextIO, inner: str, node_var: str): +def hide_sockets(node: bpy.types.Node, + file: TextIO, + inner: str, + node_var: str + ) -> None: """ Hide hidden sockets @@ -329,7 +342,25 @@ def hide_sockets(node, file: TextIO, inner: str, node_var: str): if socket.hide is True: file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") -def group_io_settings(node, file: TextIO, inner: str, io: str, node_tree_var: str, node_tree): +def group_io_settings(node: bpy.types.Node, + file: TextIO, + inner: str, + io: str, #TODO: convert to enum + node_tree_var: str, + node_tree: bpy.types.NodeTree + ) -> None: + """ + Set the settings for group input and output sockets + + Parameters: + node (bpy.types.Node) : group input/output node + file (TextIO): file we're generating the add-on into + inner (str): indentation string + io (str): whether we're generating the input or output settings + node_tree_var (str): variable name of the generated node tree + node_tree (bpy.types.NodeTree): node tree that we're generating input + and output settings for + """ if io == "input": ios = node.outputs ntio = node_tree.inputs @@ -393,7 +424,11 @@ def group_io_settings(node, file: TextIO, inner: str, io: str, node_tree_var: st file.write("\n") file.write("\n") -def color_ramp_settings(node, file: TextIO, inner: str, node_var: str): +def color_ramp_settings(node: bpy.types.Node, + file: TextIO, + inner: str, + node_var: str + ) -> None: """ Replicate a color ramp node @@ -435,7 +470,11 @@ def color_ramp_settings(node, file: TextIO, inner: str, node_var: str): color_str = vec4_to_py_str(element.color) file.write((f"{inner}{element_var}.color = {color_str}\n\n")) -def curve_node_settings(node, file: TextIO, inner: str, node_var: str): +def curve_node_settings(node: bpy.types.Node, + file: TextIO, + inner: str, + node_var: str + ) -> None: """ Sets defaults for Float, Vector, and Color curves @@ -506,8 +545,12 @@ def curve_node_settings(node, file: TextIO, inner: str, node_var: str): file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping_var}.update()\n") -def set_input_defaults(node, file: TextIO, inner: str, node_var: str, - addon_dir: str = ""): +def set_input_defaults(node: bpy.types.Node, + file: TextIO, + inner: str, + node_var: str, + addon_dir: str = "" + ) -> None: """ Sets defaults for input sockets @@ -574,7 +617,12 @@ def set_input_defaults(node, file: TextIO, inner: str, node_var: str, f" = {default_val}\n")) file.write("\n") -def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str): +def in_file_inputs(input: bpy.types.NodeSocket, + file: TextIO, + inner: str, + socket_var: str, + type: str + ) -> None: """ Sets inputs for a node input if one already exists in the blend file @@ -592,7 +640,11 @@ def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str): file.write((f"{inner}\t{socket_var}.default_value = " f"bpy.data.{type}[{name}]\n")) -def set_output_defaults(node, file: TextIO, inner: str, node_var: str): +def set_output_defaults(node: bpy.types.Node, + file: TextIO, + inner: str, + node_var: str + ) -> None: """ Some output sockets need default values set. It's rather annoying @@ -614,7 +666,11 @@ def set_output_defaults(node, file: TextIO, inner: str, node_var: str): dv = vec3_to_py_str(dv) file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) -def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): +def set_parents(node_tree: bpy.types.NodeTree, + file: TextIO, + inner: str, + node_vars: dict[bpy.types.Node, str] + ) -> None: """ Sets parents for all nodes, mostly used to put nodes in frames @@ -622,7 +678,8 @@ def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from file (TextIO): file for the generated add-on inner (str): indentation string - node_vars (dict): dictionary for (node, variable) name pairs + node_vars (dict[bpy.types.Node, str]): dictionary for node->variable name + pairs """ parent_comment = False for node in node_tree.nodes: @@ -635,7 +692,11 @@ def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): file.write(f"{inner}{node_var}.parent = {parent_var}\n") file.write("\n") -def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): +def set_locations(node_tree: bpy.types.NodeTree, + file: TextIO, + inner: str, + node_vars: dict[bpy.types.Node, str] + ) -> None: """ Set locations for all nodes @@ -643,7 +704,8 @@ def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from file (TextIO): file for the generated add-on inner (str): indentation string - node_vars (dict): dictionary for (node, variable) name pairs + node_vars (dict[bpy.types.Node, str]): dictionary for (node, variable) name + pairs """ file.write(f"{inner}#Set locations\n") @@ -653,7 +715,11 @@ def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): f"= ({node.location.x}, {node.location.y})\n")) file.write("\n") -def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): +def set_dimensions(node_tree: bpy.types.NodeTree, + file: TextIO, + inner: str, + node_vars: dict[bpy.types.Node, str] + ) -> None: """ Set dimensions for all nodes @@ -661,7 +727,8 @@ def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from file (TextIO): file for the generated add-on inner (str): indentation string - node_vars (dict): dictionary for (node, variable) name pairs + node_vars (dict[bpy.types.Node, str]): dictionary for (node, variable) name + pairs """ file.write(f"{inner}#Set dimensions\n") @@ -671,8 +738,12 @@ def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): f"= {node.width}, {node.height}\n")) file.write("\n") -def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, - node_vars: dict): +def init_links(node_tree: bpy.types.NodeTree, + file: TextIO, + inner: str, + node_tree_var: str, + node_vars: dict[bpy.types.Node, str] + ) -> None: """ Create all the links between nodes @@ -681,7 +752,8 @@ def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, file (TextIO): file we're generating the add-on into inner (str): indentation node_tree_var (str): variable name we're using for the copied node tree - node_vars (dict): dictionary containing node to variable name pairs + node_vars (dict[bpy.types.Node, str]): dictionary containing node to + variable name pairs """ if node_tree.links: @@ -716,7 +788,7 @@ def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, f".outputs[{input_idx}], " f"{out_node_var}.inputs[{output_idx}])\n")) -def create_menu_func(file: TextIO, name: str): +def create_menu_func(file: TextIO, name: str) -> None: """ Creates the menu function @@ -729,7 +801,7 @@ def create_menu_func(file: TextIO, name: str): file.write(f"\tself.layout.operator({name}.bl_idname)\n") file.write("\n") -def create_register_func(file: TextIO, name: str): +def create_register_func(file: TextIO, name: str) -> None: """ Creates the register function @@ -742,7 +814,7 @@ def create_register_func(file: TextIO, name: str): file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") file.write("\n") -def create_unregister_func(file: TextIO, name: str): +def create_unregister_func(file: TextIO, name: str) -> None: """ Creates the unregister function @@ -755,7 +827,7 @@ def create_unregister_func(file: TextIO, name: str): file.write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") file.write("\n") -def create_main_func(file: TextIO): +def create_main_func(file: TextIO) -> None: """ Creates the main function @@ -765,7 +837,7 @@ def create_main_func(file: TextIO): file.write("if __name__ == \"__main__\":\n") file.write("\tregister()") -def save_image(img, addon_dir: str): +def save_image(img: bpy.types.Image, addon_dir: str) -> None: """ Saves an image to an image directory of the add-on @@ -788,7 +860,11 @@ def save_image(img, addon_dir: str): if not os.path.exists(img_path): img.save_render(img_path) -def load_image(img, file: TextIO, inner: str, img_var: str): +def load_image(img: bpy.types.Image, + file: TextIO, + inner: str, + img_var: str + ) -> None: """ Loads an image from the add-on into a blend file and assigns it @@ -828,7 +904,11 @@ def load_image(img, file: TextIO, inner: str, img_var: str): alpha_mode = enum_to_py_str(img.alpha_mode) file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") -def image_user_settings(node, file: TextIO, inner: str, node_var: str): +def image_user_settings(node: bpy.types.Node, + file: TextIO, + inner: str, + node_var: str + ) -> None: """ Replicate the image user of an image node @@ -852,7 +932,7 @@ def image_user_settings(node, file: TextIO, inner: str, node_var: str): file.write((f"{inner}{img_usr_var}.{img_usr_attr} = " f"{getattr(img_usr, img_usr_attr)}\n")) -def zip_addon(zip_dir: str): +def zip_addon(zip_dir: str) -> None: """ Zips up the addon and removes the directory From 38d3dfae81dba008d611f2d3c82c4423a9655ced Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 27 Aug 2023 15:20:50 -0500 Subject: [PATCH 10/72] refactor: curve mappings and color ramps now handled with other node settings --- compositor.py | 12 +++--------- geo_nodes.py | 31 ++++++++++++------------------- materials.py | 14 ++------------ utils.py | 51 ++++++++++++++++++++++++++++++++++----------------- 4 files changed, 51 insertions(+), 57 deletions(-) diff --git a/compositor.py b/compositor.py index 9384b1b..beac3cc 100644 --- a/compositor.py +++ b/compositor.py @@ -56,7 +56,7 @@ ("frame_start", ST.INT)], 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), - ("frame_relative", ST.INT) + ("frame_relative", ST.INT), ("position", ST.ENUM), ("track_name", ST.STRING), #TODO: probably not right ("tracking_object", ST.STRING)], @@ -515,7 +515,7 @@ def is_outermost_node_group(level: int) -> bool: elif self.mode == 'SCRIPT' and level == 0: return True return False - """ + def process_comp_node_group(node_tree, level, node_vars, used_vars): if is_outermost_node_group(level): @@ -585,12 +585,6 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): # save_image(img, addon_dir) # load_image(img, file, inner, f"{node_var}.image") # image_user_settings(node, file, inner, node_var) - - elif node.bl_idname == 'CompositorNodeValToRGB': - color_ramp_settings(node, file, inner, node_var) - - elif node.bl_idname in curve_nodes: - curve_node_settings(node, file, inner, node_var) if self.mode == 'ADDON': set_input_defaults(node, file, inner, node_var, addon_dir) @@ -626,7 +620,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): if self.mode == 'ADDON': zip_addon(zip_dir) - """ + if self.mode == 'SCRIPT': location = "clipboard" else: diff --git a/geo_nodes.py b/geo_nodes.py index ee8c03a..50a4c58 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -135,7 +135,7 @@ 'GeometryNodeCurveEndpointSelection' : [], 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", ST.ENUM), - ("mode", ST.ENUM)], + ("mode", ST.ENUM_SET)], 'GeometryNodeInputSplineCyclic' : [], @@ -160,7 +160,7 @@ 'GeometryNodeSetCurveHandlePositions' : [("mode", ST.ENUM)], 'GeometryNodeCurveSetHandles' : [("handle_type", ST.ENUM), - ("mode", ST.ENUM)], + ("mode", ST.ENUM_SET)], 'GeometryNodeSetSplineCyclic' : [], @@ -549,13 +549,13 @@ def execute(self, context): file = StringIO("") #set to keep track of already created node trees - node_trees = set() + node_trees: set[bpy.types.NodeTree] = set() #dictionary to keep track of node->variable name pairs - node_vars = {} + node_vars: dict[bpy.types.Node, str] = {} #dictionary to keep track of variables->usage count pairs - used_vars = {} + used_vars: dict[str, int] = {} def process_geo_nodes_group(node_tree, level, node_vars, used_vars): nt_var = create_var(node_tree.name, used_vars) @@ -608,19 +608,6 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): 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) @@ -639,7 +626,13 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): attr_domain = enum_to_py_str(si.attribute_domain) file.write((f"{inner}{si_var}.attribute_domain = " f"{attr_domain}\n")) - + """ + 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") + """ if node.bl_idname != 'GeometryNodeSimulationInput': if self.mode == 'ADDON': set_input_defaults(node, file, inner, node_var, addon_dir) diff --git a/materials.py b/materials.py index 5bbdf2c..2cfab43 100644 --- a/materials.py +++ b/materials.py @@ -178,7 +178,7 @@ ("sun_elevation", ST.FLOAT), ("sun_intensity", ST.FLOAT), ("sun_rotation", ST.FLOAT), - ("sun_size", ST.FLOAT) + ("sun_size", ST.FLOAT), ("turbidity", ST.FLOAT)], 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), @@ -277,10 +277,6 @@ ("use_auto_update", ST.BOOL)] } -curve_nodes = {'ShaderNodeFloatCurve', - 'ShaderNodeVectorCurve', - 'ShaderNodeRGBCurve'} - image_nodes = {'ShaderNodeTexEnvironment', 'ShaderNodeTexImage'} @@ -403,7 +399,7 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): node_var = create_node(node, file, inner, nt_var, node_vars, used_vars) - set_settings_defaults(node, node_settings, file, inner, node_var) + set_settings_defaults(node, shader_node_settings, file, inner, node_var) hide_sockets(node, file, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': @@ -426,12 +422,6 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): load_image(img, file, inner, f"{node_var}.image") image_user_settings(node, file, inner, node_var) - 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) - if self.mode == 'ADDON': set_input_defaults(node, file, inner, node_var, addon_dir) else: diff --git a/utils.py b/utils.py index 5103827..15fe3e8 100644 --- a/utils.py +++ b/utils.py @@ -19,6 +19,7 @@ class ST(Enum): Settings Types """ ENUM = auto() + ENUM_SET = auto() STRING = auto() BOOL = auto() INT = auto() @@ -298,6 +299,8 @@ def set_settings_defaults(node: bpy.types.Node, setting_str = f"{inner}{node_var}.{setting}" if type == ST.ENUM: file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") + elif type == ST.ENUM_SET: + file.write(f"{setting_str} = {attr}\n") elif type == ST.STRING: file.write(f"{setting_str} = {str_to_py_str(attr)}\n") elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: @@ -320,6 +323,10 @@ def set_settings_defaults(node: bpy.types.Node, file.write((f"{inner}if {name} in bpy.data.objects:\n")) file.write((f"{inner}\t{node_var}.{setting} = " f"bpy.data.objects[{name}]\n")) + elif type == ST.COLOR_RAMP: + color_ramp_settings(node, file, inner, node_var, setting) + elif type == ST.CURVE_MAPPING: + curve_mapping_settings(node, file, inner, node_var, setting) def hide_sockets(node: bpy.types.Node, file: TextIO, @@ -427,7 +434,8 @@ def group_io_settings(node: bpy.types.Node, def color_ramp_settings(node: bpy.types.Node, file: TextIO, inner: str, - node_var: str + node_var: str, + color_ramp_name: str ) -> None: """ Replicate a color ramp node @@ -437,43 +445,51 @@ def color_ramp_settings(node: bpy.types.Node, file (TextIO): file we're generating the add-on into inner (str): indentation node_var (str): name of the variable we're using for the color ramp + color_ramp_name (str): name of the color ramp to be copied """ - color_ramp = node.color_ramp + color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name) + if not color_ramp: + raise ValueError(f"No color ramp named \"{color_ramp_name}\" found") + #settings + ramp_str = f"{inner}{node_var}.{color_ramp_name}" + color_mode = enum_to_py_str(color_ramp.color_mode) - file.write(f"{inner}{node_var}.color_ramp.color_mode = {color_mode}\n") + file.write(f"{ramp_str}.color_mode = {color_mode}\n") hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) - file.write((f"{inner}{node_var}.color_ramp.hue_interpolation = " + file.write((f"{ramp_str}.hue_interpolation = " f"{hue_interpolation}\n")) interpolation = enum_to_py_str(color_ramp.interpolation) - file.write((f"{inner}{node_var}.color_ramp.interpolation " + file.write((f"{ramp_str}.interpolation " f"= {interpolation}\n")) file.write("\n") #key points - file.write((f"{inner}{node_var}.color_ramp.elements.remove" - f"({node_var}.color_ramp.elements[0])\n")) + file.write(f"{inner}#initialize color ramp elements\n") + file.write((f"{ramp_str}.elements.remove" + f"({ramp_str}.elements[0])\n")) for i, element in enumerate(color_ramp.elements): element_var = f"{node_var}_cre_{i}" if i == 0: file.write(f"{inner}{element_var} = " - f"{node_var}.color_ramp.elements[{i}]\n") + f"{ramp_str}.elements[{i}]\n") file.write(f"{inner}{element_var}.position = {element.position}\n") else: file.write((f"{inner}{element_var} = " - f"{node_var}.color_ramp.elements" + f"{ramp_str}.elements" f".new({element.position})\n")) file.write((f"{inner}{element_var}.alpha = " f"{element.alpha}\n")) color_str = vec4_to_py_str(element.color) file.write((f"{inner}{element_var}.color = {color_str}\n\n")) -def curve_node_settings(node: bpy.types.Node, +def curve_mapping_settings(node: bpy.types.Node, file: TextIO, inner: str, - node_var: str + node_var: str, + curve_mapping_name: str ) -> None: """ Sets defaults for Float, Vector, and Color curves @@ -483,16 +499,16 @@ def curve_node_settings(node: bpy.types.Node, file (TextIO): file we're generating the add-on into inner (str): indentation node_var (str): variable name for the add-on's curve node + curve_mapping_name (str): name of the curve mapping to be set """ - if node.bl_idname == 'CompositorNodeTime': - mapping = node.curve #TODO: ask for consistency here? - else: - mapping = node.mapping + mapping = getattr(node, curve_mapping_name) + if not mapping: + raise ValueError(f"Curve mapping \"{curve_mapping_name}\" not found in node \"{node.bl_idname}\"") #mapping settings file.write(f"{inner}#mapping settings\n") - mapping_var = f"{inner}{node_var}.mapping" + mapping_var = f"{inner}{node_var}.{curve_mapping_name}" #extend extend = enum_to_py_str(mapping.extend) @@ -526,7 +542,8 @@ def curve_node_settings(node: bpy.types.Node, for i, curve in enumerate(mapping.curves): file.write(f"{inner}#curve {i}\n") curve_i = f"{node_var}_curve_{i}" - file.write((f"{inner}{curve_i} = {node_var}.mapping.curves[{i}]\n")) + file.write((f"{inner}{curve_i} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) for j, point in enumerate(curve.points): point_j = f"{inner}{curve_i}_point_{j}" From cf38a9cc42d03626d3c8151b50d4657c89332c47 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 27 Aug 2023 15:40:08 -0500 Subject: [PATCH 11/72] style: better type hinting --- geo_nodes.py | 21 +++++++++++++++------ materials.py | 26 ++++++++++++++++++-------- utils.py | 14 +++++++------- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/geo_nodes.py b/geo_nodes.py index 50a4c58..9d403fb 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -542,7 +542,7 @@ def execute(self, context): create_header(file, nt.name) class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), - lower = False) + lower = False) #TODO: should probably be standardized name to class name util method init_operator(file, class_name, nt_var, nt.name) file.write("\tdef execute(self, context):\n") else: @@ -557,7 +557,18 @@ def execute(self, context): #dictionary to keep track of variables->usage count pairs used_vars: dict[str, int] = {} - def process_geo_nodes_group(node_tree, level, node_vars, used_vars): + def process_geo_nodes_group(node_tree: bpy.types.NodeTree, + level: int, + ) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (bpy.types.NodeTree): node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + nt_var = create_var(node_tree.name, used_vars) outer, inner = make_indents(level) @@ -583,14 +594,12 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): if node.bl_idname == 'GeometryNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in node_trees: - process_geo_nodes_group(node_nt, level + 1, node_vars, - used_vars) + process_geo_nodes_group(node_nt, level + 1) 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) inputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: group_io_settings(node, file, inner, "output", nt_var, node_tree) @@ -673,7 +682,7 @@ def process_geo_nodes_group(node_tree, level, node_vars, used_vars): level = 2 else: level = 0 - process_geo_nodes_group(nt, level, node_vars, used_vars) + process_geo_nodes_group(nt, level) def apply_modifier(): #get object diff --git a/materials.py b/materials.py index 2cfab43..de3ad30 100644 --- a/materials.py +++ b/materials.py @@ -338,13 +338,13 @@ def create_material(indent: str): create_material("") #set to keep track of already created node trees - node_trees = set() + node_trees: set[bpy.types.NodeTree] = set() #dictionary to keep track of node->variable name pairs - node_vars = {} + node_vars: dict[bpy.types.Node, str] = {} - #keeps track of all used variables - used_vars = {} + #keeps track of all used base vareiable names and usage counts + used_vars: dict[str, int] = {} def is_outermost_node_group(level: int) -> bool: if self.mode == 'ADDON' and level == 2: @@ -353,7 +353,18 @@ def is_outermost_node_group(level: int) -> bool: return True return False - def process_mat_node_group(node_tree, level, node_vars, used_vars): + def process_mat_node_group(node_tree: bpy.types.NodeTree, + level: int + ) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (bpy.types.NodeTree): node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + if is_outermost_node_group(level): nt_var = create_var(self.material_name, used_vars) nt_name = self.material_name @@ -392,8 +403,7 @@ 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_trees.add(node_nt) node_var = create_node(node, file, inner, nt_var, node_vars, @@ -440,7 +450,7 @@ def process_mat_node_group(node_tree, level, node_vars, used_vars): level = 2 else: level = 0 - process_mat_node_group(nt, level, node_vars, used_vars) + process_mat_node_group(nt, level) if self.mode == 'ADDON': file.write("\t\treturn {'FINISHED'}\n\n") diff --git a/utils.py b/utils.py index 15fe3e8..fc07b45 100644 --- a/utils.py +++ b/utils.py @@ -7,7 +7,7 @@ import shutil from typing import TextIO, Tuple -image_dir_name = "imgs" +IMAGE_DIR_NAME = "imgs" #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketGeometry', @@ -189,13 +189,13 @@ def init_operator(file: TextIO, name: str, idname: str, label: str) -> None: file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") file.write("\n") -def create_var(name: str, used_vars: dict) -> str: +def create_var(name: str, used_vars: dict[str, int]) -> 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 (dict): dictionary containing variable names and usage counts + used_vars (dict[str, int]): dictionary containing variable names and usage counts Returns: clean_name (str): variable name for the node tree @@ -235,7 +235,7 @@ def create_node(node: bpy.types.Node, inner: str, node_tree_var: str, node_vars: dict[bpy.types.Node, str], - used_vars: set #necessary? + used_vars: dict[str, int] ) -> str: """ Initializes a new node with location, dimension, and label info @@ -247,7 +247,7 @@ def create_node(node: bpy.types.Node, node_tree_var (str): variable name for the node tree node_vars (dict): dictionary containing Node to corresponding variable name pairs - used_vars (set): set of used variable names + used_vars dict[str, int]: dictionary of base variable names to usage counts Returns: node_var (str): variable name for the node @@ -867,7 +867,7 @@ def save_image(img: bpy.types.Image, addon_dir: str) -> None: return #create image dir if one doesn't exist - img_dir = os.path.join(addon_dir, image_dir_name) + img_dir = os.path.join(addon_dir, IMAGE_DIR_NAME) if not os.path.exists(img_dir): os.mkdir(img_dir) @@ -901,7 +901,7 @@ def load_image(img: bpy.types.Image, file.write((f"{inner}base_dir = " f"os.path.dirname(os.path.abspath(__file__))\n")) file.write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{image_dir_name}\", " + f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " f"\"{img_str}\")\n")) file.write((f"{inner}{img_var} = " f"bpy.data.images.load(image_path, check_existing = True)\n")) From c4e43d41f3763a807c880e53e29fa9101fa8da78 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:01:25 -0500 Subject: [PATCH 12/72] feat: basic compositor node tree regeneration --- compositor.py | 5 +++-- materials.py | 10 ++++++---- utils.py | 3 ++- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/compositor.py b/compositor.py index beac3cc..5932e64 100644 --- a/compositor.py +++ b/compositor.py @@ -44,7 +44,8 @@ 'CompositorNodeRLayers' : [("layer", ST.ENUM), ("scene", ST.SCENE)], - 'CompositorNodeRGB' : [], + 'CompositorNodeRGB' : [], #TODO: add output to output handler + #Maybe just make setting handler handle defaults? 'CompositorNodeSceneTime' : [], @@ -268,7 +269,7 @@ ("use_max", ST.BOOL), ("use_min", ST.BOOL)], #why are all these vectors?? TODO: check to make sure it doesn't flip - 'CompositorNodeNormal' : [], + 'CompositorNodeNormal' : [], #TODO: output :( 'CompositorNodeNormalize' : [], diff --git a/materials.py b/materials.py index de3ad30..1060cab 100644 --- a/materials.py +++ b/materials.py @@ -4,6 +4,8 @@ from .utils import * from io import StringIO +MAT_VAR = "mat" + #TODO: move to a json, different ones for each blender version? shader_node_settings : dict[str, list[(str, ST)]] = { # INPUT @@ -328,9 +330,9 @@ def execute(self, context): file = StringIO("") def create_material(indent: str): - file.write((f"{indent}mat = bpy.data.materials.new(" #TODO: see if using mat effects nodes named mat + file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" f"name = {str_to_py_str(self.material_name)})\n")) - file.write(f"{indent}mat.use_nodes = True\n") + file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") if self.mode == 'ADDON': create_material("\t\t") @@ -364,7 +366,7 @@ def process_mat_node_group(node_tree: bpy.types.NodeTree, level (int): number of tabs to use for each line, used with node groups within node groups and script/add-on differences """ - + if is_outermost_node_group(level): nt_var = create_var(self.material_name, used_vars) nt_name = self.material_name @@ -379,7 +381,7 @@ def process_mat_node_group(node_tree: bpy.types.NodeTree, file.write(f"{outer}def {nt_var}_node_group():\n") if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = mat.node_tree\n") + file.write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") file.write(f"{inner}#start with a clean node tree\n") file.write(f"{inner}for node in {nt_var}.nodes:\n") file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") diff --git a/utils.py b/utils.py index fc07b45..6668bb2 100644 --- a/utils.py +++ b/utils.py @@ -99,7 +99,7 @@ def vec1_to_py_str(vec1) -> str: Returns: (str): string representation of the vector """ - return f"({vec1[0]})" + return f"[{vec1[0]}]" def vec2_to_py_str(vec2) -> str: """ @@ -295,6 +295,7 @@ def set_settings_defaults(node: bpy.types.Node, for (setting, type) in settings[node.bl_idname]: attr = getattr(node, setting, None) if not attr: + print(f"\"{node_var}.{setting}\" not found") continue setting_str = f"{inner}{node_var}.{setting}" if type == ST.ENUM: From ba3e4cba55d34562b95a12c9336590002f7d8fac Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 27 Aug 2023 16:46:44 -0500 Subject: [PATCH 13/72] feat: compositor nodes output socket default value settings --- compositor.py | 9 ++++----- utils.py | 11 +++++++---- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/compositor.py b/compositor.py index 5932e64..84580b9 100644 --- a/compositor.py +++ b/compositor.py @@ -44,8 +44,7 @@ 'CompositorNodeRLayers' : [("layer", ST.ENUM), ("scene", ST.SCENE)], - 'CompositorNodeRGB' : [], #TODO: add output to output handler - #Maybe just make setting handler handle defaults? + 'CompositorNodeRGB' : [], 'CompositorNodeSceneTime' : [], @@ -62,7 +61,7 @@ ("track_name", ST.STRING), #TODO: probably not right ("tracking_object", ST.STRING)], - 'CompositorNodeValue' : [], #TODO: double check that outputs set here + 'CompositorNodeValue' : [], # OUTPUT @@ -267,9 +266,9 @@ ("offset", ST.VEC1), ("size", ST.VEC1), ("use_max", ST.BOOL), - ("use_min", ST.BOOL)], #why are all these vectors?? TODO: check to make sure it doesn't flip + ("use_min", ST.BOOL)], - 'CompositorNodeNormal' : [], #TODO: output :( + 'CompositorNodeNormal' : [], 'CompositorNodeNormalize' : [], diff --git a/utils.py b/utils.py index 6668bb2..aebcd34 100644 --- a/utils.py +++ b/utils.py @@ -674,13 +674,16 @@ def set_output_defaults(node: bpy.types.Node, """ output_default_nodes = {'ShaderNodeValue', 'ShaderNodeRGB', - 'ShaderNodeNormal'} + 'ShaderNodeNormal', + 'CompositorNodeValue', + 'CompositorNodeRGB', + 'CompositorNodeNormal'} if node.bl_idname in output_default_nodes: - dv = node.outputs[0].default_value #TODO: see if this is still the case - if node.bl_idname == 'ShaderNodeRGB': + dv = node.outputs[0].default_value + if node.bl_idname in {'ShaderNodeRGB', 'CompositorNodeRGB'}: dv = vec4_to_py_str(list(dv)) - if node.bl_idname == 'ShaderNodeNormal': + if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}: dv = vec3_to_py_str(dv) file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) From 0f4949708d76ea28231719988b5e0d9a6e0c1ded Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:51:51 -0500 Subject: [PATCH 14/72] fix: initialize scene after creating a name to prevent conflicts --- compositor.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/compositor.py b/compositor.py index 84580b9..b6487cf 100644 --- a/compositor.py +++ b/compositor.py @@ -476,10 +476,9 @@ def execute(self, context): file.write("\tdef execute(self, context):\n") else: file = StringIO("") + if self.is_scene: def create_scene(indent: str): - file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") #TODO: see if using scene as name effects nodes named scene - #TODO: wrap in more general unique name util function file.write(f"{indent}# Generate unique scene name\n") file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") @@ -491,6 +490,7 @@ def create_scene(indent: str): file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") file.write(f"{indent}\t\ti += 1\n\n") + file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") @@ -516,8 +516,7 @@ def is_outermost_node_group(level: int) -> bool: return True return False - def process_comp_node_group(node_tree, level, node_vars, used_vars): - + def process_comp_node_group(node_tree, level, node_vars, used_vars): if is_outermost_node_group(level): nt_var = create_var(self.compositor_name, used_vars) nt_name = self.compositor_name From 12dcdbce533c70dade80b07680e03b1a6c36609d Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Sep 2023 17:54:42 -0500 Subject: [PATCH 15/72] style: consistency of compositor names --- __init__.py | 2 +- compositor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/__init__.py b/__init__.py index fc987da..a4bf342 100644 --- a/__init__.py +++ b/__init__.py @@ -42,7 +42,7 @@ def draw(self, context): compositor.NTPCompositorOperator, compositor.NTPCompositorScenesMenu, compositor.NTPCompositorGroupsMenu, - compositor.NTPCompositingPanel, + compositor.NTPCompositorPanel, geo_nodes.NTPGeoNodesOperator, geo_nodes.NTPGeoNodesMenu, geo_nodes.NTPGeoNodesPanel, diff --git a/compositor.py b/compositor.py index b6487cf..ef24c0e 100644 --- a/compositor.py +++ b/compositor.py @@ -667,7 +667,7 @@ def draw(self, context): op.compositor_name = node_group.name op.is_scene = False -class NTPCompositingPanel(bpy.types.Panel): +class NTPCompositorPanel(bpy.types.Panel): bl_label = "Compositor to Python" bl_idname = "NODE_PT_ntp_compositor" bl_space_type = 'NODE_EDITOR' From 6825040ce2117d2dd4eee03125190f1517f66130 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:03:29 -0500 Subject: [PATCH 16/72] refactor: images and image users now use settings function --- compositor.py | 12 +-- geo_nodes.py | 12 +-- materials.py | 11 +-- utils.py | 232 ++++++++++++++++++++++++++------------------------ 4 files changed, 131 insertions(+), 136 deletions(-) diff --git a/compositor.py b/compositor.py index ef24c0e..a28d01e 100644 --- a/compositor.py +++ b/compositor.py @@ -455,6 +455,7 @@ def execute(self, context): #set up names to use in generated addon comp_var = clean_string(self.compositor_name) + addon_dir = None if self.mode == 'ADDON': dir = bpy.path.abspath(context.scene.ntp_options.dir_path) if not dir or dir == "": @@ -562,7 +563,8 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): node_var = create_node(node, file, inner, nt_var, node_vars, used_vars) - set_settings_defaults(node, compositor_node_settings, file, inner, node_var) + set_settings_defaults(node, compositor_node_settings, file, + addon_dir, inner, node_var) hide_sockets(node, file, inner, node_var) if node.bl_idname == 'CompositorNodeGroup': @@ -577,14 +579,6 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: group_io_settings(node, file, inner, "output", nt_var, node_tree) outputs_set = True - - # 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") - # image_user_settings(node, file, inner, node_var) - if self.mode == 'ADDON': set_input_defaults(node, file, inner, node_var, addon_dir) else: diff --git a/geo_nodes.py b/geo_nodes.py index 9d403fb..c5502d6 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -523,6 +523,7 @@ def execute(self, context): #set up names to use in generated addon nt_var = clean_string(nt.name) + addon_dir = None if self.mode == 'ADDON': #find base directory to save new addon dir = bpy.path.abspath(context.scene.ntp_options.dir_path) @@ -608,8 +609,8 @@ def process_geo_nodes_group(node_tree: bpy.types.NodeTree, #create node node_var = create_node(node, file, inner, nt_var, node_vars, used_vars) - set_settings_defaults(node, geo_node_settings, file, inner, - node_var) + set_settings_defaults(node, geo_node_settings, file, addon_dir, + inner, node_var) hide_sockets(node, file, inner, node_var) if node.bl_idname == 'GeometryNodeGroup': @@ -635,13 +636,6 @@ def process_geo_nodes_group(node_tree: bpy.types.NodeTree, attr_domain = enum_to_py_str(si.attribute_domain) file.write((f"{inner}{si_var}.attribute_domain = " f"{attr_domain}\n")) - """ - 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") - """ if node.bl_idname != 'GeometryNodeSimulationInput': if self.mode == 'ADDON': set_input_defaults(node, file, inner, node_var, addon_dir) diff --git a/materials.py b/materials.py index 1060cab..1566ce4 100644 --- a/materials.py +++ b/materials.py @@ -307,6 +307,7 @@ def execute(self, context): #set up names to use in generated addon mat_var = clean_string(self.material_name) + addon_dir = None if self.mode == 'ADDON': dir = bpy.path.abspath(context.scene.ntp_options.dir_path) if not dir or dir == "": @@ -411,7 +412,8 @@ def process_mat_node_group(node_tree: bpy.types.NodeTree, node_var = create_node(node, file, inner, nt_var, node_vars, used_vars) - set_settings_defaults(node, shader_node_settings, file, inner, node_var) + set_settings_defaults(node, shader_node_settings, file, + addon_dir, inner, node_var) hide_sockets(node, file, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': @@ -427,13 +429,6 @@ def process_mat_node_group(node_tree: bpy.types.NodeTree, group_io_settings(node, file, inner, "output", nt_var, node_tree) outputs_set = True - 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") - image_user_settings(node, file, inner, node_var) - if self.mode == 'ADDON': set_input_defaults(node, file, inner, node_var, addon_dir) else: diff --git a/utils.py b/utils.py index aebcd34..2360636 100644 --- a/utils.py +++ b/utils.py @@ -18,6 +18,7 @@ class ST(Enum): """ Settings Types """ + # Primitives ENUM = auto() ENUM_SET = auto() STRING = auto() @@ -28,13 +29,20 @@ class ST(Enum): VEC2 = auto() VEC3 = auto() VEC4 = auto() - MATERIAL = auto() #Could use a look - OBJECT = auto() #Could take a looking at + + # Special settings + COLOR_RAMP = auto() + CURVE_MAPPING = auto() + + # Asset Library + MATERIAL = auto() # Handle with asset library + OBJECT = auto() # Handle with asset library + + # Image IMAGE = auto() #needs refactor - IMAGE_USER = auto() #unimplemented + IMAGE_USER = auto() #needs refactor MOVIE_CLIP = auto() #unimplmented - COLOR_RAMP = auto() #needs refactor - CURVE_MAPPING = auto() #needs refactor + TEXTURE = auto() #unimplemented TEXT = auto() #unimplemented SCENE = auto() #unimplemented @@ -277,7 +285,8 @@ def create_node(node: bpy.types.Node, def set_settings_defaults(node: bpy.types.Node, settings: dict[str, list[(str, ST)]], - file: TextIO, + file: TextIO, + addon_dir: str, inner: str, node_var: str ) -> None: @@ -288,16 +297,17 @@ def set_settings_defaults(node: bpy.types.Node, node (bpy.types.Node): the node object we're copying settings from settings (dict): a predefined dictionary of all settings every node has file (TextIO): file we're generating the add-on into + addon_dir (str): directory that the addon is saved into inner (str): indentation node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname in settings: - for (setting, type) in settings[node.bl_idname]: - attr = getattr(node, setting, None) + for (attr_name, type) in settings[node.bl_idname]: + attr = getattr(node, attr_name, None) if not attr: - print(f"\"{node_var}.{setting}\" not found") + print(f"\"{node_var}.{attr_name}\" not found") continue - setting_str = f"{inner}{node_var}.{setting}" + setting_str = f"{inner}{node_var}.{attr_name}" if type == ST.ENUM: file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") elif type == ST.ENUM_SET: @@ -317,17 +327,24 @@ def set_settings_defaults(node: bpy.types.Node, elif type == ST.MATERIAL: name = str_to_py_str(attr.name) file.write((f"{inner}if {name} in bpy.data.materials:\n")) - file.write((f"{inner}\t{node_var}.{setting} = " + file.write((f"{inner}\t{node_var}.{attr_name} = " f"bpy.data.materials[{name}]\n")) elif type == ST.OBJECT: name = str_to_py_str(attr.name) file.write((f"{inner}if {name} in bpy.data.objects:\n")) - file.write((f"{inner}\t{node_var}.{setting} = " + file.write((f"{inner}\t{node_var}.{attr_name} = " f"bpy.data.objects[{name}]\n")) elif type == ST.COLOR_RAMP: - color_ramp_settings(node, file, inner, node_var, setting) + color_ramp_settings(node, file, inner, node_var, attr_name) elif type == ST.CURVE_MAPPING: - curve_mapping_settings(node, file, inner, node_var, setting) + curve_mapping_settings(node, file, inner, node_var, attr_name) + elif type == ST.IMAGE: + if addon_dir is not None and attr is not None: + if attr.source in {'FILE', 'GENERATED', 'TILED'}: + save_image(attr, addon_dir) + load_image(attr, file, inner, f"{node_var}.{attr_name}") + elif type == ST.IMAGE_USER: + image_user_settings(attr, file, inner, f"{node_var}.{attr_name}") def hide_sockets(node: bpy.types.Node, file: TextIO, @@ -563,6 +580,96 @@ def curve_mapping_settings(node: bpy.types.Node, file.write(f"{inner}#update curve after changes\n") file.write(f"{mapping_var}.update()\n") +def save_image(img: bpy.types.Image, addon_dir: str) -> None: + """ + Saves an image to an image directory of the add-on + + Parameters: + img (bpy.types.Image): image to be saved + addon_dir (str): directory of the addon + """ + + if img is None: + return + + #create image dir if one doesn't exist + img_dir = os.path.join(addon_dir, IMAGE_DIR_NAME) + if not os.path.exists(img_dir): + os.mkdir(img_dir) + + #save the image + img_str = img_to_py_str(img) + img_path = f"{img_dir}/{img_str}" + if not os.path.exists(img_path): + img.save_render(img_path) + +def load_image(img: bpy.types.Image, + file: TextIO, + inner: str, + img_var: str + ) -> None: + """ + Loads an image from the add-on into a blend file and assigns it + + Parameters: + img (bpy.types.Image): Blender image from the original node group + file (TextIO): file for the generated add-on + inner (str): indentation string + img_var (str): variable name to be used for the image + """ + + if img is None: + return + + img_str = img_to_py_str(img) + + #TODO: convert to special variables + file.write(f"{inner}#load image {img_str}\n") + file.write((f"{inner}base_dir = " + f"os.path.dirname(os.path.abspath(__file__))\n")) + file.write((f"{inner}image_path = " + f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " + f"\"{img_str}\")\n")) + file.write((f"{inner}{img_var} = " + f"bpy.data.images.load(image_path, check_existing = True)\n")) + + #copy image settings + file.write(f"{inner}#set image settings\n") + + #source + source = enum_to_py_str(img.source) + file.write(f"{inner}{img_var}.source = {source}\n") + + #color space settings + color_space = enum_to_py_str(img.colorspace_settings.name) + file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") + + #alpha mode + alpha_mode = enum_to_py_str(img.alpha_mode) + file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") + +def image_user_settings(img_user: bpy.types.ImageUser, + file: TextIO, + inner: str, + img_user_var: str + ) -> None: + """ + Replicate the image user of an image node + + Parameters + img_usr (bpy.types.ImageUser): image user to be copied + file (TextIO): file we're generating the add-on into + inner (str): indentation + img_usr_var (str): variable name for the generated image user + """ + + img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", + "frame_start", "tile", "use_auto_refresh", "use_cyclic"] + + for img_usr_attr in img_usr_attrs: + file.write((f"{inner}{img_user_var}.{img_usr_attr} = " + f"{getattr(img_user, img_usr_attr)}\n")) + def set_input_defaults(node: bpy.types.Node, file: TextIO, inner: str, @@ -857,102 +964,7 @@ def create_main_func(file: TextIO) -> None: """ file.write("if __name__ == \"__main__\":\n") file.write("\tregister()") - -def save_image(img: bpy.types.Image, addon_dir: str) -> None: - """ - Saves an image to an image directory of the add-on - - Parameters: - img (bpy.types.Image): image to be saved - addon_dir (str): directory of the addon - """ - - if img is None: - return - - #create image dir if one doesn't exist - img_dir = os.path.join(addon_dir, IMAGE_DIR_NAME) - if not os.path.exists(img_dir): - os.mkdir(img_dir) - - #save the image - img_str = img_to_py_str(img) - img_path = f"{img_dir}/{img_str}" - if not os.path.exists(img_path): - img.save_render(img_path) - -def load_image(img: bpy.types.Image, - file: TextIO, - inner: str, - img_var: str - ) -> None: - """ - Loads an image from the add-on into a blend file and assigns it - - Parameters: - img (bpy.types.Image): Blender image from the original node group - file (TextIO): file for the generated add-on - inner (str): indentation string - img_var (str): variable name to be used for the image - """ - - if img is None: - return - - img_str = img_to_py_str(img) - - file.write(f"{inner}#load image {img_str}\n") - file.write((f"{inner}base_dir = " - f"os.path.dirname(os.path.abspath(__file__))\n")) - file.write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " - f"\"{img_str}\")\n")) - file.write((f"{inner}{img_var} = " - f"bpy.data.images.load(image_path, check_existing = True)\n")) - - #copy image settings - file.write(f"{inner}#set image settings\n") - - #source - source = enum_to_py_str(img.source) - file.write(f"{inner}{img_var}.source = {source}\n") - - #color space settings - color_space = enum_to_py_str(img.colorspace_settings.name) - file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") - - #alpha mode - alpha_mode = enum_to_py_str(img.alpha_mode) - file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") - -def image_user_settings(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str - ) -> None: - """ - Replicate the image user of an image node - - Parameters - node (bpy.types.Node): node object we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): name of the variable we're using for the color ramp - """ - - if not hasattr(node, "image_user"): - raise ValueError("Node must have attribute \"image_user\"") - - img_usr = node.image_user - img_usr_var = f"{node_var}.image_user" - - img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", - "frame_start", "tile", "use_auto_refresh", "use_cyclic"] - - for img_usr_attr in img_usr_attrs: - file.write((f"{inner}{img_usr_var}.{img_usr_attr} = " - f"{getattr(img_usr, img_usr_attr)}\n")) - + def zip_addon(zip_dir: str) -> None: """ Zips up the addon and removes the directory From f12189d809bc1154a17a53a3942521a24dbce1d6 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Sep 2023 19:15:45 -0500 Subject: [PATCH 17/72] typo: material used_vars comment --- materials.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/materials.py b/materials.py index 1566ce4..75be899 100644 --- a/materials.py +++ b/materials.py @@ -346,7 +346,7 @@ def create_material(indent: str): #dictionary to keep track of node->variable name pairs node_vars: dict[bpy.types.Node, str] = {} - #keeps track of all used base vareiable names and usage counts + #keeps track of all used base variable names and usage counts used_vars: dict[str, int] = {} def is_outermost_node_group(level: int) -> bool: From 0921c25d580e27321c48ad79b210c72a592937b5 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Sep 2023 18:31:58 -0500 Subject: [PATCH 18/72] fix: hue correction node removes excess points --- utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/utils.py b/utils.py index 2360636..ce6658d 100644 --- a/utils.py +++ b/utils.py @@ -562,6 +562,14 @@ def curve_mapping_settings(node: bpy.types.Node, curve_i = f"{node_var}_curve_{i}" file.write((f"{inner}{curve_i} = " f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) + + # Remove default points when CurveMap is initialized with more than + # two points (just CompositorNodeHueCorrect) + if (node.bl_idname == 'CompositorNodeHueCorrect'): + file.write((f"{inner}for i in " + f"range(len({curve_i}.points.values()) - 1, 1, -1):\n")) + file.write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") + for j, point in enumerate(curve.points): point_j = f"{inner}{curve_i}_point_{j}" From e5da19622cfbf0182febff037af5f9e287547c97 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Sep 2023 19:06:33 -0500 Subject: [PATCH 19/72] fix: 0 settings now initialized --- compositor.py | 4 ++-- utils.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/compositor.py b/compositor.py index a28d01e..385c4bd 100644 --- a/compositor.py +++ b/compositor.py @@ -314,8 +314,8 @@ ("frame_duration", ST.INT), ("frame_offset", ST.INT), ("frame_start", ST.INT), - ("has_layers", ST.BOOL), #TODO: readonly? - ("has_views", ST.BOOL), #TODO: readonly? + #("has_layers", ST.BOOL), #TODO: readonly? + #("has_views", ST.BOOL), #TODO: readonly? ("image", ST.IMAGE), ("layer", ST.ENUM), ("layer_name", ST.ENUM), diff --git a/utils.py b/utils.py index ce6658d..71a1334 100644 --- a/utils.py +++ b/utils.py @@ -304,12 +304,13 @@ def set_settings_defaults(node: bpy.types.Node, if node.bl_idname in settings: for (attr_name, type) in settings[node.bl_idname]: attr = getattr(node, attr_name, None) - if not attr: + if attr is None: print(f"\"{node_var}.{attr_name}\" not found") continue setting_str = f"{inner}{node_var}.{attr_name}" if type == ST.ENUM: - file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") + if attr != '': + file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") elif type == ST.ENUM_SET: file.write(f"{setting_str} = {attr}\n") elif type == ST.STRING: From d90b6d80a86ccdb7bcd93f4cc319586089fc266a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:27:32 -0500 Subject: [PATCH 20/72] fix: color balance now only initializes needed members --- compositor.py | 37 ++++++++++++++++++++++++++----------- utils.py | 15 +++++++++++++++ 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/compositor.py b/compositor.py index 385c4bd..1bc537f 100644 --- a/compositor.py +++ b/compositor.py @@ -92,19 +92,19 @@ 'CompositorNodeBrightContrast' : [("use_premultiply", ST.BOOL)], 'CompositorNodeColorBalance' : [("correction_method", ST.ENUM), - ("gain", ST.VEC3), - ("gamma", ST.VEC3), - ("lift", ST.VEC3), - ("offset", ST.VEC3), + ("gain", ST.COLOR), + ("gamma", ST.COLOR), + ("lift", ST.COLOR), + ("offset", ST.COLOR), ("offset_basis", ST.FLOAT), - ("power", ST.VEC3), - ("slope", ST.VEC3)], + ("power", ST.COLOR), + ("slope", ST.COLOR)], 'CompositorNodeColorCorrection' : [("blue", ST.BOOL), ("green", ST.BOOL), ("highlights_contrast", ST.FLOAT), ("highlights_gain", ST.FLOAT), - ("midtones_lift", ST.FLOAT), + ("midtones_lift", ST.FLOAT), ("midtones_saturation", ST.FLOAT), ("midtones_start", ST.FLOAT), ("red", ST.BOOL), @@ -309,7 +309,7 @@ ("unspill_red", ST.FLOAT), ("use_unspill", ST.BOOL)], - 'CompositorNodeCryptomatteV2' : [("add", ST.VEC3), + 'CompositorNodeCryptomatteV2' : [("add", ST.COLOR), ("entries", ST.CRYPTOMATTE_ENTRIES), ("frame_duration", ST.INT), ("frame_offset", ST.INT), @@ -320,16 +320,16 @@ ("layer", ST.ENUM), ("layer_name", ST.ENUM), ("matte_id", ST.STRING), - ("remove", ST.VEC3), + ("remove", ST.COLOR), ("scene", ST.SCENE), ("source", ST.ENUM), ("use_auto_refresh", ST.BOOL), ("use_cyclic", ST.BOOL), ("view", ST.ENUM)], - 'CompositorNodeCryptomatte' : [("add", ST.VEC3), #TODO: may need special handling + 'CompositorNodeCryptomatte' : [("add", ST.COLOR), #TODO: may need special handling ("matte_id", ST.STRING), - ("remove", ST.VEC3)], + ("remove", ST.COLOR)], 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), ("tolerance", ST.FLOAT)], @@ -563,6 +563,21 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): node_var = create_node(node, file, inner, nt_var, node_vars, used_vars) + if node.bl_idname == 'CompositorNodeColorBalance': + if node.correction_method == 'LIFT_GAMMA_GAIN': + lst = [("correction_method", ST.ENUM), + ("gain", ST.COLOR), + ("gamma", ST.COLOR), + ("lift", ST.COLOR)] + else: + lst = [("correction_method", ST.ENUM), + ("offset", ST.COLOR), + ("offset_basis", ST.FLOAT), + ("power", ST.COLOR), + ("slope", ST.COLOR)] + + compositor_node_settings['CompositorNodeColorBalance'] = lst + set_settings_defaults(node, compositor_node_settings, file, addon_dir, inner, node_var) hide_sockets(node, file, inner, node_var) diff --git a/utils.py b/utils.py index 71a1334..a2b0cf6 100644 --- a/utils.py +++ b/utils.py @@ -29,6 +29,7 @@ class ST(Enum): VEC2 = auto() VEC3 = auto() VEC4 = auto() + COLOR = auto() # Special settings COLOR_RAMP = auto() @@ -145,6 +146,18 @@ def vec4_to_py_str(vec4) -> str: """ return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})" +def color_to_py_str(color: mathutils.Color) -> str: + """ + Converts a mathutils.Color into a string + + Parameters: + color (mathutils.Color): a Blender color + + Returns: + (str): string version + """ + return f"mathutils.Color(({color.r}, {color.g}, {color.b}))" + def img_to_py_str(img : bpy.types.Image) -> str: """ Converts a Blender image into its string @@ -325,6 +338,8 @@ def set_settings_defaults(node: bpy.types.Node, file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") elif type == ST.VEC4: file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + elif type == ST.COLOR: + file.write(f"{setting_str} = {color_to_py_str(attr)}\n") elif type == ST.MATERIAL: name = str_to_py_str(attr.name) file.write((f"{inner}if {name} in bpy.data.materials:\n")) From 6e18e33ae8d7cc92a364bad7cc35247995fd7f9e Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Sep 2023 20:39:46 -0500 Subject: [PATCH 21/72] fix: add missing color correction attributes --- compositor.py | 41 +++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/compositor.py b/compositor.py index 1bc537f..6ff9e03 100644 --- a/compositor.py +++ b/compositor.py @@ -100,19 +100,36 @@ ("power", ST.COLOR), ("slope", ST.COLOR)], - 'CompositorNodeColorCorrection' : [("blue", ST.BOOL), - ("green", ST.BOOL), - ("highlights_contrast", ST.FLOAT), - ("highlights_gain", ST.FLOAT), - ("midtones_lift", ST.FLOAT), + 'CompositorNodeColorCorrection' : [("red", ST.BOOL), + ("green", ST.BOOL), + ("blue", ST.BOOL), + #master + ("master_saturation", ST.FLOAT), + ("master_contrast", ST.FLOAT), + ("master_gamma", ST.FLOAT), + ("master_gain", ST.FLOAT), + ("master_lift", ST.FLOAT), + #highlights + ("highlights_saturation", ST.FLOAT), + ("highlights_contrast", ST.FLOAT), + ("highlights_gamma", ST.FLOAT), + ("highlights_gain", ST.FLOAT), + ("highlights_lift", ST.FLOAT), + #midtones ("midtones_saturation", ST.FLOAT), - ("midtones_start", ST.FLOAT), - ("red", ST.BOOL), - ("shadows_contrast", ST.FLOAT), - ("shadows_gain", ST.FLOAT), - ("shadows_gamma", ST.FLOAT), - ("shadows_lift", ST.FLOAT), - ("shadows_saturation", ST.FLOAT)], + ("midtones_contrast", ST.FLOAT), + ("midtones_gamma", ST.FLOAT), + ("midtones_gain", ST.FLOAT), + ("midtones_lift", ST.FLOAT), + #shadows + ("shadows_saturation", ST.FLOAT), + ("shadows_contrast", ST.FLOAT), + ("shadows_gamma", ST.FLOAT), + ("shadows_gain", ST.FLOAT), + ("shadows_lift", ST.FLOAT), + #midtones location + ("midtones_start", ST.FLOAT), + ("midtones_end", ST.FLOAT)], 'CompositorNodeExposure' : [], From 9a2388363201325551660ef7fc94ba86dc390902 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 10 Sep 2023 03:02:46 -0500 Subject: [PATCH 22/72] docs: add downloads badge to README --- docs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index e82bbb8..1edd431 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ ![Node To Python Logo](./img/ntp.jpg "Node To Python Logo") -[![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) +[![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) ![](https://img.shields.io/github/downloads/BrendanParmer/NodeToPython/total.svg) ## About 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. From 1c8f39ca70fc010a42ffd50686d9cb1a64301bde Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 10 Sep 2023 17:32:55 -0500 Subject: [PATCH 23/72] refactor: started geo node refactor, broke operator into more functions --- compositor.py | 2 +- geo_nodes.py | 398 ++++++++++++++++++++++++++++---------------------- materials.py | 2 +- utils.py | 2 +- 4 files changed, 226 insertions(+), 178 deletions(-) diff --git a/compositor.py b/compositor.py index 6ff9e03..bb4d1e0 100644 --- a/compositor.py +++ b/compositor.py @@ -597,7 +597,7 @@ def process_comp_node_group(node_tree, level, node_vars, used_vars): set_settings_defaults(node, compositor_node_settings, file, addon_dir, inner, node_var) - hide_sockets(node, file, inner, node_var) + hide_hidden_sockets(node, file, inner, node_var) if node.bl_idname == 'CompositorNodeGroup': if node.node_tree is not None: diff --git a/geo_nodes.py b/geo_nodes.py index c5502d6..8de5d60 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -501,21 +501,222 @@ ("type", ST.ENUM)] } +class NTP_GeoNodeTree: + def __init__(self, node_tree: bpy.types.GeometryNodeTree, var_name: str): + self.node_tree: bpy.types.GeometryNodeTree = node_tree + self.var_name: str = var_name + + self.inputs_set: bool = False + self.outputs_set: bool = False + self.sim_inputs: list[bpy.types.GeometryNodeSimulationInput] = [] + +class NTP_GeoNode: + def __init__(self, node: bpy.types.GeometryNode, var_name: str): + self.node = node + self.var_name = var_name + class NTPGeoNodesOperator(bpy.types.Operator): bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} - mode : bpy.props.EnumProperty( + mode: bpy.props.EnumProperty( name = "Mode", items = [ ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") + ('ADDON', "Addon", "Create a full addon") ] ) geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") + def __init__(self): + # File/string the add-on/script is generated into + self._file: TextIO = None + + # Path to the directory of the zip file + self._zip_dir: str = None + + # Path to the directory for the generated addon + self._addon_dir: str = None + + # Set to keep track of already created node trees + self._node_trees: set[bpy.types.NodeTree] = set() + + # Dictionary to keep track of node->variable name pairs + self._node_vars: dict[bpy.types.Node, str] = {} + + # Dictionary to keep track of variables->usage count pairs + self._used_vars: dict[str, int] = {} + + def _setup_addon_directories(self, context: bpy.types.Context, nt_var: str): + #find base directory to save new addon + 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!")) #TODO: Still valid?? + return {'CANCELLED'} + + self._zip_dir = os.path.join(dir, nt_var) + self._addon_dir = os.path.join(self._zip_dir, nt_var) + + if not os.path.exists(self._addon_dir): + os.makedirs(self._addon_dir) + + def _process_geo_node_group_node(self, node: bpy.types.GeometryNodeGroup, + level: int, inner: str, node_var: str + ) -> None: + nt = node.node_tree + if nt is not None: + if nt not in self._node_trees: + self._process_geo_node_tree(nt, level + 1) + self._file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[{str_to_py_str(nt.name)}]\n")) + + def _process_sim_output_node(self, node: bpy.types.GeometryNodeSimulationOutput, + inner: str, node_var: str) -> None: + self._file.write(f"{inner}# Remove generated sim state items\n") + self._file.write(f"{inner}for item in {node_var}.state_items:\n") + self._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) + self._file.write(f"{inner}#create SSI {name}\n") + self._file.write((f"{inner}{node_var}.state_items.new" + f"({socket_type}, {name})\n")) + si_var = f"{node_var}.state_items[{i}]" + attr_domain = enum_to_py_str(si.attribute_domain) + self._file.write((f"{inner}{si_var}.attribute_domain " + f"= {attr_domain}\n")) + + def _set_socket_defaults(self, node: bpy.types.GeometryNode, inner: str, + node_var: str) -> None: + if self.mode == 'ADDON': + set_input_defaults(node, self._file, inner, node_var, self._addon_dir) + else: + set_input_defaults(node, self._file, inner, node_var) + set_output_defaults(node, self._file, inner, node_var) + + def _process_node(self, node: bpy.types.GeometryNode, + ntp_node_tree: NTP_GeoNodeTree, + inner: str, level: int) -> None: + #create node + node_var: str = create_node(node, self._file, inner, ntp_node_tree.var_name, + self._node_vars, self._used_vars) + set_settings_defaults(node, geo_node_settings, self._file, + self._addon_dir, inner, node_var) + if node.bl_idname == 'GeometryNodeGroup': + self._process_geo_node_group_node(node, level, inner, node_var) + + elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: + group_io_settings(node, self._file, inner, "input", ntp_node_tree.var_name, + ntp_node_tree.node_tree) #TODO: convert to using NTP_NodeTrees + ntp_node_tree.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: + group_io_settings(node, self._file, inner, "output", + ntp_node_tree.var_name, ntp_node_tree.node_tree) + ntp_node_tree.outputs_set = True + + elif node.bl_idname == 'GeometryNodeSimulationInput': + ntp_node_tree.sim_inputs.append(node) + + elif node.bl_idname == 'GeometryNodeSimulationOutput': + self._process_sim_output_node(node, inner, node_var) + + hide_hidden_sockets(node, self._file, inner, node_var) + + if node.bl_idname != 'GeometryNodeSimulationInput': + self._set_socket_defaults(node, inner, node_var) + + def _process_sim_zones(self, sim_inputs: list[bpy.types.GeometryNodeSimulationInput], + inner: str) -> None: + """ + Recreate simulation zones + sim_inputs (list[bpy.types.GeometryNodeSimulationInput]): list of + simulation input nodes + inner (str): identation string + """ + for sim_input in sim_inputs: + sim_output = sim_input.paired_output + + sim_input_var = self._node_vars[sim_input] + sim_output_var = self._node_vars[sim_output] + self._file.write((f"{inner}{sim_input_var}.pair_with_output" + f"({sim_output_var})\n")) + + #must set defaults after paired with output + self._set_socket_defaults(sim_input, inner, sim_input_var) + self._set_socket_defaults(sim_output, inner, sim_output_var) + + def _process_geo_node_tree(self, node_tree: bpy.types.GeometryNodeTree, + level: int) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (bpy.types.NodeTree): geometry node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + + nt_var = create_var(node_tree.name, self._used_vars) + outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? + + #initialize node group + self._file.write(f"{outer}#initialize {nt_var} node group\n") + self._file.write(f"{outer}def {nt_var}_node_group():\n") + self._file.write((f"{inner}{nt_var} = bpy.data.node_groups.new(" + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n")) + self._file.write("\n") + + #initialize nodes + self._file.write(f"{inner}#initialize {nt_var} nodes\n") + + ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) + for node in node_tree.nodes: + self._process_node(node, ntp_nt, inner, level) + + self._process_sim_zones(ntp_nt.sim_inputs, inner) + + #set look of nodes + set_parents(node_tree, self._file, inner, self._node_vars) + set_locations(node_tree, self._file, inner, self._node_vars) + set_dimensions(node_tree, self._file, inner, self._node_vars) + + #create connections + init_links(node_tree, self._file, inner, nt_var, self._node_vars) + + self._file.write(f"{inner}return {nt_var}\n") + + #create node group + self._file.write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") + return self._used_vars + + def _apply_modifier(self, nt: bpy.types.GeometryNodeTree, nt_var: str): + #get object + self._file.write(f"\t\tname = bpy.context.object.name\n") + self._file.write(f"\t\tobj = bpy.data.objects[name]\n") + + #set modifier to the one we just created + mod_name = str_to_py_str(nt.name) + self._file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " + f"type = 'NODES')\n")) + self._file.write(f"\t\tmod.node_group = {nt_var}\n") + + def _report_finished(self): + #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}") + def execute(self, context): #find node group to replicate nt = bpy.data.node_groups[self.geo_nodes_group_name] @@ -523,194 +724,41 @@ def execute(self, context): #set up names to use in generated addon nt_var = clean_string(nt.name) - addon_dir = None if self.mode == 'ADDON': - #find base directory to save new addon - 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(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") + self._setup_addon_directories(context, nt_var) + + self._file = open(f"{self._addon_dir}/__init__.py", "w") - create_header(file, nt.name) + create_header(self._file, nt.name) class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), lower = False) #TODO: should probably be standardized name to class name util method - init_operator(file, class_name, nt_var, nt.name) - file.write("\tdef execute(self, context):\n") + init_operator(self._file, class_name, nt_var, nt.name) + self._file.write("\tdef execute(self, context):\n") else: - file = StringIO("") - - #set to keep track of already created node trees - node_trees: set[bpy.types.NodeTree] = set() - - #dictionary to keep track of node->variable name pairs - node_vars: dict[bpy.types.Node, str] = {} - - #dictionary to keep track of variables->usage count pairs - used_vars: dict[str, int] = {} - - def process_geo_nodes_group(node_tree: bpy.types.NodeTree, - level: int, - ) -> None: - """ - Generates a Python function to recreate a node tree - - Parameters: - node_tree (bpy.types.NodeTree): node tree to be recreated - level (int): number of tabs to use for each line, used with - node groups within node groups and script/add-on differences - """ - - nt_var = create_var(node_tree.name, used_vars) - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'GeometryNodeTree\', " - f"name = {str_to_py_str(node_tree.name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - sim_inputs = [] - - for node in node_tree.nodes: - if node.bl_idname == 'GeometryNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in node_trees: - process_geo_nodes_group(node_nt, level + 1) - 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) - inputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - 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) - set_settings_defaults(node, geo_node_settings, file, addon_dir, - inner, 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 == '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) - file.write(f"{inner}#create SSI {name}\n") - file.write((f"{inner}{node_var}.state_items.new" - f"({socket_type}, {name})\n")) - si_var = f"{node_var}.state_items[{i}]" - attr_domain = enum_to_py_str(si.attribute_domain) - file.write((f"{inner}{si_var}.attribute_domain = " - f"{attr_domain}\n")) - if node.bl_idname != 'GeometryNodeSimulationInput': - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - #create simulation zones - for sim_input in sim_inputs: - sim_input_var = node_vars[sim_input] - sim_output_var = node_vars[sim_input.paired_output] - file.write((f"{inner}{sim_input_var}.pair_with_output" - f"({sim_output_var})\n")) - - #must set defaults after paired with output - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - 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") - - #create node group - file.write((f"\n{outer}{nt_var} = " - f"{nt_var}_node_group()\n\n")) - return used_vars + self._file = StringIO("") if self.mode == 'ADDON': level = 2 else: level = 0 - process_geo_nodes_group(nt, level) - - def apply_modifier(): - #get object - file.write(f"\t\tname = bpy.context.object.name\n") - file.write(f"\t\tobj = bpy.data.objects[name]\n") - - #set modifier to the one we just created - mod_name = str_to_py_str(nt.name) - file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')\n")) - file.write(f"\t\tmod.node_group = {nt_var}\n") - if self.mode == 'ADDON': - apply_modifier() + self._process_geo_node_tree(nt, level) - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) + if self.mode == 'ADDON': + self._apply_modifier(nt, nt_var) + self._file.write("\t\treturn {'FINISHED'}\n\n") + create_menu_func(self._file, class_name) + create_register_func(self._file, class_name) + create_unregister_func(self._file, class_name) + create_main_func(self._file) else: - context.window_manager.clipboard = file.getvalue() - file.close() + context.window_manager.clipboard = self._file.getvalue() + self._file.close() if self.mode == 'ADDON': - zip_addon(zip_dir) + zip_addon(self._zip_dir) + + self._report_finished() - #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): diff --git a/materials.py b/materials.py index 75be899..053e9b6 100644 --- a/materials.py +++ b/materials.py @@ -414,7 +414,7 @@ def process_mat_node_group(node_tree: bpy.types.NodeTree, set_settings_defaults(node, shader_node_settings, file, addon_dir, inner, node_var) - hide_sockets(node, file, inner, node_var) + hide_hidden_sockets(node, file, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': if node.node_tree is not None: diff --git a/utils.py b/utils.py index a2b0cf6..ae432d8 100644 --- a/utils.py +++ b/utils.py @@ -362,7 +362,7 @@ def set_settings_defaults(node: bpy.types.Node, elif type == ST.IMAGE_USER: image_user_settings(attr, file, inner, f"{node_var}.{attr_name}") -def hide_sockets(node: bpy.types.Node, +def hide_hidden_sockets(node: bpy.types.Node, file: TextIO, inner: str, node_var: str From f1b206a97149a89cccc66ebd28cbb485444a1c01 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 10 Sep 2023 23:06:38 -0500 Subject: [PATCH 24/72] refactor: UI node tree lists more standardized --- compositor.py | 18 +++++++++--------- geo_nodes.py | 24 ++++++++++-------------- materials.py | 10 ++++++---- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/compositor.py b/compositor.py index bb4d1e0..4a1b222 100644 --- a/compositor.py +++ b/compositor.py @@ -671,10 +671,10 @@ def draw(self, context): layout.operator_context = 'INVOKE_DEFAULT' for scene in bpy.data.scenes: if scene.node_tree: - op = layout.operator(NTPCompositorOperator.bl_idname, text=scene.name) + op = layout.operator(NTPCompositorOperator.bl_idname, + text=scene.name) op.compositor_name = scene.name op.is_scene = True - print(scene.node_tree.name) class NTPCompositorGroupsMenu(bpy.types.Menu): bl_idname = "NODE_MT_ntp_comp_groups" @@ -688,8 +688,9 @@ def draw(self, context): layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' for node_group in bpy.data.node_groups: - if isinstance(node_group, bpy.types.CompositorNodeTree): - op = layout.operator(NTPCompositorOperator.bl_idname, text=node_group.name) + if node_group.bl_idname == 'CompositorNodeTree': + op = layout.operator(NTPCompositorOperator.bl_idname, + text=node_group.name) op.compositor_name = node_group.name op.is_scene = False @@ -712,9 +713,8 @@ def draw(self, context): layout = self.layout scenes_row = layout.row() - # Disables menu when there are no materials - scenes = [scene for scene in bpy.data.scenes - if scene.node_tree is not None] + # Disables menu when there are no compositing node groups + scenes = [scene for scene in bpy.data.scenes if scene.node_tree] scenes_exist = len(scenes) > 0 scenes_row.enabled = scenes_exist @@ -724,8 +724,8 @@ def draw(self, context): text="Scene Compositor Nodes") groups_row = layout.row() - groups = [ng for ng in bpy.data.node_groups - if isinstance(ng, bpy.types.CompositorNodeTree)] + groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'CompositorNodeTree'] groups_exist = len(groups) > 0 groups_row.enabled = groups_exist diff --git a/geo_nodes.py b/geo_nodes.py index 8de5d60..210d287 100644 --- a/geo_nodes.py +++ b/geo_nodes.py @@ -510,11 +510,6 @@ def __init__(self, node_tree: bpy.types.GeometryNodeTree, var_name: str): self.outputs_set: bool = False self.sim_inputs: list[bpy.types.GeometryNodeSimulationInput] = [] -class NTP_GeoNode: - def __init__(self, node: bpy.types.GeometryNode, var_name: str): - self.node = node - self.var_name = var_name - class NTPGeoNodesOperator(bpy.types.Operator): bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" @@ -779,12 +774,14 @@ 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_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] - for geo_ng in geo_node_groups: - op = layout.operator(NTPGeoNodesOperator.bl_idname, text=geo_ng.name) - op.geo_nodes_group_name = geo_ng.name + for node_tree in bpy.data.node_groups: + if node_tree.bl_idname == 'GeometryNodeTree': + op = layout.operator(NTPGeoNodesOperator.bl_idname, + text=node_tree.name) + op.geo_nodes_group_name = node_tree.name class NTPGeoNodesPanel(bpy.types.Panel): bl_label = "Geometry Nodes to Python" @@ -807,12 +804,11 @@ def draw(self, context): row = col.row() # Disables menu when len of geometry nodes is 0 - geo_node_groups = [node - for node in bpy.data.node_groups - if node.type == 'GEOMETRY'] + geo_node_groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] geo_node_groups_exist = len(geo_node_groups) > 0 row.enabled = geo_node_groups_exist - + row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") \ No newline at end of file diff --git a/materials.py b/materials.py index 053e9b6..5126e50 100644 --- a/materials.py +++ b/materials.py @@ -486,9 +486,11 @@ def poll(cls, context): def draw(self, context): layout = self.layout.column_flow(columns=1) layout.operator_context = 'INVOKE_DEFAULT' - for mat in bpy.data.materials: #TODO: filter by node tree exists - op = layout.operator(NTPMaterialOperator.bl_idname, text=mat.name) - op.material_name = mat.name + for mat in bpy.data.materials: + if mat.node_tree: + op = layout.operator(NTPMaterialOperator.bl_idname, + text=mat.name) + op.material_name = mat.name class NTPMaterialPanel(bpy.types.Panel): bl_label = "Material to Python" @@ -510,7 +512,7 @@ def draw(self, context): row = layout.row() # Disables menu when there are no materials - materials = bpy.data.materials #TODO: filter by node tree exist + materials = [mat for mat in bpy.data.materials if mat.node_tree] materials_exist = len(materials) > 0 row.enabled = materials_exist From 2a85b7d052ee9989fb6f08e8505ffc86d38c3f42 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 11 Sep 2023 00:27:52 -0500 Subject: [PATCH 25/72] refactor: geometry logic split into a subpackage --- __init__.py | 10 +- geometry/__init__.py | 15 + geometry/menu.py | 25 ++ geo_nodes.py => geometry/node_settings.py | 320 +--------------------- geometry/node_tree.py | 10 + geometry/operator.py | 266 ++++++++++++++++++ geometry/panel.py | 32 +++ 7 files changed, 355 insertions(+), 323 deletions(-) create mode 100644 geometry/__init__.py create mode 100644 geometry/menu.py rename geo_nodes.py => geometry/node_settings.py (53%) create mode 100644 geometry/node_tree.py create mode 100644 geometry/operator.py create mode 100644 geometry/panel.py diff --git a/__init__.py b/__init__.py index a4bf342..c070e19 100644 --- a/__init__.py +++ b/__init__.py @@ -11,12 +11,12 @@ if "bpy" in locals(): import importlib importlib.reload(compositor) - importlib.reload(geo_nodes) + importlib.reload(geometry) importlib.reload(materials) importlib.reload(options) else: from . import compositor - from . import geo_nodes + from . import geometry from . import materials from . import options @@ -43,9 +43,9 @@ def draw(self, context): compositor.NTPCompositorScenesMenu, compositor.NTPCompositorGroupsMenu, compositor.NTPCompositorPanel, - geo_nodes.NTPGeoNodesOperator, - geo_nodes.NTPGeoNodesMenu, - geo_nodes.NTPGeoNodesPanel, + geometry.operator.NTPGeoNodesOperator, + geometry.menu.NTPGeoNodesMenu, + geometry.panel.NTPGeoNodesPanel, materials.NTPMaterialOperator, materials.NTPMaterialMenu, materials.NTPMaterialPanel, diff --git a/geometry/__init__.py b/geometry/__init__.py new file mode 100644 index 0000000..80134d4 --- /dev/null +++ b/geometry/__init__.py @@ -0,0 +1,15 @@ +if "bpy" in locals(): + import importlib + importlib.reload(menu) + importlib.reload(node_settings) + importlib.reload(node_tree) + importlib.reload(operator) + importlib.reload(panel) +else: + from . import menu + from . import node_settings + from . import node_tree + from . import operator + from . import panel + +import bpy \ No newline at end of file diff --git a/geometry/menu.py b/geometry/menu.py new file mode 100644 index 0000000..20299ca --- /dev/null +++ b/geometry/menu.py @@ -0,0 +1,25 @@ +import bpy +from bpy.types import Menu + +from .operator import NTPGeoNodesOperator + +class NTPGeoNodesMenu(Menu): + bl_idname = "NODE_MT_ntp_geo_nodes" + bl_label = "Select Geo Nodes" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + + geo_node_groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] + + for node_tree in bpy.data.node_groups: + if node_tree.bl_idname == 'GeometryNodeTree': + op = layout.operator(NTPGeoNodesOperator.bl_idname, + text=node_tree.name) + op.geo_nodes_group_name = node_tree.name \ No newline at end of file diff --git a/geo_nodes.py b/geometry/node_settings.py similarity index 53% rename from geo_nodes.py rename to geometry/node_settings.py index 210d287..fb2786d 100644 --- a/geo_nodes.py +++ b/geometry/node_settings.py @@ -1,8 +1,4 @@ -import bpy -import os - -from .utils import * -from io import StringIO +from ..utils import * geo_node_settings : dict[str, list[(str, ST)]] = { # ATTRIBUTE @@ -499,316 +495,4 @@ 'FunctionNodeRotateEuler' : [("space", ST.ENUM), ("type", ST.ENUM)] -} - -class NTP_GeoNodeTree: - def __init__(self, node_tree: bpy.types.GeometryNodeTree, var_name: str): - self.node_tree: bpy.types.GeometryNodeTree = node_tree - self.var_name: str = var_name - - self.inputs_set: bool = False - self.outputs_set: bool = False - self.sim_inputs: list[bpy.types.GeometryNodeSimulationInput] = [] - -class NTPGeoNodesOperator(bpy.types.Operator): - bl_idname = "node.ntp_geo_nodes" - bl_label = "Geo Nodes to Python" - bl_options = {'REGISTER', 'UNDO'} - - mode: bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - - geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") - - def __init__(self): - # File/string the add-on/script is generated into - self._file: TextIO = None - - # Path to the directory of the zip file - self._zip_dir: str = None - - # Path to the directory for the generated addon - self._addon_dir: str = None - - # Set to keep track of already created node trees - self._node_trees: set[bpy.types.NodeTree] = set() - - # Dictionary to keep track of node->variable name pairs - self._node_vars: dict[bpy.types.Node, str] = {} - - # Dictionary to keep track of variables->usage count pairs - self._used_vars: dict[str, int] = {} - - def _setup_addon_directories(self, context: bpy.types.Context, nt_var: str): - #find base directory to save new addon - 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!")) #TODO: Still valid?? - return {'CANCELLED'} - - self._zip_dir = os.path.join(dir, nt_var) - self._addon_dir = os.path.join(self._zip_dir, nt_var) - - if not os.path.exists(self._addon_dir): - os.makedirs(self._addon_dir) - - def _process_geo_node_group_node(self, node: bpy.types.GeometryNodeGroup, - level: int, inner: str, node_var: str - ) -> None: - nt = node.node_tree - if nt is not None: - if nt not in self._node_trees: - self._process_geo_node_tree(nt, level + 1) - self._file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[{str_to_py_str(nt.name)}]\n")) - - def _process_sim_output_node(self, node: bpy.types.GeometryNodeSimulationOutput, - inner: str, node_var: str) -> None: - self._file.write(f"{inner}# Remove generated sim state items\n") - self._file.write(f"{inner}for item in {node_var}.state_items:\n") - self._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) - self._file.write(f"{inner}#create SSI {name}\n") - self._file.write((f"{inner}{node_var}.state_items.new" - f"({socket_type}, {name})\n")) - si_var = f"{node_var}.state_items[{i}]" - attr_domain = enum_to_py_str(si.attribute_domain) - self._file.write((f"{inner}{si_var}.attribute_domain " - f"= {attr_domain}\n")) - - def _set_socket_defaults(self, node: bpy.types.GeometryNode, inner: str, - node_var: str) -> None: - if self.mode == 'ADDON': - set_input_defaults(node, self._file, inner, node_var, self._addon_dir) - else: - set_input_defaults(node, self._file, inner, node_var) - set_output_defaults(node, self._file, inner, node_var) - - def _process_node(self, node: bpy.types.GeometryNode, - ntp_node_tree: NTP_GeoNodeTree, - inner: str, level: int) -> None: - #create node - node_var: str = create_node(node, self._file, inner, ntp_node_tree.var_name, - self._node_vars, self._used_vars) - set_settings_defaults(node, geo_node_settings, self._file, - self._addon_dir, inner, node_var) - if node.bl_idname == 'GeometryNodeGroup': - self._process_geo_node_group_node(node, level, inner, node_var) - - elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - group_io_settings(node, self._file, inner, "input", ntp_node_tree.var_name, - ntp_node_tree.node_tree) #TODO: convert to using NTP_NodeTrees - ntp_node_tree.inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - group_io_settings(node, self._file, inner, "output", - ntp_node_tree.var_name, ntp_node_tree.node_tree) - ntp_node_tree.outputs_set = True - - elif node.bl_idname == 'GeometryNodeSimulationInput': - ntp_node_tree.sim_inputs.append(node) - - elif node.bl_idname == 'GeometryNodeSimulationOutput': - self._process_sim_output_node(node, inner, node_var) - - hide_hidden_sockets(node, self._file, inner, node_var) - - if node.bl_idname != 'GeometryNodeSimulationInput': - self._set_socket_defaults(node, inner, node_var) - - def _process_sim_zones(self, sim_inputs: list[bpy.types.GeometryNodeSimulationInput], - inner: str) -> None: - """ - Recreate simulation zones - sim_inputs (list[bpy.types.GeometryNodeSimulationInput]): list of - simulation input nodes - inner (str): identation string - """ - for sim_input in sim_inputs: - sim_output = sim_input.paired_output - - sim_input_var = self._node_vars[sim_input] - sim_output_var = self._node_vars[sim_output] - self._file.write((f"{inner}{sim_input_var}.pair_with_output" - f"({sim_output_var})\n")) - - #must set defaults after paired with output - self._set_socket_defaults(sim_input, inner, sim_input_var) - self._set_socket_defaults(sim_output, inner, sim_output_var) - - def _process_geo_node_tree(self, node_tree: bpy.types.GeometryNodeTree, - level: int) -> None: - """ - Generates a Python function to recreate a node tree - - Parameters: - node_tree (bpy.types.NodeTree): geometry node tree to be recreated - level (int): number of tabs to use for each line, used with - node groups within node groups and script/add-on differences - """ - - nt_var = create_var(node_tree.name, self._used_vars) - outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? - - #initialize node group - self._file.write(f"{outer}#initialize {nt_var} node group\n") - self._file.write(f"{outer}def {nt_var}_node_group():\n") - self._file.write((f"{inner}{nt_var} = bpy.data.node_groups.new(" - f"type = \'GeometryNodeTree\', " - f"name = {str_to_py_str(node_tree.name)})\n")) - self._file.write("\n") - - #initialize nodes - self._file.write(f"{inner}#initialize {nt_var} nodes\n") - - ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) - for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) - - self._process_sim_zones(ntp_nt.sim_inputs, inner) - - #set look of nodes - set_parents(node_tree, self._file, inner, self._node_vars) - set_locations(node_tree, self._file, inner, self._node_vars) - set_dimensions(node_tree, self._file, inner, self._node_vars) - - #create connections - init_links(node_tree, self._file, inner, nt_var, self._node_vars) - - self._file.write(f"{inner}return {nt_var}\n") - - #create node group - self._file.write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") - return self._used_vars - - def _apply_modifier(self, nt: bpy.types.GeometryNodeTree, nt_var: str): - #get object - self._file.write(f"\t\tname = bpy.context.object.name\n") - self._file.write(f"\t\tobj = bpy.data.objects[name]\n") - - #set modifier to the one we just created - mod_name = str_to_py_str(nt.name) - self._file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')\n")) - self._file.write(f"\t\tmod.node_group = {nt_var}\n") - - def _report_finished(self): - #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}") - - def execute(self, context): - #find node group to replicate - nt = bpy.data.node_groups[self.geo_nodes_group_name] - - #set up names to use in generated addon - nt_var = clean_string(nt.name) - - if self.mode == 'ADDON': - self._setup_addon_directories(context, nt_var) - - self._file = open(f"{self._addon_dir}/__init__.py", "w") - - create_header(self._file, nt.name) - class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), - lower = False) #TODO: should probably be standardized name to class name util method - init_operator(self._file, class_name, nt_var, nt.name) - self._file.write("\tdef execute(self, context):\n") - else: - self._file = StringIO("") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - self._process_geo_node_tree(nt, level) - - if self.mode == 'ADDON': - self._apply_modifier(nt, nt_var) - self._file.write("\t\treturn {'FINISHED'}\n\n") - create_menu_func(self._file, class_name) - create_register_func(self._file, class_name) - create_unregister_func(self._file, class_name) - create_main_func(self._file) - else: - context.window_manager.clipboard = self._file.getvalue() - self._file.close() - - if self.mode == 'ADDON': - zip_addon(self._zip_dir) - - self._report_finished() - - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - self.layout.prop(self, "mode") - -class NTPGeoNodesMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_geo_nodes" - bl_label = "Select Geo Nodes" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - - geo_node_groups = [node_tree for node_tree in bpy.data.node_groups - if node_tree.bl_idname == 'GeometryNodeTree'] - - for node_tree in bpy.data.node_groups: - if node_tree.bl_idname == 'GeometryNodeTree': - op = layout.operator(NTPGeoNodesOperator.bl_idname, - text=node_tree.name) - op.geo_nodes_group_name = node_tree.name - -class NTPGeoNodesPanel(bpy.types.Panel): - bl_label = "Geometry Nodes to Python" - bl_idname = "NODE_PT_geo_nodes" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - @classmethod - def poll(cls, context): - return True - - def draw_header(self, context): - layout = self.layout - - def draw(self, context): - layout = self.layout - col = layout.column() - row = col.row() - - # Disables menu when len of geometry nodes is 0 - geo_node_groups = [node_tree for node_tree in bpy.data.node_groups - if node_tree.bl_idname == 'GeometryNodeTree'] - geo_node_groups_exist = len(geo_node_groups) > 0 - row.enabled = geo_node_groups_exist - - row.alignment = 'EXPAND' - row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") \ No newline at end of file +} \ No newline at end of file diff --git a/geometry/node_tree.py b/geometry/node_tree.py new file mode 100644 index 0000000..2f42452 --- /dev/null +++ b/geometry/node_tree.py @@ -0,0 +1,10 @@ +from bpy.types import GeometryNodeTree, GeometryNodeSimulationInput + +class NTP_GeoNodeTree: + def __init__(self, node_tree: GeometryNodeTree, var_name: str): + self.node_tree: GeometryNodeTree = node_tree + self.var_name: str = var_name + + self.inputs_set: bool = False + self.outputs_set: bool = False + self.sim_inputs: list[GeometryNodeSimulationInput] = [] \ No newline at end of file diff --git a/geometry/operator.py b/geometry/operator.py new file mode 100644 index 0000000..5ba51fb --- /dev/null +++ b/geometry/operator.py @@ -0,0 +1,266 @@ +import bpy +from bpy.types import Context +from bpy.types import GeometryNodeGroup +from bpy.types import GeometryNodeSimulationInput +from bpy.types import GeometryNodeSimulationOutput +from bpy.types import GeometryNodeTree +from bpy.types import Node +from bpy.types import Operator + +import os + +from ..utils import * +from .node_tree import NTP_GeoNodeTree +from .node_settings import geo_node_settings +from io import StringIO + +class NTPGeoNodesOperator(Operator): + bl_idname = "node.ntp_geo_nodes" + bl_label = "Geo Nodes to Python" + bl_options = {'REGISTER', 'UNDO'} + + mode: bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") + + def __init__(self): + # File/string the add-on/script is generated into + self._file: TextIO = None + + # Path to the directory of the zip file + self._zip_dir: str = None + + # Path to the directory for the generated addon + self._addon_dir: str = None + + # Set to keep track of already created node trees + self._node_trees: set[GeometryNodeTree] = set() + + # Dictionary to keep track of node->variable name pairs + self._node_vars: dict[Node, str] = {} + + # Dictionary to keep track of variables->usage count pairs + self._used_vars: dict[str, int] = {} + + def _setup_addon_directories(self, context: Context, nt_var: str): + #find base directory to save new addon + 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!")) #TODO: Still valid?? + return {'CANCELLED'} + + self._zip_dir = os.path.join(dir, nt_var) + self._addon_dir = os.path.join(self._zip_dir, nt_var) + + if not os.path.exists(self._addon_dir): + os.makedirs(self._addon_dir) + + def _process_geo_node_group_node(self, node: GeometryNodeGroup, + level: int, inner: str, node_var: str + ) -> None: + nt = node.node_tree + if nt is not None: + if nt not in self._node_trees: + self._process_geo_node_tree(nt, level + 1) + self._file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[{str_to_py_str(nt.name)}]\n")) + + def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, + inner: str, node_var: str) -> None: + self._file.write(f"{inner}# Remove generated sim state items\n") + self._file.write(f"{inner}for item in {node_var}.state_items:\n") + self._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) + self._file.write(f"{inner}#create SSI {name}\n") + self._file.write((f"{inner}{node_var}.state_items.new" + f"({socket_type}, {name})\n")) + si_var = f"{node_var}.state_items[{i}]" + attr_domain = enum_to_py_str(si.attribute_domain) + self._file.write((f"{inner}{si_var}.attribute_domain " + f"= {attr_domain}\n")) + + def _set_socket_defaults(self, node: Node, inner: str, + node_var: str) -> None: + if self.mode == 'ADDON': + set_input_defaults(node, self._file, inner, node_var, self._addon_dir) + else: + set_input_defaults(node, self._file, inner, node_var) + set_output_defaults(node, self._file, inner, node_var) + + def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, + inner: str, level: int) -> None: + #create node + node_var: str = create_node(node, self._file, inner, ntp_node_tree.var_name, + self._node_vars, self._used_vars) + set_settings_defaults(node, geo_node_settings, self._file, + self._addon_dir, inner, node_var) + if node.bl_idname == 'GeometryNodeGroup': + self._process_geo_node_group_node(node, level, inner, node_var) + + elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: + group_io_settings(node, self._file, inner, "input", ntp_node_tree.var_name, + ntp_node_tree.node_tree) #TODO: convert to using NTP_NodeTrees + ntp_node_tree.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: + group_io_settings(node, self._file, inner, "output", + ntp_node_tree.var_name, ntp_node_tree.node_tree) + ntp_node_tree.outputs_set = True + + elif node.bl_idname == 'GeometryNodeSimulationInput': + ntp_node_tree.sim_inputs.append(node) + + elif node.bl_idname == 'GeometryNodeSimulationOutput': + self._process_sim_output_node(node, inner, node_var) + + hide_hidden_sockets(node, self._file, inner, node_var) + + if node.bl_idname != 'GeometryNodeSimulationInput': + self._set_socket_defaults(node, inner, node_var) + + def _process_sim_zones(self, sim_inputs: list[GeometryNodeSimulationInput], + inner: str) -> None: + """ + Recreate simulation zones + sim_inputs (list[GeometryNodeSimulationInput]): list of + simulation input nodes + inner (str): identation string + """ + for sim_input in sim_inputs: + sim_output = sim_input.paired_output + + sim_input_var = self._node_vars[sim_input] + sim_output_var = self._node_vars[sim_output] + self._file.write((f"{inner}{sim_input_var}.pair_with_output" + f"({sim_output_var})\n")) + + #must set defaults after paired with output + self._set_socket_defaults(sim_input, inner, sim_input_var) + self._set_socket_defaults(sim_output, inner, sim_output_var) + + def _process_geo_node_tree(self, node_tree: GeometryNodeTree, + level: int) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (GeometryNodeTree): geometry node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + + nt_var = create_var(node_tree.name, self._used_vars) + outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? + + #initialize node group + self._file.write(f"{outer}#initialize {nt_var} node group\n") + self._file.write(f"{outer}def {nt_var}_node_group():\n") + self._file.write((f"{inner}{nt_var} = bpy.data.node_groups.new(" + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n")) + self._file.write("\n") + + #initialize nodes + self._file.write(f"{inner}#initialize {nt_var} nodes\n") + + ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) + for node in node_tree.nodes: + self._process_node(node, ntp_nt, inner, level) + + self._process_sim_zones(ntp_nt.sim_inputs, inner) + + #set look of nodes + set_parents(node_tree, self._file, inner, self._node_vars) + set_locations(node_tree, self._file, inner, self._node_vars) + set_dimensions(node_tree, self._file, inner, self._node_vars) + + #create connections + init_links(node_tree, self._file, inner, nt_var, self._node_vars) + + self._file.write(f"{inner}return {nt_var}\n") + + #create node group + self._file.write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") + return self._used_vars + + def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): + #get object + self._file.write(f"\t\tname = bpy.context.object.name\n") + self._file.write(f"\t\tobj = bpy.data.objects[name]\n") + + #set modifier to the one we just created + mod_name = str_to_py_str(nt.name) + self._file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " + f"type = 'NODES')\n")) + self._file.write(f"\t\tmod.node_group = {nt_var}\n") + + def _report_finished(self): + #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}") + + def execute(self, context): + #find node group to replicate + nt = bpy.data.node_groups[self.geo_nodes_group_name] + + #set up names to use in generated addon + nt_var = clean_string(nt.name) + + if self.mode == 'ADDON': + self._setup_addon_directories(context, nt_var) + + self._file = open(f"{self._addon_dir}/__init__.py", "w") + + create_header(self._file, nt.name) + class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), + lower = False) #TODO: should probably be standardized name to class name util method + init_operator(self._file, class_name, nt_var, nt.name) + self._file.write("\tdef execute(self, context):\n") + else: + self._file = StringIO("") + + if self.mode == 'ADDON': + level = 2 + else: + level = 0 + self._process_geo_node_tree(nt, level) + + if self.mode == 'ADDON': + self._apply_modifier(nt, nt_var) + self._file.write("\t\treturn {'FINISHED'}\n\n") + create_menu_func(self._file, class_name) + create_register_func(self._file, class_name) + create_unregister_func(self._file, class_name) + create_main_func(self._file) + else: + context.window_manager.clipboard = self._file.getvalue() + self._file.close() + + if self.mode == 'ADDON': + zip_addon(self._zip_dir) + + self._report_finished() + + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "mode") diff --git a/geometry/panel.py b/geometry/panel.py new file mode 100644 index 0000000..c6f568b --- /dev/null +++ b/geometry/panel.py @@ -0,0 +1,32 @@ +import bpy +from bpy.types import Panel + +class NTPGeoNodesPanel(Panel): + bl_label = "Geometry Nodes to Python" + bl_idname = "NODE_PT_geo_nodes" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + col = layout.column() + row = col.row() + + # Disables menu when len of geometry nodes is 0 + geo_node_groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] + geo_node_groups_exist = len(geo_node_groups) > 0 + row.enabled = geo_node_groups_exist + + row.alignment = 'EXPAND' + row.operator_context = 'INVOKE_DEFAULT' + row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") \ No newline at end of file From b793e8c488ce5c74d19cc61c7aaafa2f6f26a574 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Tue, 12 Sep 2023 20:06:13 -0500 Subject: [PATCH 26/72] refactor: material package creation --- __init__.py | 16 +- geometry/node_settings.py | 2 +- material/__init__.py | 15 ++ material/menu.py | 20 ++ materials.py => material/node_settings.py | 250 +--------------------- material/operator.py | 203 ++++++++++++++++++ material/panel.py | 30 +++ 7 files changed, 280 insertions(+), 256 deletions(-) create mode 100644 material/__init__.py create mode 100644 material/menu.py rename materials.py => material/node_settings.py (51%) create mode 100644 material/operator.py create mode 100644 material/panel.py diff --git a/__init__.py b/__init__.py index c070e19..d64930f 100644 --- a/__init__.py +++ b/__init__.py @@ -12,12 +12,12 @@ import importlib importlib.reload(compositor) importlib.reload(geometry) - importlib.reload(materials) + importlib.reload(node_settings) importlib.reload(options) else: from . import compositor from . import geometry - from . import materials + from .material import node_settings from . import options import bpy @@ -38,18 +38,22 @@ def draw(self, context): classes = [NodeToPythonMenu, + #options options.NTPOptions, + options.NTPOptionsPanel, + #compositor compositor.NTPCompositorOperator, compositor.NTPCompositorScenesMenu, compositor.NTPCompositorGroupsMenu, compositor.NTPCompositorPanel, + #geometry geometry.operator.NTPGeoNodesOperator, geometry.menu.NTPGeoNodesMenu, geometry.panel.NTPGeoNodesPanel, - materials.NTPMaterialOperator, - materials.NTPMaterialMenu, - materials.NTPMaterialPanel, - options.NTPOptionsPanel + #material + material.operator.NTPMaterialOperator, + material.menu.NTPMaterialMenu, + material.panel.NTPMaterialPanel, ] def register(): diff --git a/geometry/node_settings.py b/geometry/node_settings.py index fb2786d..1900660 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -1,4 +1,4 @@ -from ..utils import * +from ..utils import ST geo_node_settings : dict[str, list[(str, ST)]] = { # ATTRIBUTE diff --git a/material/__init__.py b/material/__init__.py new file mode 100644 index 0000000..b7380e4 --- /dev/null +++ b/material/__init__.py @@ -0,0 +1,15 @@ +if "bpy" in locals(): + import importlib + importlib.reload(menu) + importlib.reload(node_settings) + #importlib.reload(node_tree) + importlib.reload(operator) + importlib.reload(panel) +else: + from . import menu + from . import node_settings + #from . import node_tree + from . import operator + from . import panel + +import bpy \ No newline at end of file diff --git a/material/menu.py b/material/menu.py new file mode 100644 index 0000000..069fbb5 --- /dev/null +++ b/material/menu.py @@ -0,0 +1,20 @@ +import bpy +from bpy.types import Menu +from .operator import NTPMaterialOperator + +class NTPMaterialMenu(Menu): + bl_idname = "NODE_MT_ntp_material" + bl_label = "Select Material" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for mat in bpy.data.materials: + if mat.node_tree: + op = layout.operator(NTPMaterialOperator.bl_idname, + text=mat.name) + op.material_name = mat.name \ No newline at end of file diff --git a/materials.py b/material/node_settings.py similarity index 51% rename from materials.py rename to material/node_settings.py index 5126e50..30b3b08 100644 --- a/materials.py +++ b/material/node_settings.py @@ -1,12 +1,5 @@ -import bpy -import os +from ..utils import ST -from .utils import * -from io import StringIO - -MAT_VAR = "mat" - -#TODO: move to a json, different ones for each blender version? shader_node_settings : dict[str, list[(str, ST)]] = { # INPUT 'ShaderNodeAmbientOcclusion' : [("inside", ST.BOOL), @@ -278,244 +271,3 @@ ("script", ST.TEXT), ("use_auto_update", ST.BOOL)] } - -image_nodes = {'ShaderNodeTexEnvironment', - 'ShaderNodeTexImage'} - -class NTPMaterialOperator(bpy.types.Operator): - bl_idname = "node.ntp_material" - bl_label = "Material to Python" - bl_options = {'REGISTER', 'UNDO'} - - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - material_name: bpy.props.StringProperty(name="Node Group") - - def execute(self, context): - #find node group to replicate - nt = bpy.data.materials[self.material_name].node_tree - if nt is None: - self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " - "valid material. Is Use Nodes selected?")) - return {'CANCELLED'} - - #set up names to use in generated addon - mat_var = clean_string(self.material_name) - - addon_dir = None - if self.mode == 'ADDON': - 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'} - - 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) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, self.material_name) - class_name = clean_string(self.material_name, lower=False) - init_operator(file, class_name, mat_var, self.material_name) - - file.write("\tdef execute(self, context):\n") - else: - file = StringIO("") - - def create_material(indent: str): - file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" - f"name = {str_to_py_str(self.material_name)})\n")) - file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") - - if self.mode == 'ADDON': - create_material("\t\t") - elif self.mode == 'SCRIPT': #TODO: should add option for just creating the node group - create_material("") - - #set to keep track of already created node trees - node_trees: set[bpy.types.NodeTree] = set() - - #dictionary to keep track of node->variable name pairs - node_vars: dict[bpy.types.Node, str] = {} - - #keeps track of all used base variable names and usage counts - used_vars: dict[str, int] = {} - - def is_outermost_node_group(level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - - def process_mat_node_group(node_tree: bpy.types.NodeTree, - level: int - ) -> None: - """ - Generates a Python function to recreate a node tree - - Parameters: - node_tree (bpy.types.NodeTree): node tree to be recreated - level (int): number of tabs to use for each line, used with - node groups within node groups and script/add-on differences - """ - - if is_outermost_node_group(level): - nt_var = create_var(self.material_name, used_vars) - nt_name = self.material_name - else: - nt_var = create_var(node_tree.name, used_vars) - nt_name = node_tree.name - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - - if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") - file.write(f"{inner}#start with a clean node tree\n") - file.write(f"{inner}for node in {nt_var}.nodes:\n") - file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") - else: - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - for node in node_tree.nodes: - 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_trees.add(node_nt) - - node_var = create_node(node, file, inner, nt_var, node_vars, - used_vars) - - set_settings_defaults(node, shader_node_settings, file, - addon_dir, inner, node_var) - hide_hidden_sockets(node, file, inner, node_var) - - if node.bl_idname == 'ShaderNodeGroup': - if node.node_tree is not None: - file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - 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) - outputs_set = True - - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - set_parents(node_tree, file, inner, node_vars) - set_locations(node_tree, file, inner, node_vars) - set_dimensions(node_tree, file, inner, node_vars) - - init_links(node_tree, file, inner, nt_var, node_vars) - - file.write(f"\n{outer}{nt_var}_node_group()\n\n") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - process_mat_node_group(nt, level) - - if self.mode == 'ADDON': - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) - else: - context.window_manager.clipboard = file.getvalue() - - file.close() - - if self.mode == 'ADDON': - zip_addon(zip_dir) - 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): - return context.window_manager.invoke_props_dialog(self) - def draw(self, context): - self.layout.prop(self, "mode") - -class NTPMaterialMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_material" - bl_label = "Select Material" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - for mat in bpy.data.materials: - if mat.node_tree: - op = layout.operator(NTPMaterialOperator.bl_idname, - text=mat.name) - op.material_name = mat.name - -class NTPMaterialPanel(bpy.types.Panel): - bl_label = "Material to Python" - bl_idname = "NODE_PT_mat_to_python" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - @classmethod - def poll(cls, context): - return True - - def draw_header(self, context): - layout = self.layout - - def draw(self, context): - layout = self.layout - row = layout.row() - - # Disables menu when there are no materials - materials = [mat for mat in bpy.data.materials if mat.node_tree] - materials_exist = len(materials) > 0 - row.enabled = materials_exist - - row.alignment = 'EXPAND' - row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_material", text="Materials") \ No newline at end of file diff --git a/material/operator.py b/material/operator.py new file mode 100644 index 0000000..3a4ebd4 --- /dev/null +++ b/material/operator.py @@ -0,0 +1,203 @@ +import bpy +from bpy.types import Operator + +import os +from io import StringIO + +from ..utils import * +from .node_settings import shader_node_settings + +MAT_VAR = "mat" + +class NTPMaterialOperator(Operator): + bl_idname = "node.ntp_material" + bl_label = "Material to Python" + bl_options = {'REGISTER', 'UNDO'} + + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + material_name: bpy.props.StringProperty(name="Node Group") + + def execute(self, context): + #find node group to replicate + nt = bpy.data.materials[self.material_name].node_tree + if nt is None: + self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " + "valid material. Is Use Nodes selected?")) + return {'CANCELLED'} + + #set up names to use in generated addon + mat_var = clean_string(self.material_name) + + addon_dir = None + if self.mode == 'ADDON': + 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'} + + 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) + file = open(f"{addon_dir}/__init__.py", "w") + + create_header(file, self.material_name) + class_name = clean_string(self.material_name, lower=False) + init_operator(file, class_name, mat_var, self.material_name) + + file.write("\tdef execute(self, context):\n") + else: + file = StringIO("") + + def create_material(indent: str): + file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" + f"name = {str_to_py_str(self.material_name)})\n")) + file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") + + if self.mode == 'ADDON': + create_material("\t\t") + elif self.mode == 'SCRIPT': #TODO: should add option for just creating the node group + create_material("") + + #set to keep track of already created node trees + node_trees: set[bpy.types.NodeTree] = set() + + #dictionary to keep track of node->variable name pairs + node_vars: dict[bpy.types.Node, str] = {} + + #keeps track of all used base variable names and usage counts + used_vars: dict[str, int] = {} + + def is_outermost_node_group(level: int) -> bool: + if self.mode == 'ADDON' and level == 2: + return True + elif self.mode == 'SCRIPT' and level == 0: + return True + return False + + def process_mat_node_group(node_tree: bpy.types.NodeTree, + level: int + ) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (bpy.types.NodeTree): node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + + if is_outermost_node_group(level): + nt_var = create_var(self.material_name, used_vars) + nt_name = self.material_name + else: + nt_var = create_var(node_tree.name, used_vars) + nt_name = node_tree.name + + outer, inner = make_indents(level) + + #initialize node group + file.write(f"{outer}#initialize {nt_var} node group\n") + file.write(f"{outer}def {nt_var}_node_group():\n") + + if is_outermost_node_group(level): #outermost node group + file.write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") + file.write(f"{inner}#start with a clean node tree\n") + file.write(f"{inner}for node in {nt_var}.nodes:\n") + file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + else: + file.write((f"{inner}{nt_var}" + f"= bpy.data.node_groups.new(" + f"type = \'ShaderNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + file.write("\n") + + inputs_set = False + outputs_set = False + + #initialize nodes + file.write(f"{inner}#initialize {nt_var} nodes\n") + + #dictionary to keep track of node->variable name pairs + node_vars = {} + + for node in node_tree.nodes: + 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_trees.add(node_nt) + + node_var = create_node(node, file, inner, nt_var, node_vars, + used_vars) + + set_settings_defaults(node, shader_node_settings, file, + addon_dir, inner, node_var) + hide_hidden_sockets(node, file, inner, node_var) + + if node.bl_idname == 'ShaderNodeGroup': + if node.node_tree is not None: + file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + elif node.bl_idname == 'NodeGroupInput' and not inputs_set: + 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) + outputs_set = True + + if self.mode == 'ADDON': + set_input_defaults(node, file, inner, node_var, addon_dir) + else: + set_input_defaults(node, file, inner, node_var) + set_output_defaults(node, file, inner, node_var) + + set_parents(node_tree, file, inner, node_vars) + set_locations(node_tree, file, inner, node_vars) + set_dimensions(node_tree, file, inner, node_vars) + + init_links(node_tree, file, inner, nt_var, node_vars) + + file.write(f"\n{outer}{nt_var}_node_group()\n\n") + + if self.mode == 'ADDON': + level = 2 + else: + level = 0 + process_mat_node_group(nt, level) + + if self.mode == 'ADDON': + file.write("\t\treturn {'FINISHED'}\n\n") + + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) + else: + context.window_manager.clipboard = file.getvalue() + + file.close() + + if self.mode == 'ADDON': + zip_addon(zip_dir) + 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): + return context.window_manager.invoke_props_dialog(self) + def draw(self, context): + self.layout.prop(self, "mode") \ No newline at end of file diff --git a/material/panel.py b/material/panel.py new file mode 100644 index 0000000..3266bb6 --- /dev/null +++ b/material/panel.py @@ -0,0 +1,30 @@ +import bpy +from bpy.types import Panel + +class NTPMaterialPanel(Panel): + bl_label = "Material to Python" + bl_idname = "NODE_PT_mat_to_python" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + row = layout.row() + + # Disables menu when there are no materials + materials = [mat for mat in bpy.data.materials if mat.node_tree] + materials_exist = len(materials) > 0 + row.enabled = materials_exist + + row.alignment = 'EXPAND' + row.operator_context = 'INVOKE_DEFAULT' + row.menu("NODE_MT_ntp_material", text="Materials") \ No newline at end of file From 1121841f17ad6e27feb5f3737ed578db2c27a513 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Tue, 12 Sep 2023 20:53:15 -0500 Subject: [PATCH 27/72] refactor: split material operator into functions --- geometry/operator.py | 16 +- material/node_tree.py | 9 ++ material/operator.py | 334 +++++++++++++++++++++++------------------- 3 files changed, 205 insertions(+), 154 deletions(-) create mode 100644 material/node_tree.py diff --git a/geometry/operator.py b/geometry/operator.py index 5ba51fb..bdcafe0 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -8,11 +8,11 @@ from bpy.types import Operator import os +from io import StringIO from ..utils import * from .node_tree import NTP_GeoNodeTree from .node_settings import geo_node_settings -from io import StringIO class NTPGeoNodesOperator(Operator): bl_idname = "node.ntp_geo_nodes" @@ -33,6 +33,9 @@ def __init__(self): # File/string the add-on/script is generated into self._file: TextIO = None + # Path to the save directory + self._dir: str = None + # Path to the directory of the zip file self._zip_dir: str = None @@ -50,14 +53,14 @@ def __init__(self): def _setup_addon_directories(self, context: Context, nt_var: str): #find base directory to save new addon - dir = bpy.path.abspath(context.scene.ntp_options.dir_path) - if not dir or dir == "": + self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) + if not self._dir or self._dir == "": self.report({'ERROR'}, ("NodeToPython: Save your blend file before using " "NodeToPython!")) #TODO: Still valid?? - return {'CANCELLED'} + return {'CANCELLED'} #TODO - self._zip_dir = os.path.join(dir, nt_var) + self._zip_dir = os.path.join(self._dir, nt_var) self._addon_dir = os.path.join(self._zip_dir, nt_var) if not os.path.exists(self._addon_dir): @@ -163,6 +166,7 @@ def _process_geo_node_tree(self, node_tree: GeometryNodeTree, nt_var = create_var(node_tree.name, self._used_vars) outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? + # Eventually these should go away anyways, and level of indentation depends just on the mode #initialize node group self._file.write(f"{outer}#initialize {nt_var} node group\n") @@ -211,7 +215,7 @@ def _report_finished(self): if self.mode == 'SCRIPT': location = "clipboard" else: - location = dir + location = self._dir self.report({'INFO'}, f"NodeToPython: Saved geometry nodes group to {location}") diff --git a/material/node_tree.py b/material/node_tree.py new file mode 100644 index 0000000..02c014b --- /dev/null +++ b/material/node_tree.py @@ -0,0 +1,9 @@ +from bpy.types import ShaderNodeTree + +class NTP_ShaderNodeTree: #TODO: these should derive from a single base class + def __init__(self, node_tree: ShaderNodeTree, var_name: str): + self.node_tree: ShaderNodeTree = node_tree + self.var_name: str = var_name + + self.inputs_set: bool = False + self.outputs_set: bool = False \ No newline at end of file diff --git a/material/operator.py b/material/operator.py index 3a4ebd4..f688217 100644 --- a/material/operator.py +++ b/material/operator.py @@ -1,11 +1,15 @@ import bpy +from bpy.types import Context from bpy.types import Operator +from bpy.types import Node +from bpy.types import ShaderNodeTree import os from io import StringIO from ..utils import * from .node_settings import shader_node_settings +from .node_tree import NTP_ShaderNodeTree MAT_VAR = "mat" @@ -21,183 +25,217 @@ class NTPMaterialOperator(Operator): ('ADDON', "Addon", "Create a full addon") ] ) + + #TODO: add option for general shader node groups material_name: bpy.props.StringProperty(name="Node Group") + def __init__(self): + # File/string the add-on/script is generated into + self._file: TextIO = None + + # Path to the current directory + self._dir: str = None + + # Path to the directory of the zip file + self._zip_dir: str = None + + # Path to the directory for the generated addon + self._addon_dir: str = None + + # Class named for the generated operator + self._class_name: str = None + + # Set to keep track of already created node trees + self._node_trees: set[ShaderNodeTree] = set() + + # Dictionary to keep track of node->variable name pairs + self._node_vars: dict[Node, str] = {} + + # Dictionary to keep track of variables->usage count pairs + self._used_vars: dict[str, int] = {} + + def _setup_addon_directories(self, context: Context, mat_var: str): + self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) + if not self._dir or self._dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blender file before using " + "NodeToPython!")) + return {'CANCELLED'} #TODO: check this doesn't make sad + + self._zip_dir = os.path.join(self._dir, mat_var) + self._addon_dir = os.path.join(self._zip_dir, mat_var) + if not os.path.exists(self._addon_dir): + os.makedirs(self._addon_dir) + self._file = open(f"{self._addon_dir}/__init__.py", "w") + + create_header(self._file, self.material_name) + self._class_name = clean_string(self.material_name, lower=False) + init_operator(self._file, self._class_name, mat_var, self.material_name) + + self._file.write("\tdef execute(self, context):\n") + + def _create_material(self, indent: str): + self._file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" + f"name = {str_to_py_str(self.material_name)})\n")) + self._file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") + + def _is_outermost_node_group(self, level: int) -> bool: + if self.mode == 'ADDON' and level == 2: + return True + elif self.mode == 'SCRIPT' and level == 0: + return True + return False + + def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): + #initialize node group + self._file.write(f"{outer}#initialize {nt_var} node group\n") + self._file.write(f"{outer}def {nt_var}_node_group():\n") + + if self._is_outermost_node_group(level): + self._file.write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") + self._file.write(f"{inner}#start with a clean node tree\n") + self._file.write(f"{inner}for node in {nt_var}.nodes:\n") + self._file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + else: + self._file.write((f"{inner}{nt_var}" + f"= bpy.data.node_groups.new(" + f"type = \'ShaderNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + self._file.write("\n") + + def _process_shader_group_node(self, node, level, inner, node_var): + node_nt = node.node_tree + if node_nt is not None: + if node_nt not in self._node_trees: + self._process_shader_node_tree(node_nt, level + 1) + self._node_trees.add(node_nt) + + self._file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + + def _set_socket_defaults(self, node, inner, node_var): + if self.mode == 'ADDON': + set_input_defaults(node, self._file, inner, node_var, self._addon_dir) + else: + set_input_defaults(node, self._file, inner, node_var) + set_output_defaults(node, self._file, inner, node_var) + + def _process_shader_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: str, level: int) -> None: + node_var = create_node(node, self._file, inner, ntp_node_tree.var_name, self._node_vars, + self._used_vars) + set_settings_defaults(node, shader_node_settings, self._file, + self._addon_dir, inner, node_var) + + if node.bl_idname == 'ShaderNodeGroup': + self._process_shader_group_node(node, level, inner, node_var) + + elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: + group_io_settings(node, self._file, inner, "input", ntp_node_tree.var_name, ntp_node_tree.node_tree) + ntp_node_tree.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: + group_io_settings(node, self._file, inner, "output", ntp_node_tree.var_name, ntp_node_tree.node_tree) + ntp_node_tree.outputs_set = True + + hide_hidden_sockets(node, self._file, inner, node_var) + self._set_socket_defaults(node, inner, node_var) + + def _process_shader_node_tree(self, node_tree: bpy.types.NodeTree, + level: int + ) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (bpy.types.NodeTree): node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + + if self._is_outermost_node_group(level): + nt_var = create_var(self.material_name, self._used_vars) + nt_name = self.material_name #TODO: this is probably overcomplicating things if we move to a harder material vs shader node tree difference + else: + nt_var = create_var(node_tree.name, self._used_vars) + nt_name = node_tree.name + + outer, inner = make_indents(level) + + self._initialize_shader_node_tree(outer, nt_var, level, inner, nt_name) + + ntp_nt = NTP_ShaderNodeTree(node_tree, nt_var) + + #initialize nodes + self._file.write(f"{inner}#initialize {nt_var} nodes\n") + + for node in node_tree.nodes: + self._process_shader_node(node, ntp_nt, inner, level) + + set_parents(node_tree, self._file, inner, self._node_vars) + set_locations(node_tree, self._file, inner, self._node_vars) + set_dimensions(node_tree, self._file, inner, self._node_vars) + + init_links(node_tree, self._file, inner, nt_var, self._node_vars) + + self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") + + def _report_finished(self): + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = self._dir + + self.report({'INFO'}, f"NodeToPython: Saved material to {location}") + def execute(self, context): #find node group to replicate nt = bpy.data.materials[self.material_name].node_tree if nt is None: - self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " - "valid material. Is Use Nodes selected?")) + self.report({'ERROR'}, ("NodeToPython: This doesn't seem to be a " + "valid material. Is Use Nodes selected?")) return {'CANCELLED'} #set up names to use in generated addon mat_var = clean_string(self.material_name) - addon_dir = None if self.mode == 'ADDON': - 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'} - - 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) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, self.material_name) - class_name = clean_string(self.material_name, lower=False) - init_operator(file, class_name, mat_var, self.material_name) - - file.write("\tdef execute(self, context):\n") + self._setup_addon_directories(context, mat_var) else: - file = StringIO("") + self._file = StringIO("") - def create_material(indent: str): - file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" - f"name = {str_to_py_str(self.material_name)})\n")) - file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") - if self.mode == 'ADDON': - create_material("\t\t") - elif self.mode == 'SCRIPT': #TODO: should add option for just creating the node group - create_material("") - - #set to keep track of already created node trees - node_trees: set[bpy.types.NodeTree] = set() - - #dictionary to keep track of node->variable name pairs - node_vars: dict[bpy.types.Node, str] = {} - - #keeps track of all used base variable names and usage counts - used_vars: dict[str, int] = {} - - def is_outermost_node_group(level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - - def process_mat_node_group(node_tree: bpy.types.NodeTree, - level: int - ) -> None: - """ - Generates a Python function to recreate a node tree - - Parameters: - node_tree (bpy.types.NodeTree): node tree to be recreated - level (int): number of tabs to use for each line, used with - node groups within node groups and script/add-on differences - """ - - if is_outermost_node_group(level): - nt_var = create_var(self.material_name, used_vars) - nt_name = self.material_name - else: - nt_var = create_var(node_tree.name, used_vars) - nt_name = node_tree.name - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - - if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") - file.write(f"{inner}#start with a clean node tree\n") - file.write(f"{inner}for node in {nt_var}.nodes:\n") - file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") - else: - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - for node in node_tree.nodes: - 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_trees.add(node_nt) - - node_var = create_node(node, file, inner, nt_var, node_vars, - used_vars) - - set_settings_defaults(node, shader_node_settings, file, - addon_dir, inner, node_var) - hide_hidden_sockets(node, file, inner, node_var) - - if node.bl_idname == 'ShaderNodeGroup': - if node.node_tree is not None: - file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - 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) - outputs_set = True - - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - set_parents(node_tree, file, inner, node_vars) - set_locations(node_tree, file, inner, node_vars) - set_dimensions(node_tree, file, inner, node_vars) - - init_links(node_tree, file, inner, nt_var, node_vars) - - file.write(f"\n{outer}{nt_var}_node_group()\n\n") + self._create_material("\t\t") + elif self.mode == 'SCRIPT': + self._create_material("") + if self.mode == 'ADDON': level = 2 else: level = 0 - process_mat_node_group(nt, level) + self._process_shader_node_tree(nt, level) if self.mode == 'ADDON': - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) + self._file.write("\t\treturn {'FINISHED'}\n\n") + create_menu_func(self._file, self._class_name) + create_register_func(self._file, self._class_name) + create_unregister_func(self._file, self._class_name) + create_main_func(self._file) else: - context.window_manager.clipboard = file.getvalue() + context.window_manager.clipboard = self._file.getvalue() - file.close() + self._file.close() if self.mode == 'ADDON': - zip_addon(zip_dir) - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = dir - self.report({'INFO'}, f"NodeToPython: Saved material to {location}") + zip_addon(self._zip_dir) + + self._report_finished() + return {'FINISHED'} def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) + def draw(self, context): self.layout.prop(self, "mode") \ No newline at end of file From 3140705e5e488756b666d752be544232ec14056d Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 17 Sep 2023 19:51:27 -0500 Subject: [PATCH 28/72] refactor: started moving common functionality to a base class --- NTP_NodeTree.py | 16 +++++ NTP_Operator.py | 129 +++++++++++++++++++++++++++++++++++++ geometry/node_tree.py | 10 ++- geometry/operator.py | 107 +++++++------------------------ material/node_tree.py | 11 ++-- material/operator.py | 143 +++++++++++------------------------------- 6 files changed, 213 insertions(+), 203 deletions(-) create mode 100644 NTP_NodeTree.py create mode 100644 NTP_Operator.py diff --git a/NTP_NodeTree.py b/NTP_NodeTree.py new file mode 100644 index 0000000..8903d1b --- /dev/null +++ b/NTP_NodeTree.py @@ -0,0 +1,16 @@ +from bpy.types import NodeTree + +from .utils import ST + +class NTP_NodeTree: + def __init__(self, node_tree: NodeTree, var: str): + # Blender node tree object being copied + self.node_tree: NodeTree = node_tree + + # The variable named for the regenerated node tree + self.var: str = var + + # Keep track of if we need to set the default values for the node + # tree inputs and outputs + self.inputs_set: bool = False + self.outputs_set: bool = False \ No newline at end of file diff --git a/NTP_Operator.py b/NTP_Operator.py new file mode 100644 index 0000000..e2db991 --- /dev/null +++ b/NTP_Operator.py @@ -0,0 +1,129 @@ +import bpy +from bpy.types import Context, Operator +from bpy.types import Node, NodeTree + +from io import StringIO +import os +from typing import TextIO + +from .NTP_NodeTree import NTP_NodeTree +from .utils import * + +class NTP_Operator(Operator): + """ + "Abstract" base class for all NTP operators. Blender types and abstraction + don't seem to mix well, but this should only be inherited from + """ + + bl_idname = "" + bl_label = "" + + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + def __init__(self): + super().__init__() + + # File (TextIO) or string (StringIO) the add-on/script is generated into + self._file = None + + # Path to the current directory + self._dir: str = None + + # Path to the directory of the zip file + self._zip_dir: str = None + + # Path to the directory for the generated addon + self._addon_dir: str = None + + # Class named for the generated operator + self._class_name: str = None + + # Set to keep track of already created node trees + self._node_trees: set[NodeTree] = set() + + # Dictionary to keep track of node->variable name pairs + self._node_vars: dict[Node, str] = {} + + # Dictionary to keep track of variables->usage count pairs + self._used_vars: dict[str, int] = {} + + # Dictionary used for setting node properties + self._settings: dict[str, list[(str, ST)]] = {} + + def _setup_addon_directories(self, context: Context, nt_var: str) -> None: + """ + Finds/creates directories to save add-on to + """ + #find base directory to save new addon + self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) + if not self._dir or self._dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blend file before using " + "NodeToPython!")) #TODO: Still valid?? + return {'CANCELLED'} #TODO + + self._zip_dir = os.path.join(self._dir, nt_var) + self._addon_dir = os.path.join(self._zip_dir, nt_var) + + if not os.path.exists(self._addon_dir): + os.makedirs(self._addon_dir) + + def _process_group_node_tree(self, node: Node, node_var: str, level: int, inner: str) -> None: + """ + Processes node tree of group node if one is present + """ + node_tree = node.node_tree + if node_tree is not None: + if node_tree not in self._node_trees: + self._process_node_tree(node_tree, level + 1) + self._node_trees.add(node_tree) + self._file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + + def _set_socket_defaults(self, node: Node, node_var: str, inner: str): + if self.mode == 'ADDON': + set_input_defaults(node, self._file, inner, node_var, self._addon_dir) + elif self.mode == 'SCRIPT': + set_input_defaults(node, self._file, inner, node_var) + set_output_defaults(node, self._file, inner, node_var) + + # ABSTRACT + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, + level: int) -> None: + return + + # ABSTRACT + def _process_node_tree(self, node_tree: NodeTree, level: int) -> None: + return + + def _report_finished(self, object: str): + """ + Alert user that NTP is finished + + Parameters: + object (str): the copied node tree or encapsulating structure + (geometry node modifier, material, scene, etc.) + """ + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = self._dir + self.report({'INFO'}, + f"NodeToPython: Saved {object} to {location}") + + # ABSTRACT + def execute(self): + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "mode") diff --git a/geometry/node_tree.py b/geometry/node_tree.py index 2f42452..a8408f0 100644 --- a/geometry/node_tree.py +++ b/geometry/node_tree.py @@ -1,10 +1,8 @@ from bpy.types import GeometryNodeTree, GeometryNodeSimulationInput -class NTP_GeoNodeTree: - def __init__(self, node_tree: GeometryNodeTree, var_name: str): - self.node_tree: GeometryNodeTree = node_tree - self.var_name: str = var_name +from ..NTP_NodeTree import NTP_NodeTree - self.inputs_set: bool = False - self.outputs_set: bool = False +class NTP_GeoNodeTree(NTP_NodeTree): + def __init__(self, node_tree: GeometryNodeTree, var: str): + super().__init__(node_tree, var) self.sim_inputs: list[GeometryNodeSimulationInput] = [] \ No newline at end of file diff --git a/geometry/operator.py b/geometry/operator.py index bdcafe0..2265b16 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -5,16 +5,16 @@ from bpy.types import GeometryNodeSimulationOutput from bpy.types import GeometryNodeTree from bpy.types import Node -from bpy.types import Operator import os from io import StringIO +from ..NTP_Operator import NTP_Operator from ..utils import * from .node_tree import NTP_GeoNodeTree from .node_settings import geo_node_settings -class NTPGeoNodesOperator(Operator): +class NTPGeoNodesOperator(NTP_Operator): bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" bl_options = {'REGISTER', 'UNDO'} @@ -28,55 +28,11 @@ class NTPGeoNodesOperator(Operator): ) geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") - - def __init__(self): - # File/string the add-on/script is generated into - self._file: TextIO = None - - # Path to the save directory - self._dir: str = None - - # Path to the directory of the zip file - self._zip_dir: str = None - - # Path to the directory for the generated addon - self._addon_dir: str = None - # Set to keep track of already created node trees - self._node_trees: set[GeometryNodeTree] = set() - - # Dictionary to keep track of node->variable name pairs - self._node_vars: dict[Node, str] = {} + def __init__(self): + super().__init__() + self._settings = geo_node_settings - # Dictionary to keep track of variables->usage count pairs - self._used_vars: dict[str, int] = {} - - def _setup_addon_directories(self, context: Context, nt_var: str): - #find base directory to save new addon - self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) - if not self._dir or self._dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blend file before using " - "NodeToPython!")) #TODO: Still valid?? - return {'CANCELLED'} #TODO - - self._zip_dir = os.path.join(self._dir, nt_var) - self._addon_dir = os.path.join(self._zip_dir, nt_var) - - if not os.path.exists(self._addon_dir): - os.makedirs(self._addon_dir) - - def _process_geo_node_group_node(self, node: GeometryNodeGroup, - level: int, inner: str, node_var: str - ) -> None: - nt = node.node_tree - if nt is not None: - if nt not in self._node_trees: - self._process_geo_node_tree(nt, level + 1) - self._file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[{str_to_py_str(nt.name)}]\n")) - def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, inner: str, node_var: str) -> None: self._file.write(f"{inner}# Remove generated sim state items\n") @@ -94,32 +50,24 @@ def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, self._file.write((f"{inner}{si_var}.attribute_domain " f"= {attr_domain}\n")) - def _set_socket_defaults(self, node: Node, inner: str, - node_var: str) -> None: - if self.mode == 'ADDON': - set_input_defaults(node, self._file, inner, node_var, self._addon_dir) - else: - set_input_defaults(node, self._file, inner, node_var) - set_output_defaults(node, self._file, inner, node_var) - def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, inner: str, level: int) -> None: #create node - node_var: str = create_node(node, self._file, inner, ntp_node_tree.var_name, + node_var: str = create_node(node, self._file, inner, ntp_node_tree.var, self._node_vars, self._used_vars) - set_settings_defaults(node, geo_node_settings, self._file, + set_settings_defaults(node, self._settings, self._file, self._addon_dir, inner, node_var) if node.bl_idname == 'GeometryNodeGroup': - self._process_geo_node_group_node(node, level, inner, node_var) + self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - group_io_settings(node, self._file, inner, "input", ntp_node_tree.var_name, + group_io_settings(node, self._file, inner, "input", ntp_node_tree.var, ntp_node_tree.node_tree) #TODO: convert to using NTP_NodeTrees ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: group_io_settings(node, self._file, inner, "output", - ntp_node_tree.var_name, ntp_node_tree.node_tree) + ntp_node_tree.var, ntp_node_tree.node_tree) ntp_node_tree.outputs_set = True elif node.bl_idname == 'GeometryNodeSimulationInput': @@ -131,7 +79,8 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, hide_hidden_sockets(node, self._file, inner, node_var) if node.bl_idname != 'GeometryNodeSimulationInput': - self._set_socket_defaults(node, inner, node_var) + self._set_socket_defaults(node, node_var, inner) + def _process_sim_zones(self, sim_inputs: list[GeometryNodeSimulationInput], inner: str) -> None: @@ -150,10 +99,11 @@ def _process_sim_zones(self, sim_inputs: list[GeometryNodeSimulationInput], f"({sim_output_var})\n")) #must set defaults after paired with output - self._set_socket_defaults(sim_input, inner, sim_input_var) - self._set_socket_defaults(sim_output, inner, sim_output_var) + self._set_socket_defaults(sim_input, sim_input_var, inner) + self._set_socket_defaults(sim_output, sim_output_var, inner) - def _process_geo_node_tree(self, node_tree: GeometryNodeTree, + + def _process_node_tree(self, node_tree: GeometryNodeTree, level: int) -> None: """ Generates a Python function to recreate a node tree @@ -180,6 +130,7 @@ def _process_geo_node_tree(self, node_tree: GeometryNodeTree, self._file.write(f"{inner}#initialize {nt_var} nodes\n") ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) + for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) @@ -199,6 +150,7 @@ def _process_geo_node_tree(self, node_tree: GeometryNodeTree, self._file.write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") return self._used_vars + def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): #get object self._file.write(f"\t\tname = bpy.context.object.name\n") @@ -210,14 +162,6 @@ def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): f"type = 'NODES')\n")) self._file.write(f"\t\tmod.node_group = {nt_var}\n") - def _report_finished(self): - #alert user that NTP is finished - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = self._dir - self.report({'INFO'}, - f"NodeToPython: Saved geometry nodes group to {location}") def execute(self, context): #find node group to replicate @@ -232,8 +176,7 @@ def execute(self, context): self._file = open(f"{self._addon_dir}/__init__.py", "w") create_header(self._file, nt.name) - class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), - lower = False) #TODO: should probably be standardized name to class name util method + class_name = clean_string(nt.name, lower = False) init_operator(self._file, class_name, nt_var, nt.name) self._file.write("\tdef execute(self, context):\n") else: @@ -243,7 +186,7 @@ def execute(self, context): level = 2 else: level = 0 - self._process_geo_node_tree(nt, level) + self._process_node_tree(nt, level) if self.mode == 'ADDON': self._apply_modifier(nt, nt_var) @@ -259,12 +202,6 @@ def execute(self, context): if self.mode == 'ADDON': zip_addon(self._zip_dir) - self._report_finished() - - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) + self._report_finished("geometry node group") - def draw(self, context): - self.layout.prop(self, "mode") + return {'FINISHED'} \ No newline at end of file diff --git a/material/node_tree.py b/material/node_tree.py index 02c014b..aa4822f 100644 --- a/material/node_tree.py +++ b/material/node_tree.py @@ -1,9 +1,6 @@ +from ..NTP_NodeTree import NTP_NodeTree from bpy.types import ShaderNodeTree -class NTP_ShaderNodeTree: #TODO: these should derive from a single base class - def __init__(self, node_tree: ShaderNodeTree, var_name: str): - self.node_tree: ShaderNodeTree = node_tree - self.var_name: str = var_name - - self.inputs_set: bool = False - self.outputs_set: bool = False \ No newline at end of file +class NTP_ShaderNodeTree(NTP_NodeTree): + def __init__(self, node_tree: ShaderNodeTree, var: str): + super().__init__(node_tree, var) \ No newline at end of file diff --git a/material/operator.py b/material/operator.py index f688217..1774eaf 100644 --- a/material/operator.py +++ b/material/operator.py @@ -8,77 +8,30 @@ from io import StringIO from ..utils import * +from ..NTP_Operator import NTP_Operator from .node_settings import shader_node_settings from .node_tree import NTP_ShaderNodeTree MAT_VAR = "mat" -class NTPMaterialOperator(Operator): +class NTPMaterialOperator(NTP_Operator): bl_idname = "node.ntp_material" bl_label = "Material to Python" bl_options = {'REGISTER', 'UNDO'} - - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) #TODO: add option for general shader node groups material_name: bpy.props.StringProperty(name="Node Group") def __init__(self): - # File/string the add-on/script is generated into - self._file: TextIO = None - - # Path to the current directory - self._dir: str = None - - # Path to the directory of the zip file - self._zip_dir: str = None - - # Path to the directory for the generated addon - self._addon_dir: str = None - - # Class named for the generated operator - self._class_name: str = None - - # Set to keep track of already created node trees - self._node_trees: set[ShaderNodeTree] = set() - - # Dictionary to keep track of node->variable name pairs - self._node_vars: dict[Node, str] = {} - - # Dictionary to keep track of variables->usage count pairs - self._used_vars: dict[str, int] = {} - - def _setup_addon_directories(self, context: Context, mat_var: str): - self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) - if not self._dir or self._dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blender file before using " - "NodeToPython!")) - return {'CANCELLED'} #TODO: check this doesn't make sad - - self._zip_dir = os.path.join(self._dir, mat_var) - self._addon_dir = os.path.join(self._zip_dir, mat_var) - if not os.path.exists(self._addon_dir): - os.makedirs(self._addon_dir) - self._file = open(f"{self._addon_dir}/__init__.py", "w") - - create_header(self._file, self.material_name) - self._class_name = clean_string(self.material_name, lower=False) - init_operator(self._file, self._class_name, mat_var, self.material_name) - - self._file.write("\tdef execute(self, context):\n") - + super().__init__() + self._settings = shader_node_settings + def _create_material(self, indent: str): self._file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" f"name = {str_to_py_str(self.material_name)})\n")) self._file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") + def _is_outermost_node_group(self, level: int) -> bool: if self.mode == 'ADDON' and level == 2: return True @@ -86,6 +39,7 @@ def _is_outermost_node_group(self, level: int) -> bool: return True return False + def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): #initialize node group self._file.write(f"{outer}#initialize {nt_var} node group\n") @@ -98,52 +52,35 @@ def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): self._file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") else: self._file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) + f"= bpy.data.node_groups.new(" + f"type = \'ShaderNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) self._file.write("\n") - def _process_shader_group_node(self, node, level, inner, node_var): - node_nt = node.node_tree - if node_nt is not None: - if node_nt not in self._node_trees: - self._process_shader_node_tree(node_nt, level + 1) - self._node_trees.add(node_nt) - - self._file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - - def _set_socket_defaults(self, node, inner, node_var): - if self.mode == 'ADDON': - set_input_defaults(node, self._file, inner, node_var, self._addon_dir) - else: - set_input_defaults(node, self._file, inner, node_var) - set_output_defaults(node, self._file, inner, node_var) - - def _process_shader_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: str, level: int) -> None: - node_var = create_node(node, self._file, inner, ntp_node_tree.var_name, self._node_vars, - self._used_vars) - set_settings_defaults(node, shader_node_settings, self._file, + def _process_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: str, level: int) -> None: + #create node + node_var: str = create_node(node, self._file, inner, ntp_node_tree.var, + self._node_vars, self._used_vars) + set_settings_defaults(node, self._settings, self._file, self._addon_dir, inner, node_var) - if node.bl_idname == 'ShaderNodeGroup': - self._process_shader_group_node(node, level, inner, node_var) + self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - group_io_settings(node, self._file, inner, "input", ntp_node_tree.var_name, ntp_node_tree.node_tree) + group_io_settings(node, self._file, inner, "input", + ntp_node_tree.var, ntp_node_tree.node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - group_io_settings(node, self._file, inner, "output", ntp_node_tree.var_name, ntp_node_tree.node_tree) + group_io_settings(node, self._file, inner, "output", + ntp_node_tree.var, ntp_node_tree.node_tree) ntp_node_tree.outputs_set = True hide_hidden_sockets(node, self._file, inner, node_var) - self._set_socket_defaults(node, inner, node_var) - - def _process_shader_node_tree(self, node_tree: bpy.types.NodeTree, - level: int - ) -> None: + self._set_socket_defaults(node, node_var, inner) + + + def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: """ Generates a Python function to recreate a node tree @@ -170,7 +107,7 @@ def _process_shader_node_tree(self, node_tree: bpy.types.NodeTree, self._file.write(f"{inner}#initialize {nt_var} nodes\n") for node in node_tree.nodes: - self._process_shader_node(node, ntp_nt, inner, level) + self._process_node(node, ntp_nt, inner, level) set_parents(node_tree, self._file, inner, self._node_vars) set_locations(node_tree, self._file, inner, self._node_vars) @@ -178,15 +115,9 @@ def _process_shader_node_tree(self, node_tree: bpy.types.NodeTree, init_links(node_tree, self._file, inner, nt_var, self._node_vars) - self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") + self._file.write(f"{inner}return {nt_var}\n") - def _report_finished(self): - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = self._dir - - self.report({'INFO'}, f"NodeToPython: Saved material to {location}") + self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") def execute(self, context): #find node group to replicate @@ -201,6 +132,14 @@ def execute(self, context): if self.mode == 'ADDON': self._setup_addon_directories(context, mat_var) + + self._file = open(f"{self._addon_dir}/__init__.py", "w") + + create_header(self._file, self.material_name) + self._class_name = clean_string(self.material_name, lower=False) + init_operator(self._file, self._class_name, mat_var, self.material_name) + + self._file.write("\tdef execute(self, context):\n") else: self._file = StringIO("") @@ -214,7 +153,7 @@ def execute(self, context): level = 2 else: level = 0 - self._process_shader_node_tree(nt, level) + self._process_node_tree(nt, level) if self.mode == 'ADDON': self._file.write("\t\treturn {'FINISHED'}\n\n") @@ -230,12 +169,6 @@ def execute(self, context): if self.mode == 'ADDON': zip_addon(self._zip_dir) - self._report_finished() - - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) + self._report_finished("material") - def draw(self, context): - self.layout.prop(self, "mode") \ No newline at end of file + return {'FINISHED'} \ No newline at end of file From 2a3689889766ab2fb0ab3e674f9b439cff5d56fb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 18 Nov 2023 16:57:06 -0600 Subject: [PATCH 29/72] refactor: new compositor nodes structure --- __init__.py | 8 +- compositor/__init__.py | 13 + compositor.py => compositor/node_settings.py | 308 +------------------ compositor/node_tree.py | 7 + compositor/operator.py | 230 ++++++++++++++ compositor/ui.py | 80 +++++ geometry/menu.py | 9 +- geometry/operator.py | 2 - material/operator.py | 4 +- utils.py | 122 ++++---- 10 files changed, 407 insertions(+), 376 deletions(-) create mode 100644 compositor/__init__.py rename compositor.py => compositor/node_settings.py (63%) create mode 100644 compositor/node_tree.py create mode 100644 compositor/operator.py create mode 100644 compositor/ui.py diff --git a/__init__.py b/__init__.py index d64930f..a949fee 100644 --- a/__init__.py +++ b/__init__.py @@ -42,10 +42,10 @@ def draw(self, context): options.NTPOptions, options.NTPOptionsPanel, #compositor - compositor.NTPCompositorOperator, - compositor.NTPCompositorScenesMenu, - compositor.NTPCompositorGroupsMenu, - compositor.NTPCompositorPanel, + compositor.operator.NTPCompositorOperator, + compositor.ui.NTPCompositorScenesMenu, + compositor.ui.NTPCompositorGroupsMenu, + compositor.ui.NTPCompositorPanel, #geometry geometry.operator.NTPGeoNodesOperator, geometry.menu.NTPGeoNodesMenu, diff --git a/compositor/__init__.py b/compositor/__init__.py new file mode 100644 index 0000000..e2707b0 --- /dev/null +++ b/compositor/__init__.py @@ -0,0 +1,13 @@ +if "bpy" in locals(): + import importlib + importlib.reload(node_settings) + importlib.reload(node_tree) + importlib.reload(operator) + importlib.reload(ui) +else: + from . import node_settings + from . import node_tree + from . import operator + from . import ui + +import bpy \ No newline at end of file diff --git a/compositor.py b/compositor/node_settings.py similarity index 63% rename from compositor.py rename to compositor/node_settings.py index 4a1b222..ff8a377 100644 --- a/compositor.py +++ b/compositor/node_settings.py @@ -1,16 +1,4 @@ -import bpy -import os - -from .utils import * -from io import StringIO - -SCENE_VAR = "scene" -BASE_NAME_VAR = "base_name" -END_NAME_VAR = "end_name" - -ntp_vars = {SCENE_VAR, BASE_NAME_VAR, END_NAME_VAR} -#TODO: do something similar for geo nodes and materials, should be useful for -# possible conflicts between ntp_vars and node vars +from ..utils import ST compositor_node_settings : dict[str, list[(str, ST)]] = { # INPUT @@ -439,297 +427,3 @@ # LAYOUT 'CompositorNodeSwitch' : [("check", ST.BOOL)] } - -class NTPCompositorOperator(bpy.types.Operator): - bl_idname = "node.compositor_to_python" - bl_label = "Compositor to Python" - bl_options = {'REGISTER', 'UNDO'} - - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - - compositor_name: bpy.props.StringProperty(name="Node Group") - is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") - - def execute(self, context): - #find node group to replicate - if self.is_scene: - nt = bpy.data.scenes[self.compositor_name].node_tree - else: - nt = bpy.data.node_groups[self.compositor_name] - if nt is None: - #shouldn't happen - self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " - "valid compositor node tree. Is Use Nodes " - "selected?")) - return {'CANCELLED'} - - #set up names to use in generated addon - comp_var = clean_string(self.compositor_name) - - addon_dir = None - if self.mode == 'ADDON': - 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'} - - zip_dir = os.path.join(dir, comp_var) - addon_dir = os.path.join(zip_dir, comp_var) - if not os.path.exists(addon_dir): - os.makedirs(addon_dir) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, self.compositor_name) - class_name = clean_string(self.compositor_name, lower=False) - init_operator(file, class_name, comp_var, self.compositor_name) - - file.write("\tdef execute(self, context):\n") - else: - file = StringIO("") - - if self.is_scene: - def create_scene(indent: str): - #TODO: wrap in more general unique name util function - file.write(f"{indent}# Generate unique scene name\n") - file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") - file.write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") - file.write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - file.write(f"{indent}\ti = 1\n") - file.write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - file.write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - file.write(f"{indent}\t\ti += 1\n\n") - - file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") - file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") - file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") - file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") - - if self.mode == 'ADDON': - create_scene("\t\t") - elif self.mode == 'SCRIPT': - create_scene("") - - #set to keep track of already created node trees - node_trees = set() - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - #keeps track of all used variables - used_vars = {} - - def is_outermost_node_group(level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - - def process_comp_node_group(node_tree, level, node_vars, used_vars): - if is_outermost_node_group(level): - nt_var = create_var(self.compositor_name, used_vars) - nt_name = self.compositor_name - else: - nt_var = create_var(node_tree.name, used_vars) - nt_name = node_tree.name - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - - if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") - file.write(f"{inner}#start with a clean node tree\n") - file.write(f"{inner}for node in {nt_var}.nodes:\n") - file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") - else: - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'CompositorNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - for node in node_tree.nodes: - if node.bl_idname == 'CompositorNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in node_trees: - process_comp_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, - used_vars) - - if node.bl_idname == 'CompositorNodeColorBalance': - if node.correction_method == 'LIFT_GAMMA_GAIN': - lst = [("correction_method", ST.ENUM), - ("gain", ST.COLOR), - ("gamma", ST.COLOR), - ("lift", ST.COLOR)] - else: - lst = [("correction_method", ST.ENUM), - ("offset", ST.COLOR), - ("offset_basis", ST.FLOAT), - ("power", ST.COLOR), - ("slope", ST.COLOR)] - - compositor_node_settings['CompositorNodeColorBalance'] = lst - - set_settings_defaults(node, compositor_node_settings, file, - addon_dir, inner, node_var) - hide_hidden_sockets(node, file, inner, node_var) - - if node.bl_idname == 'CompositorNodeGroup': - if node.node_tree is not None: - file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - 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) - outputs_set = True - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - set_parents(node_tree, file, inner, node_vars) - set_locations(node_tree, file, inner, node_vars) - set_dimensions(node_tree, file, inner, node_vars) - - init_links(node_tree, file, inner, nt_var, node_vars) - - file.write(f"\n{outer}{nt_var}_node_group()\n\n") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - process_comp_node_group(nt, level, node_vars, used_vars) - - if self.mode == 'ADDON': - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) - else: - context.window_manager.clipboard = file.getvalue() - - file.close() - - if self.mode == 'ADDON': - zip_addon(zip_dir) - - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = dir - self.report({'INFO'}, f"NodeToPython: Saved compositor nodes to {location}") - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - def draw(self, context): - self.layout.prop(self, "mode") - -class NTPCompositorScenesMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_comp_scenes" - bl_label = "Select " - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - for scene in bpy.data.scenes: - if scene.node_tree: - op = layout.operator(NTPCompositorOperator.bl_idname, - text=scene.name) - op.compositor_name = scene.name - op.is_scene = True - -class NTPCompositorGroupsMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_comp_groups" - bl_label = "Select " - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - for node_group in bpy.data.node_groups: - if node_group.bl_idname == 'CompositorNodeTree': - op = layout.operator(NTPCompositorOperator.bl_idname, - text=node_group.name) - op.compositor_name = node_group.name - op.is_scene = False - -class NTPCompositorPanel(bpy.types.Panel): - bl_label = "Compositor to Python" - bl_idname = "NODE_PT_ntp_compositor" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - @classmethod - def poll(cls, context): - return True - - def draw_header(self, context): - layout = self.layout - - def draw(self, context): - layout = self.layout - scenes_row = layout.row() - - # Disables menu when there are no compositing node groups - scenes = [scene for scene in bpy.data.scenes if scene.node_tree] - scenes_exist = len(scenes) > 0 - scenes_row.enabled = scenes_exist - - scenes_row.alignment = 'EXPAND' - scenes_row.operator_context = 'INVOKE_DEFAULT' - scenes_row.menu("NODE_MT_ntp_comp_scenes", - text="Scene Compositor Nodes") - - groups_row = layout.row() - groups = [node_tree for node_tree in bpy.data.node_groups - if node_tree.bl_idname == 'CompositorNodeTree'] - groups_exist = len(groups) > 0 - groups_row.enabled = groups_exist - - groups_row.alignment = 'EXPAND' - groups_row.operator_context = 'INVOKE_DEFAULT' - groups_row.menu("NODE_MT_ntp_comp_groups", - text="Group Compositor Nodes") diff --git a/compositor/node_tree.py b/compositor/node_tree.py new file mode 100644 index 0000000..d799df0 --- /dev/null +++ b/compositor/node_tree.py @@ -0,0 +1,7 @@ +from bpy.types import CompositorNodeTree + +from ..NTP_NodeTree import NTP_NodeTree + +class NTP_CompositorNodeTree(NTP_NodeTree): + def __init__(self, node_tree: CompositorNodeTree, var: str): + super().__init__(node_tree, var) \ No newline at end of file diff --git a/compositor/operator.py b/compositor/operator.py new file mode 100644 index 0000000..dcb5be8 --- /dev/null +++ b/compositor/operator.py @@ -0,0 +1,230 @@ +import bpy +import os + +from ..utils import * +from io import StringIO +from .node_settings import compositor_node_settings + +SCENE_VAR = "scene" +BASE_NAME_VAR = "base_name" +END_NAME_VAR = "end_name" + +ntp_vars = {SCENE_VAR, BASE_NAME_VAR, END_NAME_VAR} + +class NTPCompositorOperator(bpy.types.Operator): + bl_idname = "node.compositor_to_python" + bl_label = "Compositor to Python" + bl_options = {'REGISTER', 'UNDO'} + + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + compositor_name: bpy.props.StringProperty(name="Node Group") + is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") + + def execute(self, context): + #find node group to replicate + if self.is_scene: + nt = bpy.data.scenes[self.compositor_name].node_tree + else: + nt = bpy.data.node_groups[self.compositor_name] + if nt is None: + #shouldn't happen + self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " + "valid compositor node tree. Is Use Nodes " + "selected?")) + return {'CANCELLED'} + + #set up names to use in generated addon + comp_var = clean_string(self.compositor_name) + + addon_dir = None + if self.mode == 'ADDON': + 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'} + + zip_dir = os.path.join(dir, comp_var) + addon_dir = os.path.join(zip_dir, comp_var) + if not os.path.exists(addon_dir): + os.makedirs(addon_dir) + file = open(f"{addon_dir}/__init__.py", "w") + + create_header(file, self.compositor_name) + class_name = clean_string(self.compositor_name, lower=False) + init_operator(file, class_name, comp_var, self.compositor_name) + + file.write("\tdef execute(self, context):\n") + else: + file = StringIO("") + + if self.is_scene: + def create_scene(indent: str): + #TODO: wrap in more general unique name util function + file.write(f"{indent}# Generate unique scene name\n") + file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") + file.write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") + file.write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + file.write(f"{indent}\ti = 1\n") + file.write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + file.write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + file.write(f"{indent}\t\ti += 1\n\n") + + file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") + file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") + file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") + file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") + + if self.mode == 'ADDON': + create_scene("\t\t") + elif self.mode == 'SCRIPT': + create_scene("") + + #set to keep track of already created node trees + node_trees = set() + + #dictionary to keep track of node->variable name pairs + node_vars = {} + + #keeps track of all used variables + used_vars = {} + + def is_outermost_node_group(level: int) -> bool: + if self.mode == 'ADDON' and level == 2: + return True + elif self.mode == 'SCRIPT' and level == 0: + return True + return False + + def process_comp_node_group(node_tree, level, node_vars, used_vars): + if is_outermost_node_group(level): + nt_var = create_var(self.compositor_name, used_vars) + nt_name = self.compositor_name + else: + nt_var = create_var(node_tree.name, used_vars) + nt_name = node_tree.name + + outer, inner = make_indents(level) + + #initialize node group + file.write(f"{outer}#initialize {nt_var} node group\n") + file.write(f"{outer}def {nt_var}_node_group():\n") + + if is_outermost_node_group(level): #outermost node group + file.write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") + file.write(f"{inner}#start with a clean node tree\n") + file.write(f"{inner}for node in {nt_var}.nodes:\n") + file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + else: + file.write((f"{inner}{nt_var}" + f"= bpy.data.node_groups.new(" + f"type = \'CompositorNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + file.write("\n") + + inputs_set = False + outputs_set = False + + #initialize nodes + file.write(f"{inner}#initialize {nt_var} nodes\n") + + #dictionary to keep track of node->variable name pairs + node_vars = {} + + for node in node_tree.nodes: + if node.bl_idname == 'CompositorNodeGroup': + node_nt = node.node_tree + if node_nt is not None and node_nt not in node_trees: + process_comp_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, + used_vars) + + if node.bl_idname == 'CompositorNodeColorBalance': + if node.correction_method == 'LIFT_GAMMA_GAIN': + lst = [("correction_method", ST.ENUM), + ("gain", ST.COLOR), + ("gamma", ST.COLOR), + ("lift", ST.COLOR)] + else: + lst = [("correction_method", ST.ENUM), + ("offset", ST.COLOR), + ("offset_basis", ST.FLOAT), + ("power", ST.COLOR), + ("slope", ST.COLOR)] + + compositor_node_settings['CompositorNodeColorBalance'] = lst + + set_settings_defaults(node, compositor_node_settings, file, + addon_dir, inner, node_var) + hide_hidden_sockets(node, file, inner, node_var) + + if node.bl_idname == 'CompositorNodeGroup': + if node.node_tree is not None: + file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + elif node.bl_idname == 'NodeGroupInput' and not inputs_set: + 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) + outputs_set = True + if self.mode == 'ADDON': + set_input_defaults(node, file, inner, node_var, addon_dir) + else: + set_input_defaults(node, file, inner, node_var) + set_output_defaults(node, file, inner, node_var) + + set_parents(node_tree, file, inner, node_vars) + set_locations(node_tree, file, inner, node_vars) + set_dimensions(node_tree, file, inner, node_vars) + + init_links(node_tree, file, inner, nt_var, node_vars) + + file.write(f"\n{outer}{nt_var}_node_group()\n\n") + + if self.mode == 'ADDON': + level = 2 + else: + level = 0 + process_comp_node_group(nt, level, node_vars, used_vars) + + if self.mode == 'ADDON': + file.write("\t\treturn {'FINISHED'}\n\n") + + create_menu_func(file, class_name) + create_register_func(file, class_name) + create_unregister_func(file, class_name) + create_main_func(file) + else: + context.window_manager.clipboard = file.getvalue() + + file.close() + + if self.mode == 'ADDON': + zip_addon(zip_dir) + + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = dir + self.report({'INFO'}, f"NodeToPython: Saved compositor nodes to {location}") + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + def draw(self, context): + self.layout.prop(self, "mode") \ No newline at end of file diff --git a/compositor/ui.py b/compositor/ui.py new file mode 100644 index 0000000..a1003ca --- /dev/null +++ b/compositor/ui.py @@ -0,0 +1,80 @@ +import bpy +from bpy.types import Panel +from bpy.types import Menu +from .operator import NTPCompositorOperator + +class NTPCompositorPanel(Panel): + bl_label = "Compositor to Python" + bl_idname = "NODE_PT_ntp_compositor" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + scenes_row = layout.row() + + # Disables menu when there are no compositing node groups + scenes = [scene for scene in bpy.data.scenes if scene.node_tree] + scenes_exist = len(scenes) > 0 + scenes_row.enabled = scenes_exist + + scenes_row.alignment = 'EXPAND' + scenes_row.operator_context = 'INVOKE_DEFAULT' + scenes_row.menu("NODE_MT_ntp_comp_scenes", + text="Scene Compositor Nodes") + + groups_row = layout.row() + groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'CompositorNodeTree'] + groups_exist = len(groups) > 0 + groups_row.enabled = groups_exist + + groups_row.alignment = 'EXPAND' + groups_row.operator_context = 'INVOKE_DEFAULT' + groups_row.menu("NODE_MT_ntp_comp_groups", + text="Group Compositor Nodes") + +class NTPCompositorScenesMenu(Menu): + bl_idname = "NODE_MT_ntp_comp_scenes" + bl_label = "Select " + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for scene in bpy.data.scenes: + if scene.node_tree: + op = layout.operator(NTPCompositorOperator.bl_idname, + text=scene.name) + op.compositor_name = scene.name + op.is_scene = True + +class NTPCompositorGroupsMenu(Menu): + bl_idname = "NODE_MT_ntp_comp_groups" + bl_label = "Select " + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for node_group in bpy.data.node_groups: + if node_group.bl_idname == 'CompositorNodeTree': + op = layout.operator(NTPCompositorOperator.bl_idname, + text=node_group.name) + op.compositor_name = node_group.name + op.is_scene = False \ No newline at end of file diff --git a/geometry/menu.py b/geometry/menu.py index 20299ca..7d5561f 100644 --- a/geometry/menu.py +++ b/geometry/menu.py @@ -18,8 +18,7 @@ def draw(self, context): geo_node_groups = [node_tree for node_tree in bpy.data.node_groups if node_tree.bl_idname == 'GeometryNodeTree'] - for node_tree in bpy.data.node_groups: - if node_tree.bl_idname == 'GeometryNodeTree': - op = layout.operator(NTPGeoNodesOperator.bl_idname, - text=node_tree.name) - op.geo_nodes_group_name = node_tree.name \ No newline at end of file + for node_tree in geo_node_groups: + op = layout.operator(NTPGeoNodesOperator.bl_idname, + text=node_tree.name) + op.geo_nodes_group_name = node_tree.name \ No newline at end of file diff --git a/geometry/operator.py b/geometry/operator.py index 2265b16..b49d9ca 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -1,6 +1,4 @@ import bpy -from bpy.types import Context -from bpy.types import GeometryNodeGroup from bpy.types import GeometryNodeSimulationInput from bpy.types import GeometryNodeSimulationOutput from bpy.types import GeometryNodeTree diff --git a/material/operator.py b/material/operator.py index 1774eaf..a3613ff 100644 --- a/material/operator.py +++ b/material/operator.py @@ -1,6 +1,4 @@ import bpy -from bpy.types import Context -from bpy.types import Operator from bpy.types import Node from bpy.types import ShaderNodeTree @@ -85,7 +83,7 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: Generates a Python function to recreate a node tree Parameters: - node_tree (bpy.types.NodeTree): node tree to be recreated + node_tree (NodeTree): node tree to be recreated level (int): number of tabs to use for each line, used with node groups within node groups and script/add-on differences """ diff --git a/utils.py b/utils.py index ae432d8..fe77587 100644 --- a/utils.py +++ b/utils.py @@ -186,7 +186,7 @@ def create_header(file: TextIO, name: str) -> None: file.write("\t\"author\" : \"Node To Python\",\n") file.write("\t\"version\" : (1, 0, 0),\n") file.write(f"\t\"blender\" : {bpy.app.version},\n") - file.write("\t\"location\" : \"Object\",\n") + file.write("\t\"location\" : \"Object\",\n") #TODO file.write("\t\"category\" : \"Node\"\n") file.write("}\n") file.write("\n") @@ -232,6 +232,7 @@ def create_var(name: str, used_vars: dict[str, int]) -> str: used_vars[var] = 0 return clean_name +#TODO: reconsider node tree definitions within node tree definitions def make_indents(level: int) -> Tuple[str, str]: """ Returns strings with the correct number of indentations @@ -314,53 +315,57 @@ def set_settings_defaults(node: bpy.types.Node, inner (str): indentation node_var (str): name of the variable we're using for the node in our add-on """ - if node.bl_idname in settings: - for (attr_name, type) in settings[node.bl_idname]: - attr = getattr(node, attr_name, None) - if attr is None: - print(f"\"{node_var}.{attr_name}\" not found") - continue - setting_str = f"{inner}{node_var}.{attr_name}" - if type == ST.ENUM: - if attr != '': - file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") - elif type == ST.ENUM_SET: - file.write(f"{setting_str} = {attr}\n") - elif type == ST.STRING: - file.write(f"{setting_str} = {str_to_py_str(attr)}\n") - elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: - file.write(f"{setting_str} = {attr}\n") - elif type == ST.VEC1: - file.write(f"{setting_str} = {vec1_to_py_str(attr)}\n") - elif type == ST.VEC2: - file.write(f"{setting_str} = {vec2_to_py_str(attr)}\n") - elif type == ST.VEC3: - file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") - elif type == ST.VEC4: - file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") - elif type == ST.COLOR: - file.write(f"{setting_str} = {color_to_py_str(attr)}\n") - elif type == ST.MATERIAL: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.materials:\n")) - file.write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.materials[{name}]\n")) - elif type == ST.OBJECT: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.objects:\n")) - file.write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.objects[{name}]\n")) - elif type == ST.COLOR_RAMP: - color_ramp_settings(node, file, inner, node_var, attr_name) - elif type == ST.CURVE_MAPPING: - curve_mapping_settings(node, file, inner, node_var, attr_name) - elif type == ST.IMAGE: - if addon_dir is not None and attr is not None: - if attr.source in {'FILE', 'GENERATED', 'TILED'}: - save_image(attr, addon_dir) - load_image(attr, file, inner, f"{node_var}.{attr_name}") - elif type == ST.IMAGE_USER: - image_user_settings(attr, file, inner, f"{node_var}.{attr_name}") + if node.bl_idname not in settings: + print((f"NodeToPython: couldn't find {node.bl_idname} in settings." + f"Your Blender version may not be supported")) + return + + for (attr_name, type) in settings[node.bl_idname]: + attr = getattr(node, attr_name, None) + if attr is None: + print(f"\"{node_var}.{attr_name}\" not found") + continue + setting_str = f"{inner}{node_var}.{attr_name}" + if type == ST.ENUM: + if attr != '': + file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") + elif type == ST.ENUM_SET: + file.write(f"{setting_str} = {attr}\n") + elif type == ST.STRING: + file.write(f"{setting_str} = {str_to_py_str(attr)}\n") + elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: + file.write(f"{setting_str} = {attr}\n") + elif type == ST.VEC1: + file.write(f"{setting_str} = {vec1_to_py_str(attr)}\n") + elif type == ST.VEC2: + file.write(f"{setting_str} = {vec2_to_py_str(attr)}\n") + elif type == ST.VEC3: + file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") + elif type == ST.VEC4: + file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + elif type == ST.COLOR: + file.write(f"{setting_str} = {color_to_py_str(attr)}\n") + elif type == ST.MATERIAL: + name = str_to_py_str(attr.name) + file.write((f"{inner}if {name} in bpy.data.materials:\n")) + file.write((f"{inner}\t{node_var}.{attr_name} = " + f"bpy.data.materials[{name}]\n")) + elif type == ST.OBJECT: + name = str_to_py_str(attr.name) + file.write((f"{inner}if {name} in bpy.data.objects:\n")) + file.write((f"{inner}\t{node_var}.{attr_name} = " + f"bpy.data.objects[{name}]\n")) + elif type == ST.COLOR_RAMP: + color_ramp_settings(node, file, inner, node_var, attr_name) + elif type == ST.CURVE_MAPPING: + curve_mapping_settings(node, file, inner, node_var, attr_name) + elif type == ST.IMAGE: + if addon_dir is not None and attr is not None: + if attr.source in {'FILE', 'GENERATED', 'TILED'}: + save_image(attr, addon_dir) + load_image(attr, file, inner, f"{node_var}.{attr_name}") + elif type == ST.IMAGE_USER: + image_user_settings(attr, file, inner, f"{node_var}.{attr_name}") def hide_hidden_sockets(node: bpy.types.Node, file: TextIO, @@ -408,6 +413,7 @@ def group_io_settings(node: bpy.types.Node, else: ios = node.inputs ntio = node_tree.outputs + file.write(f"{inner}#{node_tree_var} {io}s\n") for i, inout in enumerate(ios): if inout.bl_idname == 'NodeSocketVirtual': @@ -420,6 +426,7 @@ def group_io_settings(node: bpy.types.Node, socket_var = f"{node_tree_var}.{io}s[{i}]" if inout.type in default_sockets: + #TODO: separate default socket function #default value if inout.type == 'RGBA': dv = vec4_to_py_str(socket.default_value) @@ -514,17 +521,18 @@ def color_ramp_settings(node: bpy.types.Node, file.write((f"{inner}{element_var} = " f"{ramp_str}.elements" f".new({element.position})\n")) + file.write((f"{inner}{element_var}.alpha = " f"{element.alpha}\n")) color_str = vec4_to_py_str(element.color) file.write((f"{inner}{element_var}.color = {color_str}\n\n")) def curve_mapping_settings(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str, - curve_mapping_name: str - ) -> None: + file: TextIO, + inner: str, + node_var: str, + curve_mapping_name: str + ) -> None: """ Sets defaults for Float, Vector, and Color curves @@ -538,7 +546,8 @@ def curve_mapping_settings(node: bpy.types.Node, mapping = getattr(node, curve_mapping_name) if not mapping: - raise ValueError(f"Curve mapping \"{curve_mapping_name}\" not found in node \"{node.bl_idname}\"") + raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " + f"in node \"{node.bl_idname}\"")) #mapping settings file.write(f"{inner}#mapping settings\n") @@ -574,6 +583,7 @@ def curve_mapping_settings(node: bpy.types.Node, #create curves for i, curve in enumerate(mapping.curves): + #TODO: curve function file.write(f"{inner}#curve {i}\n") curve_i = f"{node_var}_curve_{i}" file.write((f"{inner}{curve_i} = " @@ -587,6 +597,7 @@ def curve_mapping_settings(node: bpy.types.Node, file.write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") for j, point in enumerate(curve.points): + #TODO: point function point_j = f"{inner}{curve_i}_point_{j}" loc = point.location @@ -716,6 +727,7 @@ def set_input_defaults(node: bpy.types.Node, for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults and not input.is_linked: + #TODO: this could be cleaner socket_var = f"{node_var}.inputs[{i}]" #colors @@ -787,7 +799,7 @@ def in_file_inputs(input: bpy.types.NodeSocket, name = str_to_py_str(input.default_value.name) file.write(f"{inner}if {name} in bpy.data.{type}:\n") file.write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) + f"bpy.data.{type}[{name}]\n")) def set_output_defaults(node: bpy.types.Node, file: TextIO, From 41e33eae48bc193f360ec7cec5ca0fb1007ea995 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 18 Nov 2023 17:47:07 -0600 Subject: [PATCH 30/72] refactor: compositor partial cleanup --- NTP_Operator.py | 9 +- compositor/operator.py | 317 +++++++++++++++++++---------------------- material/operator.py | 10 +- utils.py | 1 + 4 files changed, 160 insertions(+), 177 deletions(-) diff --git a/NTP_Operator.py b/NTP_Operator.py index e2db991..371618c 100644 --- a/NTP_Operator.py +++ b/NTP_Operator.py @@ -30,7 +30,7 @@ def __init__(self): super().__init__() # File (TextIO) or string (StringIO) the add-on/script is generated into - self._file = None + self._file : TextIO = None # Path to the current directory self._dir: str = None @@ -74,6 +74,13 @@ def _setup_addon_directories(self, context: Context, nt_var: str) -> None: if not os.path.exists(self._addon_dir): os.makedirs(self._addon_dir) + def _is_outermost_node_group(self, level: int) -> bool: + if self.mode == 'ADDON' and level == 2: + return True + elif self.mode == 'SCRIPT' and level == 0: + return True + return False + def _process_group_node_tree(self, node: Node, node_var: str, level: int, inner: str) -> None: """ Processes node tree of group node if one is present diff --git a/compositor/operator.py b/compositor/operator.py index dcb5be8..06bde78 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -1,6 +1,10 @@ import bpy import os +from bpy.types import Node + +from ..NTP_Operator import NTP_Operator +from .node_tree import NTP_CompositorNodeTree from ..utils import * from io import StringIO from .node_settings import compositor_node_settings @@ -11,8 +15,8 @@ ntp_vars = {SCENE_VAR, BASE_NAME_VAR, END_NAME_VAR} -class NTPCompositorOperator(bpy.types.Operator): - bl_idname = "node.compositor_to_python" +class NTPCompositorOperator(NTP_Operator): + bl_idname = "node.ntp_compositor" bl_label = "Compositor to Python" bl_options = {'REGISTER', 'UNDO'} @@ -27,12 +31,133 @@ class NTPCompositorOperator(bpy.types.Operator): compositor_name: bpy.props.StringProperty(name="Node Group") is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") + def __init__(self): + super().__init__() + self._settings = compositor_node_settings + + + def _create_scene(self, indent: str): + #TODO: wrap in more general unique name util function + self._file.write(f"{indent}# Generate unique scene name\n") + self._file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") + self._file.write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") + self._file.write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + self._file.write(f"{indent}\ti = 1\n") + self._file.write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + self._file.write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + self._file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + self._file.write(f"{indent}\t\ti += 1\n\n") + + self._file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") + self._file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") + self._file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") + self._file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") + + def _initialize_compositor_node_tree(self, outer, nt_var, level, inner, nt_name): + #initialize node group + self._file.write(f"{outer}#initialize {nt_var} node group\n") + self._file.write(f"{outer}def {nt_var}_node_group():\n") + + if self._is_outermost_node_group(level): #outermost node group + self._file.write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") + self._file.write(f"{inner}#start with a clean node tree\n") + self._file.write(f"{inner}for node in {nt_var}.nodes:\n") + self._file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + else: + self._file.write((f"{inner}{nt_var}" + f"= bpy.data.node_groups.new(" + f"type = \'CompositorNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + self._file.write("\n") + + def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, level: int): + if node.bl_idname == 'CompositorNodeGroup': + node_nt = node.node_tree + if node_nt is not None and node_nt not in self._node_trees: + self._process_comp_node_group(node_nt, level + 1, self._node_vars, + self._used_vars) + self._node_trees.add(node_nt) + + node_var: str = create_node(node, self._file, inner, ntp_nt.var, self._node_vars, + self._used_vars) + + if node.bl_idname == 'CompositorNodeColorBalance': + if node.correction_method == 'LIFT_GAMMA_GAIN': + lst = [("correction_method", ST.ENUM), + ("gain", ST.COLOR), + ("gamma", ST.COLOR), + ("lift", ST.COLOR)] + else: + lst = [("correction_method", ST.ENUM), + ("offset", ST.COLOR), + ("offset_basis", ST.FLOAT), + ("power", ST.COLOR), + ("slope", ST.COLOR)] + + compositor_node_settings['CompositorNodeColorBalance'] = lst + + set_settings_defaults(node, compositor_node_settings, self._file, + self._addon_dir, inner, node_var) + hide_hidden_sockets(node, self._file, inner, node_var) + + if node.bl_idname == 'CompositorNodeGroup': + if node.node_tree is not None: + self._file.write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) + elif node.bl_idname == 'NodeGroupInput' and not inputs_set: + group_io_settings(node, self._file, inner, "input", ntp_nt.var, ntp_nt.node_tree) + inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: + group_io_settings(node, self._file, inner, "output", ntp_nt.var, ntp_nt.node_tree) + outputs_set = True + + self._set_socket_defaults(node, node_var, inner) + + def _process_node_tree(self, node_tree, level): + """ + Generates a Python function to recreate a compositor node tree + + Parameters: + node_tree (NodeTree): node tree to be recreated + level (int): number of tabs to use for each line + + """ + if self._is_outermost_node_group(level): + nt_var = create_var(self.compositor_name, self._used_vars) + nt_name = self.compositor_name + else: + nt_var = create_var(node_tree.name, self._used_vars) + nt_name = node_tree.name + + outer, inner = make_indents(level) + + self._initialize_compositor_node_tree(outer, nt_var, level, inner, nt_name) + + ntp_nt = NTP_CompositorNodeTree(node_tree, nt_var) + + #initialize nodes + self._file.write(f"{inner}#initialize {nt_var} nodes\n") + + for node in node_tree.nodes: + self._process_node(node, ntp_nt, inner, level) + + set_parents(node_tree, self._file, inner, self._node_vars) + set_locations(node_tree, self._file, inner, self._node_vars) + set_dimensions(node_tree, self._file, inner, self._node_vars) + + init_links(node_tree, self._file, inner, nt_var, self._node_vars) + + self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") + def execute(self, context): #find node group to replicate if self.is_scene: nt = bpy.data.scenes[self.compositor_name].node_tree else: nt = bpy.data.node_groups[self.compositor_name] + if nt is None: #shouldn't happen self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " @@ -43,188 +168,46 @@ def execute(self, context): #set up names to use in generated addon comp_var = clean_string(self.compositor_name) - addon_dir = None if self.mode == 'ADDON': - 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'} - - zip_dir = os.path.join(dir, comp_var) - addon_dir = os.path.join(zip_dir, comp_var) - if not os.path.exists(addon_dir): - os.makedirs(addon_dir) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, self.compositor_name) + self._setup_addon_directories(context, comp_var) + + self._file = open(f"{self._addon_dir}/__init__.py", "w") + + create_header(self._file, self.compositor_name) class_name = clean_string(self.compositor_name, lower=False) - init_operator(file, class_name, comp_var, self.compositor_name) + init_operator(self._file, class_name, comp_var, self.compositor_name) - file.write("\tdef execute(self, context):\n") + self._file.write("\tdef execute(self, context):\n") else: - file = StringIO("") + self._file = StringIO("") if self.is_scene: - def create_scene(indent: str): - #TODO: wrap in more general unique name util function - file.write(f"{indent}# Generate unique scene name\n") - file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") - file.write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") - file.write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - file.write(f"{indent}\ti = 1\n") - file.write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - file.write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - file.write(f"{indent}\t\ti += 1\n\n") - - file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") - file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") - file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") - file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") - if self.mode == 'ADDON': - create_scene("\t\t") + self._create_scene("\t\t") elif self.mode == 'SCRIPT': - create_scene("") - - #set to keep track of already created node trees - node_trees = set() - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - #keeps track of all used variables - used_vars = {} - - def is_outermost_node_group(level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - - def process_comp_node_group(node_tree, level, node_vars, used_vars): - if is_outermost_node_group(level): - nt_var = create_var(self.compositor_name, used_vars) - nt_name = self.compositor_name - else: - nt_var = create_var(node_tree.name, used_vars) - nt_name = node_tree.name - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - - if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") - file.write(f"{inner}#start with a clean node tree\n") - file.write(f"{inner}for node in {nt_var}.nodes:\n") - file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") - else: - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'CompositorNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - for node in node_tree.nodes: - if node.bl_idname == 'CompositorNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in node_trees: - process_comp_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, - used_vars) - - if node.bl_idname == 'CompositorNodeColorBalance': - if node.correction_method == 'LIFT_GAMMA_GAIN': - lst = [("correction_method", ST.ENUM), - ("gain", ST.COLOR), - ("gamma", ST.COLOR), - ("lift", ST.COLOR)] - else: - lst = [("correction_method", ST.ENUM), - ("offset", ST.COLOR), - ("offset_basis", ST.FLOAT), - ("power", ST.COLOR), - ("slope", ST.COLOR)] - - compositor_node_settings['CompositorNodeColorBalance'] = lst - - set_settings_defaults(node, compositor_node_settings, file, - addon_dir, inner, node_var) - hide_hidden_sockets(node, file, inner, node_var) - - if node.bl_idname == 'CompositorNodeGroup': - if node.node_tree is not None: - file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - 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) - outputs_set = True - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - set_parents(node_tree, file, inner, node_vars) - set_locations(node_tree, file, inner, node_vars) - set_dimensions(node_tree, file, inner, node_vars) - - init_links(node_tree, file, inner, nt_var, node_vars) - - file.write(f"\n{outer}{nt_var}_node_group()\n\n") - + self._create_scene("") + if self.mode == 'ADDON': level = 2 else: level = 0 - process_comp_node_group(nt, level, node_vars, used_vars) + self._process_node_tree(nt, level) if self.mode == 'ADDON': - file.write("\t\treturn {'FINISHED'}\n\n") + self._file.write("\t\treturn {'FINISHED'}\n\n") - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) + create_menu_func(self._file, class_name) + create_register_func(self._file, class_name) + create_unregister_func(self._file, class_name) + create_main_func(self._file) else: - context.window_manager.clipboard = file.getvalue() + context.window_manager.clipboard = self._file.getvalue() - file.close() + self._file.close() if self.mode == 'ADDON': - zip_addon(zip_dir) + zip_addon(self._zip_dir) - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = dir - self.report({'INFO'}, f"NodeToPython: Saved compositor nodes to {location}") - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - def draw(self, context): - self.layout.prop(self, "mode") \ No newline at end of file + self._report_finished("compositor nodes") + + return {'FINISHED'} \ No newline at end of file diff --git a/material/operator.py b/material/operator.py index a3613ff..282021b 100644 --- a/material/operator.py +++ b/material/operator.py @@ -28,15 +28,6 @@ def _create_material(self, indent: str): self._file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" f"name = {str_to_py_str(self.material_name)})\n")) self._file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") - - - def _is_outermost_node_group(self, level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): #initialize node group @@ -61,6 +52,7 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: st self._node_vars, self._used_vars) set_settings_defaults(node, self._settings, self._file, self._addon_dir, inner, node_var) + if node.bl_idname == 'ShaderNodeGroup': self._process_group_node_tree(node, node_var, level, inner) diff --git a/utils.py b/utils.py index fe77587..5d41f4e 100644 --- a/utils.py +++ b/utils.py @@ -191,6 +191,7 @@ def create_header(file: TextIO, name: str) -> None: file.write("}\n") file.write("\n") file.write("import bpy\n") + file.write("import mathutils\n") file.write("import os\n") file.write("\n") From e2c7a678f93313d9d0bd6f8076caea39dd09c501 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 19 Nov 2023 16:23:10 -0600 Subject: [PATCH 31/72] refactor: moved a lot from the utils module to the base NTP_Operator class --- NTP_Operator.py | 744 ++++++++++++++++++++++++++++++++++++- compositor/operator.py | 42 +-- geometry/operator.py | 38 +- material/operator.py | 42 +-- utils.py | 822 +---------------------------------------- 5 files changed, 797 insertions(+), 891 deletions(-) diff --git a/NTP_Operator.py b/NTP_Operator.py index 371618c..ab019cf 100644 --- a/NTP_Operator.py +++ b/NTP_Operator.py @@ -74,6 +74,45 @@ def _setup_addon_directories(self, context: Context, nt_var: str) -> None: if not os.path.exists(self._addon_dir): os.makedirs(self._addon_dir) + def _create_header(self, name: str) -> None: + """ + Sets up the bl_info and imports the Blender API + + Parameters: + file (TextIO): the file for the generated add-on + name (str): name of the add-on + """ + + self._file.write("bl_info = {\n") + self._file.write(f"\t\"name\" : \"{name}\",\n") + self._file.write("\t\"author\" : \"Node To Python\",\n") + self._file.write("\t\"version\" : (1, 0, 0),\n") + self._file.write(f"\t\"blender\" : {bpy.app.version},\n") + self._file.write("\t\"location\" : \"Object\",\n") #TODO + self._file.write("\t\"category\" : \"Node\"\n") + self._file.write("}\n") + self._file.write("\n") + self._file.write("import bpy\n") + self._file.write("import mathutils\n") + self._file.write("import os\n") + self._file.write("\n") + + def _init_operator(self, idname: str, label: str) -> None: + """ + Initializes the add-on's operator + + Parameters: + file (TextIO): the file for the generated add-on + name (str): name for the class + idname (str): name for the operator + label (str): appearence inside Blender + """ + self._file.write(f"class {self._class_name}(bpy.types.Operator):\n") + self._file.write(f"\tbl_idname = \"object.{idname}\"\n") + self._file.write(f"\tbl_label = \"{label}\"\n") + self._file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") + self._file.write("\n") + def _is_outermost_node_group(self, level: int) -> bool: if self.mode == 'ADDON' and level == 2: return True @@ -81,7 +120,8 @@ def _is_outermost_node_group(self, level: int) -> bool: return True return False - def _process_group_node_tree(self, node: Node, node_var: str, level: int, inner: str) -> None: + def _process_group_node_tree(self, node: Node, node_var: str, level: int, + inner: str) -> None: """ Processes node tree of group node if one is present """ @@ -94,12 +134,704 @@ def _process_group_node_tree(self, node: Node, node_var: str, level: int, inner: f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) + def _create_var(self, name: str) -> 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 (dict[str, int]): dictionary containing variable names and usage counts + + Returns: + clean_name (str): variable name for the node tree + """ + if name == "": + name = "unnamed" + clean_name = clean_string(name) + var = clean_name + if var in self._used_vars: + self._used_vars[var] += 1 + return f"{clean_name}_{self._used_vars[var]}" + else: + self._used_vars[var] = 0 + return clean_name + + def _create_node(self, node: bpy.types.Node, inner: str, node_tree_var: str + ) -> str: + """ + Initializes a new node with location, dimension, and label info + + Parameters: + node (bpy.types.Node): node to be copied + inner (str): indentation level for this logic + node_tree_var (str): variable name for the node tree + Returns: + node_var (str): variable name for the node + """ + + self._file.write(f"{inner}#node {node.name}\n") + + node_var = self._create_var(node.name) + self._node_vars[node] = node_var + + self._file.write((f"{inner}{node_var} " + f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) + #label + if node.label: + self._file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") + + #color + if node.use_custom_color: + self._file.write(f"{inner}{node_var}.use_custom_color = True\n") + self._file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") + + #mute + if node.mute: + self._file.write(f"{inner}{node_var}.mute = True\n") + + return node_var + + def _set_settings_defaults(self, node: bpy.types.Node, inner: str, + node_var: str) -> None: + """ + Sets the defaults for any settings a node may have + + Parameters: + node (bpy.types.Node): the node object we're copying settings from + inner (str): indentation + node_var (str): name of the variable we're using for the node in our add-on + """ + if node.bl_idname not in self._settings: + print((f"NodeToPython: couldn't find {node.bl_idname} in settings." + f"Your Blender version may not be supported")) + return + + for (attr_name, type) in self._settings[node.bl_idname]: + attr = getattr(node, attr_name, None) + if attr is None: + print(f"\"{node_var}.{attr_name}\" not found") + continue + setting_str = f"{inner}{node_var}.{attr_name}" + if type == ST.ENUM: + if attr != '': + self._file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") + elif type == ST.ENUM_SET: + self._file.write(f"{setting_str} = {attr}\n") + elif type == ST.STRING: + self._file.write(f"{setting_str} = {str_to_py_str(attr)}\n") + elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: + self._file.write(f"{setting_str} = {attr}\n") + elif type == ST.VEC1: + self._file.write(f"{setting_str} = {vec1_to_py_str(attr)}\n") + elif type == ST.VEC2: + self._file.write(f"{setting_str} = {vec2_to_py_str(attr)}\n") + elif type == ST.VEC3: + self._file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") + elif type == ST.VEC4: + self._file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + elif type == ST.COLOR: + self._file.write(f"{setting_str} = {color_to_py_str(attr)}\n") + elif type == ST.MATERIAL: + name = str_to_py_str(attr.name) + self._file.write((f"{inner}if {name} in bpy.data.materials:\n")) + self._file.write((f"{inner}\t{node_var}.{attr_name} = " + f"bpy.data.materials[{name}]\n")) + elif type == ST.OBJECT: + name = str_to_py_str(attr.name) + self._file.write((f"{inner}if {name} in bpy.data.objects:\n")) + self._file.write((f"{inner}\t{node_var}.{attr_name} = " + f"bpy.data.objects[{name}]\n")) + elif type == ST.COLOR_RAMP: + self._color_ramp_settings(node, inner, node_var, attr_name) + elif type == ST.CURVE_MAPPING: + self._curve_mapping_settings(node, inner, node_var, attr_name) + elif type == ST.IMAGE: + if self._addon_dir is not None and attr is not None: + if attr.source in {'FILE', 'GENERATED', 'TILED'}: + self._save_image(attr) + self._load_image(attr, inner, f"{node_var}.{attr_name}") + elif type == ST.IMAGE_USER: + self._image_user_settings(attr, inner, f"{node_var}.{attr_name}") + + def _group_io_settings(self, node: bpy.types.Node, inner: str, + io: str, #TODO: convert to enum + node_tree_var: str, + node_tree: bpy.types.NodeTree) -> None: + """ + Set the settings for group input and output sockets + + Parameters: + node (bpy.types.Node) : group input/output node + inner (str): indentation string + io (str): whether we're generating the input or output settings + node_tree_var (str): variable name of the generated node tree + node_tree (bpy.types.NodeTree): node tree that we're generating input + and output settings for + """ + if io == "input": + ios = node.outputs #TODO: this doesn't seem right + """ + Are sockets and node tree default ios really coupled like this?? + """ + ntio = node_tree.inputs + else: + ios = node.inputs + ntio = node_tree.outputs + + self._file.write(f"{inner}#{node_tree_var} {io}s\n") + for i, inout in enumerate(ios): + if inout.bl_idname == 'NodeSocketVirtual': + continue + self._file.write(f"{inner}#{io} {inout.name}\n") + idname = enum_to_py_str(inout.bl_idname) + name = str_to_py_str(inout.name) + self._file.write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") + socket = ntio[i] + socket_var = f"{node_tree_var}.{io}s[{i}]" + + if inout.type in default_sockets: + #TODO: separate default socket function + #default value + if inout.type == 'RGBA': + dv = vec4_to_py_str(socket.default_value) + elif inout.type == 'VECTOR': + dv = vec3_to_py_str(socket.default_value) + else: + dv = socket.default_value + self._file.write(f"{inner}{socket_var}.default_value = {dv}\n") + + #min value + if hasattr(socket, "min_value"): + self._file.write(f"{inner}{socket_var}.min_value = {socket.min_value}\n") + #max value + if hasattr(socket, "min_value"): + self._file.write((f"{inner}{socket_var}.max_value = {socket.max_value}\n")) + + #default attribute name + if hasattr(socket, "default_attribute_name"): + if socket.default_attribute_name != "": + dan = str_to_py_str(socket.default_attribute_name) + self._file.write((f"{inner}{socket_var}" + f".default_attribute_name = {dan}\n")) + + #attribute domain + if hasattr(socket, "attribute_domain"): + ad = enum_to_py_str(socket.attribute_domain) + self._file.write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + + #tooltip + if socket.description != "": + description = str_to_py_str(socket.description) + self._file.write((f"{inner}{socket_var}.description = {description}\n")) + + #hide_value + if socket.hide_value is True: + self._file.write(f"{inner}{socket_var}.hide_value = True\n") + + #hide in modifier + if hasattr(socket, "hide_in_modifier"): + if socket.hide_in_modifier is True: + self._file.write(f"{inner}{socket_var}.hide_in_modifier = True\n") + + self._file.write("\n") + self._file.write("\n") + + def _set_input_defaults(self, node: bpy.types.Node, inner: str, + node_var: str) -> None: + """ + Sets defaults for input sockets + + Parameters: + node (bpy.types.Node): node we're setting inputs for + inner (str): indentation + node_var (str): variable name we're using for the copied node + addon_dir (str): directory of the add-on, for if we need to save other + objects for the add-on + """ + if node.bl_idname == 'NodeReroute': + return + + for i, input in enumerate(node.inputs): + if input.bl_idname not in dont_set_defaults and not input.is_linked: + #TODO: this could be cleaner + socket_var = f"{node_var}.inputs[{i}]" + + #colors + if input.bl_idname == 'NodeSocketColor': + default_val = vec4_to_py_str(input.default_value) + + #vector types + elif "Vector" in input.bl_idname: + default_val = vec3_to_py_str(input.default_value) + + #strings + elif input.bl_idname == 'NodeSocketString': + default_val = str_to_py_str(input.default_value) + + #images + elif input.bl_idname == 'NodeSocketImage': + img = input.default_value + if img is not None and self._addon_dir != None: #write in a better way + self._save_image(img) + self._load_image(img, inner, f"{socket_var}.default_value") + default_val = None + + #materials + elif input.bl_idname == 'NodeSocketMaterial': + self._in_file_inputs(input, inner, socket_var, "materials") + default_val = None + + #collections + elif input.bl_idname == 'NodeSocketCollection': + self._in_file_inputs(input, inner, socket_var, "collections") + default_val = None + + #objects + elif input.bl_idname == 'NodeSocketObject': + self._in_file_inputs(input, inner, socket_var, "objects") + default_val = None + + #textures + elif input.bl_idname == 'NodeSocketTexture': + self._in_file_inputs(input, inner, socket_var, "textures") + default_val = None + + else: + default_val = input.default_value + if default_val is not None: + self._file.write(f"{inner}#{input.identifier}\n") + self._file.write((f"{inner}{socket_var}.default_value" + f" = {default_val}\n")) + self._file.write("\n") + + def _set_output_defaults(self, node: bpy.types.Node, + inner: str, node_var: str) -> None: + """ + Some output sockets need default values set. It's rather annoying + + Parameters: + node (bpy.types.Node): node for the output we're setting + inner (str): indentation string + node_var (str): variable name for the node we're setting output defaults for + """ + #TODO: probably should define elsewhere + output_default_nodes = {'ShaderNodeValue', + 'ShaderNodeRGB', + 'ShaderNodeNormal', + 'CompositorNodeValue', + 'CompositorNodeRGB', + 'CompositorNodeNormal'} + + if node.bl_idname not in output_default_nodes: + return + + dv = node.outputs[0].default_value + if node.bl_idname in {'ShaderNodeRGB', 'CompositorNodeRGB'}: + dv = vec4_to_py_str(list(dv)) + if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}: + dv = vec3_to_py_str(dv) + self._file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) + + def _in_file_inputs(self, input: bpy.types.NodeSocket, + inner: str, + socket_var: str, + type: str + ) -> None: + """ + Sets inputs for a node input if one already exists in the blend file + + Parameters: + input (bpy.types.NodeSocket): input socket we're setting the value for + inner (str): indentation string + socket_var (str): variable name we're using for the socket + type (str): from what section of bpy.data to pull the default value from + """ + + if input.default_value is None: + return + name = str_to_py_str(input.default_value.name) + self._file.write(f"{inner}if {name} in bpy.data.{type}:\n") + self._file.write((f"{inner}\t{socket_var}.default_value = " + f"bpy.data.{type}[{name}]\n")) + + def _color_ramp_settings(self, node: bpy.types.Node, + inner: str, + node_var: str, + color_ramp_name: str) -> None: + """ + Replicate a color ramp node + + Parameters + node (bpy.types.Node): node object we're copying settings from + inner (str): indentation + node_var (str): name of the variable we're using for the color ramp + color_ramp_name (str): name of the color ramp to be copied + """ + + color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name) + if not color_ramp: + raise ValueError(f"No color ramp named \"{color_ramp_name}\" found") + + #settings + ramp_str = f"{inner}{node_var}.{color_ramp_name}" + + color_mode = enum_to_py_str(color_ramp.color_mode) + self._file.write(f"{ramp_str}.color_mode = {color_mode}\n") + + hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) + self._file.write((f"{ramp_str}.hue_interpolation = " + f"{hue_interpolation}\n")) + interpolation = enum_to_py_str(color_ramp.interpolation) + self._file.write((f"{ramp_str}.interpolation " + f"= {interpolation}\n")) + self._file.write("\n") + + #key points + self._file.write(f"{inner}#initialize color ramp elements\n") + self._file.write((f"{ramp_str}.elements.remove" + f"({ramp_str}.elements[0])\n")) + for i, element in enumerate(color_ramp.elements): + element_var = f"{node_var}_cre_{i}" + if i == 0: + self._file.write(f"{inner}{element_var} = " + f"{ramp_str}.elements[{i}]\n") + self._file.write(f"{inner}{element_var}.position = {element.position}\n") + else: + self._file.write((f"{inner}{element_var} = " + f"{ramp_str}.elements" + f".new({element.position})\n")) + + self._file.write((f"{inner}{element_var}.alpha = " + f"{element.alpha}\n")) + color_str = vec4_to_py_str(element.color) + self._file.write((f"{inner}{element_var}.color = {color_str}\n\n")) + + def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, + node_var: str, curve_mapping_name: str + ) -> None: + """ + Sets defaults for Float, Vector, and Color curves + + Parameters: + node (bpy.types.Node): curve node we're copying settings from + file (TextIO): file we're generating the add-on into + inner (str): indentation + node_var (str): variable name for the add-on's curve node + curve_mapping_name (str): name of the curve mapping to be set + """ + + mapping = getattr(node, curve_mapping_name) + if not mapping: + raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " + f"in node \"{node.bl_idname}\"")) + + #mapping settings + self._file.write(f"{inner}#mapping settings\n") + mapping_var = f"{inner}{node_var}.{curve_mapping_name}" + + #extend + extend = enum_to_py_str(mapping.extend) + self._file.write(f"{mapping_var}.extend = {extend}\n") + #tone + tone = enum_to_py_str(mapping.tone) + self._file.write(f"{mapping_var}.tone = {tone}\n") + + #black level + b_lvl_str = vec3_to_py_str(mapping.black_level) + self._file.write((f"{mapping_var}.black_level = {b_lvl_str}\n")) + #white level + w_lvl_str = vec3_to_py_str(mapping.white_level) + self._file.write((f"{mapping_var}.white_level = {w_lvl_str}\n")) + + #minima and maxima + min_x = mapping.clip_min_x + self._file.write(f"{mapping_var}.clip_min_x = {min_x}\n") + min_y = mapping.clip_min_y + self._file.write(f"{mapping_var}.clip_min_y = {min_y}\n") + max_x = mapping.clip_max_x + self._file.write(f"{mapping_var}.clip_max_x = {max_x}\n") + max_y = mapping.clip_max_y + self._file.write(f"{mapping_var}.clip_max_y = {max_y}\n") + + #use_clip + use_clip = mapping.use_clip + self._file.write(f"{mapping_var}.use_clip = {use_clip}\n") + + #create curves + for i, curve in enumerate(mapping.curves): + #TODO: curve function + self._file.write(f"{inner}#curve {i}\n") + curve_i = f"{node_var}_curve_{i}" + self._file.write((f"{inner}{curve_i} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) + + # Remove default points when CurveMap is initialized with more than + # two points (just CompositorNodeHueCorrect) + if (node.bl_idname == 'CompositorNodeHueCorrect'): + self._file.write((f"{inner}for i in " + f"range(len({curve_i}.points.values()) - 1, 1, -1):\n")) + self._file.write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") + + for j, point in enumerate(curve.points): + #TODO: point function + point_j = f"{inner}{curve_i}_point_{j}" + + loc = point.location + loc_str = f"{loc[0]}, {loc[1]}" + if j < 2: + self._file.write(f"{point_j} = {curve_i}.points[{j}]\n") + self._file.write(f"{point_j}.location = ({loc_str})\n") + else: + self._file.write((f"{point_j} = {curve_i}.points.new({loc_str})\n")) + + handle = enum_to_py_str(point.handle_type) + self._file.write(f"{point_j}.handle_type = {handle}\n") + + #update curve + self._file.write(f"{inner}#update curve after changes\n") + self._file.write(f"{mapping_var}.update()\n") + + def _save_image(self, img: bpy.types.Image) -> None: + """ + Saves an image to an image directory of the add-on + + Parameters: + img (bpy.types.Image): image to be saved + """ + + if img is None: + return + + #create image dir if one doesn't exist + img_dir = os.path.join(self._addon_dir, IMAGE_DIR_NAME) + if not os.path.exists(img_dir): + os.mkdir(img_dir) + + #save the image + img_str = img_to_py_str(img) + img_path = f"{img_dir}/{img_str}" + if not os.path.exists(img_path): + img.save_render(img_path) + + def _load_image(self, img: bpy.types.Image, + inner: str, + img_var: str + ) -> None: + """ + Loads an image from the add-on into a blend file and assigns it + + Parameters: + img (bpy.types.Image): Blender image from the original node group + inner (str): indentation string + img_var (str): variable name to be used for the image + """ + + if img is None: + return + + img_str = img_to_py_str(img) + + #TODO: convert to special variables + self._file.write(f"{inner}#load image {img_str}\n") + self._file.write((f"{inner}base_dir = " + f"os.path.dirname(os.path.abspath(__file__))\n")) + self._file.write((f"{inner}image_path = " + f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " + f"\"{img_str}\")\n")) + self._file.write((f"{inner}{img_var} = " + f"bpy.data.images.load(image_path, check_existing = True)\n")) + + #copy image settings + self._file.write(f"{inner}#set image settings\n") + + #source + source = enum_to_py_str(img.source) + self._file.write(f"{inner}{img_var}.source = {source}\n") + + #color space settings + color_space = enum_to_py_str(img.colorspace_settings.name) + self._file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") + + #alpha mode + alpha_mode = enum_to_py_str(img.alpha_mode) + self._file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") + + def _image_user_settings(self, img_user: bpy.types.ImageUser, + inner: str, + img_user_var: str) -> None: + """ + Replicate the image user of an image node + + Parameters + img_usr (bpy.types.ImageUser): image user to be copied + inner (str): indentation + img_usr_var (str): variable name for the generated image user + """ + + img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", + "frame_start", "tile", "use_auto_refresh", "use_cyclic"] + + for img_usr_attr in img_usr_attrs: + self._file.write((f"{inner}{img_user_var}.{img_usr_attr} = " + f"{getattr(img_user, img_usr_attr)}\n")) + + def _set_parents(self, node_tree: bpy.types.NodeTree, + inner: str) -> None: + """ + Sets parents for all nodes, mostly used to put nodes in frames + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from + inner (str): indentation string + """ + parent_comment = False + for node in node_tree.nodes: + if node is not None and node.parent is not None: + if not parent_comment: + self._file.write(f"{inner}#Set parents\n") + parent_comment = True + node_var = self._node_vars[node] + parent_var = self._node_vars[node.parent] + self._file.write(f"{inner}{node_var}.parent = {parent_var}\n") + self._file.write("\n") + + def _set_locations(self, node_tree: bpy.types.NodeTree, inner: str) -> None: + """ + Set locations for all nodes + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from + inner (str): indentation string + """ + + self._file.write(f"{inner}#Set locations\n") + for node in node_tree.nodes: + node_var = self._node_vars[node] + self._file.write((f"{inner}{node_var}.location " + f"= ({node.location.x}, {node.location.y})\n")) + self._file.write("\n") + + def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, + ) -> None: + """ + Set dimensions for all nodes + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from + inner (str): indentation string + """ + self._file.write(f"{inner}#Set dimensions\n") + for node in node_tree.nodes: + node_var = self._node_vars[node] + self._file.write((f"{inner}{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}\n")) + self._file.write("\n") + + def _init_links(self, node_tree: bpy.types.NodeTree, + inner: str, + node_tree_var: str) -> None: + """ + Create all the links between nodes + + Parameters: + node_tree (bpy.types.NodeTree): node tree we're copying + inner (str): indentation + node_tree_var (str): variable name we're using for the copied node tree + """ + + if node_tree.links: + self._file.write(f"{inner}#initialize {node_tree_var} links\n") + for link in node_tree.links: + in_node_var = self._node_vars[link.from_node] + input_socket = link.from_socket + + """ + Blender's socket dictionary doesn't guarantee + unique keys, which has caused much wailing and + gnashing of teeth. This is a quick fix that + doesn't run quick + """ + #TODO: try using index() method + for i, item in enumerate(link.from_node.outputs.items()): + if item[1] == input_socket: + input_idx = i + break + + out_node_var = self._node_vars[link.to_node] + output_socket = link.to_socket + + for i, item in enumerate(link.to_node.inputs.items()): + if item[1] == output_socket: + output_idx = i + break + + self._file.write((f"{inner}#{in_node_var}.{input_socket.name} " + f"-> {out_node_var}.{output_socket.name}\n")) + self._file.write((f"{inner}{node_tree_var}.links.new({in_node_var}" + f".outputs[{input_idx}], " + f"{out_node_var}.inputs[{output_idx}])\n")) + + def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, + node_var: str) -> None: + """ + Hide hidden sockets + + Parameters: + node (bpy.types.Node): node object we're copying socket settings from + inner (str): indentation string + node_var (str): name of the variable we're using for this node + """ + for i, socket in enumerate(node.inputs): + if socket.hide is True: + self._file.write(f"{inner}{node_var}.inputs[{i}].hide = True\n") + for i, socket in enumerate(node.outputs): + if socket.hide is True: + self._file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") + def _set_socket_defaults(self, node: Node, node_var: str, inner: str): - if self.mode == 'ADDON': - set_input_defaults(node, self._file, inner, node_var, self._addon_dir) - elif self.mode == 'SCRIPT': - set_input_defaults(node, self._file, inner, node_var) - set_output_defaults(node, self._file, inner, node_var) + self._set_input_defaults(node, inner, node_var) + self._set_output_defaults(node, inner, node_var) + + def _create_menu_func(self) -> None: + """ + Creates the menu function + """ + + self._file.write("def menu_func(self, context):\n") + self._file.write(f"\tself.layout.operator({self._class_name}.bl_idname)\n") + self._file.write("\n") + + def _create_register_func(self) -> None: + """ + Creates the register function + """ + self._file.write("def register():\n") + self._file.write(f"\tbpy.utils.register_class({self._class_name})\n") + self._file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") + self._file.write("\n") + + def _create_unregister_func(self) -> None: + """ + Creates the unregister function + """ + self._file.write("def unregister():\n") + self._file.write(f"\tbpy.utils.unregister_class({self._class_name})\n") + self._file.write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") + self._file.write("\n") + + def _create_main_func(self) -> None: + """ + Creates the main function + """ + self._file.write("if __name__ == \"__main__\":\n") + self._file.write("\tregister()") + + def _zip_addon(self) -> None: + """ + Zips up the addon and removes the directory + """ + shutil.make_archive(self._zip_dir, "zip", self._zip_dir) + shutil.rmtree(self._zip_dir) # ABSTRACT def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, diff --git a/compositor/operator.py b/compositor/operator.py index 06bde78..a4989d6 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -78,8 +78,7 @@ def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, self._used_vars) self._node_trees.add(node_nt) - node_var: str = create_node(node, self._file, inner, ntp_nt.var, self._node_vars, - self._used_vars) + node_var: str = self._create_node(node, inner, ntp_nt.var) if node.bl_idname == 'CompositorNodeColorBalance': if node.correction_method == 'LIFT_GAMMA_GAIN': @@ -94,11 +93,10 @@ def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, ("power", ST.COLOR), ("slope", ST.COLOR)] - compositor_node_settings['CompositorNodeColorBalance'] = lst + self._settings['CompositorNodeColorBalance'] = lst - set_settings_defaults(node, compositor_node_settings, self._file, - self._addon_dir, inner, node_var) - hide_hidden_sockets(node, self._file, inner, node_var) + self._set_settings_defaults(node, inner, node_var) + self._hide_hidden_sockets(node, inner, node_var) if node.bl_idname == 'CompositorNodeGroup': if node.node_tree is not None: @@ -106,11 +104,11 @@ def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - group_io_settings(node, self._file, inner, "input", ntp_nt.var, ntp_nt.node_tree) + self._group_io_settings(node, inner, "input", ntp_nt.var, ntp_nt.node_tree) inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - group_io_settings(node, self._file, inner, "output", ntp_nt.var, ntp_nt.node_tree) + self._group_io_settings(node, inner, "output", ntp_nt.var, ntp_nt.node_tree) outputs_set = True self._set_socket_defaults(node, node_var, inner) @@ -125,10 +123,10 @@ def _process_node_tree(self, node_tree, level): """ if self._is_outermost_node_group(level): - nt_var = create_var(self.compositor_name, self._used_vars) + nt_var = self._create_var(self.compositor_name) nt_name = self.compositor_name else: - nt_var = create_var(node_tree.name, self._used_vars) + nt_var = self._create_var(node_tree.name) nt_name = node_tree.name outer, inner = make_indents(level) @@ -143,11 +141,11 @@ def _process_node_tree(self, node_tree, level): for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) - set_parents(node_tree, self._file, inner, self._node_vars) - set_locations(node_tree, self._file, inner, self._node_vars) - set_dimensions(node_tree, self._file, inner, self._node_vars) + self._set_parents(node_tree, inner) + self._set_locations(node_tree, inner) + self._set_dimensions(node_tree, inner) - init_links(node_tree, self._file, inner, nt_var, self._node_vars) + self._init_links(node_tree, inner, nt_var) self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") @@ -173,9 +171,9 @@ def execute(self, context): self._file = open(f"{self._addon_dir}/__init__.py", "w") - create_header(self._file, self.compositor_name) - class_name = clean_string(self.compositor_name, lower=False) - init_operator(self._file, class_name, comp_var, self.compositor_name) + self._create_header(self.compositor_name) + self._class_name = clean_string(self.compositor_name, lower=False) + self._init_operator(comp_var, self.compositor_name) self._file.write("\tdef execute(self, context):\n") else: @@ -196,17 +194,17 @@ def execute(self, context): if self.mode == 'ADDON': self._file.write("\t\treturn {'FINISHED'}\n\n") - create_menu_func(self._file, class_name) - create_register_func(self._file, class_name) - create_unregister_func(self._file, class_name) - create_main_func(self._file) + self._create_menu_func() + self._create_register_func() + self._create_unregister_func() + self._create_main_func() else: context.window_manager.clipboard = self._file.getvalue() self._file.close() if self.mode == 'ADDON': - zip_addon(self._zip_dir) + self._zip_addon() self._report_finished("compositor nodes") diff --git a/geometry/operator.py b/geometry/operator.py index b49d9ca..facb8d1 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -51,20 +51,18 @@ def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, inner: str, level: int) -> None: #create node - node_var: str = create_node(node, self._file, inner, ntp_node_tree.var, - self._node_vars, self._used_vars) - set_settings_defaults(node, self._settings, self._file, - self._addon_dir, inner, node_var) + node_var: str = self._create_node(node, inner, ntp_node_tree.var) + self._set_settings_defaults(node, inner, node_var) if node.bl_idname == 'GeometryNodeGroup': self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - group_io_settings(node, self._file, inner, "input", ntp_node_tree.var, + self._group_io_settings(node, inner, "input", ntp_node_tree.var, ntp_node_tree.node_tree) #TODO: convert to using NTP_NodeTrees ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - group_io_settings(node, self._file, inner, "output", + self._group_io_settings(node, inner, "output", ntp_node_tree.var, ntp_node_tree.node_tree) ntp_node_tree.outputs_set = True @@ -74,7 +72,7 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, elif node.bl_idname == 'GeometryNodeSimulationOutput': self._process_sim_output_node(node, inner, node_var) - hide_hidden_sockets(node, self._file, inner, node_var) + self._hide_hidden_sockets(node, inner, node_var) if node.bl_idname != 'GeometryNodeSimulationInput': self._set_socket_defaults(node, node_var, inner) @@ -112,7 +110,7 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, node groups within node groups and script/add-on differences """ - nt_var = create_var(node_tree.name, self._used_vars) + nt_var = self._create_var(node_tree.name) outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? # Eventually these should go away anyways, and level of indentation depends just on the mode @@ -135,12 +133,12 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, self._process_sim_zones(ntp_nt.sim_inputs, inner) #set look of nodes - set_parents(node_tree, self._file, inner, self._node_vars) - set_locations(node_tree, self._file, inner, self._node_vars) - set_dimensions(node_tree, self._file, inner, self._node_vars) + self._set_parents(node_tree, inner) + self._set_locations(node_tree, inner) + self._set_dimensions(node_tree, inner) #create connections - init_links(node_tree, self._file, inner, nt_var, self._node_vars) + self._init_links(node_tree, inner, nt_var) self._file.write(f"{inner}return {nt_var}\n") @@ -173,9 +171,9 @@ def execute(self, context): self._file = open(f"{self._addon_dir}/__init__.py", "w") - create_header(self._file, nt.name) - class_name = clean_string(nt.name, lower = False) - init_operator(self._file, class_name, nt_var, nt.name) + self._create_header(nt.name) + self._class_name = clean_string(nt.name, lower = False) + self._init_operator(nt_var, nt.name) self._file.write("\tdef execute(self, context):\n") else: self._file = StringIO("") @@ -189,16 +187,16 @@ def execute(self, context): if self.mode == 'ADDON': self._apply_modifier(nt, nt_var) self._file.write("\t\treturn {'FINISHED'}\n\n") - create_menu_func(self._file, class_name) - create_register_func(self._file, class_name) - create_unregister_func(self._file, class_name) - create_main_func(self._file) + self._create_menu_func() + self._create_register_func() + self._create_unregister_func() + self._create_main_func() else: context.window_manager.clipboard = self._file.getvalue() self._file.close() if self.mode == 'ADDON': - zip_addon(self._zip_dir) + self._zip_addon() self._report_finished("geometry node group") diff --git a/material/operator.py b/material/operator.py index 282021b..9077d5b 100644 --- a/material/operator.py +++ b/material/operator.py @@ -48,28 +48,26 @@ def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): def _process_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: str, level: int) -> None: #create node - node_var: str = create_node(node, self._file, inner, ntp_node_tree.var, - self._node_vars, self._used_vars) - set_settings_defaults(node, self._settings, self._file, - self._addon_dir, inner, node_var) + node_var: str = self._create_node(node, inner, ntp_node_tree.var) + self._set_settings_defaults(node, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': self._process_group_node_tree(node, node_var, level, inner) - + #TODO: should probably be lumped into one function, + #it's always called like this and we're double checking it elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - group_io_settings(node, self._file, inner, "input", + self._group_io_settings(node, inner, "input", ntp_node_tree.var, ntp_node_tree.node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - group_io_settings(node, self._file, inner, "output", + self._group_io_settings(node, inner, "output", ntp_node_tree.var, ntp_node_tree.node_tree) ntp_node_tree.outputs_set = True - hide_hidden_sockets(node, self._file, inner, node_var) + self._hide_hidden_sockets(node, inner, node_var) self._set_socket_defaults(node, node_var, inner) - def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: """ Generates a Python function to recreate a node tree @@ -81,10 +79,10 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: """ if self._is_outermost_node_group(level): - nt_var = create_var(self.material_name, self._used_vars) + nt_var = self._create_var(self.material_name) nt_name = self.material_name #TODO: this is probably overcomplicating things if we move to a harder material vs shader node tree difference else: - nt_var = create_var(node_tree.name, self._used_vars) + nt_var = self._create_var(node_tree.name) nt_name = node_tree.name outer, inner = make_indents(level) @@ -99,11 +97,11 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) - set_parents(node_tree, self._file, inner, self._node_vars) - set_locations(node_tree, self._file, inner, self._node_vars) - set_dimensions(node_tree, self._file, inner, self._node_vars) + self._set_parents(node_tree, inner) + self._set_locations(node_tree, inner) + self._set_dimensions(node_tree, inner) - init_links(node_tree, self._file, inner, nt_var, self._node_vars) + self._init_links(node_tree, inner, nt_var) self._file.write(f"{inner}return {nt_var}\n") @@ -125,9 +123,9 @@ def execute(self, context): self._file = open(f"{self._addon_dir}/__init__.py", "w") - create_header(self._file, self.material_name) + self._create_header(self.material_name) self._class_name = clean_string(self.material_name, lower=False) - init_operator(self._file, self._class_name, mat_var, self.material_name) + self._init_operator(mat_var, self.material_name) self._file.write("\tdef execute(self, context):\n") else: @@ -147,17 +145,17 @@ def execute(self, context): if self.mode == 'ADDON': self._file.write("\t\treturn {'FINISHED'}\n\n") - create_menu_func(self._file, self._class_name) - create_register_func(self._file, self._class_name) - create_unregister_func(self._file, self._class_name) - create_main_func(self._file) + self._create_menu_func() + self._create_register_func() + self._create_unregister_func() + self._create_main_func() else: context.window_manager.clipboard = self._file.getvalue() self._file.close() if self.mode == 'ADDON': - zip_addon(self._zip_dir) + self._zip_addon(self._zip_dir) self._report_finished("material") diff --git a/utils.py b/utils.py index 5d41f4e..bec9a44 100644 --- a/utils.py +++ b/utils.py @@ -172,67 +172,6 @@ def img_to_py_str(img : bpy.types.Image) -> str: format = img.file_format.lower() return f"{name}.{format}" -def create_header(file: TextIO, name: str) -> None: - """ - Sets up the bl_info and imports the Blender API - - Parameters: - file (TextIO): the file for the generated add-on - name (str): name of the add-on - """ - - file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{name}\",\n") - file.write("\t\"author\" : \"Node To Python\",\n") - file.write("\t\"version\" : (1, 0, 0),\n") - file.write(f"\t\"blender\" : {bpy.app.version},\n") - file.write("\t\"location\" : \"Object\",\n") #TODO - file.write("\t\"category\" : \"Node\"\n") - file.write("}\n") - file.write("\n") - file.write("import bpy\n") - file.write("import mathutils\n") - file.write("import os\n") - file.write("\n") - -def init_operator(file: TextIO, name: str, idname: str, label: str) -> None: - """ - Initializes the add-on's operator - - Parameters: - file (TextIO): the file for the generated add-on - name (str): name for the class - idname (str): name for the operator - label (str): appearence inside Blender - """ - file.write(f"class {name}(bpy.types.Operator):\n") - file.write(f"\tbl_idname = \"object.{idname}\"\n") - file.write(f"\tbl_label = \"{label}\"\n") - file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - file.write("\n") - -def create_var(name: str, used_vars: dict[str, int]) -> 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 (dict[str, int]): dictionary containing variable names and usage counts - - Returns: - clean_name (str): variable name for the node tree - """ - if name == "": - name = "unnamed" - clean_name = clean_string(name) - var = clean_name - if var in used_vars: - used_vars[var] += 1 - return f"{clean_name}_{used_vars[var]}" - else: - used_vars[var] = 0 - return clean_name - #TODO: reconsider node tree definitions within node tree definitions def make_indents(level: int) -> Tuple[str, str]: """ @@ -251,763 +190,4 @@ def make_indents(level: int) -> Tuple[str, str]: """ outer = "\t"*level inner = "\t"*(level + 1) - return outer, inner - -def create_node(node: bpy.types.Node, - file: TextIO, - inner: str, - node_tree_var: str, - node_vars: dict[bpy.types.Node, str], - used_vars: dict[str, int] - ) -> str: - """ - Initializes a new node with location, dimension, and label info - - Parameters: - node (bpy.types.Node): node to be copied - file (TextIO): file containing the generated add-on - inner (str): indentation level for this logic - node_tree_var (str): variable name for the node tree - node_vars (dict): dictionary containing Node to corresponding variable name - pairs - used_vars dict[str, int]: dictionary of base variable names to usage counts - - Returns: - node_var (str): variable name for the node - """ - - file.write(f"{inner}#node {node.name}\n") - - node_var = create_var(node.name, used_vars) - node_vars[node] = node_var - - file.write((f"{inner}{node_var} " - f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) - #label - if node.label: - file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") - - #color - if node.use_custom_color: - file.write(f"{inner}{node_var}.use_custom_color = True\n") - file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") - - #mute - if node.mute: - file.write(f"{inner}{node_var}.mute = True\n") - - return node_var - -def set_settings_defaults(node: bpy.types.Node, - settings: dict[str, list[(str, ST)]], - file: TextIO, - addon_dir: str, - inner: str, - node_var: str - ) -> None: - """ - Sets the defaults for any settings a node may have - - Parameters: - node (bpy.types.Node): the node object we're copying settings from - settings (dict): a predefined dictionary of all settings every node has - file (TextIO): file we're generating the add-on into - addon_dir (str): directory that the addon is saved into - inner (str): indentation - node_var (str): name of the variable we're using for the node in our add-on - """ - if node.bl_idname not in settings: - print((f"NodeToPython: couldn't find {node.bl_idname} in settings." - f"Your Blender version may not be supported")) - return - - for (attr_name, type) in settings[node.bl_idname]: - attr = getattr(node, attr_name, None) - if attr is None: - print(f"\"{node_var}.{attr_name}\" not found") - continue - setting_str = f"{inner}{node_var}.{attr_name}" - if type == ST.ENUM: - if attr != '': - file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") - elif type == ST.ENUM_SET: - file.write(f"{setting_str} = {attr}\n") - elif type == ST.STRING: - file.write(f"{setting_str} = {str_to_py_str(attr)}\n") - elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: - file.write(f"{setting_str} = {attr}\n") - elif type == ST.VEC1: - file.write(f"{setting_str} = {vec1_to_py_str(attr)}\n") - elif type == ST.VEC2: - file.write(f"{setting_str} = {vec2_to_py_str(attr)}\n") - elif type == ST.VEC3: - file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") - elif type == ST.VEC4: - file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") - elif type == ST.COLOR: - file.write(f"{setting_str} = {color_to_py_str(attr)}\n") - elif type == ST.MATERIAL: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.materials:\n")) - file.write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.materials[{name}]\n")) - elif type == ST.OBJECT: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.objects:\n")) - file.write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.objects[{name}]\n")) - elif type == ST.COLOR_RAMP: - color_ramp_settings(node, file, inner, node_var, attr_name) - elif type == ST.CURVE_MAPPING: - curve_mapping_settings(node, file, inner, node_var, attr_name) - elif type == ST.IMAGE: - if addon_dir is not None and attr is not None: - if attr.source in {'FILE', 'GENERATED', 'TILED'}: - save_image(attr, addon_dir) - load_image(attr, file, inner, f"{node_var}.{attr_name}") - elif type == ST.IMAGE_USER: - image_user_settings(attr, file, inner, f"{node_var}.{attr_name}") - -def hide_hidden_sockets(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str - ) -> None: - """ - Hide hidden sockets - - Parameters: - node (bpy.types.Node): node object we're copying socket settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation string - node_var (str): name of the variable we're using for this node - """ - for i, socket in enumerate(node.inputs): - if socket.hide is True: - file.write(f"{inner}{node_var}.inputs[{i}].hide = True\n") - for i, socket in enumerate(node.outputs): - if socket.hide is True: - file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") - -def group_io_settings(node: bpy.types.Node, - file: TextIO, - inner: str, - io: str, #TODO: convert to enum - node_tree_var: str, - node_tree: bpy.types.NodeTree - ) -> None: - """ - Set the settings for group input and output sockets - - Parameters: - node (bpy.types.Node) : group input/output node - file (TextIO): file we're generating the add-on into - inner (str): indentation string - io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating input - and output settings for - """ - if io == "input": - ios = node.outputs - ntio = node_tree.inputs - else: - ios = node.inputs - ntio = node_tree.outputs - - file.write(f"{inner}#{node_tree_var} {io}s\n") - for i, inout in enumerate(ios): - if inout.bl_idname == 'NodeSocketVirtual': - continue - file.write(f"{inner}#{io} {inout.name}\n") - idname = enum_to_py_str(inout.bl_idname) - name = str_to_py_str(inout.name) - file.write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") - socket = ntio[i] - socket_var = f"{node_tree_var}.{io}s[{i}]" - - if inout.type in default_sockets: - #TODO: separate default socket function - #default value - if inout.type == 'RGBA': - dv = vec4_to_py_str(socket.default_value) - elif inout.type == 'VECTOR': - dv = vec3_to_py_str(socket.default_value) - else: - dv = socket.default_value - file.write(f"{inner}{socket_var}.default_value = {dv}\n") - - #min value - if hasattr(socket, "min_value"): - file.write(f"{inner}{socket_var}.min_value = {socket.min_value}\n") - #max value - if hasattr(socket, "min_value"): - file.write((f"{inner}{socket_var}.max_value = {socket.max_value}\n")) - - #default attribute name - if hasattr(socket, "default_attribute_name"): - if socket.default_attribute_name != "": - dan = str_to_py_str(socket.default_attribute_name) - file.write((f"{inner}{socket_var}" - f".default_attribute_name = {dan}\n")) - - #attribute domain - if hasattr(socket, "attribute_domain"): - ad = enum_to_py_str(socket.attribute_domain) - file.write(f"{inner}{socket_var}.attribute_domain = {ad}\n") - - #tooltip - if socket.description != "": - description = str_to_py_str(socket.description) - file.write((f"{inner}{socket_var}.description = {description}\n")) - - #hide_value - if socket.hide_value is True: - file.write(f"{inner}{socket_var}.hide_value = True\n") - - #hide in modifier - if hasattr(socket, "hide_in_modifier"): - if socket.hide_in_modifier is True: - file.write(f"{inner}{socket_var}.hide_in_modifier = True\n") - - file.write("\n") - file.write("\n") - -def color_ramp_settings(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str, - color_ramp_name: str - ) -> None: - """ - Replicate a color ramp node - - Parameters - node (bpy.types.Node): node object we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): name of the variable we're using for the color ramp - color_ramp_name (str): name of the color ramp to be copied - """ - - color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name) - if not color_ramp: - raise ValueError(f"No color ramp named \"{color_ramp_name}\" found") - - #settings - ramp_str = f"{inner}{node_var}.{color_ramp_name}" - - color_mode = enum_to_py_str(color_ramp.color_mode) - file.write(f"{ramp_str}.color_mode = {color_mode}\n") - - hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) - file.write((f"{ramp_str}.hue_interpolation = " - f"{hue_interpolation}\n")) - interpolation = enum_to_py_str(color_ramp.interpolation) - file.write((f"{ramp_str}.interpolation " - f"= {interpolation}\n")) - file.write("\n") - - #key points - file.write(f"{inner}#initialize color ramp elements\n") - file.write((f"{ramp_str}.elements.remove" - f"({ramp_str}.elements[0])\n")) - for i, element in enumerate(color_ramp.elements): - element_var = f"{node_var}_cre_{i}" - if i == 0: - file.write(f"{inner}{element_var} = " - f"{ramp_str}.elements[{i}]\n") - file.write(f"{inner}{element_var}.position = {element.position}\n") - else: - file.write((f"{inner}{element_var} = " - f"{ramp_str}.elements" - f".new({element.position})\n")) - - file.write((f"{inner}{element_var}.alpha = " - f"{element.alpha}\n")) - color_str = vec4_to_py_str(element.color) - file.write((f"{inner}{element_var}.color = {color_str}\n\n")) - -def curve_mapping_settings(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str, - curve_mapping_name: str - ) -> None: - """ - Sets defaults for Float, Vector, and Color curves - - Parameters: - node (bpy.types.Node): curve node we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): variable name for the add-on's curve node - curve_mapping_name (str): name of the curve mapping to be set - """ - - mapping = getattr(node, curve_mapping_name) - if not mapping: - raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " - f"in node \"{node.bl_idname}\"")) - - #mapping settings - file.write(f"{inner}#mapping settings\n") - mapping_var = f"{inner}{node_var}.{curve_mapping_name}" - - #extend - extend = enum_to_py_str(mapping.extend) - file.write(f"{mapping_var}.extend = {extend}\n") - #tone - tone = enum_to_py_str(mapping.tone) - file.write(f"{mapping_var}.tone = {tone}\n") - - #black level - b_lvl_str = vec3_to_py_str(mapping.black_level) - file.write((f"{mapping_var}.black_level = {b_lvl_str}\n")) - #white level - w_lvl_str = vec3_to_py_str(mapping.white_level) - file.write((f"{mapping_var}.white_level = {w_lvl_str}\n")) - - #minima and maxima - min_x = mapping.clip_min_x - file.write(f"{mapping_var}.clip_min_x = {min_x}\n") - min_y = mapping.clip_min_y - file.write(f"{mapping_var}.clip_min_y = {min_y}\n") - max_x = mapping.clip_max_x - file.write(f"{mapping_var}.clip_max_x = {max_x}\n") - max_y = mapping.clip_max_y - file.write(f"{mapping_var}.clip_max_y = {max_y}\n") - - #use_clip - use_clip = mapping.use_clip - file.write(f"{mapping_var}.use_clip = {use_clip}\n") - - #create curves - for i, curve in enumerate(mapping.curves): - #TODO: curve function - file.write(f"{inner}#curve {i}\n") - curve_i = f"{node_var}_curve_{i}" - file.write((f"{inner}{curve_i} = " - f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) - - # Remove default points when CurveMap is initialized with more than - # two points (just CompositorNodeHueCorrect) - if (node.bl_idname == 'CompositorNodeHueCorrect'): - file.write((f"{inner}for i in " - f"range(len({curve_i}.points.values()) - 1, 1, -1):\n")) - file.write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") - - for j, point in enumerate(curve.points): - #TODO: point function - point_j = f"{inner}{curve_i}_point_{j}" - - loc = point.location - loc_str = f"{loc[0]}, {loc[1]}" - if j < 2: - file.write(f"{point_j} = {curve_i}.points[{j}]\n") - file.write(f"{point_j}.location = ({loc_str})\n") - else: - file.write((f"{point_j} = {curve_i}.points.new({loc_str})\n")) - - handle = enum_to_py_str(point.handle_type) - file.write(f"{point_j}.handle_type = {handle}\n") - - #update curve - file.write(f"{inner}#update curve after changes\n") - file.write(f"{mapping_var}.update()\n") - -def save_image(img: bpy.types.Image, addon_dir: str) -> None: - """ - Saves an image to an image directory of the add-on - - Parameters: - img (bpy.types.Image): image to be saved - addon_dir (str): directory of the addon - """ - - if img is None: - return - - #create image dir if one doesn't exist - img_dir = os.path.join(addon_dir, IMAGE_DIR_NAME) - if not os.path.exists(img_dir): - os.mkdir(img_dir) - - #save the image - img_str = img_to_py_str(img) - img_path = f"{img_dir}/{img_str}" - if not os.path.exists(img_path): - img.save_render(img_path) - -def load_image(img: bpy.types.Image, - file: TextIO, - inner: str, - img_var: str - ) -> None: - """ - Loads an image from the add-on into a blend file and assigns it - - Parameters: - img (bpy.types.Image): Blender image from the original node group - file (TextIO): file for the generated add-on - inner (str): indentation string - img_var (str): variable name to be used for the image - """ - - if img is None: - return - - img_str = img_to_py_str(img) - - #TODO: convert to special variables - file.write(f"{inner}#load image {img_str}\n") - file.write((f"{inner}base_dir = " - f"os.path.dirname(os.path.abspath(__file__))\n")) - file.write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " - f"\"{img_str}\")\n")) - file.write((f"{inner}{img_var} = " - f"bpy.data.images.load(image_path, check_existing = True)\n")) - - #copy image settings - file.write(f"{inner}#set image settings\n") - - #source - source = enum_to_py_str(img.source) - file.write(f"{inner}{img_var}.source = {source}\n") - - #color space settings - color_space = enum_to_py_str(img.colorspace_settings.name) - file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") - - #alpha mode - alpha_mode = enum_to_py_str(img.alpha_mode) - file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") - -def image_user_settings(img_user: bpy.types.ImageUser, - file: TextIO, - inner: str, - img_user_var: str - ) -> None: - """ - Replicate the image user of an image node - - Parameters - img_usr (bpy.types.ImageUser): image user to be copied - file (TextIO): file we're generating the add-on into - inner (str): indentation - img_usr_var (str): variable name for the generated image user - """ - - img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", - "frame_start", "tile", "use_auto_refresh", "use_cyclic"] - - for img_usr_attr in img_usr_attrs: - file.write((f"{inner}{img_user_var}.{img_usr_attr} = " - f"{getattr(img_user, img_usr_attr)}\n")) - -def set_input_defaults(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str, - addon_dir: str = "" - ) -> None: - """ - Sets defaults for input sockets - - Parameters: - node (bpy.types.Node): node we're setting inputs for - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): variable name we're using for the copied node - addon_dir (str): directory of the add-on, for if we need to save other - objects for the add-on - """ - if node.bl_idname == 'NodeReroute': - return - - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults and not input.is_linked: - #TODO: this could be cleaner - socket_var = f"{node_var}.inputs[{i}]" - - #colors - if input.bl_idname == 'NodeSocketColor': - default_val = vec4_to_py_str(input.default_value) - - #vector types - elif "Vector" in input.bl_idname: - default_val = vec3_to_py_str(input.default_value) - - #strings - elif input.bl_idname == 'NodeSocketString': - default_val = str_to_py_str(input.default_value) - - #images - elif input.bl_idname == 'NodeSocketImage': - img = input.default_value - if img is not None and addon_dir != "": #write in a better way - save_image(img, addon_dir) - load_image(img, file, inner, f"{socket_var}.default_value") - default_val = None - - #materials - elif input.bl_idname == 'NodeSocketMaterial': - in_file_inputs(input, file, inner, socket_var, "materials") - default_val = None - - #collections - elif input.bl_idname == 'NodeSocketCollection': - in_file_inputs(input, file, inner, socket_var, "collections") - default_val = None - - #objects - elif input.bl_idname == 'NodeSocketObject': - in_file_inputs(input, file, inner, socket_var, "objects") - default_val = None - - #textures - elif input.bl_idname == 'NodeSocketTexture': - in_file_inputs(input, file, inner, socket_var, "textures") - default_val = None - - else: - default_val = input.default_value - if default_val is not None: - file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{socket_var}.default_value" - f" = {default_val}\n")) - file.write("\n") - -def in_file_inputs(input: bpy.types.NodeSocket, - file: TextIO, - inner: str, - socket_var: str, - type: str - ) -> None: - """ - Sets inputs for a node input if one already exists in the blend file - - Parameters: - input (bpy.types.NodeSocket): input socket we're setting the value for - file (TextIO): file we're writing the add-on into - inner (str): indentation string - socket_var (str): variable name we're using for the socket - type (str): from what section of bpy.data to pull the default value from - """ - - if input.default_value is not None: - name = str_to_py_str(input.default_value.name) - file.write(f"{inner}if {name} in bpy.data.{type}:\n") - file.write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) - -def set_output_defaults(node: bpy.types.Node, - file: TextIO, - inner: str, - node_var: str - ) -> None: - """ - Some output sockets need default values set. It's rather annoying - - Parameters: - node (bpy.types.Node): node for the output we're setting - file (TextIO): file we're generating the add-on into - inner (str): indentation string - node_var (str): variable name for the node we're setting output defaults for - """ - output_default_nodes = {'ShaderNodeValue', - 'ShaderNodeRGB', - 'ShaderNodeNormal', - 'CompositorNodeValue', - 'CompositorNodeRGB', - 'CompositorNodeNormal'} - - if node.bl_idname in output_default_nodes: - dv = node.outputs[0].default_value - if node.bl_idname in {'ShaderNodeRGB', 'CompositorNodeRGB'}: - dv = vec4_to_py_str(list(dv)) - if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}: - dv = vec3_to_py_str(dv) - file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) - -def set_parents(node_tree: bpy.types.NodeTree, - file: TextIO, - inner: str, - node_vars: dict[bpy.types.Node, str] - ) -> None: - """ - Sets parents for all nodes, mostly used to put nodes in frames - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - file (TextIO): file for the generated add-on - inner (str): indentation string - node_vars (dict[bpy.types.Node, str]): dictionary for node->variable name - pairs - """ - parent_comment = False - for node in node_tree.nodes: - if node is not None and node.parent is not None: - if not parent_comment: - file.write(f"{inner}#Set parents\n") - parent_comment = True - node_var = node_vars[node] - parent_var = node_vars[node.parent] - file.write(f"{inner}{node_var}.parent = {parent_var}\n") - file.write("\n") - -def set_locations(node_tree: bpy.types.NodeTree, - file: TextIO, - inner: str, - node_vars: dict[bpy.types.Node, str] - ) -> None: - """ - Set locations for all nodes - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - file (TextIO): file for the generated add-on - inner (str): indentation string - node_vars (dict[bpy.types.Node, str]): dictionary for (node, variable) name - pairs - """ - - file.write(f"{inner}#Set locations\n") - for node in node_tree.nodes: - node_var = node_vars[node] - file.write((f"{inner}{node_var}.location " - f"= ({node.location.x}, {node.location.y})\n")) - file.write("\n") - -def set_dimensions(node_tree: bpy.types.NodeTree, - file: TextIO, - inner: str, - node_vars: dict[bpy.types.Node, str] - ) -> None: - """ - Set dimensions for all nodes - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - file (TextIO): file for the generated add-on - inner (str): indentation string - node_vars (dict[bpy.types.Node, str]): dictionary for (node, variable) name - pairs - """ - - file.write(f"{inner}#Set dimensions\n") - for node in node_tree.nodes: - node_var = node_vars[node] - file.write((f"{inner}{node_var}.width, {node_var}.height " - f"= {node.width}, {node.height}\n")) - file.write("\n") - -def init_links(node_tree: bpy.types.NodeTree, - file: TextIO, - inner: str, - node_tree_var: str, - node_vars: dict[bpy.types.Node, str] - ) -> None: - """ - Create all the links between nodes - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're copying - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_tree_var (str): variable name we're using for the copied node tree - node_vars (dict[bpy.types.Node, str]): dictionary containing node to - variable name pairs - """ - - if node_tree.links: - file.write(f"{inner}#initialize {node_tree_var} links\n") - for link in node_tree.links: - in_node_var = node_vars[link.from_node] - input_socket = link.from_socket - - """ - Blender's socket dictionary doesn't guarantee - unique keys, which has caused much wailing and - gnashing of teeth. This is a quick fix that - doesn't run quick - """ - #TODO: try using index() method - for i, item in enumerate(link.from_node.outputs.items()): - if item[1] == input_socket: - input_idx = i - break - - out_node_var = node_vars[link.to_node] - output_socket = link.to_socket - - for i, item in enumerate(link.to_node.inputs.items()): - if item[1] == output_socket: - output_idx = i - break - - file.write((f"{inner}#{in_node_var}.{input_socket.name} " - f"-> {out_node_var}.{output_socket.name}\n")) - file.write((f"{inner}{node_tree_var}.links.new({in_node_var}" - f".outputs[{input_idx}], " - f"{out_node_var}.inputs[{output_idx}])\n")) - -def create_menu_func(file: TextIO, name: str) -> None: - """ - Creates the menu function - - Parameters: - file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class - """ - - file.write("def menu_func(self, context):\n") - file.write(f"\tself.layout.operator({name}.bl_idname)\n") - file.write("\n") - -def create_register_func(file: TextIO, name: str) -> None: - """ - Creates the register function - - Parameters: - file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class - """ - file.write("def register():\n") - file.write(f"\tbpy.utils.register_class({name})\n") - file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - file.write("\n") - -def create_unregister_func(file: TextIO, name: str) -> None: - """ - Creates the unregister function - - Parameters: - file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class - """ - file.write("def unregister():\n") - file.write(f"\tbpy.utils.unregister_class({name})\n") - file.write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") - file.write("\n") - -def create_main_func(file: TextIO) -> None: - """ - Creates the main function - - Parameters: - file (TextIO): file we're generating the add-on into - """ - file.write("if __name__ == \"__main__\":\n") - file.write("\tregister()") - -def zip_addon(zip_dir: str) -> None: - """ - Zips up the addon and removes the directory - - Parameters: - zip_dir (str): path to the top-level addon directory - """ - shutil.make_archive(zip_dir, "zip", zip_dir) - shutil.rmtree(zip_dir) \ No newline at end of file + return outer, inner \ No newline at end of file From ce1a976cdb63f9645d991bbcf61650a73c5bcb1c Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 19 Nov 2023 17:03:13 -0600 Subject: [PATCH 32/72] refactor: moved geo/shader nodes ui stuff into common module --- __init__.py | 8 ++++---- geometry/__init__.py | 6 ++---- geometry/menu.py | 24 ------------------------ geometry/{panel.py => ui.py} | 25 ++++++++++++++++++++++++- material/__init__.py | 10 ++++------ material/menu.py | 20 -------------------- material/{panel.py => ui.py} | 21 ++++++++++++++++++++- 7 files changed, 54 insertions(+), 60 deletions(-) delete mode 100644 geometry/menu.py rename geometry/{panel.py => ui.py} (56%) delete mode 100644 material/menu.py rename material/{panel.py => ui.py} (54%) diff --git a/__init__.py b/__init__.py index a949fee..0668ee9 100644 --- a/__init__.py +++ b/__init__.py @@ -48,12 +48,12 @@ def draw(self, context): compositor.ui.NTPCompositorPanel, #geometry geometry.operator.NTPGeoNodesOperator, - geometry.menu.NTPGeoNodesMenu, - geometry.panel.NTPGeoNodesPanel, + geometry.ui.NTPGeoNodesMenu, + geometry.ui.NTPGeoNodesPanel, #material material.operator.NTPMaterialOperator, - material.menu.NTPMaterialMenu, - material.panel.NTPMaterialPanel, + material.ui.NTPMaterialMenu, + material.ui.NTPMaterialPanel, ] def register(): diff --git a/geometry/__init__.py b/geometry/__init__.py index 80134d4..e2707b0 100644 --- a/geometry/__init__.py +++ b/geometry/__init__.py @@ -1,15 +1,13 @@ if "bpy" in locals(): import importlib - importlib.reload(menu) importlib.reload(node_settings) importlib.reload(node_tree) importlib.reload(operator) - importlib.reload(panel) + importlib.reload(ui) else: - from . import menu from . import node_settings from . import node_tree from . import operator - from . import panel + from . import ui import bpy \ No newline at end of file diff --git a/geometry/menu.py b/geometry/menu.py deleted file mode 100644 index 7d5561f..0000000 --- a/geometry/menu.py +++ /dev/null @@ -1,24 +0,0 @@ -import bpy -from bpy.types import Menu - -from .operator import NTPGeoNodesOperator - -class NTPGeoNodesMenu(Menu): - bl_idname = "NODE_MT_ntp_geo_nodes" - bl_label = "Select Geo Nodes" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - - geo_node_groups = [node_tree for node_tree in bpy.data.node_groups - if node_tree.bl_idname == 'GeometryNodeTree'] - - for node_tree in geo_node_groups: - op = layout.operator(NTPGeoNodesOperator.bl_idname, - text=node_tree.name) - op.geo_nodes_group_name = node_tree.name \ No newline at end of file diff --git a/geometry/panel.py b/geometry/ui.py similarity index 56% rename from geometry/panel.py rename to geometry/ui.py index c6f568b..b3a8283 100644 --- a/geometry/panel.py +++ b/geometry/ui.py @@ -1,5 +1,8 @@ import bpy from bpy.types import Panel +from bpy.types import Menu + +from .operator import NTPGeoNodesOperator class NTPGeoNodesPanel(Panel): bl_label = "Geometry Nodes to Python" @@ -29,4 +32,24 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") \ No newline at end of file + row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") + +class NTPGeoNodesMenu(Menu): + bl_idname = "NODE_MT_ntp_geo_nodes" + bl_label = "Select Geo Nodes" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + + geo_node_groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] + + for node_tree in geo_node_groups: + op = layout.operator(NTPGeoNodesOperator.bl_idname, + text=node_tree.name) + op.geo_nodes_group_name = node_tree.name \ No newline at end of file diff --git a/material/__init__.py b/material/__init__.py index b7380e4..e2707b0 100644 --- a/material/__init__.py +++ b/material/__init__.py @@ -1,15 +1,13 @@ if "bpy" in locals(): import importlib - importlib.reload(menu) importlib.reload(node_settings) - #importlib.reload(node_tree) + importlib.reload(node_tree) importlib.reload(operator) - importlib.reload(panel) + importlib.reload(ui) else: - from . import menu from . import node_settings - #from . import node_tree + from . import node_tree from . import operator - from . import panel + from . import ui import bpy \ No newline at end of file diff --git a/material/menu.py b/material/menu.py deleted file mode 100644 index 069fbb5..0000000 --- a/material/menu.py +++ /dev/null @@ -1,20 +0,0 @@ -import bpy -from bpy.types import Menu -from .operator import NTPMaterialOperator - -class NTPMaterialMenu(Menu): - bl_idname = "NODE_MT_ntp_material" - bl_label = "Select Material" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - for mat in bpy.data.materials: - if mat.node_tree: - op = layout.operator(NTPMaterialOperator.bl_idname, - text=mat.name) - op.material_name = mat.name \ No newline at end of file diff --git a/material/panel.py b/material/ui.py similarity index 54% rename from material/panel.py rename to material/ui.py index 3266bb6..1bf6181 100644 --- a/material/panel.py +++ b/material/ui.py @@ -1,5 +1,7 @@ import bpy from bpy.types import Panel +from bpy.types import Menu +from .operator import NTPMaterialOperator class NTPMaterialPanel(Panel): bl_label = "Material to Python" @@ -27,4 +29,21 @@ def draw(self, context): row.alignment = 'EXPAND' row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_material", text="Materials") \ No newline at end of file + row.menu("NODE_MT_ntp_material", text="Materials") + +class NTPMaterialMenu(Menu): + bl_idname = "NODE_MT_ntp_material" + bl_label = "Select Material" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for mat in bpy.data.materials: + if mat.node_tree: + op = layout.operator(NTPMaterialOperator.bl_idname, + text=mat.name) + op.material_name = mat.name \ No newline at end of file From 94df0cbcdeaabb4b6d312b4ed40612a3e0b19824 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:14:56 -0600 Subject: [PATCH 33/72] refactor: cleanup, group io stuff --- NTP_Operator.py | 100 ++++++++++++++++++++++++++---------------------- 1 file changed, 54 insertions(+), 46 deletions(-) diff --git a/NTP_Operator.py b/NTP_Operator.py index ab019cf..789be59 100644 --- a/NTP_Operator.py +++ b/NTP_Operator.py @@ -1,8 +1,7 @@ import bpy from bpy.types import Context, Operator -from bpy.types import Node, NodeTree +from bpy.types import Node, NodeTree, NodeSocketInterface -from io import StringIO import os from typing import TextIO @@ -156,8 +155,7 @@ def _create_var(self, name: str) -> str: self._used_vars[var] = 0 return clean_name - def _create_node(self, node: bpy.types.Node, inner: str, node_tree_var: str - ) -> str: + def _create_node(self, node: Node, inner: str, node_tree_var: str) -> str: """ Initializes a new node with location, dimension, and label info @@ -191,8 +189,8 @@ def _create_node(self, node: bpy.types.Node, inner: str, node_tree_var: str return node_var - def _set_settings_defaults(self, node: bpy.types.Node, inner: str, - node_var: str) -> None: + def _set_settings_defaults(self, node: Node, inner: str, node_var: str + ) -> None: """ Sets the defaults for any settings a node may have @@ -203,7 +201,7 @@ def _set_settings_defaults(self, node: bpy.types.Node, inner: str, """ if node.bl_idname not in self._settings: print((f"NodeToPython: couldn't find {node.bl_idname} in settings." - f"Your Blender version may not be supported")) + f"Your Blender version may not be supported")) return for (attr_name, type) in self._settings[node.bl_idname]: @@ -253,6 +251,35 @@ def _set_settings_defaults(self, node: bpy.types.Node, inner: str, elif type == ST.IMAGE_USER: self._image_user_settings(attr, inner, f"{node_var}.{attr_name}") + def _set_group_socket_default(self, socket_interface: NodeSocketInterface, + inner: str, socket_var: str) -> None: + """ + Set a node group input/output's default properties if they exist + + Parameters: + socket_interface (NodeSocketInterface): socket interface associated + with the input/output + inner (str): indentation string + socket_var (str): variable name for the socket + """ + if socket_interface.type not in default_sockets: + return + + if socket_interface.type == 'RGBA': + dv = vec4_to_py_str(socket_interface.default_value) + elif socket_interface.type == 'VECTOR': + dv = vec3_to_py_str(socket_interface.default_value) + else: + dv = socket_interface.default_value + self._file.write(f"{inner}{socket_var}.default_value = {dv}\n") + + #min value + if hasattr(socket_interface, "min_value"): + self._file.write(f"{inner}{socket_var}.min_value = {socket_interface.min_value}\n") + #max value + if hasattr(socket_interface, "min_value"): + self._file.write((f"{inner}{socket_var}.max_value = {socket_interface.max_value}\n")) + def _group_io_settings(self, node: bpy.types.Node, inner: str, io: str, #TODO: convert to enum node_tree_var: str, @@ -269,68 +296,49 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, and output settings for """ if io == "input": - ios = node.outputs #TODO: this doesn't seem right - """ - Are sockets and node tree default ios really coupled like this?? - """ - ntio = node_tree.inputs + io_sockets = node.outputs + io_socket_interfaces = node_tree.inputs else: - ios = node.inputs - ntio = node_tree.outputs + io_sockets = node.inputs + io_socket_interfaces = node_tree.outputs self._file.write(f"{inner}#{node_tree_var} {io}s\n") - for i, inout in enumerate(ios): + for i, inout in enumerate(io_sockets): if inout.bl_idname == 'NodeSocketVirtual': continue self._file.write(f"{inner}#{io} {inout.name}\n") idname = enum_to_py_str(inout.bl_idname) name = str_to_py_str(inout.name) self._file.write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") - socket = ntio[i] + socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - if inout.type in default_sockets: - #TODO: separate default socket function - #default value - if inout.type == 'RGBA': - dv = vec4_to_py_str(socket.default_value) - elif inout.type == 'VECTOR': - dv = vec3_to_py_str(socket.default_value) - else: - dv = socket.default_value - self._file.write(f"{inner}{socket_var}.default_value = {dv}\n") - - #min value - if hasattr(socket, "min_value"): - self._file.write(f"{inner}{socket_var}.min_value = {socket.min_value}\n") - #max value - if hasattr(socket, "min_value"): - self._file.write((f"{inner}{socket_var}.max_value = {socket.max_value}\n")) + self._set_group_socket_default(socket_interface, inner, socket_var) #default attribute name - if hasattr(socket, "default_attribute_name"): - if socket.default_attribute_name != "": - dan = str_to_py_str(socket.default_attribute_name) + if hasattr(socket_interface, "default_attribute_name"): + if socket_interface.default_attribute_name != "": + dan = str_to_py_str(socket_interface.default_attribute_name) self._file.write((f"{inner}{socket_var}" f".default_attribute_name = {dan}\n")) #attribute domain - if hasattr(socket, "attribute_domain"): - ad = enum_to_py_str(socket.attribute_domain) + if hasattr(socket_interface, "attribute_domain"): + ad = enum_to_py_str(socket_interface.attribute_domain) self._file.write(f"{inner}{socket_var}.attribute_domain = {ad}\n") #tooltip - if socket.description != "": - description = str_to_py_str(socket.description) + if socket_interface.description != "": + description = str_to_py_str(socket_interface.description) self._file.write((f"{inner}{socket_var}.description = {description}\n")) #hide_value - if socket.hide_value is True: + if socket_interface.hide_value is True: self._file.write(f"{inner}{socket_var}.hide_value = True\n") #hide in modifier - if hasattr(socket, "hide_in_modifier"): - if socket.hide_in_modifier is True: + if hasattr(socket_interface, "hide_in_modifier"): + if socket_interface.hide_in_modifier is True: self._file.write(f"{inner}{socket_var}.hide_in_modifier = True\n") self._file.write("\n") @@ -505,10 +513,10 @@ def _color_ramp_settings(self, node: bpy.types.Node, f"{element.alpha}\n")) color_str = vec4_to_py_str(element.color) self._file.write((f"{inner}{element_var}.color = {color_str}\n\n")) - + def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, - node_var: str, curve_mapping_name: str - ) -> None: + node_var: str, curve_mapping_name: str + ) -> None: """ Sets defaults for Float, Vector, and Color curves From 5449347fae9fc9a59f275a1b7ff236345fc41769 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:17:14 -0600 Subject: [PATCH 34/72] fix: material import typo --- __init__.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/__init__.py b/__init__.py index 0668ee9..2378569 100644 --- a/__init__.py +++ b/__init__.py @@ -12,16 +12,17 @@ import importlib importlib.reload(compositor) importlib.reload(geometry) - importlib.reload(node_settings) + importlib.reload(material) importlib.reload(options) else: from . import compositor from . import geometry - from .material import node_settings + from . import material from . import options import bpy + class NodeToPythonMenu(bpy.types.Menu): bl_idname = "NODE_MT_node_to_python" bl_label = "Node To Python" @@ -35,8 +36,6 @@ def draw(self, context): layout.operator_context = 'INVOKE_DEFAULT' - - classes = [NodeToPythonMenu, #options options.NTPOptions, From 03c40a2b048f819ecf37193070f698f3359e092f Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 20 Nov 2023 23:33:05 -0600 Subject: [PATCH 35/72] refactor: abstracted write function --- NTP_Operator.py | 298 +++++++++++++++++++++-------------------- compositor/operator.py | 64 ++++----- geometry/operator.py | 51 ++++--- material/operator.py | 35 +++-- 4 files changed, 225 insertions(+), 223 deletions(-) diff --git a/NTP_Operator.py b/NTP_Operator.py index 789be59..50e6c6a 100644 --- a/NTP_Operator.py +++ b/NTP_Operator.py @@ -55,6 +55,9 @@ def __init__(self): # Dictionary used for setting node properties self._settings: dict[str, list[(str, ST)]] = {} + def _write(self, string: str): + self._file.write(string) + def _setup_addon_directories(self, context: Context, nt_var: str) -> None: """ Finds/creates directories to save add-on to @@ -82,19 +85,19 @@ def _create_header(self, name: str) -> None: name (str): name of the add-on """ - self._file.write("bl_info = {\n") - self._file.write(f"\t\"name\" : \"{name}\",\n") - self._file.write("\t\"author\" : \"Node To Python\",\n") - self._file.write("\t\"version\" : (1, 0, 0),\n") - self._file.write(f"\t\"blender\" : {bpy.app.version},\n") - self._file.write("\t\"location\" : \"Object\",\n") #TODO - self._file.write("\t\"category\" : \"Node\"\n") - self._file.write("}\n") - self._file.write("\n") - self._file.write("import bpy\n") - self._file.write("import mathutils\n") - self._file.write("import os\n") - self._file.write("\n") + self._write("bl_info = {\n") + self._write(f"\t\"name\" : \"{name}\",\n") + self._write("\t\"author\" : \"Node To Python\",\n") + self._write("\t\"version\" : (1, 0, 0),\n") + self._write(f"\t\"blender\" : {bpy.app.version},\n") + self._write("\t\"location\" : \"Object\",\n") #TODO + self._write("\t\"category\" : \"Node\"\n") + self._write("}\n") + self._write("\n") + self._write("import bpy\n") + self._write("import mathutils\n") + self._write("import os\n") + self._write("\n") def _init_operator(self, idname: str, label: str) -> None: """ @@ -106,11 +109,11 @@ def _init_operator(self, idname: str, label: str) -> None: idname (str): name for the operator label (str): appearence inside Blender """ - self._file.write(f"class {self._class_name}(bpy.types.Operator):\n") - self._file.write(f"\tbl_idname = \"object.{idname}\"\n") - self._file.write(f"\tbl_label = \"{label}\"\n") - self._file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - self._file.write("\n") + self._write(f"class {self._class_name}(bpy.types.Operator):\n") + self._write(f"\tbl_idname = \"object.{idname}\"\n") + self._write(f"\tbl_label = \"{label}\"\n") + self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") + self._write("\n") def _is_outermost_node_group(self, level: int) -> bool: if self.mode == 'ADDON' and level == 2: @@ -129,9 +132,8 @@ def _process_group_node_tree(self, node: Node, node_var: str, level: int, if node_tree not in self._node_trees: self._process_node_tree(node_tree, level + 1) self._node_trees.add(node_tree) - self._file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + self._write((f"{inner}{node_var}.node_tree = bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) def _create_var(self, name: str) -> str: """ @@ -167,25 +169,25 @@ def _create_node(self, node: Node, inner: str, node_tree_var: str) -> str: node_var (str): variable name for the node """ - self._file.write(f"{inner}#node {node.name}\n") + self._write(f"{inner}#node {node.name}\n") node_var = self._create_var(node.name) self._node_vars[node] = node_var - self._file.write((f"{inner}{node_var} " + self._write((f"{inner}{node_var} " f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) #label if node.label: - self._file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") + self._write(f"{inner}{node_var}.label = \"{node.label}\"\n") #color if node.use_custom_color: - self._file.write(f"{inner}{node_var}.use_custom_color = True\n") - self._file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") + self._write(f"{inner}{node_var}.use_custom_color = True\n") + self._write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") #mute if node.mute: - self._file.write(f"{inner}{node_var}.mute = True\n") + self._write(f"{inner}{node_var}.mute = True\n") return node_var @@ -212,33 +214,33 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str setting_str = f"{inner}{node_var}.{attr_name}" if type == ST.ENUM: if attr != '': - self._file.write(f"{setting_str} = {enum_to_py_str(attr)}\n") + self._write(f"{setting_str} = {enum_to_py_str(attr)}\n") elif type == ST.ENUM_SET: - self._file.write(f"{setting_str} = {attr}\n") + self._write(f"{setting_str} = {attr}\n") elif type == ST.STRING: - self._file.write(f"{setting_str} = {str_to_py_str(attr)}\n") + self._write(f"{setting_str} = {str_to_py_str(attr)}\n") elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: - self._file.write(f"{setting_str} = {attr}\n") + self._write(f"{setting_str} = {attr}\n") elif type == ST.VEC1: - self._file.write(f"{setting_str} = {vec1_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec1_to_py_str(attr)}\n") elif type == ST.VEC2: - self._file.write(f"{setting_str} = {vec2_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec2_to_py_str(attr)}\n") elif type == ST.VEC3: - self._file.write(f"{setting_str} = {vec3_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec3_to_py_str(attr)}\n") elif type == ST.VEC4: - self._file.write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec4_to_py_str(attr)}\n") elif type == ST.COLOR: - self._file.write(f"{setting_str} = {color_to_py_str(attr)}\n") + self._write(f"{setting_str} = {color_to_py_str(attr)}\n") elif type == ST.MATERIAL: name = str_to_py_str(attr.name) - self._file.write((f"{inner}if {name} in bpy.data.materials:\n")) - self._file.write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.materials[{name}]\n")) + self._write((f"{inner}if {name} in bpy.data.materials:\n")) + self._write((f"{inner}\t{node_var}.{attr_name} = " + f"bpy.data.materials[{name}]\n")) elif type == ST.OBJECT: name = str_to_py_str(attr.name) - self._file.write((f"{inner}if {name} in bpy.data.objects:\n")) - self._file.write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.objects[{name}]\n")) + self._write((f"{inner}if {name} in bpy.data.objects:\n")) + self._write((f"{inner}\t{node_var}.{attr_name} = " + f"bpy.data.objects[{name}]\n")) elif type == ST.COLOR_RAMP: self._color_ramp_settings(node, inner, node_var, attr_name) elif type == ST.CURVE_MAPPING: @@ -271,14 +273,16 @@ def _set_group_socket_default(self, socket_interface: NodeSocketInterface, dv = vec3_to_py_str(socket_interface.default_value) else: dv = socket_interface.default_value - self._file.write(f"{inner}{socket_var}.default_value = {dv}\n") + self._write(f"{inner}{socket_var}.default_value = {dv}\n") #min value if hasattr(socket_interface, "min_value"): - self._file.write(f"{inner}{socket_var}.min_value = {socket_interface.min_value}\n") + min_val = socket_interface.min_value + self._write(f"{inner}{socket_var}.min_value = {min_val}\n") #max value if hasattr(socket_interface, "min_value"): - self._file.write((f"{inner}{socket_var}.max_value = {socket_interface.max_value}\n")) + max_val = socket_interface.max_value + self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) def _group_io_settings(self, node: bpy.types.Node, inner: str, io: str, #TODO: convert to enum @@ -302,14 +306,14 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, io_sockets = node.inputs io_socket_interfaces = node_tree.outputs - self._file.write(f"{inner}#{node_tree_var} {io}s\n") + self._write(f"{inner}#{node_tree_var} {io}s\n") for i, inout in enumerate(io_sockets): if inout.bl_idname == 'NodeSocketVirtual': continue - self._file.write(f"{inner}#{io} {inout.name}\n") + self._write(f"{inner}#{io} {inout.name}\n") idname = enum_to_py_str(inout.bl_idname) name = str_to_py_str(inout.name) - self._file.write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") + self._write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" @@ -319,30 +323,30 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, if hasattr(socket_interface, "default_attribute_name"): if socket_interface.default_attribute_name != "": dan = str_to_py_str(socket_interface.default_attribute_name) - self._file.write((f"{inner}{socket_var}" - f".default_attribute_name = {dan}\n")) + self._write((f"{inner}{socket_var}" + f".default_attribute_name = {dan}\n")) #attribute domain if hasattr(socket_interface, "attribute_domain"): ad = enum_to_py_str(socket_interface.attribute_domain) - self._file.write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") #tooltip if socket_interface.description != "": description = str_to_py_str(socket_interface.description) - self._file.write((f"{inner}{socket_var}.description = {description}\n")) + self._write((f"{inner}{socket_var}.description = {description}\n")) #hide_value if socket_interface.hide_value is True: - self._file.write(f"{inner}{socket_var}.hide_value = True\n") + self._write(f"{inner}{socket_var}.hide_value = True\n") #hide in modifier if hasattr(socket_interface, "hide_in_modifier"): if socket_interface.hide_in_modifier is True: - self._file.write(f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write(f"{inner}{socket_var}.hide_in_modifier = True\n") - self._file.write("\n") - self._file.write("\n") + self._write("\n") + self._write("\n") def _set_input_defaults(self, node: bpy.types.Node, inner: str, node_var: str) -> None: @@ -407,10 +411,10 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, else: default_val = input.default_value if default_val is not None: - self._file.write(f"{inner}#{input.identifier}\n") - self._file.write((f"{inner}{socket_var}.default_value" + self._write(f"{inner}#{input.identifier}\n") + self._write((f"{inner}{socket_var}.default_value" f" = {default_val}\n")) - self._file.write("\n") + self._write("\n") def _set_output_defaults(self, node: bpy.types.Node, inner: str, node_var: str) -> None: @@ -438,7 +442,7 @@ def _set_output_defaults(self, node: bpy.types.Node, dv = vec4_to_py_str(list(dv)) if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}: dv = vec3_to_py_str(dv) - self._file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) + self._write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) def _in_file_inputs(self, input: bpy.types.NodeSocket, inner: str, @@ -458,9 +462,9 @@ def _in_file_inputs(self, input: bpy.types.NodeSocket, if input.default_value is None: return name = str_to_py_str(input.default_value.name) - self._file.write(f"{inner}if {name} in bpy.data.{type}:\n") - self._file.write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) + self._write(f"{inner}if {name} in bpy.data.{type}:\n") + self._write((f"{inner}\t{socket_var}.default_value = " + f"bpy.data.{type}[{name}]\n")) def _color_ramp_settings(self, node: bpy.types.Node, inner: str, @@ -484,35 +488,35 @@ def _color_ramp_settings(self, node: bpy.types.Node, ramp_str = f"{inner}{node_var}.{color_ramp_name}" color_mode = enum_to_py_str(color_ramp.color_mode) - self._file.write(f"{ramp_str}.color_mode = {color_mode}\n") + self._write(f"{ramp_str}.color_mode = {color_mode}\n") hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) - self._file.write((f"{ramp_str}.hue_interpolation = " - f"{hue_interpolation}\n")) + self._write((f"{ramp_str}.hue_interpolation = " + f"{hue_interpolation}\n")) interpolation = enum_to_py_str(color_ramp.interpolation) - self._file.write((f"{ramp_str}.interpolation " - f"= {interpolation}\n")) - self._file.write("\n") + self._write((f"{ramp_str}.interpolation " + f"= {interpolation}\n")) + self._write("\n") #key points - self._file.write(f"{inner}#initialize color ramp elements\n") - self._file.write((f"{ramp_str}.elements.remove" + self._write(f"{inner}#initialize color ramp elements\n") + self._write((f"{ramp_str}.elements.remove" f"({ramp_str}.elements[0])\n")) for i, element in enumerate(color_ramp.elements): element_var = f"{node_var}_cre_{i}" if i == 0: - self._file.write(f"{inner}{element_var} = " - f"{ramp_str}.elements[{i}]\n") - self._file.write(f"{inner}{element_var}.position = {element.position}\n") + self._write(f"{inner}{element_var} = " + f"{ramp_str}.elements[{i}]\n") + self._write(f"{inner}{element_var}.position = {element.position}\n") else: - self._file.write((f"{inner}{element_var} = " - f"{ramp_str}.elements" - f".new({element.position})\n")) + self._write((f"{inner}{element_var} = " + f"{ramp_str}.elements" + f".new({element.position})\n")) - self._file.write((f"{inner}{element_var}.alpha = " - f"{element.alpha}\n")) + self._write((f"{inner}{element_var}.alpha = " + f"{element.alpha}\n")) color_str = vec4_to_py_str(element.color) - self._file.write((f"{inner}{element_var}.color = {color_str}\n\n")) + self._write((f"{inner}{element_var}.color = {color_str}\n\n")) def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, node_var: str, curve_mapping_name: str @@ -534,51 +538,51 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, f"in node \"{node.bl_idname}\"")) #mapping settings - self._file.write(f"{inner}#mapping settings\n") + self._write(f"{inner}#mapping settings\n") mapping_var = f"{inner}{node_var}.{curve_mapping_name}" #extend extend = enum_to_py_str(mapping.extend) - self._file.write(f"{mapping_var}.extend = {extend}\n") + self._write(f"{mapping_var}.extend = {extend}\n") #tone tone = enum_to_py_str(mapping.tone) - self._file.write(f"{mapping_var}.tone = {tone}\n") + self._write(f"{mapping_var}.tone = {tone}\n") #black level b_lvl_str = vec3_to_py_str(mapping.black_level) - self._file.write((f"{mapping_var}.black_level = {b_lvl_str}\n")) + self._write((f"{mapping_var}.black_level = {b_lvl_str}\n")) #white level w_lvl_str = vec3_to_py_str(mapping.white_level) - self._file.write((f"{mapping_var}.white_level = {w_lvl_str}\n")) + self._write((f"{mapping_var}.white_level = {w_lvl_str}\n")) #minima and maxima min_x = mapping.clip_min_x - self._file.write(f"{mapping_var}.clip_min_x = {min_x}\n") + self._write(f"{mapping_var}.clip_min_x = {min_x}\n") min_y = mapping.clip_min_y - self._file.write(f"{mapping_var}.clip_min_y = {min_y}\n") + self._write(f"{mapping_var}.clip_min_y = {min_y}\n") max_x = mapping.clip_max_x - self._file.write(f"{mapping_var}.clip_max_x = {max_x}\n") + self._write(f"{mapping_var}.clip_max_x = {max_x}\n") max_y = mapping.clip_max_y - self._file.write(f"{mapping_var}.clip_max_y = {max_y}\n") + self._write(f"{mapping_var}.clip_max_y = {max_y}\n") #use_clip use_clip = mapping.use_clip - self._file.write(f"{mapping_var}.use_clip = {use_clip}\n") + self._write(f"{mapping_var}.use_clip = {use_clip}\n") #create curves for i, curve in enumerate(mapping.curves): #TODO: curve function - self._file.write(f"{inner}#curve {i}\n") + self._write(f"{inner}#curve {i}\n") curve_i = f"{node_var}_curve_{i}" - self._file.write((f"{inner}{curve_i} = " - f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) + self._write((f"{inner}{curve_i} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) # Remove default points when CurveMap is initialized with more than # two points (just CompositorNodeHueCorrect) if (node.bl_idname == 'CompositorNodeHueCorrect'): - self._file.write((f"{inner}for i in " - f"range(len({curve_i}.points.values()) - 1, 1, -1):\n")) - self._file.write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") + self._write((f"{inner}for i in range" + f"(len({curve_i}.points.values()) - 1, 1, -1):\n")) + self._write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") for j, point in enumerate(curve.points): #TODO: point function @@ -587,17 +591,17 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, loc = point.location loc_str = f"{loc[0]}, {loc[1]}" if j < 2: - self._file.write(f"{point_j} = {curve_i}.points[{j}]\n") - self._file.write(f"{point_j}.location = ({loc_str})\n") + self._write(f"{point_j} = {curve_i}.points[{j}]\n") + self._write(f"{point_j}.location = ({loc_str})\n") else: - self._file.write((f"{point_j} = {curve_i}.points.new({loc_str})\n")) + self._write((f"{point_j} = {curve_i}.points.new({loc_str})\n")) handle = enum_to_py_str(point.handle_type) - self._file.write(f"{point_j}.handle_type = {handle}\n") + self._write(f"{point_j}.handle_type = {handle}\n") #update curve - self._file.write(f"{inner}#update curve after changes\n") - self._file.write(f"{mapping_var}.update()\n") + self._write(f"{inner}#update curve after changes\n") + self._write(f"{mapping_var}.update()\n") def _save_image(self, img: bpy.types.Image) -> None: """ @@ -640,29 +644,29 @@ def _load_image(self, img: bpy.types.Image, img_str = img_to_py_str(img) #TODO: convert to special variables - self._file.write(f"{inner}#load image {img_str}\n") - self._file.write((f"{inner}base_dir = " - f"os.path.dirname(os.path.abspath(__file__))\n")) - self._file.write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " - f"\"{img_str}\")\n")) - self._file.write((f"{inner}{img_var} = " - f"bpy.data.images.load(image_path, check_existing = True)\n")) + self._write(f"{inner}#load image {img_str}\n") + self._write((f"{inner}base_dir = " + f"os.path.dirname(os.path.abspath(__file__))\n")) + self._write((f"{inner}image_path = " + f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " + f"\"{img_str}\")\n")) + self._write((f"{inner}{img_var} = bpy.data.images.load" + f"(image_path, check_existing = True)\n")) #copy image settings - self._file.write(f"{inner}#set image settings\n") + self._write(f"{inner}#set image settings\n") #source source = enum_to_py_str(img.source) - self._file.write(f"{inner}{img_var}.source = {source}\n") + self._write(f"{inner}{img_var}.source = {source}\n") #color space settings color_space = enum_to_py_str(img.colorspace_settings.name) - self._file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") + self._write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") #alpha mode alpha_mode = enum_to_py_str(img.alpha_mode) - self._file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") + self._write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") def _image_user_settings(self, img_user: bpy.types.ImageUser, inner: str, @@ -680,8 +684,8 @@ def _image_user_settings(self, img_user: bpy.types.ImageUser, "frame_start", "tile", "use_auto_refresh", "use_cyclic"] for img_usr_attr in img_usr_attrs: - self._file.write((f"{inner}{img_user_var}.{img_usr_attr} = " - f"{getattr(img_user, img_usr_attr)}\n")) + self._write((f"{inner}{img_user_var}.{img_usr_attr} = " + f"{getattr(img_user, img_usr_attr)}\n")) def _set_parents(self, node_tree: bpy.types.NodeTree, inner: str) -> None: @@ -696,12 +700,12 @@ def _set_parents(self, node_tree: bpy.types.NodeTree, for node in node_tree.nodes: if node is not None and node.parent is not None: if not parent_comment: - self._file.write(f"{inner}#Set parents\n") + self._write(f"{inner}#Set parents\n") parent_comment = True node_var = self._node_vars[node] parent_var = self._node_vars[node.parent] - self._file.write(f"{inner}{node_var}.parent = {parent_var}\n") - self._file.write("\n") + self._write(f"{inner}{node_var}.parent = {parent_var}\n") + self._write("\n") def _set_locations(self, node_tree: bpy.types.NodeTree, inner: str) -> None: """ @@ -712,12 +716,12 @@ def _set_locations(self, node_tree: bpy.types.NodeTree, inner: str) -> None: inner (str): indentation string """ - self._file.write(f"{inner}#Set locations\n") + self._write(f"{inner}#Set locations\n") for node in node_tree.nodes: node_var = self._node_vars[node] - self._file.write((f"{inner}{node_var}.location " + self._write((f"{inner}{node_var}.location " f"= ({node.location.x}, {node.location.y})\n")) - self._file.write("\n") + self._write("\n") def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, ) -> None: @@ -728,12 +732,12 @@ def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from inner (str): indentation string """ - self._file.write(f"{inner}#Set dimensions\n") + self._write(f"{inner}#Set dimensions\n") for node in node_tree.nodes: node_var = self._node_vars[node] - self._file.write((f"{inner}{node_var}.width, {node_var}.height " - f"= {node.width}, {node.height}\n")) - self._file.write("\n") + self._write((f"{inner}{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}\n")) + self._write("\n") def _init_links(self, node_tree: bpy.types.NodeTree, inner: str, @@ -748,7 +752,7 @@ def _init_links(self, node_tree: bpy.types.NodeTree, """ if node_tree.links: - self._file.write(f"{inner}#initialize {node_tree_var} links\n") + self._write(f"{inner}#initialize {node_tree_var} links\n") for link in node_tree.links: in_node_var = self._node_vars[link.from_node] input_socket = link.from_socket @@ -773,11 +777,11 @@ def _init_links(self, node_tree: bpy.types.NodeTree, output_idx = i break - self._file.write((f"{inner}#{in_node_var}.{input_socket.name} " - f"-> {out_node_var}.{output_socket.name}\n")) - self._file.write((f"{inner}{node_tree_var}.links.new({in_node_var}" - f".outputs[{input_idx}], " - f"{out_node_var}.inputs[{output_idx}])\n")) + self._write((f"{inner}#{in_node_var}.{input_socket.name} " + f"-> {out_node_var}.{output_socket.name}\n")) + self._write((f"{inner}{node_tree_var}.links.new({in_node_var}" + f".outputs[{input_idx}], " + f"{out_node_var}.inputs[{output_idx}])\n")) def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, node_var: str) -> None: @@ -791,10 +795,10 @@ def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, """ for i, socket in enumerate(node.inputs): if socket.hide is True: - self._file.write(f"{inner}{node_var}.inputs[{i}].hide = True\n") + self._write(f"{inner}{node_var}.inputs[{i}].hide = True\n") for i, socket in enumerate(node.outputs): if socket.hide is True: - self._file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") + self._write(f"{inner}{node_var}.outputs[{i}].hide = True\n") def _set_socket_defaults(self, node: Node, node_var: str, inner: str): self._set_input_defaults(node, inner, node_var) @@ -805,34 +809,34 @@ def _create_menu_func(self) -> None: Creates the menu function """ - self._file.write("def menu_func(self, context):\n") - self._file.write(f"\tself.layout.operator({self._class_name}.bl_idname)\n") - self._file.write("\n") + self._write("def menu_func(self, context):\n") + self._write(f"\tself.layout.operator({self._class_name}.bl_idname)\n") + self._write("\n") def _create_register_func(self) -> None: """ Creates the register function """ - self._file.write("def register():\n") - self._file.write(f"\tbpy.utils.register_class({self._class_name})\n") - self._file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - self._file.write("\n") + self._write("def register():\n") + self._write(f"\tbpy.utils.register_class({self._class_name})\n") + self._write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") + self._write("\n") def _create_unregister_func(self) -> None: """ Creates the unregister function """ - self._file.write("def unregister():\n") - self._file.write(f"\tbpy.utils.unregister_class({self._class_name})\n") - self._file.write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") - self._file.write("\n") + self._write("def unregister():\n") + self._write(f"\tbpy.utils.unregister_class({self._class_name})\n") + self._write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") + self._write("\n") def _create_main_func(self) -> None: """ Creates the main function """ - self._file.write("if __name__ == \"__main__\":\n") - self._file.write("\tregister()") + self._write("if __name__ == \"__main__\":\n") + self._write("\tregister()") def _zip_addon(self) -> None: """ diff --git a/compositor/operator.py b/compositor/operator.py index a4989d6..3e7ffff 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -38,37 +38,37 @@ def __init__(self): def _create_scene(self, indent: str): #TODO: wrap in more general unique name util function - self._file.write(f"{indent}# Generate unique scene name\n") - self._file.write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") - self._file.write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") - self._file.write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - self._file.write(f"{indent}\ti = 1\n") - self._file.write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - self._file.write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - self._file.write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - self._file.write(f"{indent}\t\ti += 1\n\n") - - self._file.write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") - self._file.write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") - self._file.write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") - self._file.write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") + self._write(f"{indent}# Generate unique scene name\n") + self._write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") + self._write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") + self._write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + self._write(f"{indent}\ti = 1\n") + self._write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + self._write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") + self._write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") + self._write(f"{indent}\t\ti += 1\n\n") + + self._write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") + self._write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") + self._write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") + self._write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") def _initialize_compositor_node_tree(self, outer, nt_var, level, inner, nt_name): #initialize node group - self._file.write(f"{outer}#initialize {nt_var} node group\n") - self._file.write(f"{outer}def {nt_var}_node_group():\n") + self._write(f"{outer}#initialize {nt_var} node group\n") + self._write(f"{outer}def {nt_var}_node_group():\n") if self._is_outermost_node_group(level): #outermost node group - self._file.write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") - self._file.write(f"{inner}#start with a clean node tree\n") - self._file.write(f"{inner}for node in {nt_var}.nodes:\n") - self._file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + self._write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") + self._write(f"{inner}#start with a clean node tree\n") + self._write(f"{inner}for node in {nt_var}.nodes:\n") + self._write(f"{inner}\t{nt_var}.nodes.remove(node)\n") else: - self._file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'CompositorNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - self._file.write("\n") + self._write((f"{inner}{nt_var}" + f"= bpy.data.node_groups.new(" + f"type = \'CompositorNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + self._write("\n") def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, level: int): if node.bl_idname == 'CompositorNodeGroup': @@ -100,9 +100,9 @@ def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, if node.bl_idname == 'CompositorNodeGroup': if node.node_tree is not None: - self._file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + self._write((f"{inner}{node_var}.node_tree = " + f"bpy.data.node_groups" + f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: self._group_io_settings(node, inner, "input", ntp_nt.var, ntp_nt.node_tree) inputs_set = True @@ -136,7 +136,7 @@ def _process_node_tree(self, node_tree, level): ntp_nt = NTP_CompositorNodeTree(node_tree, nt_var) #initialize nodes - self._file.write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"{inner}#initialize {nt_var} nodes\n") for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) @@ -147,7 +147,7 @@ def _process_node_tree(self, node_tree, level): self._init_links(node_tree, inner, nt_var) - self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") + self._write(f"\n{outer}{nt_var}_node_group()\n\n") def execute(self, context): #find node group to replicate @@ -175,7 +175,7 @@ def execute(self, context): self._class_name = clean_string(self.compositor_name, lower=False) self._init_operator(comp_var, self.compositor_name) - self._file.write("\tdef execute(self, context):\n") + self._write("\tdef execute(self, context):\n") else: self._file = StringIO("") @@ -192,7 +192,7 @@ def execute(self, context): self._process_node_tree(nt, level) if self.mode == 'ADDON': - self._file.write("\t\treturn {'FINISHED'}\n\n") + self._write("\t\treturn {'FINISHED'}\n\n") self._create_menu_func() self._create_register_func() diff --git a/geometry/operator.py b/geometry/operator.py index facb8d1..a222b7d 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -33,20 +33,19 @@ def __init__(self): def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, inner: str, node_var: str) -> None: - self._file.write(f"{inner}# Remove generated sim state items\n") - self._file.write(f"{inner}for item in {node_var}.state_items:\n") - self._file.write(f"{inner}\t{node_var}.state_items.remove(item)\n") + self._write(f"{inner}# Remove generated sim state items\n") + self._write(f"{inner}for item in {node_var}.state_items:\n") + self._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) - self._file.write(f"{inner}#create SSI {name}\n") - self._file.write((f"{inner}{node_var}.state_items.new" - f"({socket_type}, {name})\n")) + self._write(f"{inner}#create SSI {name}\n") + self._write((f"{inner}{node_var}.state_items.new" + f"({socket_type}, {name})\n")) si_var = f"{node_var}.state_items[{i}]" attr_domain = enum_to_py_str(si.attribute_domain) - self._file.write((f"{inner}{si_var}.attribute_domain " - f"= {attr_domain}\n")) + self._write((f"{inner}{si_var}.attribute_domain = {attr_domain}\n")) def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, inner: str, level: int) -> None: @@ -91,8 +90,8 @@ def _process_sim_zones(self, sim_inputs: list[GeometryNodeSimulationInput], sim_input_var = self._node_vars[sim_input] sim_output_var = self._node_vars[sim_output] - self._file.write((f"{inner}{sim_input_var}.pair_with_output" - f"({sim_output_var})\n")) + self._write((f"{inner}{sim_input_var}.pair_with_output" + f"({sim_output_var})\n")) #must set defaults after paired with output self._set_socket_defaults(sim_input, sim_input_var, inner) @@ -115,15 +114,15 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, # Eventually these should go away anyways, and level of indentation depends just on the mode #initialize node group - self._file.write(f"{outer}#initialize {nt_var} node group\n") - self._file.write(f"{outer}def {nt_var}_node_group():\n") - self._file.write((f"{inner}{nt_var} = bpy.data.node_groups.new(" - f"type = \'GeometryNodeTree\', " - f"name = {str_to_py_str(node_tree.name)})\n")) - self._file.write("\n") + self._write(f"{outer}#initialize {nt_var} node group\n") + self._write(f"{outer}def {nt_var}_node_group():\n") + self._write((f"{inner}{nt_var} = bpy.data.node_groups.new(" + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n")) + self._write("\n") #initialize nodes - self._file.write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"{inner}#initialize {nt_var} nodes\n") ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) @@ -140,23 +139,23 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, #create connections self._init_links(node_tree, inner, nt_var) - self._file.write(f"{inner}return {nt_var}\n") + self._write(f"{inner}return {nt_var}\n") #create node group - self._file.write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") + self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") return self._used_vars def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): #get object - self._file.write(f"\t\tname = bpy.context.object.name\n") - self._file.write(f"\t\tobj = bpy.data.objects[name]\n") + self._write(f"\t\tname = bpy.context.object.name\n") + self._write(f"\t\tobj = bpy.data.objects[name]\n") #set modifier to the one we just created mod_name = str_to_py_str(nt.name) - self._file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')\n")) - self._file.write(f"\t\tmod.node_group = {nt_var}\n") + self._write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " + f"type = 'NODES')\n")) + self._write(f"\t\tmod.node_group = {nt_var}\n") def execute(self, context): @@ -174,7 +173,7 @@ def execute(self, context): self._create_header(nt.name) self._class_name = clean_string(nt.name, lower = False) self._init_operator(nt_var, nt.name) - self._file.write("\tdef execute(self, context):\n") + self._write("\tdef execute(self, context):\n") else: self._file = StringIO("") @@ -186,7 +185,7 @@ def execute(self, context): if self.mode == 'ADDON': self._apply_modifier(nt, nt_var) - self._file.write("\t\treturn {'FINISHED'}\n\n") + self._write("\t\treturn {'FINISHED'}\n\n") self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/material/operator.py b/material/operator.py index 9077d5b..0a42168 100644 --- a/material/operator.py +++ b/material/operator.py @@ -25,26 +25,25 @@ def __init__(self): self._settings = shader_node_settings def _create_material(self, indent: str): - self._file.write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" + self._write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" f"name = {str_to_py_str(self.material_name)})\n")) - self._file.write(f"{indent}{MAT_VAR}.use_nodes = True\n") + self._write(f"{indent}{MAT_VAR}.use_nodes = True\n") def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): #initialize node group - self._file.write(f"{outer}#initialize {nt_var} node group\n") - self._file.write(f"{outer}def {nt_var}_node_group():\n") + self._write(f"{outer}#initialize {nt_var} node group\n") + self._write(f"{outer}def {nt_var}_node_group():\n") if self._is_outermost_node_group(level): - self._file.write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") - self._file.write(f"{inner}#start with a clean node tree\n") - self._file.write(f"{inner}for node in {nt_var}.nodes:\n") - self._file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + self._write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") + self._write(f"{inner}#start with a clean node tree\n") + self._write(f"{inner}for node in {nt_var}.nodes:\n") + self._write(f"{inner}\t{nt_var}.nodes.remove(node)\n") else: - self._file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - self._file.write("\n") + self._write((f"{inner}{nt_var} = bpy.data.node_groups.new(" + f"type = \'ShaderNodeTree\', " + f"name = {str_to_py_str(nt_name)})\n")) + self._write("\n") def _process_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: str, level: int) -> None: #create node @@ -92,7 +91,7 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: ntp_nt = NTP_ShaderNodeTree(node_tree, nt_var) #initialize nodes - self._file.write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"{inner}#initialize {nt_var} nodes\n") for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) @@ -103,9 +102,9 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: self._init_links(node_tree, inner, nt_var) - self._file.write(f"{inner}return {nt_var}\n") + self._write(f"{inner}return {nt_var}\n") - self._file.write(f"\n{outer}{nt_var}_node_group()\n\n") + self._write(f"\n{outer}{nt_var}_node_group()\n\n") def execute(self, context): #find node group to replicate @@ -127,7 +126,7 @@ def execute(self, context): self._class_name = clean_string(self.material_name, lower=False) self._init_operator(mat_var, self.material_name) - self._file.write("\tdef execute(self, context):\n") + self._write("\tdef execute(self, context):\n") else: self._file = StringIO("") @@ -144,7 +143,7 @@ def execute(self, context): self._process_node_tree(nt, level) if self.mode == 'ADDON': - self._file.write("\t\treturn {'FINISHED'}\n\n") + self._write("\t\treturn {'FINISHED'}\n\n") self._create_menu_func() self._create_register_func() self._create_unregister_func() From 01f5950beea6c8f2c26787592997508804ffd1ad Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 15:37:02 -0600 Subject: [PATCH 36/72] refactor: use NTP_NodeTree class in group_io_settings function --- NTP_Operator.py | 6 ++++-- compositor/operator.py | 4 ++-- geometry/operator.py | 6 ++---- material/operator.py | 8 ++------ 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/NTP_Operator.py b/NTP_Operator.py index 50e6c6a..d76c6a9 100644 --- a/NTP_Operator.py +++ b/NTP_Operator.py @@ -286,8 +286,7 @@ def _set_group_socket_default(self, socket_interface: NodeSocketInterface, def _group_io_settings(self, node: bpy.types.Node, inner: str, io: str, #TODO: convert to enum - node_tree_var: str, - node_tree: bpy.types.NodeTree) -> None: + ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets @@ -299,6 +298,9 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, node_tree (bpy.types.NodeTree): node tree that we're generating input and output settings for """ + node_tree_var = ntp_node_tree.var + node_tree = ntp_node_tree.node_tree + if io == "input": io_sockets = node.outputs io_socket_interfaces = node_tree.inputs diff --git a/compositor/operator.py b/compositor/operator.py index 3e7ffff..94c5d60 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -104,11 +104,11 @@ def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - self._group_io_settings(node, inner, "input", ntp_nt.var, ntp_nt.node_tree) + self._group_io_settings(node, inner, "input", ntp_nt) inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - self._group_io_settings(node, inner, "output", ntp_nt.var, ntp_nt.node_tree) + self._group_io_settings(node, inner, "output", ntp_nt) outputs_set = True self._set_socket_defaults(node, node_var, inner) diff --git a/geometry/operator.py b/geometry/operator.py index a222b7d..d1f55f1 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -56,13 +56,11 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree.var, - ntp_node_tree.node_tree) #TODO: convert to using NTP_NodeTrees + self._group_io_settings(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", - ntp_node_tree.var, ntp_node_tree.node_tree) + self._group_io_settings(node, inner, "output", ntp_node_tree) ntp_node_tree.outputs_set = True elif node.bl_idname == 'GeometryNodeSimulationInput': diff --git a/material/operator.py b/material/operator.py index 0a42168..880bb50 100644 --- a/material/operator.py +++ b/material/operator.py @@ -52,16 +52,12 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: st if node.bl_idname == 'ShaderNodeGroup': self._process_group_node_tree(node, node_var, level, inner) - #TODO: should probably be lumped into one function, - #it's always called like this and we're double checking it elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", - ntp_node_tree.var, ntp_node_tree.node_tree) + self._group_io_settings(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", - ntp_node_tree.var, ntp_node_tree.node_tree) + self._group_io_settings(node, inner, "output", ntp_node_tree) ntp_node_tree.outputs_set = True self._hide_hidden_sockets(node, inner, node_var) From 7743d5856da6ac9d87f17a594f1532588fb4a97b Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 15:56:59 -0600 Subject: [PATCH 37/72] refactor: cleanup, removed child ntp_node_tree classes --- compositor/node_tree.py | 7 ------- compositor/operator.py | 9 ++++----- geometry/node_tree.py | 8 -------- geometry/operator.py | 7 +++---- material/node_tree.py | 6 ------ material/operator.py | 9 ++++----- NTP_NodeTree.py => ntp_node_tree.py | 2 -- NTP_Operator.py => ntp_operator.py | 2 +- 8 files changed, 12 insertions(+), 38 deletions(-) delete mode 100644 compositor/node_tree.py delete mode 100644 geometry/node_tree.py delete mode 100644 material/node_tree.py rename NTP_NodeTree.py => ntp_node_tree.py (95%) rename NTP_Operator.py => ntp_operator.py (99%) diff --git a/compositor/node_tree.py b/compositor/node_tree.py deleted file mode 100644 index d799df0..0000000 --- a/compositor/node_tree.py +++ /dev/null @@ -1,7 +0,0 @@ -from bpy.types import CompositorNodeTree - -from ..NTP_NodeTree import NTP_NodeTree - -class NTP_CompositorNodeTree(NTP_NodeTree): - def __init__(self, node_tree: CompositorNodeTree, var: str): - super().__init__(node_tree, var) \ No newline at end of file diff --git a/compositor/operator.py b/compositor/operator.py index 94c5d60..b521e77 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -1,10 +1,9 @@ import bpy -import os from bpy.types import Node -from ..NTP_Operator import NTP_Operator -from .node_tree import NTP_CompositorNodeTree +from ..ntp_operator import NTP_Operator +from ..ntp_node_tree import NTP_NodeTree from ..utils import * from io import StringIO from .node_settings import compositor_node_settings @@ -70,7 +69,7 @@ def _initialize_compositor_node_tree(self, outer, nt_var, level, inner, nt_name) f"name = {str_to_py_str(nt_name)})\n")) self._write("\n") - def _process_node(self, node: Node, ntp_nt: NTP_CompositorNodeTree, inner: str, level: int): + def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str, level: int): if node.bl_idname == 'CompositorNodeGroup': node_nt = node.node_tree if node_nt is not None and node_nt not in self._node_trees: @@ -133,7 +132,7 @@ def _process_node_tree(self, node_tree, level): self._initialize_compositor_node_tree(outer, nt_var, level, inner, nt_name) - ntp_nt = NTP_CompositorNodeTree(node_tree, nt_var) + ntp_nt = NTP_NodeTree(node_tree, nt_var) #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") diff --git a/geometry/node_tree.py b/geometry/node_tree.py deleted file mode 100644 index a8408f0..0000000 --- a/geometry/node_tree.py +++ /dev/null @@ -1,8 +0,0 @@ -from bpy.types import GeometryNodeTree, GeometryNodeSimulationInput - -from ..NTP_NodeTree import NTP_NodeTree - -class NTP_GeoNodeTree(NTP_NodeTree): - def __init__(self, node_tree: GeometryNodeTree, var: str): - super().__init__(node_tree, var) - self.sim_inputs: list[GeometryNodeSimulationInput] = [] \ No newline at end of file diff --git a/geometry/operator.py b/geometry/operator.py index d1f55f1..a0a7fde 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -4,12 +4,11 @@ from bpy.types import GeometryNodeTree from bpy.types import Node -import os from io import StringIO -from ..NTP_Operator import NTP_Operator +from ..ntp_operator import NTP_Operator +from ..ntp_node_tree import NTP_NodeTree from ..utils import * -from .node_tree import NTP_GeoNodeTree from .node_settings import geo_node_settings class NTPGeoNodesOperator(NTP_Operator): @@ -47,7 +46,7 @@ def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, attr_domain = enum_to_py_str(si.attribute_domain) self._write((f"{inner}{si_var}.attribute_domain = {attr_domain}\n")) - def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, level: int) -> None: #create node node_var: str = self._create_node(node, inner, ntp_node_tree.var) diff --git a/material/node_tree.py b/material/node_tree.py deleted file mode 100644 index aa4822f..0000000 --- a/material/node_tree.py +++ /dev/null @@ -1,6 +0,0 @@ -from ..NTP_NodeTree import NTP_NodeTree -from bpy.types import ShaderNodeTree - -class NTP_ShaderNodeTree(NTP_NodeTree): - def __init__(self, node_tree: ShaderNodeTree, var: str): - super().__init__(node_tree, var) \ No newline at end of file diff --git a/material/operator.py b/material/operator.py index 880bb50..cc84f24 100644 --- a/material/operator.py +++ b/material/operator.py @@ -2,13 +2,12 @@ from bpy.types import Node from bpy.types import ShaderNodeTree -import os from io import StringIO from ..utils import * -from ..NTP_Operator import NTP_Operator +from ..ntp_operator import NTP_Operator +from ..ntp_node_tree import NTP_NodeTree from .node_settings import shader_node_settings -from .node_tree import NTP_ShaderNodeTree MAT_VAR = "mat" @@ -45,7 +44,7 @@ def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): f"name = {str_to_py_str(nt_name)})\n")) self._write("\n") - def _process_node(self, node: Node, ntp_node_tree: NTP_ShaderNodeTree, inner: str, level: int) -> None: + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, level: int) -> None: #create node node_var: str = self._create_node(node, inner, ntp_node_tree.var) self._set_settings_defaults(node, inner, node_var) @@ -84,7 +83,7 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: self._initialize_shader_node_tree(outer, nt_var, level, inner, nt_name) - ntp_nt = NTP_ShaderNodeTree(node_tree, nt_var) + ntp_nt = NTP_NodeTree(node_tree, nt_var) #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") diff --git a/NTP_NodeTree.py b/ntp_node_tree.py similarity index 95% rename from NTP_NodeTree.py rename to ntp_node_tree.py index 8903d1b..5469ba1 100644 --- a/NTP_NodeTree.py +++ b/ntp_node_tree.py @@ -1,7 +1,5 @@ from bpy.types import NodeTree -from .utils import ST - class NTP_NodeTree: def __init__(self, node_tree: NodeTree, var: str): # Blender node tree object being copied diff --git a/NTP_Operator.py b/ntp_operator.py similarity index 99% rename from NTP_Operator.py rename to ntp_operator.py index d76c6a9..32acf0c 100644 --- a/NTP_Operator.py +++ b/ntp_operator.py @@ -5,7 +5,7 @@ import os from typing import TextIO -from .NTP_NodeTree import NTP_NodeTree +from .ntp_node_tree import NTP_NodeTree from .utils import * class NTP_Operator(Operator): From 30b26dc04dceb6ff1ed3ee9c05b94e590fea1962 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 15:59:05 -0600 Subject: [PATCH 38/72] fix: removed imports for deleted modules --- compositor/__init__.py | 2 -- geometry/__init__.py | 2 -- material/__init__.py | 2 -- 3 files changed, 6 deletions(-) diff --git a/compositor/__init__.py b/compositor/__init__.py index e2707b0..ceb3a04 100644 --- a/compositor/__init__.py +++ b/compositor/__init__.py @@ -1,12 +1,10 @@ if "bpy" in locals(): import importlib importlib.reload(node_settings) - importlib.reload(node_tree) importlib.reload(operator) importlib.reload(ui) else: from . import node_settings - from . import node_tree from . import operator from . import ui diff --git a/geometry/__init__.py b/geometry/__init__.py index e2707b0..ceb3a04 100644 --- a/geometry/__init__.py +++ b/geometry/__init__.py @@ -1,12 +1,10 @@ if "bpy" in locals(): import importlib importlib.reload(node_settings) - importlib.reload(node_tree) importlib.reload(operator) importlib.reload(ui) else: from . import node_settings - from . import node_tree from . import operator from . import ui diff --git a/material/__init__.py b/material/__init__.py index e2707b0..ceb3a04 100644 --- a/material/__init__.py +++ b/material/__init__.py @@ -1,12 +1,10 @@ if "bpy" in locals(): import importlib importlib.reload(node_settings) - importlib.reload(node_tree) importlib.reload(operator) importlib.reload(ui) else: from . import node_settings - from . import node_tree from . import operator from . import ui From 433d3b51f105c65cc6011dc5223b52348d031d01 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 16:32:56 -0600 Subject: [PATCH 39/72] fix: cleaned up reference to old NTP_NodeTree --- geometry/operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/geometry/operator.py b/geometry/operator.py index a0a7fde..d1893a9 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -121,7 +121,7 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") - ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) + ntp_nt = NTP_NodeTree(node_tree, nt_var) for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) From f63b85424c1605fd7551e2e3658516dad00bd043 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 16:40:06 -0600 Subject: [PATCH 40/72] fix: restore necessary child NTP_GeoNodeTree class --- geometry/node_tree.py | 8 ++++++++ geometry/operator.py | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 geometry/node_tree.py diff --git a/geometry/node_tree.py b/geometry/node_tree.py new file mode 100644 index 0000000..64be218 --- /dev/null +++ b/geometry/node_tree.py @@ -0,0 +1,8 @@ +from bpy.types import GeometryNodeTree, GeometryNodeSimulationInput + +from ..ntp_node_tree import NTP_NodeTree + +class NTP_GeoNodeTree(NTP_NodeTree): + def __init__(self, node_tree: GeometryNodeTree, var: str): + super().__init__(node_tree, var) + self.sim_inputs: list[GeometryNodeSimulationInput] = [] diff --git a/geometry/operator.py b/geometry/operator.py index d1893a9..79f9cf1 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -7,8 +7,8 @@ from io import StringIO from ..ntp_operator import NTP_Operator -from ..ntp_node_tree import NTP_NodeTree from ..utils import * +from .node_tree import NTP_GeoNodeTree from .node_settings import geo_node_settings class NTPGeoNodesOperator(NTP_Operator): @@ -46,7 +46,7 @@ def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, attr_domain = enum_to_py_str(si.attribute_domain) self._write((f"{inner}{si_var}.attribute_domain = {attr_domain}\n")) - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, + def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, inner: str, level: int) -> None: #create node node_var: str = self._create_node(node, inner, ntp_node_tree.var) @@ -121,7 +121,7 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") - ntp_nt = NTP_NodeTree(node_tree, nt_var) + ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) From 87dd46e4e336b3b146d31f8c8d37919b9a7e0e53 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 16:41:45 -0600 Subject: [PATCH 41/72] fix: import geo node tree module in package --- geometry/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/geometry/__init__.py b/geometry/__init__.py index ceb3a04..e50e3c0 100644 --- a/geometry/__init__.py +++ b/geometry/__init__.py @@ -1,9 +1,11 @@ if "bpy" in locals(): import importlib + importlib.reload(node_tree) importlib.reload(node_settings) importlib.reload(operator) importlib.reload(ui) else: + from . import node_tree from . import node_settings from . import operator from . import ui From 532ba17106175ec20855ca6af1f046d136ec660b Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 2 Dec 2023 17:20:37 -0600 Subject: [PATCH 42/72] feat: node names now copied also --- ntp_operator.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ntp_operator.py b/ntp_operator.py index 32acf0c..4a8b699 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -179,6 +179,9 @@ def _create_node(self, node: Node, inner: str, node_tree_var: str) -> str: #label if node.label: self._write(f"{inner}{node_var}.label = \"{node.label}\"\n") + + #name + self._write(f"{inner}{node_var}.name = \"{node.name}\"\n") #color if node.use_custom_color: From 9b883ccbaf548806a9ca4244961b95a7aec4d7bb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Dec 2023 15:36:13 -0600 Subject: [PATCH 43/72] fix: NodeToPython now registers in v4.0 --- geometry/__init__.py | 4 +-- ntp_operator.py | 67 ++++++++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/geometry/__init__.py b/geometry/__init__.py index e50e3c0..e2707b0 100644 --- a/geometry/__init__.py +++ b/geometry/__init__.py @@ -1,12 +1,12 @@ if "bpy" in locals(): import importlib - importlib.reload(node_tree) importlib.reload(node_settings) + importlib.reload(node_tree) importlib.reload(operator) importlib.reload(ui) else: - from . import node_tree from . import node_settings + from . import node_tree from . import operator from . import ui diff --git a/ntp_operator.py b/ntp_operator.py index 4a8b699..a0651e9 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -1,6 +1,9 @@ import bpy from bpy.types import Context, Operator -from bpy.types import Node, NodeTree, NodeSocketInterface +from bpy.types import Node, NodeTree + +if bpy.app.version < (4, 0, 0): + from bpy.types import NodeSocketInterface import os from typing import TextIO @@ -255,37 +258,38 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str self._load_image(attr, inner, f"{node_var}.{attr_name}") elif type == ST.IMAGE_USER: self._image_user_settings(attr, inner, f"{node_var}.{attr_name}") + + if bpy.app.version < (4, 0, 0): + def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, + inner: str, socket_var: str) -> None: + """ + Set a node group input/output's default properties if they exist - def _set_group_socket_default(self, socket_interface: NodeSocketInterface, - inner: str, socket_var: str) -> None: - """ - Set a node group input/output's default properties if they exist - - Parameters: - socket_interface (NodeSocketInterface): socket interface associated - with the input/output - inner (str): indentation string - socket_var (str): variable name for the socket - """ - if socket_interface.type not in default_sockets: - return + Parameters: + socket_interface (NodeSocketInterface): socket interface associated + with the input/output + inner (str): indentation string + socket_var (str): variable name for the socket + """ + if socket_interface.type not in default_sockets: + return - if socket_interface.type == 'RGBA': - dv = vec4_to_py_str(socket_interface.default_value) - elif socket_interface.type == 'VECTOR': - dv = vec3_to_py_str(socket_interface.default_value) - else: - dv = socket_interface.default_value - self._write(f"{inner}{socket_var}.default_value = {dv}\n") - - #min value - if hasattr(socket_interface, "min_value"): - min_val = socket_interface.min_value - self._write(f"{inner}{socket_var}.min_value = {min_val}\n") - #max value - if hasattr(socket_interface, "min_value"): - max_val = socket_interface.max_value - self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) + if socket_interface.type == 'RGBA': + dv = vec4_to_py_str(socket_interface.default_value) + elif socket_interface.type == 'VECTOR': + dv = vec3_to_py_str(socket_interface.default_value) + else: + dv = socket_interface.default_value + self._write(f"{inner}{socket_var}.default_value = {dv}\n") + + #min value + if hasattr(socket_interface, "min_value"): + min_val = socket_interface.min_value + self._write(f"{inner}{socket_var}.min_value = {min_val}\n") + #max value + if hasattr(socket_interface, "min_value"): + max_val = socket_interface.max_value + self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) def _group_io_settings(self, node: bpy.types.Node, inner: str, io: str, #TODO: convert to enum @@ -322,7 +326,8 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - self._set_group_socket_default(socket_interface, inner, socket_var) + if bpy.app.version < (4, 0, 0): + self._set_group_socket_default_v3(socket_interface, inner, socket_var) #default attribute name if hasattr(socket_interface, "default_attribute_name"): From 23a863e2cf452435577a03bfe94f97a6cee33644 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Dec 2023 15:51:37 -0600 Subject: [PATCH 44/72] fix: compositing nodes now run, no group settings --- compositor/operator.py | 6 +- geometry/operator.py | 6 +- material/operator.py | 6 +- ntp_operator.py | 258 ++++++++++++++++++++++------------------- 4 files changed, 149 insertions(+), 127 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index b521e77..c5829c3 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -103,11 +103,13 @@ def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str, level: int f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - self._group_io_settings(node, inner, "input", ntp_nt) + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, "input", ntp_nt) inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - self._group_io_settings(node, inner, "output", ntp_nt) + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, "output", ntp_nt) outputs_set = True self._set_socket_defaults(node, node_var, inner) diff --git a/geometry/operator.py b/geometry/operator.py index 79f9cf1..21c7c9b 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -55,11 +55,13 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, "output", ntp_node_tree) ntp_node_tree.outputs_set = True elif node.bl_idname == 'GeometryNodeSimulationInput': diff --git a/material/operator.py b/material/operator.py index cc84f24..48147de 100644 --- a/material/operator.py +++ b/material/operator.py @@ -52,11 +52,13 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, lev if node.bl_idname == 'ShaderNodeGroup': self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, "output", ntp_node_tree) ntp_node_tree.outputs_set = True self._hide_hidden_sockets(node, inner, node_var) diff --git a/ntp_operator.py b/ntp_operator.py index a0651e9..b5c308f 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -11,18 +11,19 @@ from .ntp_node_tree import NTP_NodeTree from .utils import * + class NTP_Operator(Operator): """ "Abstract" base class for all NTP operators. Blender types and abstraction don't seem to mix well, but this should only be inherited from """ - + bl_idname = "" bl_label = "" - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ + mode: bpy.props.EnumProperty( + name="Mode", + items=[ ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), ('ADDON', "Addon", "Create a full addon") ] @@ -32,7 +33,7 @@ def __init__(self): super().__init__() # File (TextIO) or string (StringIO) the add-on/script is generated into - self._file : TextIO = None + self._file: TextIO = None # Path to the current directory self._dir: str = None @@ -65,13 +66,13 @@ def _setup_addon_directories(self, context: Context, nt_var: str) -> None: """ Finds/creates directories to save add-on to """ - #find base directory to save new addon + # find base directory to save new addon self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) if not self._dir or self._dir == "": - self.report({'ERROR'}, + self.report({'ERROR'}, ("NodeToPython: Save your blend file before using " - "NodeToPython!")) #TODO: Still valid?? - return {'CANCELLED'} #TODO + "NodeToPython!")) # TODO: Still valid?? + return {'CANCELLED'} # TODO self._zip_dir = os.path.join(self._dir, nt_var) self._addon_dir = os.path.join(self._zip_dir, nt_var) @@ -93,7 +94,7 @@ def _create_header(self, name: str) -> None: self._write("\t\"author\" : \"Node To Python\",\n") self._write("\t\"version\" : (1, 0, 0),\n") self._write(f"\t\"blender\" : {bpy.app.version},\n") - self._write("\t\"location\" : \"Object\",\n") #TODO + self._write("\t\"location\" : \"Object\",\n") # TODO self._write("\t\"category\" : \"Node\"\n") self._write("}\n") self._write("\n") @@ -125,7 +126,7 @@ def _is_outermost_node_group(self, level: int) -> bool: return True return False - def _process_group_node_tree(self, node: Node, node_var: str, level: int, + def _process_group_node_tree(self, node: Node, node_var: str, level: int, inner: str) -> None: """ Processes node tree of group node if one is present @@ -179,26 +180,27 @@ def _create_node(self, node: Node, inner: str, node_tree_var: str) -> str: self._write((f"{inner}{node_var} " f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) - #label + # label if node.label: self._write(f"{inner}{node_var}.label = \"{node.label}\"\n") - - #name + + # name self._write(f"{inner}{node_var}.name = \"{node.name}\"\n") - #color + # color if node.use_custom_color: self._write(f"{inner}{node_var}.use_custom_color = True\n") - self._write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") + self._write( + f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") - #mute + # mute if node.mute: self._write(f"{inner}{node_var}.mute = True\n") - + return node_var def _set_settings_defaults(self, node: Node, inner: str, node_var: str - ) -> None: + ) -> None: """ Sets the defaults for any settings a node may have @@ -255,13 +257,15 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str if self._addon_dir is not None and attr is not None: if attr.source in {'FILE', 'GENERATED', 'TILED'}: self._save_image(attr) - self._load_image(attr, inner, f"{node_var}.{attr_name}") + self._load_image( + attr, inner, f"{node_var}.{attr_name}") elif type == ST.IMAGE_USER: - self._image_user_settings(attr, inner, f"{node_var}.{attr_name}") - + self._image_user_settings( + attr, inner, f"{node_var}.{attr_name}") + if bpy.app.version < (4, 0, 0): - def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, - inner: str, socket_var: str) -> None: + def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, + inner: str, socket_var: str) -> None: """ Set a node group input/output's default properties if they exist @@ -282,17 +286,17 @@ def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, dv = socket_interface.default_value self._write(f"{inner}{socket_var}.default_value = {dv}\n") - #min value + # min value if hasattr(socket_interface, "min_value"): min_val = socket_interface.min_value self._write(f"{inner}{socket_var}.min_value = {min_val}\n") - #max value + # max value if hasattr(socket_interface, "min_value"): max_val = socket_interface.max_value self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) - def _group_io_settings(self, node: bpy.types.Node, inner: str, - io: str, #TODO: convert to enum + def _group_io_settings(self, node: bpy.types.Node, inner: str, + io: str, # TODO: convert to enum ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets @@ -322,43 +326,48 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, self._write(f"{inner}#{io} {inout.name}\n") idname = enum_to_py_str(inout.bl_idname) name = str_to_py_str(inout.name) - self._write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") + self._write( + f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" if bpy.app.version < (4, 0, 0): - self._set_group_socket_default_v3(socket_interface, inner, socket_var) - - #default attribute name + self._set_group_socket_default_v3( + socket_interface, inner, socket_var) + + # default attribute name if hasattr(socket_interface, "default_attribute_name"): if socket_interface.default_attribute_name != "": - dan = str_to_py_str(socket_interface.default_attribute_name) + dan = str_to_py_str( + socket_interface.default_attribute_name) self._write((f"{inner}{socket_var}" f".default_attribute_name = {dan}\n")) - #attribute domain + # attribute domain if hasattr(socket_interface, "attribute_domain"): ad = enum_to_py_str(socket_interface.attribute_domain) self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") - #tooltip + # tooltip if socket_interface.description != "": description = str_to_py_str(socket_interface.description) - self._write((f"{inner}{socket_var}.description = {description}\n")) + self._write( + (f"{inner}{socket_var}.description = {description}\n")) - #hide_value + # hide_value if socket_interface.hide_value is True: self._write(f"{inner}{socket_var}.hide_value = True\n") - #hide in modifier + # hide in modifier if hasattr(socket_interface, "hide_in_modifier"): if socket_interface.hide_in_modifier is True: - self._write(f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write( + f"{inner}{socket_var}.hide_in_modifier = True\n") self._write("\n") self._write("\n") - def _set_input_defaults(self, node: bpy.types.Node, inner: str, + def _set_input_defaults(self, node: bpy.types.Node, inner: str, node_var: str) -> None: """ Sets defaults for input sockets @@ -375,45 +384,47 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults and not input.is_linked: - #TODO: this could be cleaner + # TODO: this could be cleaner socket_var = f"{node_var}.inputs[{i}]" - #colors + # colors if input.bl_idname == 'NodeSocketColor': default_val = vec4_to_py_str(input.default_value) - #vector types + # vector types elif "Vector" in input.bl_idname: default_val = vec3_to_py_str(input.default_value) - #strings + # strings elif input.bl_idname == 'NodeSocketString': default_val = str_to_py_str(input.default_value) - #images + # images elif input.bl_idname == 'NodeSocketImage': img = input.default_value - if img is not None and self._addon_dir != None: #write in a better way + if img is not None and self._addon_dir != None: # write in a better way self._save_image(img) - self._load_image(img, inner, f"{socket_var}.default_value") + self._load_image( + img, inner, f"{socket_var}.default_value") default_val = None - #materials + # materials elif input.bl_idname == 'NodeSocketMaterial': self._in_file_inputs(input, inner, socket_var, "materials") default_val = None - #collections + # collections elif input.bl_idname == 'NodeSocketCollection': - self._in_file_inputs(input, inner, socket_var, "collections") + self._in_file_inputs( + input, inner, socket_var, "collections") default_val = None - #objects + # objects elif input.bl_idname == 'NodeSocketObject': self._in_file_inputs(input, inner, socket_var, "objects") default_val = None - - #textures + + # textures elif input.bl_idname == 'NodeSocketTexture': self._in_file_inputs(input, inner, socket_var, "textures") default_val = None @@ -436,11 +447,11 @@ def _set_output_defaults(self, node: bpy.types.Node, inner (str): indentation string node_var (str): variable name for the node we're setting output defaults for """ - #TODO: probably should define elsewhere - output_default_nodes = {'ShaderNodeValue', - 'ShaderNodeRGB', + # TODO: probably should define elsewhere + output_default_nodes = {'ShaderNodeValue', + 'ShaderNodeRGB', 'ShaderNodeNormal', - 'CompositorNodeValue', + 'CompositorNodeValue', 'CompositorNodeRGB', 'CompositorNodeNormal'} @@ -454,11 +465,11 @@ def _set_output_defaults(self, node: bpy.types.Node, dv = vec3_to_py_str(dv) self._write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) - def _in_file_inputs(self, input: bpy.types.NodeSocket, - inner: str, - socket_var: str, - type: str - ) -> None: + def _in_file_inputs(self, input: bpy.types.NodeSocket, + inner: str, + socket_var: str, + type: str + ) -> None: """ Sets inputs for a node input if one already exists in the blend file @@ -476,8 +487,8 @@ def _in_file_inputs(self, input: bpy.types.NodeSocket, self._write((f"{inner}\t{socket_var}.default_value = " f"bpy.data.{type}[{name}]\n")) - def _color_ramp_settings(self, node: bpy.types.Node, - inner: str, + def _color_ramp_settings(self, node: bpy.types.Node, + inner: str, node_var: str, color_ramp_name: str) -> None: """ @@ -492,9 +503,10 @@ def _color_ramp_settings(self, node: bpy.types.Node, color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name) if not color_ramp: - raise ValueError(f"No color ramp named \"{color_ramp_name}\" found") + raise ValueError( + f"No color ramp named \"{color_ramp_name}\" found") - #settings + # settings ramp_str = f"{inner}{node_var}.{color_ramp_name}" color_mode = enum_to_py_str(color_ramp.color_mode) @@ -508,7 +520,7 @@ def _color_ramp_settings(self, node: bpy.types.Node, f"= {interpolation}\n")) self._write("\n") - #key points + # key points self._write(f"{inner}#initialize color ramp elements\n") self._write((f"{ramp_str}.elements.remove" f"({ramp_str}.elements[0])\n")) @@ -517,7 +529,8 @@ def _color_ramp_settings(self, node: bpy.types.Node, if i == 0: self._write(f"{inner}{element_var} = " f"{ramp_str}.elements[{i}]\n") - self._write(f"{inner}{element_var}.position = {element.position}\n") + self._write( + f"{inner}{element_var}.position = {element.position}\n") else: self._write((f"{inner}{element_var} = " f"{ramp_str}.elements" @@ -527,10 +540,10 @@ def _color_ramp_settings(self, node: bpy.types.Node, f"{element.alpha}\n")) color_str = vec4_to_py_str(element.color) self._write((f"{inner}{element_var}.color = {color_str}\n\n")) - - def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, + + def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, node_var: str, curve_mapping_name: str - ) -> None: + ) -> None: """ Sets defaults for Float, Vector, and Color curves @@ -545,27 +558,27 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, mapping = getattr(node, curve_mapping_name) if not mapping: raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " - f"in node \"{node.bl_idname}\"")) + f"in node \"{node.bl_idname}\"")) - #mapping settings + # mapping settings self._write(f"{inner}#mapping settings\n") mapping_var = f"{inner}{node_var}.{curve_mapping_name}" - #extend + # extend extend = enum_to_py_str(mapping.extend) self._write(f"{mapping_var}.extend = {extend}\n") - #tone + # tone tone = enum_to_py_str(mapping.tone) self._write(f"{mapping_var}.tone = {tone}\n") - #black level + # black level b_lvl_str = vec3_to_py_str(mapping.black_level) self._write((f"{mapping_var}.black_level = {b_lvl_str}\n")) - #white level + # white level w_lvl_str = vec3_to_py_str(mapping.white_level) self._write((f"{mapping_var}.white_level = {w_lvl_str}\n")) - #minima and maxima + # minima and maxima min_x = mapping.clip_min_x self._write(f"{mapping_var}.clip_min_x = {min_x}\n") min_y = mapping.clip_min_y @@ -575,13 +588,13 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, max_y = mapping.clip_max_y self._write(f"{mapping_var}.clip_max_y = {max_y}\n") - #use_clip + # use_clip use_clip = mapping.use_clip self._write(f"{mapping_var}.use_clip = {use_clip}\n") - #create curves + # create curves for i, curve in enumerate(mapping.curves): - #TODO: curve function + # TODO: curve function self._write(f"{inner}#curve {i}\n") curve_i = f"{node_var}_curve_{i}" self._write((f"{inner}{curve_i} = " @@ -592,10 +605,11 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, if (node.bl_idname == 'CompositorNodeHueCorrect'): self._write((f"{inner}for i in range" f"(len({curve_i}.points.values()) - 1, 1, -1):\n")) - self._write(f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") - + self._write( + f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") + for j, point in enumerate(curve.points): - #TODO: point function + # TODO: point function point_j = f"{inner}{curve_i}_point_{j}" loc = point.location @@ -604,12 +618,13 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, self._write(f"{point_j} = {curve_i}.points[{j}]\n") self._write(f"{point_j}.location = ({loc_str})\n") else: - self._write((f"{point_j} = {curve_i}.points.new({loc_str})\n")) + self._write( + (f"{point_j} = {curve_i}.points.new({loc_str})\n")) handle = enum_to_py_str(point.handle_type) self._write(f"{point_j}.handle_type = {handle}\n") - - #update curve + + # update curve self._write(f"{inner}#update curve after changes\n") self._write(f"{mapping_var}.update()\n") @@ -624,19 +639,19 @@ def _save_image(self, img: bpy.types.Image) -> None: if img is None: return - #create image dir if one doesn't exist + # create image dir if one doesn't exist img_dir = os.path.join(self._addon_dir, IMAGE_DIR_NAME) if not os.path.exists(img_dir): os.mkdir(img_dir) - #save the image + # save the image img_str = img_to_py_str(img) img_path = f"{img_dir}/{img_str}" if not os.path.exists(img_path): img.save_render(img_path) def _load_image(self, img: bpy.types.Image, - inner: str, + inner: str, img_var: str ) -> None: """ @@ -650,10 +665,10 @@ def _load_image(self, img: bpy.types.Image, if img is None: return - + img_str = img_to_py_str(img) - #TODO: convert to special variables + # TODO: convert to special variables self._write(f"{inner}#load image {img_str}\n") self._write((f"{inner}base_dir = " f"os.path.dirname(os.path.abspath(__file__))\n")) @@ -663,24 +678,25 @@ def _load_image(self, img: bpy.types.Image, self._write((f"{inner}{img_var} = bpy.data.images.load" f"(image_path, check_existing = True)\n")) - #copy image settings + # copy image settings self._write(f"{inner}#set image settings\n") - #source + # source source = enum_to_py_str(img.source) self._write(f"{inner}{img_var}.source = {source}\n") - #color space settings + # color space settings color_space = enum_to_py_str(img.colorspace_settings.name) - self._write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") - - #alpha mode + self._write( + f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") + + # alpha mode alpha_mode = enum_to_py_str(img.alpha_mode) self._write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") def _image_user_settings(self, img_user: bpy.types.ImageUser, - inner: str, - img_user_var: str) -> None: + inner: str, + img_user_var: str) -> None: """ Replicate the image user of an image node @@ -691,14 +707,14 @@ def _image_user_settings(self, img_user: bpy.types.ImageUser, """ img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", - "frame_start", "tile", "use_auto_refresh", "use_cyclic"] - + "frame_start", "tile", "use_auto_refresh", "use_cyclic"] + for img_usr_attr in img_usr_attrs: self._write((f"{inner}{img_user_var}.{img_usr_attr} = " f"{getattr(img_user, img_usr_attr)}\n")) def _set_parents(self, node_tree: bpy.types.NodeTree, - inner: str) -> None: + inner: str) -> None: """ Sets parents for all nodes, mostly used to put nodes in frames @@ -733,8 +749,8 @@ def _set_locations(self, node_tree: bpy.types.NodeTree, inner: str) -> None: f"= ({node.location.x}, {node.location.y})\n")) self._write("\n") - def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, - ) -> None: + def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, + ) -> None: """ Set dimensions for all nodes @@ -749,8 +765,8 @@ def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, f"= {node.width}, {node.height}\n")) self._write("\n") - def _init_links(self, node_tree: bpy.types.NodeTree, - inner: str, + def _init_links(self, node_tree: bpy.types.NodeTree, + inner: str, node_tree_var: str) -> None: """ Create all the links between nodes @@ -762,39 +778,39 @@ def _init_links(self, node_tree: bpy.types.NodeTree, """ if node_tree.links: - self._write(f"{inner}#initialize {node_tree_var} links\n") + self._write(f"{inner}#initialize {node_tree_var} links\n") for link in node_tree.links: in_node_var = self._node_vars[link.from_node] input_socket = link.from_socket - + """ Blender's socket dictionary doesn't guarantee unique keys, which has caused much wailing and gnashing of teeth. This is a quick fix that doesn't run quick """ - #TODO: try using index() method + # TODO: try using index() method for i, item in enumerate(link.from_node.outputs.items()): if item[1] == input_socket: input_idx = i break - + out_node_var = self._node_vars[link.to_node] output_socket = link.to_socket - + for i, item in enumerate(link.to_node.inputs.items()): if item[1] == output_socket: output_idx = i break - + self._write((f"{inner}#{in_node_var}.{input_socket.name} " f"-> {out_node_var}.{output_socket.name}\n")) self._write((f"{inner}{node_tree_var}.links.new({in_node_var}" f".outputs[{input_idx}], " f"{out_node_var}.inputs[{output_idx}])\n")) - def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, - node_var: str) -> None: + def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, + node_var: str) -> None: """ Hide hidden sockets @@ -808,7 +824,7 @@ def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, self._write(f"{inner}{node_var}.inputs[{i}].hide = True\n") for i, socket in enumerate(node.outputs): if socket.hide is True: - self._write(f"{inner}{node_var}.outputs[{i}].hide = True\n") + self._write(f"{inner}{node_var}.outputs[{i}].hide = True\n") def _set_socket_defaults(self, node: Node, node_var: str, inner: str): self._set_input_defaults(node, inner, node_var) @@ -847,7 +863,7 @@ def _create_main_func(self) -> None: """ self._write("if __name__ == \"__main__\":\n") self._write("\tregister()") - + def _zip_addon(self) -> None: """ Zips up the addon and removes the directory @@ -856,13 +872,13 @@ def _zip_addon(self) -> None: shutil.rmtree(self._zip_dir) # ABSTRACT - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, level: int) -> None: return # ABSTRACT def _process_node_tree(self, node_tree: NodeTree, level: int) -> None: - return + return def _report_finished(self, object: str): """ @@ -876,9 +892,9 @@ def _report_finished(self, object: str): location = "clipboard" else: location = self._dir - self.report({'INFO'}, + self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}") - + # ABSTRACT def execute(self): return {'FINISHED'} From 344e4c5df216cec0d854614c0ae60b5d9f8b00a0 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 9 Dec 2023 16:00:14 -0600 Subject: [PATCH 45/72] feat: support for rotation sockets --- ntp_operator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ntp_operator.py b/ntp_operator.py index b5c308f..9762d02 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -395,6 +395,10 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, elif "Vector" in input.bl_idname: default_val = vec3_to_py_str(input.default_value) + #rotation types + elif input.bl_idname == 'NodeSocketRotation': + default_val = vec3_to_py_str(input.default_value) + # strings elif input.bl_idname == 'NodeSocketString': default_val = str_to_py_str(input.default_value) From 89d9d48f38f7116e27c985f4eda2003de4d778c6 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Wed, 3 Jan 2024 00:26:28 -0600 Subject: [PATCH 46/72] feat: 4.0 node tree changes --- compositor/operator.py | 6 +- geometry/operator.py | 6 +- material/operator.py | 6 +- ntp_operator.py | 287 +++++++++++++++++++++++++++++++++-------- utils.py | 3 - 5 files changed, 236 insertions(+), 72 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index c5829c3..b521e77 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -103,13 +103,11 @@ def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str, level: int f"bpy.data.node_groups" f"[\"{node.node_tree.name}\"]\n")) elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, "input", ntp_nt) + self._group_io_settings(node, inner, "input", ntp_nt) inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, "output", ntp_nt) + self._group_io_settings(node, inner, "output", ntp_nt) outputs_set = True self._set_socket_defaults(node, node_var, inner) diff --git a/geometry/operator.py b/geometry/operator.py index 21c7c9b..79f9cf1 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -55,13 +55,11 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, "input", ntp_node_tree) + self._group_io_settings(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, "output", ntp_node_tree) + self._group_io_settings(node, inner, "output", ntp_node_tree) ntp_node_tree.outputs_set = True elif node.bl_idname == 'GeometryNodeSimulationInput': diff --git a/material/operator.py b/material/operator.py index 48147de..cc84f24 100644 --- a/material/operator.py +++ b/material/operator.py @@ -52,13 +52,11 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, lev if node.bl_idname == 'ShaderNodeGroup': self._process_group_node_tree(node, node_var, level, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, "input", ntp_node_tree) + self._group_io_settings(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, "output", ntp_node_tree) + self._group_io_settings(node, inner, "output", ntp_node_tree) ntp_node_tree.outputs_set = True self._hide_hidden_sockets(node, inner, node_var) diff --git a/ntp_operator.py b/ntp_operator.py index 9762d02..e7da207 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -29,6 +29,20 @@ class NTP_Operator(Operator): ] ) + #node tree input sockets that have default properties + if bpy.app.version < (4, 0, 0): + default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} + else: + nondefault_sockets_v4 = { + bpy.types.NodeTreeInterfaceSocketCollection, + bpy.types.NodeTreeInterfaceSocketGeometry, + bpy.types.NodeTreeInterfaceSocketImage, + bpy.types.NodeTreeInterfaceSocketMaterial, + bpy.types.NodeTreeInterfaceSocketObject, + bpy.types.NodeTreeInterfaceSocketShader, + bpy.types.NodeTreeInterfaceSocketTexture + } + def __init__(self): super().__init__() @@ -275,7 +289,7 @@ def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, inner (str): indentation string socket_var (str): variable name for the socket """ - if socket_interface.type not in default_sockets: + if socket_interface.type not in self.default_sockets_v3: return if socket_interface.type == 'RGBA': @@ -295,77 +309,236 @@ def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, max_val = socket_interface.max_value self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) - def _group_io_settings(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: - """ - Set the settings for group input and output sockets + def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: + """ + Set the settings for group input and output sockets - Parameters: - node (bpy.types.Node) : group input/output node - inner (str): indentation string - io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating input - and output settings for - """ - node_tree_var = ntp_node_tree.var - node_tree = ntp_node_tree.node_tree + Parameters: + node (bpy.types.Node) : group input/output node + inner (str): indentation string + io (str): whether we're generating the input or output settings + node_tree_var (str): variable name of the generated node tree + node_tree (bpy.types.NodeTree): node tree that we're generating + input and output settings for + """ + node_tree_var = ntp_node_tree.var + node_tree = ntp_node_tree.node_tree - if io == "input": - io_sockets = node.outputs - io_socket_interfaces = node_tree.inputs - else: - io_sockets = node.inputs - io_socket_interfaces = node_tree.outputs + if io == "input": + io_sockets = node.outputs + io_socket_interfaces = node_tree.inputs + else: + io_sockets = node.inputs + io_socket_interfaces = node_tree.outputs + + self._write(f"{inner}#{node_tree_var} {io}s\n") + for i, inout in enumerate(io_sockets): + if inout.bl_idname == 'NodeSocketVirtual': + continue + self._write(f"{inner}#{io} {inout.name}\n") + idname = enum_to_py_str(inout.bl_idname) + name = str_to_py_str(inout.name) + self._write( + f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") + socket_interface = io_socket_interfaces[i] + socket_var = f"{node_tree_var}.{io}s[{i}]" + + self._set_group_socket_default_v3(socket_interface, inner, + socket_var) + + # default attribute name + if hasattr(socket_interface, "default_attribute_name"): + if socket_interface.default_attribute_name != "": + dan = str_to_py_str( + socket_interface.default_attribute_name) + self._write((f"{inner}{socket_var}" + f".default_attribute_name = {dan}\n")) + + # attribute domain + if hasattr(socket_interface, "attribute_domain"): + ad = enum_to_py_str(socket_interface.attribute_domain) + self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + + # tooltip + if socket_interface.description != "": + description = str_to_py_str(socket_interface.description) + self._write( + (f"{inner}{socket_var}.description = {description}\n")) - self._write(f"{inner}#{node_tree_var} {io}s\n") - for i, inout in enumerate(io_sockets): - if inout.bl_idname == 'NodeSocketVirtual': - continue - self._write(f"{inner}#{io} {inout.name}\n") - idname = enum_to_py_str(inout.bl_idname) - name = str_to_py_str(inout.name) - self._write( - f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") - socket_interface = io_socket_interfaces[i] - socket_var = f"{node_tree_var}.{io}s[{i}]" + # hide_value + if socket_interface.hide_value is True: + self._write(f"{inner}{socket_var}.hide_value = True\n") + + # hide in modifier + if hasattr(socket_interface, "hide_in_modifier"): + if socket_interface.hide_in_modifier is True: + self._write( + f"{inner}{socket_var}.hide_in_modifier = True\n") + + self._write("\n") + self._write("\n") + + elif bpy.app.version >= (4, 0, 0): + def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInterfaceSocket, + inner: str, socket_var: str) -> None: + """ + Set a node group input/output's default properties if they exist + + Parameters: + socket_interface (NodeTreeInterfaceSocket): socket interface associated + with the input/output + inner (str): indentation string + socket_var (str): variable name for the socket + """ + if type(socket_interface) in self.nondefault_sockets_v4: + return - if bpy.app.version < (4, 0, 0): - self._set_group_socket_default_v3( - socket_interface, inner, socket_var) + dv = socket_interface.default_value - # default attribute name - if hasattr(socket_interface, "default_attribute_name"): + if type(socket_interface) == bpy.types.NodeTreeInterfaceSocketColor: + dv = vec4_to_py_str(dv) + elif type(dv) in {mathutils.Vector, mathutils.Euler}: + dv = vec3_to_py_str(dv) + elif type(dv) == str: + dv = str_to_py_str(dv) + self._write(f"{inner}{socket_var}.default_value = {dv}\n") + + # min value + if hasattr(socket_interface, "min_value"): + min_val = socket_interface.min_value + self._write(f"{inner}{socket_var}.min_value = {min_val}\n") + # max value + if hasattr(socket_interface, "min_value"): + max_val = socket_interface.max_value + self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) + + def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: + """ + Set the settings for group input and output sockets + + Parameters: + node (bpy.types.Node) : group input/output node + inner (str): indentation string + io (str): whether we're generating the input or output settings + node_tree_var (str): variable name of the generated node tree + node_tree (bpy.types.NodeTree): node tree that we're generating + input and output settings for + """ + node_tree_var = ntp_node_tree.var + node_tree = ntp_node_tree.node_tree + + if io == "input": + io_sockets = node.outputs # Might be removeable, + # think we can get all the info from the inouts + # from the socket interfaces, need to double check. + # If so, then we can just run these at the initialization + # of the node tree, meaning we can clean up the clunky + # Group Input/Group Output node reliance, two calls + # Should be pretty easy to add in panels afterwards, + # looks like those are tied fairly close to the new socket + # system + items_tree = node_tree.interface.items_tree + io_socket_interfaces = [item for item in items_tree + if item.item_type == 'SOCKET' + and item.in_out == 'INPUT'] + else: + io_sockets = node.inputs + items_tree = node_tree.interface.items_tree + io_socket_interfaces = [item for item in items_tree + if item.item_type == 'SOCKET' + and item.in_out == 'OUTPUT'] + + self._write(f"{inner}#{node_tree_var} {io}s\n") + for i, socket_interface in enumerate(io_socket_interfaces): + self._write(f"{inner}#{io} {socket_interface.name}\n") + + socket_interface: bpy.types.NodeTreeInterfaceSocket = io_socket_interfaces[i] + + #initialization + socket_var = clean_string(socket_interface.name) + "_socket" + name = str_to_py_str(socket_interface.name) + in_out_enum = enum_to_py_str(socket_interface.in_out) + + socket_type = enum_to_py_str(socket_interface.bl_socket_idname) + """ + I might be missing something, but the Python API's set up a bit + weird here now. The new socket initialization only accepts types + from a list of basic ones, but there doesn't seem to be a way of + retrieving just this basic typewithout the subtype information. + """ + if 'Float' in socket_type: + socket_type = enum_to_py_str('NodeSocketFloat') + elif 'Int' in socket_type: + socket_type = enum_to_py_str('NodeSocketInt') + elif 'Vector' in socket_type: + socket_type = enum_to_py_str('NodeSocketVector') + + + self._write(f"{inner}{socket_var} = " + f"{node_tree_var}.interface.new_socket(" + f"name = {name}, in_out={in_out_enum}, " + f"socket_type = {socket_type})\n") + + #subtype + if hasattr(socket_interface, "subtype"): + subtype = enum_to_py_str(socket_interface.subtype) + self._write(f"{inner}{socket_var}.subtype = {subtype}\n") + + self._set_group_socket_default_v4(socket_interface, inner, + socket_var) + + # default attribute name if socket_interface.default_attribute_name != "": - dan = str_to_py_str( - socket_interface.default_attribute_name) - self._write((f"{inner}{socket_var}" - f".default_attribute_name = {dan}\n")) + dan = str_to_py_str(socket_interface.default_attribute_name) + self._write((f"{inner}{socket_var}.default_attribute_name = {dan}\n")) - # attribute domain - if hasattr(socket_interface, "attribute_domain"): + # attribute domain ad = enum_to_py_str(socket_interface.attribute_domain) self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") - # tooltip - if socket_interface.description != "": - description = str_to_py_str(socket_interface.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) + # tooltip + if socket_interface.description != "": + description = str_to_py_str(socket_interface.description) + self._write( + (f"{inner}{socket_var}.description = {description}\n")) - # hide_value - if socket_interface.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") + # hide_value + if socket_interface.hide_value is True: + self._write(f"{inner}{socket_var}.hide_value = True\n") - # hide in modifier - if hasattr(socket_interface, "hide_in_modifier"): + # hide in modifier if socket_interface.hide_in_modifier is True: - self._write( - f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write(f"{inner}{socket_var}.hide_in_modifier = True\n") + + #force non field + if socket_interface.force_non_field is True: + self._write(f"{inner}{socket_var}.force_non_field = True\n") + self._write("\n") self._write("\n") - self._write("\n") + + def _group_io_settings(self, node: bpy.types.Node, inner: str, + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: + """ + Set the settings for group input and output sockets + + Parameters: + node (bpy.types.Node) : group input/output node + inner (str): indentation string + io (str): whether we're generating the input or output settings + node_tree_var (str): variable name of the generated node tree + node_tree (bpy.types.NodeTree): node tree that we're generating + input and output settings for + """ + if bpy.app.version < (4, 0, 0): + self._group_io_settings_v3(node, inner, io, ntp_node_tree) + else: + self._group_io_settings_v4(node, inner, io, ntp_node_tree) def _set_input_defaults(self, node: bpy.types.Node, inner: str, node_var: str) -> None: diff --git a/utils.py b/utils.py index bec9a44..68e6798 100644 --- a/utils.py +++ b/utils.py @@ -55,9 +55,6 @@ class ST(Enum): FILE_SLOTS = auto() LAYER_SLOTS = auto() #unimplemented -#node tree input sockets that have default properties -default_sockets = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} - def clean_string(string: str, lower: bool = True) -> str: """ Cleans up a string for use as a variable or file name From 38553b58abe3d73f60204951820b6ab0a01dbdc6 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 15:08:14 -0600 Subject: [PATCH 47/72] feat: geo node modifier and tool setting --- geometry/operator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/geometry/operator.py b/geometry/operator.py index 79f9cf1..90f45a9 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -118,6 +118,10 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, f"name = {str_to_py_str(node_tree.name)})\n")) self._write("\n") + if bpy.app.version >= (4, 0, 0): + self._write(f"{inner}{nt_var}.is_modifier = {node_tree.is_modifier}\n") + self._write(f"{inner}{nt_var}.is_tool = {node_tree.is_tool}\n") + #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") From 2a9c54533c4c87f1ba4da84f55b35e75051dad19 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 15:09:16 -0600 Subject: [PATCH 48/72] fix: generic node types no longer warned about when trying to apply settings --- compositor/node_settings.py | 11 +++++++++-- geometry/node_settings.py | 9 ++++++++- material/node_settings.py | 10 +++++++++- 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/compositor/node_settings.py b/compositor/node_settings.py index ff8a377..781eee6 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -414,7 +414,7 @@ ("offset_y", ST.FLOAT), ("space", ST.ENUM)], - 'CompositorNodeStablize' : [("clip", ST.MOVIE_CLIP), + 'CompositorNodeStabilize' : [("clip", ST.MOVIE_CLIP), ("filter_type", ST.ENUM), ("invert", ST.BOOL)], @@ -425,5 +425,12 @@ # LAYOUT - 'CompositorNodeSwitch' : [("check", ST.BOOL)] + 'CompositorNodeSwitch' : [("check", ST.BOOL)], + + + # MISC + 'NodeFrame' : [], + 'NodeGroupInput' : [], + 'NodeGroupOutput' : [], + 'NodeReroute' : [] } diff --git a/geometry/node_settings.py b/geometry/node_settings.py index 1900660..50d645e 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -494,5 +494,12 @@ ("pivot_axis", ST.ENUM)], 'FunctionNodeRotateEuler' : [("space", ST.ENUM), - ("type", ST.ENUM)] + ("type", ST.ENUM)], + + # MISC + 'NodeFrame' : [], + 'NodeGroupInput' : [], + 'NodeGroupOutput' : [], + 'NodeReroute' : [] + } \ No newline at end of file diff --git a/material/node_settings.py b/material/node_settings.py index 30b3b08..c6b339e 100644 --- a/material/node_settings.py +++ b/material/node_settings.py @@ -215,6 +215,8 @@ 'ShaderNodeMapping' : [("vector_type", ST.ENUM)], + 'ShaderNodeNormal' : [], + 'ShaderNodeNormalMap' : [("space", ST.ENUM), ("uv_map", ST.STRING)], #TODO @@ -269,5 +271,11 @@ ("filepath", ST.STRING), ("mode", ST.ENUM), ("script", ST.TEXT), - ("use_auto_update", ST.BOOL)] + ("use_auto_update", ST.BOOL)], + + # MISC + 'NodeFrame' : [], + 'NodeGroupInput' : [], + 'NodeGroupOutput' : [], + 'NodeReroute' : [] } From 55f41901f7308701bd02f863c3e68a385637a21b Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 15:58:11 -0600 Subject: [PATCH 49/72] feat: new geo node tree flags --- geometry/node_settings.py | 95 ++++++++++++--------------------------- geometry/operator.py | 14 ++++-- 2 files changed, 39 insertions(+), 70 deletions(-) diff --git a/geometry/node_settings.py b/geometry/node_settings.py index 50d645e..7596821 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -43,16 +43,16 @@ 'NodeGroupInput' : [], # Input > Scene + 'GeometryNodeTool3DCursor' : [], + 'GeometryNodeCollectionInfo' : [("transform_space", ST.ENUM)], 'GeometryNodeImageInfo' : [], - 'GeometryNodeIsViewport' : [], 'GeometryNodeObjectInfo' : [("transform_space", ST.ENUM)], 'GeometryNodeSelfObject' : [], - 'GeometryNodeInputSceneTime' : [], @@ -64,21 +64,18 @@ # GEOMETRY 'GeometryNodeJoinGeometry' : [], - 'GeometryNodeGeometryToInstance' : [], # Geometry > Read 'GeometryNodeInputID' : [], - 'GeometryNodeInputIndex' : [], 'GeometryNodeInputNamedAttribute' : [("data_type", ST.ENUM)], 'GeometryNodeInputNormal' : [], - 'GeometryNodeInputPosition' : [], - 'GeometryNodeInputRadius' : [], + 'GeometryNodeToolSelection' : [], # Geometry > Sample 'GeometryNodeProximity' : [("target_element", ST.ENUM)], @@ -95,13 +92,12 @@ 'GeometryNodeSampleNearest' : [("domain", ST.ENUM)], # Geometry > Write - 'GeometryNodeSetID' : [], - - 'GeometryNodeSetPosition' : [], + 'GeometryNodeSetID' : [], + 'GeometryNodeSetPosition' : [], + 'GeometryNodeToolSetSelection' : [], # Geometry > Operations 'GeometryNodeBoundBox' : [], - 'GeometryNodeConvexHull' : [], 'GeometryNodeDeleteGeometry' : [("domain", ST.ENUM), @@ -112,7 +108,6 @@ 'GeometryNodeMergeByDistance' : [("mode", ST.ENUM)], 'GeometryNodeTransform' : [], - 'GeometryNodeSeparateComponents' : [], 'GeometryNodeSeparateGeometry' : [("domain", ST.ENUM)], @@ -121,24 +116,17 @@ # CURVE # Curve > Read 'GeometryNodeInputCurveHandlePositions' : [], - 'GeometryNodeCurveLength' : [], - 'GeometryNodeInputTangent' : [], - 'GeometryNodeInputCurveTilt' : [], - 'GeometryNodeCurveEndpointSelection' : [], 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", ST.ENUM), ("mode", ST.ENUM_SET)], 'GeometryNodeInputSplineCyclic' : [], - 'GeometryNodeSplineLength' : [], - 'GeometryNodeSplineParameter' : [], - 'GeometryNodeInputSplineResolution' : [], # Curve > Sample @@ -150,7 +138,6 @@ 'GeometryNodeSetCurveNormal' : [("mode", ST.ENUM)], 'GeometryNodeSetCurveRadius' : [], - 'GeometryNodeSetCurveTilt' : [], 'GeometryNodeSetCurveHandlePositions' : [("mode", ST.ENUM)], @@ -159,7 +146,6 @@ ("mode", ST.ENUM_SET)], 'GeometryNodeSetSplineCyclic' : [], - 'GeometryNodeSetSplineResolution' : [], 'GeometryNodeCurveSplineType' : [("spline_type", ST.ENUM)], @@ -180,7 +166,6 @@ 'GeometryNodeResampleCurve' : [("mode", ST.ENUM)], 'GeometryNodeReverseCurve' : [], - 'GeometryNodeSubdivideCurve' : [], 'GeometryNodeTrimCurve' : [("mode", ST.ENUM)], @@ -195,7 +180,6 @@ 'GeometryNodeCurvePrimitiveLine' : [("mode", ST.ENUM)], 'GeometryNodeCurveSpiral' : [], - 'GeometryNodeCurveQuadraticBezier' : [], 'GeometryNodeCurvePrimitiveQuadrilateral' : [("mode", ST.ENUM)], @@ -204,54 +188,38 @@ # Curve > Topology 'GeometryNodeOffsetPointInCurve' : [], - 'GeometryNodeCurveOfPoint' : [], - 'GeometryNodePointsOfCurve' : [], # INSTANCES 'GeometryNodeInstanceOnPoints' : [], - 'GeometryNodeInstancesToPoints' : [], 'GeometryNodeRealizeInstances' : [("legacy_behavior", ST.BOOL)], 'GeometryNodeRotateInstances' : [], - 'GeometryNodeScaleInstances' : [], - 'GeometryNodeTranslateInstances' : [], - 'GeometryNodeInputInstanceRotation' : [], - 'GeometryNodeInputInstanceScale' : [], # MESH # Mesh > Read 'GeometryNodeInputMeshEdgeAngle' : [], - 'GeometryNodeInputMeshEdgeNeighbors' : [], - 'GeometryNodeInputMeshEdgeVertices' : [], - 'GeometryNodeEdgesToFaceGroups' : [], - 'GeometryNodeInputMeshFaceArea' : [], - 'GeometryNodeInputMeshFaceNeighbors' : [], - + 'GeometryNodeToolFaceSet' : [], 'GeometryNodeMeshFaceSetBoundaries' : [], - 'GeometryNodeInputMeshFaceIsPlanar' : [], - 'GeometryNodeInputShadeSmooth' : [], - + 'GeometryNodeInputEdgeSmooth' : [], 'GeometryNodeInputMeshIsland' : [], - 'GeometryNodeInputShortestEdgePaths' : [], - 'GeometryNodeInputMeshVertexNeighbors' : [], # Mesh > Sample @@ -260,13 +228,12 @@ 'GeometryNodeSampleUVSurface' : [("data_type", ST.ENUM)], # Mesh > Write + 'GeometryNodeToolSetFaceSet' : [], 'GeometryNodeSetShadeSmooth' : [], # Mesh > Operations 'GeometryNodeDualMesh' : [], - 'GeometryNodeEdgePathsToCurves' : [], - 'GeometryNodeEdgePathsToSelection' : [], 'GeometryNodeExtrudeMesh' : [("mode", ST.ENUM)], @@ -285,7 +252,6 @@ ("scale_mode", ST.ENUM)], 'GeometryNodeSplitEdges' : [], - 'GeometryNodeSubdivideMesh' : [], 'GeometryNodeSubdivisionSurface' : [("boundary_smooth", ST.ENUM), @@ -302,7 +268,6 @@ 'GeometryNodeMeshCylinder' : [("fill_type", ST.ENUM)], 'GeometryNodeMeshGrid' : [], - 'GeometryNodeMeshIcoSphere' : [], 'GeometryNodeMeshCircle' : [("fill_type", ST.ENUM)], @@ -314,17 +279,11 @@ # Mesh > Topology 'GeometryNodeCornersOfFace' : [], - 'GeometryNodeCornersOfVertex' : [], - 'GeometryNodeEdgesOfCorner' : [], - 'GeometryNodeEdgesOfVertex' : [], - 'GeometryNodeFaceOfCorner' : [], - 'GeometryNodeOffsetCornerInFace' : [], - 'GeometryNodeVertexOfCorner' : [], # Mesh > UV @@ -340,7 +299,7 @@ ("use_legacy_normal", ST.BOOL)], 'GeometryNodePoints' : [], - + 'GeometryNodePointsToCurves' : [], 'GeometryNodePointsToVertices' : [], 'GeometryNodePointsToVolume' : [("resolution_mode", ST.ENUM)], @@ -350,25 +309,19 @@ # VOLUME 'GeometryNodeVolumeCube' : [], - 'GeometryNodeVolumeToMesh' : [("resolution_mode", ST.ENUM)], # SIMULATION 'GeometryNodeSimulationInput' : [], - 'GeometryNodeSimulationOutput' : [], # MATERIAL 'GeometryNodeReplaceMaterial' : [], - 'GeometryNodeInputMaterialIndex' : [], - 'GeometryNodeMaterialSelection' : [], - 'GeometryNodeSetMaterial' : [], - 'GeometryNodeSetMaterialIndex' : [], @@ -413,7 +366,10 @@ 'FunctionNodeRandomValue' : [("data_type", ST.ENUM)], - 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], + 'GeometryNodeRepeatInput' : [], + 'GeometryNodeRepeatOutput' : [], + + 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], # Utilities > Color 'ShaderNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], @@ -430,11 +386,8 @@ # Utilities > Text 'GeometryNodeStringJoin' : [], - 'FunctionNodeReplaceString' : [], - 'FunctionNodeSliceString' : [], - 'FunctionNodeStringLength' : [], 'GeometryNodeStringToCurves' : [("align_x", ST.ENUM), @@ -444,7 +397,6 @@ ("pivot_mode", ST.ENUM)], 'FunctionNodeValueToString' : [], - 'FunctionNodeInputSpecialCharacters' : [], # Utilities > Vector @@ -456,7 +408,6 @@ ("rotation_type", ST.ENUM)], 'ShaderNodeCombineXYZ' : [], - 'ShaderNodeSeparateXYZ' : [], # Utilities > Field @@ -490,11 +441,21 @@ ("use_clamp", ST.BOOL)], # Utilities > Rotation - 'FunctionNodeAlignEulerToVector' : [("axis", ST.ENUM), - ("pivot_axis", ST.ENUM)], + 'FunctionNodeAlignEulerToVector' : [("axis", ST.ENUM), + ("pivot_axis", ST.ENUM)], + + 'FunctionNodeAxisAngleToRotation' : [], + 'FunctionNodeEulerToRotation' : [], + 'FunctionNodeInvertRotation' : [], - 'FunctionNodeRotateEuler' : [("space", ST.ENUM), - ("type", ST.ENUM)], + 'FunctionNodeRotateEuler' : [("space", ST.ENUM), + ("type", ST.ENUM)], + + 'FunctionNodeRotateVector' : [], + 'FunctionNodeRotationToAxisAngle' : [], + 'FunctionNodeRotationToEuler' : [], + 'FunctionNodeRotationToQuaternion' : [], + 'FunctionNodeQuaternionToRotation' : [], # MISC 'NodeFrame' : [], diff --git a/geometry/operator.py b/geometry/operator.py index 90f45a9..a9f0db3 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -119,9 +119,17 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, self._write("\n") if bpy.app.version >= (4, 0, 0): - self._write(f"{inner}{nt_var}.is_modifier = {node_tree.is_modifier}\n") - self._write(f"{inner}{nt_var}.is_tool = {node_tree.is_tool}\n") - + geo_node_tree_flags = ["is_mode_edit", + "is_mode_sculpt", + "is_modifier", + "is_tool", + "is_type_curve", + "is_type_mesh", + "is_type_point_cloud"] + for flag in geo_node_tree_flags: + self._write(f"{inner}{nt_var}.{flag} = {getattr(node_tree, flag)}\n") + self._write("\n") + #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") From e6ad11cc5a0df9af1a813966d0910ac4291d3550 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 16:10:04 -0600 Subject: [PATCH 50/72] feat: more robust variable name creation --- utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/utils.py b/utils.py index 68e6798..7a3af7d 100644 --- a/utils.py +++ b/utils.py @@ -2,10 +2,9 @@ import mathutils from enum import Enum, auto -import os +import keyword import re -import shutil -from typing import TextIO, Tuple +from typing import Tuple IMAGE_DIR_NAME = "imgs" @@ -63,13 +62,19 @@ def clean_string(string: str, lower: bool = True) -> str: string (str): The input string Returns: - clean_str: The input string with nasty characters converted to underscores + string (str): The input string ready to be used as a variable """ if lower: string = string.lower() - clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string) - return clean_str + string = re.sub(r"[^a-zA-Z0-9_]", '_', string) + + if keyword.iskeyword(string): + string = "_" + string + elif not (string[0].isalpha() or string[0] == '_'): + string = "_" + string + + return string def enum_to_py_str(enum: str) -> str: """ From ae630f6518558bce6657357c6721c50ee1400607 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 16:19:01 -0600 Subject: [PATCH 51/72] feat: only write geo node tool settings if tool --- geometry/operator.py | 25 ++++++++++++++++--------- utils.py | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/geometry/operator.py b/geometry/operator.py index a9f0db3..716214b 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -119,15 +119,22 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, self._write("\n") if bpy.app.version >= (4, 0, 0): - geo_node_tree_flags = ["is_mode_edit", - "is_mode_sculpt", - "is_modifier", - "is_tool", - "is_type_curve", - "is_type_mesh", - "is_type_point_cloud"] - for flag in geo_node_tree_flags: - self._write(f"{inner}{nt_var}.{flag} = {getattr(node_tree, flag)}\n") + is_mod = node_tree.is_modifier + is_tool = node_tree.is_tool + if is_mod: + self._write(f"{inner}{nt_var}.is_modifier = True\n") + if is_tool: + self._write(f"{inner}{nt_var}.is_tool = True\n") + + tool_flags = ["is_mode_edit", + "is_mode_sculpt", + "is_type_curve", + "is_type_mesh", + "is_type_point_cloud"] + + for flag in tool_flags: + self._write(f"{inner}{nt_var}.{flag} = " + f"{getattr(node_tree, flag)}\n") self._write("\n") #initialize nodes diff --git a/utils.py b/utils.py index 7a3af7d..1fbdf16 100644 --- a/utils.py +++ b/utils.py @@ -62,7 +62,7 @@ def clean_string(string: str, lower: bool = True) -> str: string (str): The input string Returns: - string (str): The input string ready to be used as a variable + string (str): The input string ready to be used as a variable/file """ if lower: From a75050ddaa0a9fc6ab55a1eee1d33854e197f500 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 16:39:57 -0600 Subject: [PATCH 52/72] feat: new shader node settings --- material/node_settings.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/material/node_settings.py b/material/node_settings.py index c6b339e..2aa1f34 100644 --- a/material/node_settings.py +++ b/material/node_settings.py @@ -16,21 +16,13 @@ 'ShaderNodeVertexColor' : [("layer_name", ST.STRING)], #TODO: separate color attribute type? 'ShaderNodeHairInfo' : [], - 'ShaderNodeFresnel' : [], - 'ShaderNodeNewGeometry' : [], - 'ShaderNodeLayerWeight' : [], - 'ShaderNodeLightPath' : [], - 'ShaderNodeObjectInfo' : [], - 'ShaderNodeParticleInfo' : [], - 'ShaderNodePointInfo' : [], - 'ShaderNodeRGB' : [], 'ShaderNodeTangent' : [("axis", ST.ENUM), @@ -46,7 +38,6 @@ ("uv_map", ST.STRING)], #TODO: see ShaderNodeTangent 'ShaderNodeValue' : [], - 'ShaderNodeVolumeInfo' : [], 'ShaderNodeWireframe' : [("use_pixel_size", ST.BOOL)], @@ -77,9 +68,7 @@ 'ShaderNodeBsdfAnisotropic' : [("distribution", ST.ENUM)], 'ShaderNodeBackground' : [], - 'ShaderNodeBsdfDiffuse' : [], - 'ShaderNodeEmission' : [], 'ShaderNodeBsdfGlass' : [("distribution", ST.ENUM)], @@ -89,18 +78,20 @@ 'ShaderNodeBsdfHair' : [("component", ST.ENUM)], 'ShaderNodeHoldout' : [], - 'ShaderNodeMixShader' : [], 'ShaderNodeBsdfPrincipled' : [("distribution", ST.ENUM), ("subsurface_method", ST.ENUM)], - 'ShaderNodeBsdfHairPrincipled' : [("parametrization", ST.ENUM)], + 'ShaderNodeBsdfHairPrincipled' : [("model", ST.ENUM), + ("parametrization", ST.ENUM)], 'ShaderNodeVolumePrincipled' : [], 'ShaderNodeBsdfRefraction' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfSheen' : [("distribution", ST.ENUM)], + 'ShaderNodeEeveeSpecular' : [], 'ShaderNodeSubsurfaceScattering' : [("falloff", ST.ENUM)], @@ -108,13 +99,9 @@ 'ShaderNodeBsdfToon' : [("component", ST.ENUM)], 'ShaderNodeBsdfTranslucent' : [], - 'ShaderNodeBsdfTransparent' : [], - 'ShaderNodeBsdfVelvet' : [], - 'ShaderNodeVolumeAbsorption' : [], - 'ShaderNodeVolumeScatter' : [], @@ -149,7 +136,8 @@ 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", ST.ENUM), ("musgrave_type", ST.ENUM)], - 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM)], + 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM), + ("normalize", ST.BOOL)], 'ShaderNodeTexPointDensity' : [("interpolation", ST.ENUM), ("object", ST.OBJECT), @@ -178,6 +166,7 @@ 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), ("feature", ST.ENUM), + ("normalize", ST.BOOL), ("voronoi_dimensions", ST.ENUM)], 'ShaderNodeTexWave' : [("bands_direction", ST.ENUM), From a3975dd794e07041f954dfc001bbb323fd80c2bc Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 17:12:23 -0600 Subject: [PATCH 53/72] feat: added settings for node frames and group outputs --- compositor/node_settings.py | 53 ++++++++++++++++++++++--------------- geometry/node_settings.py | 13 ++++++--- material/node_settings.py | 13 ++++++--- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/compositor/node_settings.py b/compositor/node_settings.py index 781eee6..982832a 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -29,28 +29,25 @@ 'CompositorNodeMovieClip' : [("clip", ST.MOVIE_CLIP)], - 'CompositorNodeRLayers' : [("layer", ST.ENUM), - ("scene", ST.SCENE)], + 'CompositorNodeTexture' : [("node_output", ST.INT), #TODO: ?? + ("texture", ST.TEXTURE)], + + # Input > Constant 'CompositorNodeRGB' : [], + 'CompositorNodeValue' : [], - 'CompositorNodeSceneTime' : [], + # Input > Scene - 'CompositorNodeTexture' : [("node_output", ST.INT), #TODO: ?? - ("texture", ST.TEXTURE)], + 'CompositorNodeRLayers' : [("layer", ST.ENUM), + ("scene", ST.SCENE)], + + 'CompositorNodeSceneTime' : [], 'CompositorNodeTime' : [("curve", ST.CURVE_MAPPING), ("frame_end", ST.INT), ("frame_start", ST.INT)], - 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), - ("frame_relative", ST.INT), - ("position", ST.ENUM), - ("track_name", ST.STRING), #TODO: probably not right - ("tracking_object", ST.STRING)], - - 'CompositorNodeValue' : [], - # OUTPUT 'CompositorNodeComposite' : [("use_alpha", ST.BOOL)], @@ -62,16 +59,14 @@ ("format", ST.IMAGE_FORMAT_SETTINGS), ("layer_slots", ST.LAYER_SLOTS)], - 'CompositorNodeLevels' : [("channel", ST.ENUM)], - - 'CompositorNodeSplitViewer' : [("axis", ST.ENUM), - ("factor", ST.INT)], - 'CompositorNodeViewer' : [("center_x", ST.FLOAT), ("center_y", ST.FLOAT), ("tile_order", ST.ENUM), ("use_alpha", ST.BOOL)], + 'CompositorNodeSplitViewer' : [("axis", ST.ENUM), + ("factor", ST.INT)], + # COLOR 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), @@ -423,14 +418,28 @@ 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), ("wrap_axis", ST.ENUM)], + # TRACKING + 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), + ("frame_relative", ST.INT), + ("position", ST.ENUM), + ("track_name", ST.STRING), #TODO: probably not right + ("tracking_object", ST.STRING)], + + # UTILITIES + 'CompositorNodeLevels' : [("channel", ST.ENUM)], # LAYOUT 'CompositorNodeSwitch' : [("check", ST.BOOL)], # MISC - 'NodeFrame' : [], - 'NodeGroupInput' : [], - 'NodeGroupOutput' : [], - 'NodeReroute' : [] + 'NodeFrame' : [("label_size", ST.INT), + ("shrink", ST.BOOL), + ("text", ST.TEXT)], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + + 'NodeReroute' : [] } diff --git a/geometry/node_settings.py b/geometry/node_settings.py index 7596821..02352ae 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -458,9 +458,14 @@ 'FunctionNodeQuaternionToRotation' : [], # MISC - 'NodeFrame' : [], - 'NodeGroupInput' : [], - 'NodeGroupOutput' : [], - 'NodeReroute' : [] + 'NodeFrame' : [("label_size", ST.INT), + ("shrink", ST.BOOL), + ("text", ST.TEXT)], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + + 'NodeReroute' : [] } \ No newline at end of file diff --git a/material/node_settings.py b/material/node_settings.py index 2aa1f34..0858187 100644 --- a/material/node_settings.py +++ b/material/node_settings.py @@ -263,8 +263,13 @@ ("use_auto_update", ST.BOOL)], # MISC - 'NodeFrame' : [], - 'NodeGroupInput' : [], - 'NodeGroupOutput' : [], - 'NodeReroute' : [] + 'NodeFrame' : [("label_size", ST.INT), + ("shrink", ST.BOOL), + ("text", ST.TEXT)], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + + 'NodeReroute' : [] } From 4135354a30c4224b16176d730804bd176d4eb24a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 6 Jan 2024 18:10:08 -0600 Subject: [PATCH 54/72] feat: added kuwahara, reorganized compositor node settings to align with blender v4 add menu --- compositor/node_settings.py | 368 +++++++++++++++++++----------------- 1 file changed, 195 insertions(+), 173 deletions(-) diff --git a/compositor/node_settings.py b/compositor/node_settings.py index 982832a..71fcf18 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -69,9 +69,23 @@ # COLOR - 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), - ("use_premultiply", ST.BOOL)], + 'CompositorNodePremulKey' : [("mapping", ST.ENUM)], + + 'CompositorNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + + 'CompositorNodeConvertColorSpace' : [("from_color_space", ST.ENUM), + ("to_color_space", ST.ENUM)], + + + 'CompositorNodeSetAlpha' : [("mode", ST.ENUM)], + + 'CompositorNodeInvert' : [("invert_alpha", ST.BOOL), + ("invert_rgb", ST.BOOL)], + 'CompositorNodeRGBToBW' : [], + + + # Color > Adjust 'CompositorNodeBrightContrast' : [("use_premultiply", ST.BOOL)], 'CompositorNodeColorBalance' : [("correction_method", ST.ENUM), @@ -115,22 +129,12 @@ ("midtones_end", ST.FLOAT)], 'CompositorNodeExposure' : [], - 'CompositorNodeGamma' : [], 'CompositorNodeHueCorrect' : [("mapping", ST.CURVE_MAPPING)], 'CompositorNodeHueSat' : [], - 'CompositorNodeInvert' : [("invert_alpha", ST.BOOL), - ("invert_rgb", ST.BOOL)], - - 'CompositorNodeMixRGB' : [("blend_type", ST.ENUM), - ("use_alpha", ST.BOOL), - ("use_clamp", ST.BOOL)], #TODO: what is update() method for? - - 'CompositorNodePosterize' : [], - 'CompositorNodeCurveRGB' : [("mapping", ST.CURVE_MAPPING)], 'CompositorNodeTonemap' : [("adaptation", ST.FLOAT), @@ -142,46 +146,73 @@ ("offset", ST.FLOAT), ("tonemap_type", ST.ENUM)], - 'CompositorNodeZcombine' : [("use_alpha", ST.BOOL), - ("use_antialias_z", ST.BOOL)], + + # Color > Mix + 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), + ("use_premultiply", ST.BOOL)], + 'CompositorNodeCombineColor' : [("mode", ST.ENUM), + ("ycc_mode", ST.ENUM)], - # CONVERTER - 'CompositorNodePremulKey' : [("mapping", ST.ENUM)], + 'CompositorNodeSeparateColor' : [("mode", ST.ENUM), + ("ycc_mode", ST.ENUM)], - 'CompositorNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + 'CompositorNodeMixRGB' : [("blend_type", ST.ENUM), + ("use_alpha", ST.BOOL), + ("use_clamp", ST.BOOL)], #TODO: what is update() method for? + + 'CompositorNodeZcombine' : [("use_alpha", ST.BOOL), + ("use_antialias_z", ST.BOOL)], - 'CompositorNodeConvertColorSpace' : [("from_color_space", ST.ENUM), - ("to_color_space", ST.ENUM)], - 'CompositorNodeCombineColor' : [("mode", ST.ENUM), - ("ycc_mode", ST.ENUM)], - 'CompositorNodeCombineXYZ' : [], + # FILTER + 'CompositorNodeAntiAliasing' : [("contrast_limit", ST.FLOAT), + ("corner_rounding", ST.FLOAT), + ("threshold", ST.FLOAT)], + + 'CompositorNodeDenoise' : [("prefilter", ST.ENUM), + ("use_hdr", ST.BOOL)], - 'CompositorNodeIDMask' : [("index", ST.INT), - ("use_antialiasing", ST.BOOL)], + 'CompositorNodeDespeckle' : [("threshold", ST.FLOAT), + ("threshold_neighbor", ST.FLOAT)], - 'CompositorNodeMath' : [("operation", ST.ENUM), - ("use_clamp", ST.BOOL)], - 'CompositorNodeRGBToBW' : [], + 'CompositorNodeDilateErode' : [("distance", ST.INT), + ("edge", ST.FLOAT), + ("falloff", ST.ENUM), + ("mode", ST.ENUM)], - 'CompositorNodeSeparateColor' : [("mode", ST.ENUM), - ("ycc_mode", ST.ENUM)], + 'CompositorNodeInpaint' : [("distance", ST.INT)], - 'CompositorNodeSeparateXYZ' : [], - 'CompositorNodeSetAlpha' : [("mode", ST.ENUM)], + 'CompositorNodeFilter' : [("filter_type", ST.ENUM)], - 'CompositorNodeSwitchView' : [], + 'CompositorNodeGlare' : [("angle_offset", ST.FLOAT), + ("color_modulation", ST.FLOAT), + ("fade", ST.FLOAT), + ("glare_type", ST.ENUM), + ("iterations", ST.INT), + ("mix", ST.FLOAT), + ("quality", ST.ENUM), + ("size", ST.INT), + ("streaks", ST.INT), + ("threshold", ST.FLOAT), + ("use_rotate_45", ST.BOOL)], + + 'CompositorNodeKuwahara' : [("eccentricity", ST.FLOAT), + ("sharpness", ST.FLOAT), + ("size", ST.INT), + ("uniformity", ST.INT), + ("variation", ST.ENUM)], + 'CompositorNodePixelate' : [], + 'CompositorNodePosterize' : [], - # FILTER - 'CompositorNodeAntiAliasing' : [("contrast_limit", ST.FLOAT), - ("corner_rounding", ST.FLOAT), - ("threshold", ST.FLOAT)], + 'CompositorNodeSunBeams' : [("ray_length", ST.FLOAT), + ("source", ST.VEC2)], + # Filter > Blur 'CompositorNodeBilateralblur' : [("iterations", ST.INT), ("sigma_color", ST.FLOAT), ("sigma_space", ST.FLOAT)], @@ -214,14 +245,6 @@ ("use_zbuffer", ST.BOOL), ("z_scale", ST.FLOAT)], - 'CompositorNodeDespeckle' : [("threshold", ST.FLOAT), - ("threshold_neighbor", ST.FLOAT)], - - 'CompositorNodeDilateErode' : [("distance", ST.INT), - ("edge", ST.FLOAT), - ("falloff", ST.ENUM), - ("mode", ST.ENUM)], - 'CompositorNodeDBlur' : [("angle", ST.FLOAT), ("center_x", ST.FLOAT), ("center_y", ST.FLOAT), @@ -229,93 +252,73 @@ ("iterations", ST.INT), ("spin", ST.FLOAT), ("zoom", ST.FLOAT)], - - 'CompositorNodeFilter' : [("filter_type", ST.ENUM)], - - 'CompositorNodeGlare' : [("angle_offset", ST.FLOAT), - ("color_modulation", ST.FLOAT), - ("fade", ST.FLOAT), - ("glare_type", ST.ENUM), - ("iterations", ST.INT), - ("mix", ST.FLOAT), - ("quality", ST.ENUM), - ("size", ST.INT), - ("streaks", ST.INT), - ("threshold", ST.FLOAT), - ("use_rotate_45", ST.BOOL)], - - 'CompositorNodeInpaint' : [("distance", ST.INT)], - - 'CompositorNodePixelate' : [], - - 'CompositorNodeSunBeams' : [("ray_length", ST.FLOAT), - ("source", ST.VEC2)], - + 'CompositorNodeVecBlur' : [("factor", ST.FLOAT), ("samples", ST.INT), ("speed_max", ST.INT), ("speed_min", ST.INT), ("use_curved", ST.BOOL)], - - # VECTOR - 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], - - 'CompositorNodeMapValue' : [("max", ST.VEC1), - ("min", ST.VEC1), - ("offset", ST.VEC1), - ("size", ST.VEC1), - ("use_max", ST.BOOL), - ("use_min", ST.BOOL)], - - 'CompositorNodeNormal' : [], - - 'CompositorNodeNormalize' : [], - - 'CompositorNodeCurveVec' : [("mapping", ST.CURVE_MAPPING)], - - - # MATTE - 'CompositorNodeBoxMask' : [("height", ST.FLOAT), - ("mask_type", ST.ENUM), - ("rotation", ST.FLOAT), - ("width", ST.FLOAT), - ("x", ST.FLOAT), - ("y", ST.FLOAT)], - - 'CompositorNodeChannelMatte' : [("color_space", ST.ENUM), - ("limit_channel", ST.ENUM), - ("limit_max", ST.FLOAT), - ("limit_method", ST.ENUM), - ("limit_min", ST.FLOAT), - ("matte_channel", ST.ENUM)], - - 'CompositorNodeChromaMatte' : [("gain", ST.FLOAT), - ("lift", ST.FLOAT), + + # KEYING + 'CompositorNodeChannelMatte' : [("color_space", ST.ENUM), + ("limit_channel", ST.ENUM), + ("limit_max", ST.FLOAT), + ("limit_method", ST.ENUM), + ("limit_min", ST.FLOAT), + ("matte_channel", ST.ENUM)], + + 'CompositorNodeChromaMatte' : [("gain", ST.FLOAT), + ("lift", ST.FLOAT), ("shadow_adjust", ST.FLOAT), - ("threshold", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeColorMatte' : [("color_hue", ST.FLOAT), - ("color_saturation", ST.FLOAT), - ("color_value", ST.FLOAT)], - - 'CompositorNodeColorSpill' : [("channel", ST.ENUM), - ("limit_channel", ST.ENUM), - ("limit_method", ST.ENUM), - ("ratio", ST.FLOAT), - ("unspill_blue", ST.FLOAT), - ("unspill_green", ST.FLOAT), - ("unspill_red", ST.FLOAT), - ("use_unspill", ST.BOOL)], - + ("threshold", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeColorMatte' : [("color_hue", ST.FLOAT), + ("color_saturation", ST.FLOAT), + ("color_value", ST.FLOAT)], + + 'CompositorNodeColorSpill' : [("channel", ST.ENUM), + ("limit_channel", ST.ENUM), + ("limit_method", ST.ENUM), + ("ratio", ST.FLOAT), + ("unspill_blue", ST.FLOAT), + ("unspill_green", ST.FLOAT), + ("unspill_red", ST.FLOAT), + ("use_unspill", ST.BOOL)], + + 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeDistanceMatte' : [("channel", ST.ENUM), + ("falloff", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeKeying' : [("blur_post", ST.INT), + ("blur_pre", ST.INT), + ("clip_black", ST.FLOAT), + ("clip_white", ST.FLOAT), + ("despill_balance", ST.FLOAT), + ("despill_factor", ST.FLOAT), + ("dilate_distance", ST.INT), + ("edge_kernel_radius", ST.INT), + ("edge_kernel_tolerance", ST.FLOAT), + ("feather_distance", ST.INT), + ("feather_falloff", ST.ENUM), + ("screen_balance", ST.FLOAT)], + + 'CompositorNodeKeyingScreen' : [("clip", ST.MOVIE_CLIP), + ("tracing_object", ST.STRING)], + + 'CompositorNodeLumaMatte' : [("limit_max", ST.FLOAT), + ("limit_min", ST.FLOAT)], + + # MASK 'CompositorNodeCryptomatteV2' : [("add", ST.COLOR), ("entries", ST.CRYPTOMATTE_ENTRIES), ("frame_duration", ST.INT), ("frame_offset", ST.INT), ("frame_start", ST.INT), - #("has_layers", ST.BOOL), #TODO: readonly? - #("has_views", ST.BOOL), #TODO: readonly? ("image", ST.IMAGE), ("layer", ST.ENUM), ("layer_name", ST.ENUM), @@ -331,15 +334,13 @@ ("matte_id", ST.STRING), ("remove", ST.COLOR)], - 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), - ("tolerance", ST.FLOAT)], - 'CompositorNodeDistanceMatte' : [("channel", ST.ENUM), - ("falloff", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeDoubleEdgeMask' : [("edge_mode", ST.ENUM), - ("inner_mode", ST.ENUM)], + 'CompositorNodeBoxMask' : [("height", ST.FLOAT), + ("mask_type", ST.ENUM), + ("rotation", ST.FLOAT), + ("width", ST.FLOAT), + ("x", ST.FLOAT), + ("y", ST.FLOAT)], 'CompositorNodeEllipseMask' : [("height", ST.FLOAT), ("mask_type", ST.ENUM), @@ -348,27 +349,49 @@ ("x", ST.FLOAT), ("y", ST.FLOAT)], - 'CompositorNodeKeying' : [("blur_post", ST.INT), - ("blur_pre", ST.INT), - ("clip_black", ST.FLOAT), - ("clip_white", ST.FLOAT), - ("despill_balance", ST.FLOAT), - ("despill_factor", ST.FLOAT), - ("dilate_distance", ST.INT), - ("edge_kernel_radius", ST.INT), - ("edge_kernel_tolerance", ST.FLOAT), - ("feather_distance", ST.INT), - ("feather_falloff", ST.ENUM), - ("screen_balance", ST.FLOAT)], - 'CompositorNodeKeyingScreen' : [("clip", ST.MOVIE_CLIP), - ("tracing_object", ST.STRING)], + 'CompositorNodeDoubleEdgeMask' : [("edge_mode", ST.ENUM), + ("inner_mode", ST.ENUM)], + + 'CompositorNodeIDMask' : [("index", ST.INT), + ("use_antialiasing", ST.BOOL)], + + + # TRACKING + 'CompositorNodePlaneTrackDeform' : [("clip", ST.MOVIE_CLIP), + ("motion_blur_samples", ST.INT), + ("motion_blur_shutter", ST.FLOAT), + ("plane_track_name", ST.STRING), + ("tracking_object", ST.STRING), + ("use_motion_blur", ST.BOOL)], + + 'CompositorNodeStabilize' : [("clip", ST.MOVIE_CLIP), + ("filter_type", ST.ENUM), + ("invert", ST.BOOL)], + + 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), + ("frame_relative", ST.INT), + ("position", ST.ENUM), + ("track_name", ST.STRING), #TODO: probably not right + ("tracking_object", ST.STRING)], + + - 'CompositorNodeLumaMatte' : [("limit_max", ST.FLOAT), - ("limit_min", ST.FLOAT)], + # TRANSFORM + 'CompositorNodeRotate' : [("filter_type", ST.ENUM)], + + 'CompositorNodeScale' : [("frame_method", ST.ENUM), + ("offset_x", ST.FLOAT), + ("offset_y", ST.FLOAT), + ("space", ST.ENUM)], + + + 'CompositorNodeTransform' : [("filter_type", ST.ENUM)], + + 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), + ("wrap_axis", ST.ENUM)], - # DISTORT 'CompositorNodeCornerPin' : [], 'CompositorNodeCrop' : [("max_x", ST.INT), @@ -382,54 +405,53 @@ ("relative", ST.BOOL), ("use_crop_size", ST.BOOL)], + 'CompositorNodeDisplace' : [], 'CompositorNodeFlip' : [("axis", ST.ENUM)], + 'CompositorNodeMapUV' : [("alpha", ST.INT)], + + 'CompositorNodeLensdist' : [("use_fit", ST.BOOL), ("use_jitter", ST.BOOL), ("use_projector", ST.BOOL)], - 'CompositorNodeMapUV' : [("alpha", ST.INT)], - 'CompositorNodeMovieDistortion' : [("clip", ST.MOVIE_CLIP), ("distortion_type", ST.ENUM)], - 'CompositorNodePlaneTrackDeform' : [("clip", ST.MOVIE_CLIP), - ("motion_blur_samples", ST.INT), - ("motion_blur_shutter", ST.FLOAT), - ("plane_track_name", ST.STRING), - ("tracking_object", ST.STRING), - ("use_motion_blur", ST.BOOL)], - 'CompositorNodeRotate' : [("filter_type", ST.ENUM)], - 'CompositorNodeScale' : [("frame_method", ST.ENUM), - ("offset_x", ST.FLOAT), - ("offset_y", ST.FLOAT), - ("space", ST.ENUM)], + #UTILITIES + 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], - 'CompositorNodeStabilize' : [("clip", ST.MOVIE_CLIP), - ("filter_type", ST.ENUM), - ("invert", ST.BOOL)], + 'CompositorNodeMapValue' : [("max", ST.VEC1), + ("min", ST.VEC1), + ("offset", ST.VEC1), + ("size", ST.VEC1), + ("use_max", ST.BOOL), + ("use_min", ST.BOOL)], - 'CompositorNodeTransform' : [("filter_type", ST.ENUM)], + 'CompositorNodeMath' : [("operation", ST.ENUM), + ("use_clamp", ST.BOOL)], + + + 'CompositorNodeLevels' : [("channel", ST.ENUM)], + + 'CompositorNodeNormalize' : [], - 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), - ("wrap_axis", ST.ENUM)], - # TRACKING - 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), - ("frame_relative", ST.INT), - ("position", ST.ENUM), - ("track_name", ST.STRING), #TODO: probably not right - ("tracking_object", ST.STRING)], + 'CompositorNodeSwitch' : [("check", ST.BOOL)], + + 'CompositorNodeSwitchView' : [], - # UTILITIES - 'CompositorNodeLevels' : [("channel", ST.ENUM)], - # LAYOUT - 'CompositorNodeSwitch' : [("check", ST.BOOL)], + # VECTOR + 'CompositorNodeCombineXYZ' : [], + 'CompositorNodeSeparateXYZ' : [], + 'CompositorNodeNormal' : [], + + 'CompositorNodeCurveVec' : [("mapping", ST.CURVE_MAPPING)], # MISC From 3287f2c45d2a681fbffa228db915d72b26320fbb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 7 Jan 2024 13:33:59 -0600 Subject: [PATCH 55/72] feat: repeat zones --- compositor/node_settings.py | 2 +- geometry/node_settings.py | 2 +- geometry/node_tree.py | 14 ++++- geometry/operator.py | 115 ++++++++++++++++++++++-------------- ntp_operator.py | 3 +- 5 files changed, 87 insertions(+), 49 deletions(-) diff --git a/compositor/node_settings.py b/compositor/node_settings.py index 71fcf18..f2780a5 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -422,7 +422,7 @@ - #UTILITIES + # UTILITIES 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], 'CompositorNodeMapValue' : [("max", ST.VEC1), diff --git a/geometry/node_settings.py b/geometry/node_settings.py index 02352ae..8714fea 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -367,7 +367,7 @@ 'FunctionNodeRandomValue' : [("data_type", ST.ENUM)], 'GeometryNodeRepeatInput' : [], - 'GeometryNodeRepeatOutput' : [], + 'GeometryNodeRepeatOutput' : [("inspection_index", ST.INT)], 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], diff --git a/geometry/node_tree.py b/geometry/node_tree.py index 64be218..73b3565 100644 --- a/geometry/node_tree.py +++ b/geometry/node_tree.py @@ -1,8 +1,18 @@ -from bpy.types import GeometryNodeTree, GeometryNodeSimulationInput +import bpy +from bpy.types import GeometryNodeTree + +if bpy.app.version >= (3, 6, 0): + from bpy.types import GeometryNodeSimulationInput + +if bpy.app.version > (4, 0, 0): + from bpy.types import GeometryNodeRepeatInput from ..ntp_node_tree import NTP_NodeTree class NTP_GeoNodeTree(NTP_NodeTree): def __init__(self, node_tree: GeometryNodeTree, var: str): super().__init__(node_tree, var) - self.sim_inputs: list[GeometryNodeSimulationInput] = [] + if bpy.app.version >= (3, 6, 0): + self.sim_inputs: list[GeometryNodeSimulationInput] = [] + if bpy.app.version >= (4, 0, 0): + self.repeat_inputs: list[GeometryNodeRepeatInput] = [] diff --git a/geometry/operator.py b/geometry/operator.py index 716214b..6adbc56 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -1,9 +1,14 @@ import bpy -from bpy.types import GeometryNodeSimulationInput -from bpy.types import GeometryNodeSimulationOutput -from bpy.types import GeometryNodeTree +from bpy.types import GeometryNode, GeometryNodeTree from bpy.types import Node +if bpy.app.version >= (3, 6, 0): + from bpy.types import GeometryNodeSimulationInput + from bpy.types import GeometryNodeSimulationOutput +if bpy.app.version >= (4, 0, 0): + from bpy.types import GeometryNodeRepeatInput + from bpy.types import GeometryNodeRepeatOutput + from io import StringIO from ..ntp_operator import NTP_Operator @@ -30,21 +35,34 @@ def __init__(self): super().__init__() self._settings = geo_node_settings - def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, - inner: str, node_var: str) -> None: - self._write(f"{inner}# Remove generated sim state items\n") - self._write(f"{inner}for item in {node_var}.state_items:\n") - self._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) - self._write(f"{inner}#create SSI {name}\n") - self._write((f"{inner}{node_var}.state_items.new" - f"({socket_type}, {name})\n")) - si_var = f"{node_var}.state_items[{i}]" - attr_domain = enum_to_py_str(si.attribute_domain) - self._write((f"{inner}{si_var}.attribute_domain = {attr_domain}\n")) + if bpy.app.version >= (3, 6, 0): + def _process_zone_output_node(self, node: GeometryNode, inner: str, + node_var: str) -> None: + is_sim = False + if node.bl_idname == 'GeometryNodeSimulationOutput': + items = "state_items" + is_sim = True + elif node.bl_idname == 'GeometryNodeRepeatOutput': + items = "repeat_items" + else: + self.report({'WARNING'}, f"{node.bl_idname} is not recognized " + f" as avalid zone output") + + self._write(f"{inner}# Remove generated {items}\n") + self._write(f"{inner}for item in {node_var}.{items}:\n") + self._write(f"{inner}\t{node_var}.{items}.remove(item)\n") + + for i, item in enumerate(getattr(node, items)): + socket_type = enum_to_py_str(item.socket_type) + name = str_to_py_str(item.name) + self._write(f"{inner}# Create item {name}\n") + self._write(f"{inner}{node_var}.{items}.new" + f"({socket_type}, {name})\n") + if is_sim: + item_var = f"{node_var}.{items}[{i}]" + attr_domain = enum_to_py_str(item.attribute_domain) + self._write((f"{inner}{item_var}.attribute_domain = " + f"{attr_domain}\n")) def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, inner: str, level: int) -> None: @@ -66,34 +84,42 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, ntp_node_tree.sim_inputs.append(node) elif node.bl_idname == 'GeometryNodeSimulationOutput': - self._process_sim_output_node(node, inner, node_var) + self._process_zone_output_node(node, inner, node_var) + + elif node.bl_idname == 'GeometryNodeRepeatInput': + ntp_node_tree.repeat_inputs.append(node) + + elif node.bl_idname == 'GeometryNodeRepeatOutput': + self._process_zone_output_node(node, inner, node_var) self._hide_hidden_sockets(node, inner, node_var) - if node.bl_idname != 'GeometryNodeSimulationInput': + if node.bl_idname not in {'GeometryNodeSimulationInput', 'GeometryNodeRepeatInput'}: self._set_socket_defaults(node, node_var, inner) - - def _process_sim_zones(self, sim_inputs: list[GeometryNodeSimulationInput], - inner: str) -> None: - """ - Recreate simulation zones - sim_inputs (list[GeometryNodeSimulationInput]): list of - simulation input nodes - inner (str): identation string - """ - for sim_input in sim_inputs: - sim_output = sim_input.paired_output - - sim_input_var = self._node_vars[sim_input] - sim_output_var = self._node_vars[sim_output] - self._write((f"{inner}{sim_input_var}.pair_with_output" - f"({sim_output_var})\n")) - - #must set defaults after paired with output - self._set_socket_defaults(sim_input, sim_input_var, inner) - self._set_socket_defaults(sim_output, sim_output_var, inner) - + if bpy.app.version >= (3, 6, 0): + def _process_zones(self, zone_inputs: list[GeometryNode], + inner: str) -> None: + """ + Recreates a zone + zone_inputs (list[GeometryNodeSimulationInput]): list of + simulation input nodes + inner (str): identation string + """ + for zone_input in zone_inputs: + zone_output = zone_input.paired_output + + zone_input_var = self._node_vars[zone_input] + zone_output_var = self._node_vars[zone_output] + + self._write(f"{inner}#Process zone input {zone_input.name}\n") + self._write((f"{inner}{zone_input_var}.pair_with_output" + f"({zone_output_var})\n")) + + #must set defaults after paired with output + self._set_socket_defaults(zone_input, zone_input_var, inner) + self._set_socket_defaults(zone_output, zone_output_var, inner) + self._write("\n") def _process_node_tree(self, node_tree: GeometryNodeTree, level: int) -> None: @@ -145,8 +171,11 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, for node in node_tree.nodes: self._process_node(node, ntp_nt, inner, level) - self._process_sim_zones(ntp_nt.sim_inputs, inner) - + if bpy.app.version >= (3, 6, 0): + self._process_zones(ntp_nt.sim_inputs, inner) + if bpy.app.version >= (4, 0, 0): + self._process_zones(ntp_nt.repeat_inputs, inner) + #set look of nodes self._set_parents(node_tree, inner) self._set_locations(node_tree, inner) diff --git a/ntp_operator.py b/ntp_operator.py index e7da207..1d1a50f 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -1069,8 +1069,7 @@ def _report_finished(self, object: str): location = "clipboard" else: location = self._dir - self.report({'INFO'}, - f"NodeToPython: Saved {object} to {location}") + self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}") # ABSTRACT def execute(self): From 5dece56195f22ba5ba0757cecec1b77c306222d6 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 7 Jan 2024 13:41:57 -0600 Subject: [PATCH 56/72] feat: replace print statements with Blender reporting --- geometry/operator.py | 4 ++-- ntp_operator.py | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/geometry/operator.py b/geometry/operator.py index 6adbc56..c0eccb0 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -45,8 +45,8 @@ def _process_zone_output_node(self, node: GeometryNode, inner: str, elif node.bl_idname == 'GeometryNodeRepeatOutput': items = "repeat_items" else: - self.report({'WARNING'}, f"{node.bl_idname} is not recognized " - f" as avalid zone output") + self.report({'WARNING'}, f"NodeToPython: {node.bl_idname} is " + f"not recognized as a valid zone output") self._write(f"{inner}# Remove generated {items}\n") self._write(f"{inner}for item in {node_var}.{items}:\n") diff --git a/ntp_operator.py b/ntp_operator.py index 1d1a50f..d73127f 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -224,15 +224,22 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname not in self._settings: - print((f"NodeToPython: couldn't find {node.bl_idname} in settings." - f"Your Blender version may not be supported")) + self.report({'WARNING'}, + (f"NodeToPython: couldn't find {node.bl_idname} in " + f"settings. Your Blender version may not be supported")) return for (attr_name, type) in self._settings[node.bl_idname]: + if not hasattr(node, attr_name): + self.report({'WARNING'}, + f"NodeToPython: Couldn't find attribute " + f"\"{attr_name}\" for node {node.name} of type " + f"{node.bl_idname}") + continue attr = getattr(node, attr_name, None) if attr is None: - print(f"\"{node_var}.{attr_name}\" not found") continue + setting_str = f"{inner}{node_var}.{attr_name}" if type == ST.ENUM: if attr != '': From 2528ec2f1e19db5f058becf744564773d5a3efcf Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 7 Jan 2024 13:51:28 -0600 Subject: [PATCH 57/72] feat: add domain attribute to set shade smooth geo node --- geometry/node_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/geometry/node_settings.py b/geometry/node_settings.py index 8714fea..a1e5011 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -229,7 +229,8 @@ # Mesh > Write 'GeometryNodeToolSetFaceSet' : [], - 'GeometryNodeSetShadeSmooth' : [], + + 'GeometryNodeSetShadeSmooth' : [("domain", ST.ENUM)], # Mesh > Operations 'GeometryNodeDualMesh' : [], From 8b5e4fe331e5082537c682c66877ab41c88771e1 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 13 Jan 2024 21:50:13 -0600 Subject: [PATCH 58/72] fix: added array_to_py_str function --- ntp_operator.py | 3 +++ utils.py | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/ntp_operator.py b/ntp_operator.py index d73127f..a25a7ea 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -1,6 +1,7 @@ import bpy from bpy.types import Context, Operator from bpy.types import Node, NodeTree +from bpy_types import bpy_types if bpy.app.version < (4, 0, 0): from bpy.types import NodeSocketInterface @@ -408,6 +409,8 @@ def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInter dv = vec4_to_py_str(dv) elif type(dv) in {mathutils.Vector, mathutils.Euler}: dv = vec3_to_py_str(dv) + elif type(dv) == bpy_types.bpy_prop_array: + dv = array_to_py_str(dv) elif type(dv) == str: dv = str_to_py_str(dv) self._write(f"{inner}{socket_var}.default_value = {dv}\n") diff --git a/utils.py b/utils.py index 1fbdf16..5f587ce 100644 --- a/utils.py +++ b/utils.py @@ -1,4 +1,5 @@ import bpy +from bpy_types import bpy_types import mathutils from enum import Enum, auto @@ -148,6 +149,24 @@ def vec4_to_py_str(vec4) -> str: """ return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})" +def array_to_py_str(array: bpy_types.bpy_prop_array) -> str: + """ + Converts a bpy_prop_array into a string + + Parameters: + array (bpy_prop_array): Blender Python array + + Returns: + (str): string version + """ + string = "(" + for i in range(0, array.__len__()): + if i > 0: + string += ", " + string += f"{array[i]}" + string += ")" + return string + def color_to_py_str(color: mathutils.Color) -> str: """ Converts a mathutils.Color into a string From 86f6c0f3488cdcade930f643759438667914bf1a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 13 Jan 2024 23:08:43 -0600 Subject: [PATCH 59/72] fix: group logic better accounts for dependencies --- compositor/operator.py | 69 ++++++++++---------- geometry/operator.py | 15 +++-- material/operator.py | 65 ++++++++++++------- ntp_operator.py | 139 +++++++++++++++++++++++++++-------------- 4 files changed, 178 insertions(+), 110 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index b521e77..7631244 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -52,31 +52,25 @@ def _create_scene(self, indent: str): self._write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") self._write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") - def _initialize_compositor_node_tree(self, outer, nt_var, level, inner, nt_name): - #initialize node group - self._write(f"{outer}#initialize {nt_var} node group\n") - self._write(f"{outer}def {nt_var}_node_group():\n") - - if self._is_outermost_node_group(level): #outermost node group - self._write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") + def _initialize_compositor_node_tree(self, outer, ntp_nt, nt_name): + #initialize node group + self._write(f"{outer}#initialize {nt_name} node group\n") + self._write(f"{outer}def {ntp_nt.var}_node_group():\n") + + inner = f"{outer}\t" + if ntp_nt.node_tree == self._base_node_tree: + self._write(f"{inner}{ntp_nt.var} = {SCENE_VAR}.node_tree\n") self._write(f"{inner}#start with a clean node tree\n") - self._write(f"{inner}for node in {nt_var}.nodes:\n") - self._write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + self._write(f"{inner}for node in {ntp_nt.var}.nodes:\n") + self._write(f"{inner}\t{ntp_nt.var}.nodes.remove(node)\n") else: - self._write((f"{inner}{nt_var}" + self._write((f"{inner}{ntp_nt.var}" f"= bpy.data.node_groups.new(" f"type = \'CompositorNodeTree\', " f"name = {str_to_py_str(nt_name)})\n")) self._write("\n") - def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str, level: int): - if node.bl_idname == 'CompositorNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in self._node_trees: - self._process_comp_node_group(node_nt, level + 1, self._node_vars, - self._used_vars) - self._node_trees.add(node_nt) - + def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str): node_var: str = self._create_node(node, inner, ntp_nt.var) if node.bl_idname == 'CompositorNodeColorBalance': @@ -98,17 +92,14 @@ def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str, level: int self._hide_hidden_sockets(node, inner, node_var) if node.bl_idname == 'CompositorNodeGroup': - if node.node_tree is not None: - self._write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: + self._process_group_node_tree(node, node_var, inner) + elif node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: self._group_io_settings(node, inner, "input", ntp_nt) - inputs_set = True + ntp_nt.inputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: self._group_io_settings(node, inner, "output", ntp_nt) - outputs_set = True + ntp_nt.outputs_set = True self._set_socket_defaults(node, node_var, inner) @@ -121,7 +112,7 @@ def _process_node_tree(self, node_tree, level): level (int): number of tabs to use for each line """ - if self._is_outermost_node_group(level): + if node_tree == self._base_node_tree: nt_var = self._create_var(self.compositor_name) nt_name = self.compositor_name else: @@ -130,15 +121,14 @@ def _process_node_tree(self, node_tree, level): outer, inner = make_indents(level) - self._initialize_compositor_node_tree(outer, nt_var, level, inner, nt_name) - ntp_nt = NTP_NodeTree(node_tree, nt_var) + self._initialize_compositor_node_tree(outer, ntp_nt, nt_name) #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) + self._process_node(node, ntp_nt, inner) self._set_parents(node_tree, inner) self._set_locations(node_tree, inner) @@ -146,16 +136,19 @@ def _process_node_tree(self, node_tree, level): self._init_links(node_tree, inner, nt_var) - self._write(f"\n{outer}{nt_var}_node_group()\n\n") + self._write(f"{inner}return {nt_var}\n") + + self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") + self._node_trees[node_tree] = nt_var def execute(self, context): #find node group to replicate if self.is_scene: - nt = bpy.data.scenes[self.compositor_name].node_tree + self._base_node_tree = bpy.data.scenes[self.compositor_name].node_tree else: - nt = bpy.data.node_groups[self.compositor_name] + self._base_node_tree = bpy.data.node_groups[self.compositor_name] - if nt is None: + if self._base_node_tree is None: #shouldn't happen self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " "valid compositor node tree. Is Use Nodes " @@ -187,8 +180,12 @@ def execute(self, context): if self.mode == 'ADDON': level = 2 else: - level = 0 - self._process_node_tree(nt, level) + level = 0 + + node_trees_to_process = self._topological_sort(self._base_node_tree) + + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree, level) if self.mode == 'ADDON': self._write("\t\treturn {'FINISHED'}\n\n") diff --git a/geometry/operator.py b/geometry/operator.py index c0eccb0..fcf0ab4 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -65,12 +65,12 @@ def _process_zone_output_node(self, node: GeometryNode, inner: str, f"{attr_domain}\n")) def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, - inner: str, level: int) -> None: + inner: str) -> None: #create node node_var: str = self._create_node(node, inner, ntp_node_tree.var) self._set_settings_defaults(node, inner, node_var) if node.bl_idname == 'GeometryNodeGroup': - self._process_group_node_tree(node, node_var, level, inner) + self._process_group_node_tree(node, node_var, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: self._group_io_settings(node, inner, "input", ntp_node_tree) @@ -169,7 +169,7 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) + self._process_node(node, ntp_nt, inner) if bpy.app.version >= (3, 6, 0): self._process_zones(ntp_nt.sim_inputs, inner) @@ -188,7 +188,8 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, #create node group self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") - return self._used_vars + + self._node_trees[node_tree] = nt_var def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): @@ -226,7 +227,11 @@ def execute(self, context): level = 2 else: level = 0 - self._process_node_tree(nt, level) + + node_trees_to_process = self._topological_sort(nt) + + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree, level) if self.mode == 'ADDON': self._apply_modifier(nt, nt_var) diff --git a/material/operator.py b/material/operator.py index cc84f24..4e914e5 100644 --- a/material/operator.py +++ b/material/operator.py @@ -28,29 +28,46 @@ def _create_material(self, indent: str): f"name = {str_to_py_str(self.material_name)})\n")) self._write(f"{indent}{MAT_VAR}.use_nodes = True\n") - def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): - #initialize node group - self._write(f"{outer}#initialize {nt_var} node group\n") - self._write(f"{outer}def {nt_var}_node_group():\n") + def _initialize_shader_node_tree(self, outer: str, ntp_node_tree: NTP_NodeTree, nt_name: str) -> None: + """ + Initialize the shader node group + + Parameters: + outer (str): indentation level + ntp_node_tree (NTP_NodeTree): node tree to be generated and + variable to use + nt_name (str): name to use for the node tree + """ + self._write(f"{outer}#initialize {nt_name} node group\n") + self._write(f"{outer}def {ntp_node_tree.var}_node_group():\n") - if self._is_outermost_node_group(level): - self._write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") + inner = f"{outer}\t" + if ntp_node_tree.node_tree == self._base_node_tree: + self._write(f"{inner}{ntp_node_tree.var} = {MAT_VAR}.node_tree\n") self._write(f"{inner}#start with a clean node tree\n") - self._write(f"{inner}for node in {nt_var}.nodes:\n") - self._write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + self._write(f"{inner}for node in {ntp_node_tree.var}.nodes:\n") + self._write(f"{inner}\t{ntp_node_tree.var}.nodes.remove(node)\n") else: - self._write((f"{inner}{nt_var} = bpy.data.node_groups.new(" + self._write((f"{inner}{ntp_node_tree.var} = bpy.data.node_groups.new(" f"type = \'ShaderNodeTree\', " f"name = {str_to_py_str(nt_name)})\n")) self._write("\n") - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, level: int) -> None: - #create node + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str) -> None: + """ + Creates a node and sets settings, inputs, outputs, and cosmetics + + Parameters: + node (Node): node to process + ntp_node_tree (NTP_NodeTree): node tree the node belongs to, and + variable to use + inner + """ node_var: str = self._create_node(node, inner, ntp_node_tree.var) self._set_settings_defaults(node, inner, node_var) if node.bl_idname == 'ShaderNodeGroup': - self._process_group_node_tree(node, node_var, level, inner) + self._process_group_node_tree(node, node_var, inner) elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: self._group_io_settings(node, inner, "input", ntp_node_tree) ntp_node_tree.inputs_set = True @@ -72,7 +89,7 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: node groups within node groups and script/add-on differences """ - if self._is_outermost_node_group(level): + if node_tree == self._base_node_tree: nt_var = self._create_var(self.material_name) nt_name = self.material_name #TODO: this is probably overcomplicating things if we move to a harder material vs shader node tree difference else: @@ -81,15 +98,15 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: outer, inner = make_indents(level) - self._initialize_shader_node_tree(outer, nt_var, level, inner, nt_name) - ntp_nt = NTP_NodeTree(node_tree, nt_var) + self._initialize_shader_node_tree(outer, ntp_nt, nt_name) + #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) + self._process_node(node, ntp_nt, inner) self._set_parents(node_tree, inner) self._set_locations(node_tree, inner) @@ -99,12 +116,15 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: self._write(f"{inner}return {nt_var}\n") - self._write(f"\n{outer}{nt_var}_node_group()\n\n") + self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") + + self._node_trees[node_tree] = nt_var + def execute(self, context): #find node group to replicate - nt = bpy.data.materials[self.material_name].node_tree - if nt is None: + self._base_node_tree = bpy.data.materials[self.material_name].node_tree + if self._base_node_tree is None: self.report({'ERROR'}, ("NodeToPython: This doesn't seem to be a " "valid material. Is Use Nodes selected?")) return {'CANCELLED'} @@ -130,12 +150,15 @@ def execute(self, context): elif self.mode == 'SCRIPT': self._create_material("") - if self.mode == 'ADDON': level = 2 else: level = 0 - self._process_node_tree(nt, level) + + node_trees_to_process = self._topological_sort(self._base_node_tree) + + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree, level) if self.mode == 'ADDON': self._write("\t\treturn {'FINISHED'}\n\n") diff --git a/ntp_operator.py b/ntp_operator.py index a25a7ea..1a6385a 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -30,7 +30,7 @@ class NTP_Operator(Operator): ] ) - #node tree input sockets that have default properties + # node tree input sockets that have default properties if bpy.app.version < (4, 0, 0): default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} else: @@ -62,8 +62,11 @@ def __init__(self): # Class named for the generated operator self._class_name: str = None - # Set to keep track of already created node trees - self._node_trees: set[NodeTree] = set() + # Base node tree we're converting + self._base_node_tree: NodeTree = None + + # Dictionary to keep track of node tree->variable name pairs + self._node_trees: dict[NodeTree, str] = {} # Dictionary to keep track of node->variable name pairs self._node_vars: dict[Node, str] = {} @@ -134,25 +137,61 @@ def _init_operator(self, idname: str, label: str) -> None: self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") self._write("\n") - def _is_outermost_node_group(self, level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False + def _topological_sort(self, node_tree: NodeTree) -> list[NodeTree]: + """ + Perform a topological sort on the node graph to determine dependencies + and which node groups need processed first + + Parameters: + node_tree (NodeTree): the base node tree to convert - def _process_group_node_tree(self, node: Node, node_var: str, level: int, - inner: str) -> None: + Returns: + (list[NodeTree]): the node trees in order of processing + """ + if isinstance(node_tree, bpy.types.CompositorNodeTree): + group_node_type = 'CompositorNodeGroup' + elif isinstance(node_tree, bpy.types.GeometryNodeTree): + group_node_type = 'GeometryNodeGroup' + elif isinstance(node_tree, bpy.types.ShaderNodeTree): + group_node_type = 'ShaderNodeGroup' + + visited = set() + result: list[NodeTree] = [] + + def dfs(nt: NodeTree) -> None: + """ + Helper function to perform depth-first search on a NodeTree + + Parameters: + nt (NodeTree): current node tree in the dependency graph + """ + if nt not in visited: + visited.add(nt) + for group_node in [node for node in nt.nodes + if node.bl_idname == group_node_type]: + if group_node.node_tree not in visited: + dfs(group_node.node_tree) + result.append(nt) + + dfs(node_tree) + + return result + + + def _process_group_node_tree(self, node: Node, node_var: str, inner: str + ) -> None: """ Processes node tree of group node if one is present """ node_tree = node.node_tree - if node_tree is not None: - if node_tree not in self._node_trees: - self._process_node_tree(node_tree, level + 1) - self._node_trees.add(node_tree) - self._write((f"{inner}{node_var}.node_tree = bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + if node_tree is None: + return + if node_tree in self._node_trees: + nt_var = self._node_trees[node_tree] + self._write((f"{inner}{node_var}.node_tree = {nt_var}\n")) + else: + self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " + f"wasn't properly initialized")) def _create_var(self, name: str) -> str: """ @@ -160,7 +199,8 @@ def _create_var(self, name: str) -> str: Parameters: name (str): basic string we'd like to create the variable name out of - used_vars (dict[str, int]): dictionary containing variable names and usage counts + used_vars (dict[str, int]): dictionary containing variable names and + usage counts Returns: clean_name (str): variable name for the node tree @@ -232,7 +272,7 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str for (attr_name, type) in self._settings[node.bl_idname]: if not hasattr(node, attr_name): - self.report({'WARNING'}, + self.report({'WARNING'}, f"NodeToPython: Couldn't find attribute " f"\"{attr_name}\" for node {node.name} of type " f"{node.bl_idname}") @@ -318,8 +358,8 @@ def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets @@ -353,7 +393,7 @@ def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - self._set_group_socket_default_v3(socket_interface, inner, + self._set_group_socket_default_v3(socket_interface, inner, socket_var) # default attribute name @@ -367,7 +407,8 @@ def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, # attribute domain if hasattr(socket_interface, "attribute_domain"): ad = enum_to_py_str(socket_interface.attribute_domain) - self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + self._write( + f"{inner}{socket_var}.attribute_domain = {ad}\n") # tooltip if socket_interface.description != "": @@ -387,7 +428,7 @@ def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, self._write("\n") self._write("\n") - + elif bpy.app.version >= (4, 0, 0): def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInterfaceSocket, inner: str, socket_var: str) -> None: @@ -425,8 +466,8 @@ def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInter self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets @@ -442,7 +483,7 @@ def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, node_tree = ntp_node_tree.node_tree if io == "input": - io_sockets = node.outputs # Might be removeable, + io_sockets = node.outputs # Might be removeable, # think we can get all the info from the inouts # from the socket interfaces, need to double check. # If so, then we can just run these at the initialization @@ -452,23 +493,23 @@ def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, # looks like those are tied fairly close to the new socket # system items_tree = node_tree.interface.items_tree - io_socket_interfaces = [item for item in items_tree - if item.item_type == 'SOCKET' + io_socket_interfaces = [item for item in items_tree + if item.item_type == 'SOCKET' and item.in_out == 'INPUT'] else: io_sockets = node.inputs items_tree = node_tree.interface.items_tree - io_socket_interfaces = [item for item in items_tree - if item.item_type == 'SOCKET' + io_socket_interfaces = [item for item in items_tree + if item.item_type == 'SOCKET' and item.in_out == 'OUTPUT'] self._write(f"{inner}#{node_tree_var} {io}s\n") for i, socket_interface in enumerate(io_socket_interfaces): self._write(f"{inner}#{io} {socket_interface.name}\n") - + socket_interface: bpy.types.NodeTreeInterfaceSocket = io_socket_interfaces[i] - #initialization + # initialization socket_var = clean_string(socket_interface.name) + "_socket" name = str_to_py_str(socket_interface.name) in_out_enum = enum_to_py_str(socket_interface.in_out) @@ -486,25 +527,26 @@ def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, socket_type = enum_to_py_str('NodeSocketInt') elif 'Vector' in socket_type: socket_type = enum_to_py_str('NodeSocketVector') - self._write(f"{inner}{socket_var} = " f"{node_tree_var}.interface.new_socket(" f"name = {name}, in_out={in_out_enum}, " f"socket_type = {socket_type})\n") - #subtype + # subtype if hasattr(socket_interface, "subtype"): subtype = enum_to_py_str(socket_interface.subtype) self._write(f"{inner}{socket_var}.subtype = {subtype}\n") - self._set_group_socket_default_v4(socket_interface, inner, + self._set_group_socket_default_v4(socket_interface, inner, socket_var) # default attribute name if socket_interface.default_attribute_name != "": - dan = str_to_py_str(socket_interface.default_attribute_name) - self._write((f"{inner}{socket_var}.default_attribute_name = {dan}\n")) + dan = str_to_py_str( + socket_interface.default_attribute_name) + self._write( + (f"{inner}{socket_var}.default_attribute_name = {dan}\n")) # attribute domain ad = enum_to_py_str(socket_interface.attribute_domain) @@ -522,18 +564,20 @@ def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, # hide in modifier if socket_interface.hide_in_modifier is True: - self._write(f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write( + f"{inner}{socket_var}.hide_in_modifier = True\n") - #force non field + # force non field if socket_interface.force_non_field is True: - self._write(f"{inner}{socket_var}.force_non_field = True\n") + self._write( + f"{inner}{socket_var}.force_non_field = True\n") self._write("\n") self._write("\n") - def _group_io_settings(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + def _group_io_settings(self, node: bpy.types.Node, inner: str, + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets @@ -578,10 +622,10 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, elif "Vector" in input.bl_idname: default_val = vec3_to_py_str(input.default_value) - #rotation types + # rotation types elif input.bl_idname == 'NodeSocketRotation': default_val = vec3_to_py_str(input.default_value) - + # strings elif input.bl_idname == 'NodeSocketString': default_val = str_to_py_str(input.default_value) @@ -1059,8 +1103,7 @@ def _zip_addon(self) -> None: shutil.rmtree(self._zip_dir) # ABSTRACT - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, - level: int) -> None: + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str) -> None: return # ABSTRACT From 630300ae7d33e18a94c29e665c1bf80d44fbf756 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 14 Jan 2024 21:58:44 -0600 Subject: [PATCH 60/72] feat: node panels --- geometry/operator.py | 3 + ntp_operator.py | 237 +++++++++++++++++++++++++------------------ 2 files changed, 139 insertions(+), 101 deletions(-) diff --git a/geometry/operator.py b/geometry/operator.py index fcf0ab4..f5b405d 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -168,6 +168,9 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) + if bpy.app.version >= (4, 0, 0): + self._tree_interface_settings_v4(inner, ntp_nt) + for node in node_tree.nodes: self._process_node(node, ntp_nt, inner) diff --git a/ntp_operator.py b/ntp_operator.py index 1a6385a..6ea64e9 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -5,6 +5,8 @@ if bpy.app.version < (4, 0, 0): from bpy.types import NodeSocketInterface +else: + from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceItem import os from typing import TextIO @@ -465,114 +467,149 @@ def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInter max_val = socket_interface.max_value self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) - def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + def _tree_interface_settings_v4(self, inner: str, + ntp_nt: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets Parameters: - node (bpy.types.Node) : group input/output node inner (str): indentation string - io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating - input and output settings for + ntp_nt (NTP_NodeTree): the node tree to set the interface for """ - node_tree_var = ntp_node_tree.var - node_tree = ntp_node_tree.node_tree - - if io == "input": - io_sockets = node.outputs # Might be removeable, - # think we can get all the info from the inouts - # from the socket interfaces, need to double check. - # If so, then we can just run these at the initialization - # of the node tree, meaning we can clean up the clunky - # Group Input/Group Output node reliance, two calls - # Should be pretty easy to add in panels afterwards, - # looks like those are tied fairly close to the new socket - # system - items_tree = node_tree.interface.items_tree - io_socket_interfaces = [item for item in items_tree - if item.item_type == 'SOCKET' - and item.in_out == 'INPUT'] - else: - io_sockets = node.inputs - items_tree = node_tree.interface.items_tree - io_socket_interfaces = [item for item in items_tree - if item.item_type == 'SOCKET' - and item.in_out == 'OUTPUT'] - - self._write(f"{inner}#{node_tree_var} {io}s\n") - for i, socket_interface in enumerate(io_socket_interfaces): - self._write(f"{inner}#{io} {socket_interface.name}\n") - - socket_interface: bpy.types.NodeTreeInterfaceSocket = io_socket_interfaces[i] - - # initialization - socket_var = clean_string(socket_interface.name) + "_socket" - name = str_to_py_str(socket_interface.name) - in_out_enum = enum_to_py_str(socket_interface.in_out) - - socket_type = enum_to_py_str(socket_interface.bl_socket_idname) - """ - I might be missing something, but the Python API's set up a bit - weird here now. The new socket initialization only accepts types - from a list of basic ones, but there doesn't seem to be a way of - retrieving just this basic typewithout the subtype information. - """ - if 'Float' in socket_type: - socket_type = enum_to_py_str('NodeSocketFloat') - elif 'Int' in socket_type: - socket_type = enum_to_py_str('NodeSocketInt') - elif 'Vector' in socket_type: - socket_type = enum_to_py_str('NodeSocketVector') - - self._write(f"{inner}{socket_var} = " - f"{node_tree_var}.interface.new_socket(" - f"name = {name}, in_out={in_out_enum}, " - f"socket_type = {socket_type})\n") - - # subtype - if hasattr(socket_interface, "subtype"): - subtype = enum_to_py_str(socket_interface.subtype) - self._write(f"{inner}{socket_var}.subtype = {subtype}\n") - - self._set_group_socket_default_v4(socket_interface, inner, - socket_var) - - # default attribute name - if socket_interface.default_attribute_name != "": - dan = str_to_py_str( - socket_interface.default_attribute_name) - self._write( - (f"{inner}{socket_var}.default_attribute_name = {dan}\n")) - # attribute domain - ad = enum_to_py_str(socket_interface.attribute_domain) - self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") - - # tooltip - if socket_interface.description != "": - description = str_to_py_str(socket_interface.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) - - # hide_value - if socket_interface.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") - - # hide in modifier - if socket_interface.hide_in_modifier is True: - self._write( - f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write(f"{inner}#{ntp_nt.var} interface\n") + panel_dict: dict[NodeTreeInterfacePanel, str] = {} + items_processed: set[NodeTreeInterfaceItem] = set() - # force non field - if socket_interface.force_non_field is True: - self._write( - f"{inner}{socket_var}.force_non_field = True\n") + def _process_items(parent: NodeTreeInterfacePanel): + if parent is None: + items = ntp_nt.node_tree.interface.items_tree + else: + items = parent.interface_items + + for item in items: + if item.parent.index != -1 and item.parent not in panel_dict: + continue # child of panel not processed yet + if item in items_processed: + continue + + items_processed.add(item) + + print(item.name, items_processed) + + if item.item_type == 'SOCKET': + self._write(f"{inner}#Socket {item.name}\n") + # initialization + socket_var = clean_string(item.name) + "_socket" + name = str_to_py_str(item.name) + in_out_enum = enum_to_py_str(item.in_out) + + socket_type = enum_to_py_str(item.bl_socket_idname) + """ + I might be missing something, but the Python API's set up a bit + weird here now. The new socket initialization only accepts types + from a list of basic ones, but there doesn't seem to be a way of + retrieving just this basic type without the subtype information. + """ + if 'Float' in socket_type: + socket_type = enum_to_py_str('NodeSocketFloat') + elif 'Int' in socket_type: + socket_type = enum_to_py_str('NodeSocketInt') + elif 'Vector' in socket_type: + socket_type = enum_to_py_str('NodeSocketVector') + + if parent is None: + optional_parent_str = "" + else: + optional_parent_str = f", parent = {panel_dict[parent]}" + + self._write(f"{inner}{socket_var} = " + f"{ntp_nt.var}.interface.new_socket(" + f"name = {name}, in_out={in_out_enum}, " + f"socket_type = {socket_type}" + f"{optional_parent_str})\n") + + # subtype + if hasattr(item, "subtype"): + subtype = enum_to_py_str(item.subtype) + self._write(f"{inner}{socket_var}.subtype = {subtype}\n") + + self._set_group_socket_default_v4(item, inner, + socket_var) + + # default attribute name + if item.default_attribute_name != "": + dan = str_to_py_str( + item.default_attribute_name) + self._write( + (f"{inner}{socket_var}.default_attribute_name = {dan}\n")) + + # attribute domain + ad = enum_to_py_str(item.attribute_domain) + self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + + # hide_value + if item.hide_value is True: + self._write(f"{inner}{socket_var}.hide_value = True\n") + + # hide in modifier + if item.hide_in_modifier is True: + self._write( + f"{inner}{socket_var}.hide_in_modifier = True\n") + + # force non field + if item.force_non_field is True: + self._write( + f"{inner}{socket_var}.force_non_field = True\n") + + # tooltip + if item.description != "": + description = str_to_py_str(item.description) + self._write( + (f"{inner}{socket_var}.description = {description}\n")) + + self._write("\n") + + elif item.item_type == 'PANEL': + + self._write(f"{inner}#Panel {item.name}\n") + + panel_var = clean_string(item.name) + "_panel" + panel_dict[item] = panel_var + + description_str = "" + if item.description != "": + description_str = f", description = {str_to_py_str(item.description)}" + + closed_str = "" + if item.default_closed is True: + closed_str = f", default_closed=True" + + parent_str = "" + if parent is not None: + parent_str = f", parent = {panel_dict[parent]}" + + + self._write(f"{inner}{panel_var} = " + f"{ntp_nt.var}.interface.new_panel(" + f"{str_to_py_str(item.name)}{description_str}" + f"{closed_str}{parent_str})\n") + + # tooltip + if item.description != "": + description = str_to_py_str(item.description) + self._write( + (f"{inner}{panel_var}.description = {description}\n")) + + panel_dict[item] = panel_var + + if len(item.interface_items) > 0: + _process_items(item) + + self._write("\n") + + _process_items(None) - self._write("\n") self._write("\n") def _group_io_settings(self, node: bpy.types.Node, inner: str, @@ -591,8 +628,6 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, """ if bpy.app.version < (4, 0, 0): self._group_io_settings_v3(node, inner, io, ntp_node_tree) - else: - self._group_io_settings_v4(node, inner, io, ntp_node_tree) def _set_input_defaults(self, node: bpy.types.Node, inner: str, node_var: str) -> None: From 43871dee35ebc6d57e537fef2058b5e5e902dbd8 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:26:13 -0600 Subject: [PATCH 61/72] refactor: cleanup --- compositor/operator.py | 17 ++- geometry/operator.py | 20 +-- material/operator.py | 21 +-- ntp_node_tree.py | 10 +- ntp_operator.py | 310 ++++++++++++++++++++--------------------- 5 files changed, 195 insertions(+), 183 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 7631244..1a03a96 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -93,13 +93,15 @@ def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str): if node.bl_idname == 'CompositorNodeGroup': self._process_group_node_tree(node, node_var, inner) - elif node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: - self._group_io_settings(node, inner, "input", ntp_nt) - ntp_nt.inputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: - self._group_io_settings(node, inner, "output", ntp_nt) - ntp_nt.outputs_set = True + if bpy.app.version < (4, 0, 0): + if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: + self._group_io_settings(node, inner, "input", ntp_nt) + ntp_nt.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: + self._group_io_settings(node, inner, "output", ntp_nt) + ntp_nt.outputs_set = True self._set_socket_defaults(node, node_var, inner) @@ -124,6 +126,9 @@ def _process_node_tree(self, node_tree, level): ntp_nt = NTP_NodeTree(node_tree, nt_var) self._initialize_compositor_node_tree(outer, ntp_nt, nt_name) + if bpy.app.version >= (4, 0, 0): + self._tree_interface_settings(inner, ntp_nt) + #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") diff --git a/geometry/operator.py b/geometry/operator.py index f5b405d..9698229 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -69,16 +69,18 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, #create node node_var: str = self._create_node(node, inner, ntp_node_tree.var) self._set_settings_defaults(node, inner, node_var) - if node.bl_idname == 'GeometryNodeGroup': - self._process_group_node_tree(node, node_var, inner) - elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) - ntp_node_tree.inputs_set = True + if bpy.app.version < (4, 0, 0): + if node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: + self._group_io_settings(node, inner, "input", ntp_node_tree) + ntp_node_tree.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: + self._group_io_settings(node, inner, "output", ntp_node_tree) + ntp_node_tree.outputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) - ntp_node_tree.outputs_set = True + if node.bl_idname == 'GeometryNodeGroup': + self._process_group_node_tree(node, node_var, inner) elif node.bl_idname == 'GeometryNodeSimulationInput': ntp_node_tree.sim_inputs.append(node) @@ -169,7 +171,7 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) if bpy.app.version >= (4, 0, 0): - self._tree_interface_settings_v4(inner, ntp_nt) + self._tree_interface_settings(inner, ntp_nt) for node in node_tree.nodes: self._process_node(node, ntp_nt, inner) diff --git a/material/operator.py b/material/operator.py index 4e914e5..72bd339 100644 --- a/material/operator.py +++ b/material/operator.py @@ -65,16 +65,18 @@ def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str) -> """ node_var: str = self._create_node(node, inner, ntp_node_tree.var) self._set_settings_defaults(node, inner, node_var) - + + if bpy.app.version < (4, 0, 0): + if node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: + self._group_io_settings(node, inner, "input", ntp_node_tree) + ntp_node_tree.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: + self._group_io_settings(node, inner, "output", ntp_node_tree) + ntp_node_tree.outputs_set = True + if node.bl_idname == 'ShaderNodeGroup': self._process_group_node_tree(node, node_var, inner) - elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) - ntp_node_tree.inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) - ntp_node_tree.outputs_set = True self._hide_hidden_sockets(node, inner, node_var) self._set_socket_defaults(node, node_var, inner) @@ -102,6 +104,9 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: self._initialize_shader_node_tree(outer, ntp_nt, nt_name) + if bpy.app.version >= (4, 0, 0): + self._tree_interface_settings(inner, ntp_nt) + #initialize nodes self._write(f"{inner}#initialize {nt_var} nodes\n") diff --git a/ntp_node_tree.py b/ntp_node_tree.py index 5469ba1..a551d01 100644 --- a/ntp_node_tree.py +++ b/ntp_node_tree.py @@ -1,4 +1,5 @@ from bpy.types import NodeTree +import bpy class NTP_NodeTree: def __init__(self, node_tree: NodeTree, var: str): @@ -8,7 +9,8 @@ def __init__(self, node_tree: NodeTree, var: str): # The variable named for the regenerated node tree self.var: str = var - # Keep track of if we need to set the default values for the node - # tree inputs and outputs - self.inputs_set: bool = False - self.outputs_set: bool = False \ No newline at end of file + if bpy.app.version < (4, 0, 0): + # Keep track of if we need to set the default values for the node + # tree inputs and outputs + self.inputs_set: bool = False + self.outputs_set: bool = False \ No newline at end of file diff --git a/ntp_operator.py b/ntp_operator.py index 6ea64e9..516dd85 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -6,7 +6,7 @@ if bpy.app.version < (4, 0, 0): from bpy.types import NodeSocketInterface else: - from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceItem + from bpy.types import NodeTreeInterfaceItem, NodeTreeInterfacePanel, NodeTreeInterfaceSocket import os from typing import TextIO @@ -328,10 +328,11 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str attr, inner, f"{node_var}.{attr_name}") if bpy.app.version < (4, 0, 0): - def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, + def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, inner: str, socket_var: str) -> None: """ Set a node group input/output's default properties if they exist + Helper function to _group_io_settings() Parameters: socket_interface (NodeSocketInterface): socket interface associated @@ -359,9 +360,9 @@ def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, max_val = socket_interface.max_value self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) - def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + def _group_io_settings(self, node: bpy.types.Node, inner: str, + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets @@ -395,7 +396,7 @@ def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - self._set_group_socket_default_v3(socket_interface, inner, + self._set_group_socket_defaults(socket_interface, inner, socket_var) # default attribute name @@ -432,10 +433,10 @@ def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, self._write("\n") elif bpy.app.version >= (4, 0, 0): - def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInterfaceSocket, - inner: str, socket_var: str) -> None: + def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, + inner: str, socket_var: str) -> None: """ - Set a node group input/output's default properties if they exist + Set a node tree input/output's default properties if they exist Parameters: socket_interface (NodeTreeInterfaceSocket): socket interface associated @@ -467,8 +468,149 @@ def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInter max_val = socket_interface.max_value self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) - def _tree_interface_settings_v4(self, inner: str, - ntp_nt: NTP_NodeTree) -> None: + def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, + parent: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel, str], + ntp_nt: NTP_NodeTree) -> None: + self._write(f"{inner}#Socket {socket.name}\n") + # initialization + socket_var = clean_string(socket.name) + "_socket" + name = str_to_py_str(socket.name) + in_out_enum = enum_to_py_str(socket.in_out) + + socket_type = enum_to_py_str(socket.bl_socket_idname) + """ + I might be missing something, but the Python API's set up a bit + weird here now. The new socket initialization only accepts types + from a list of basic ones, but there doesn't seem to be a way of + retrieving just this basic type without the subtype information. + """ + if 'Float' in socket_type: + socket_type = enum_to_py_str('NodeSocketFloat') + elif 'Int' in socket_type: + socket_type = enum_to_py_str('NodeSocketInt') + elif 'Vector' in socket_type: + socket_type = enum_to_py_str('NodeSocketVector') + + if parent is None: + optional_parent_str = "" + else: + optional_parent_str = f", parent = {panel_dict[parent]}" + + self._write(f"{inner}{socket_var} = " + f"{ntp_nt.var}.interface.new_socket(" + f"name = {name}, in_out={in_out_enum}, " + f"socket_type = {socket_type}" + f"{optional_parent_str})\n") + + # subtype + if hasattr(socket, "subtype"): + subtype = enum_to_py_str(socket.subtype) + self._write(f"{inner}{socket_var}.subtype = {subtype}\n") + + self._set_tree_socket_defaults(socket, inner, socket_var) + + # default attribute name + if socket.default_attribute_name != "": + dan = str_to_py_str( + socket.default_attribute_name) + self._write( + (f"{inner}{socket_var}.default_attribute_name = {dan}\n")) + + # attribute domain + ad = enum_to_py_str(socket.attribute_domain) + self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + + # hide_value + if socket.hide_value is True: + self._write(f"{inner}{socket_var}.hide_value = True\n") + + # hide in modifier + if socket.hide_in_modifier is True: + self._write( + f"{inner}{socket_var}.hide_in_modifier = True\n") + + # force non field + if socket.force_non_field is True: + self._write( + f"{inner}{socket_var}.force_non_field = True\n") + + # tooltip + if socket.description != "": + description = str_to_py_str(socket.description) + self._write( + (f"{inner}{socket_var}.description = {description}\n")) + + self._write("\n") + + def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel], + items_processed: set[NodeTreeInterfacePanel], + parent: NodeTreeInterfacePanel, ntp_nt: NTP_NodeTree): + self._write(f"{inner}#Panel {panel.name}\n") + + panel_var = clean_string(panel.name) + "_panel" + panel_dict[panel] = panel_var + + description_str = "" + if panel.description != "": + description_str = f", description = {str_to_py_str(panel.description)}" + + closed_str = "" + if panel.default_closed is True: + closed_str = f", default_closed=True" + + parent_str = "" + if parent is not None: + parent_str = f", parent = {panel_dict[parent]}" + + + self._write(f"{inner}{panel_var} = " + f"{ntp_nt.var}.interface.new_panel(" + f"{str_to_py_str(panel.name)}{description_str}" + f"{closed_str}{parent_str})\n") + + # tooltip + if panel.description != "": + description = str_to_py_str(panel.description) + self._write( + (f"{inner}{panel_var}.description = {description}\n")) + + panel_dict[panel] = panel_var + + if len(panel.interface_items) > 0: + self._process_items(inner, panel, panel_dict, items_processed, ntp_nt) + + self._write("\n") + + def _process_items(self, inner: str, parent: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel], + items_processed: set[NodeTreeInterfacePanel], + ntp_nt: NTP_NodeTree) -> None: + if parent is None: + items = ntp_nt.node_tree.interface.items_tree + else: + items = parent.interface_items + + for item in items: + if item.parent.index != -1 and item.parent not in panel_dict: + continue # child of panel not processed yet + if item in items_processed: + continue + + items_processed.add(item) + + print(item.name, items_processed) + + if item.item_type == 'SOCKET': + self._create_socket(inner, item, parent, panel_dict, ntp_nt) + + elif item.item_type == 'PANEL': + self._create_panel(inner, item, panel_dict, items_processed, + parent, ntp_nt) + + def _tree_interface_settings(self, inner: str, ntp_nt: NTP_NodeTree + ) -> None: """ Set the settings for group input and output sockets @@ -481,154 +623,10 @@ def _tree_interface_settings_v4(self, inner: str, panel_dict: dict[NodeTreeInterfacePanel, str] = {} items_processed: set[NodeTreeInterfaceItem] = set() - def _process_items(parent: NodeTreeInterfacePanel): - if parent is None: - items = ntp_nt.node_tree.interface.items_tree - else: - items = parent.interface_items - - for item in items: - if item.parent.index != -1 and item.parent not in panel_dict: - continue # child of panel not processed yet - if item in items_processed: - continue - - items_processed.add(item) - - print(item.name, items_processed) - - if item.item_type == 'SOCKET': - self._write(f"{inner}#Socket {item.name}\n") - # initialization - socket_var = clean_string(item.name) + "_socket" - name = str_to_py_str(item.name) - in_out_enum = enum_to_py_str(item.in_out) - - socket_type = enum_to_py_str(item.bl_socket_idname) - """ - I might be missing something, but the Python API's set up a bit - weird here now. The new socket initialization only accepts types - from a list of basic ones, but there doesn't seem to be a way of - retrieving just this basic type without the subtype information. - """ - if 'Float' in socket_type: - socket_type = enum_to_py_str('NodeSocketFloat') - elif 'Int' in socket_type: - socket_type = enum_to_py_str('NodeSocketInt') - elif 'Vector' in socket_type: - socket_type = enum_to_py_str('NodeSocketVector') - - if parent is None: - optional_parent_str = "" - else: - optional_parent_str = f", parent = {panel_dict[parent]}" - - self._write(f"{inner}{socket_var} = " - f"{ntp_nt.var}.interface.new_socket(" - f"name = {name}, in_out={in_out_enum}, " - f"socket_type = {socket_type}" - f"{optional_parent_str})\n") - - # subtype - if hasattr(item, "subtype"): - subtype = enum_to_py_str(item.subtype) - self._write(f"{inner}{socket_var}.subtype = {subtype}\n") - - self._set_group_socket_default_v4(item, inner, - socket_var) - - # default attribute name - if item.default_attribute_name != "": - dan = str_to_py_str( - item.default_attribute_name) - self._write( - (f"{inner}{socket_var}.default_attribute_name = {dan}\n")) - - # attribute domain - ad = enum_to_py_str(item.attribute_domain) - self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") - - # hide_value - if item.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") - - # hide in modifier - if item.hide_in_modifier is True: - self._write( - f"{inner}{socket_var}.hide_in_modifier = True\n") - - # force non field - if item.force_non_field is True: - self._write( - f"{inner}{socket_var}.force_non_field = True\n") - - # tooltip - if item.description != "": - description = str_to_py_str(item.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) - - self._write("\n") - - elif item.item_type == 'PANEL': - - self._write(f"{inner}#Panel {item.name}\n") - - panel_var = clean_string(item.name) + "_panel" - panel_dict[item] = panel_var - - description_str = "" - if item.description != "": - description_str = f", description = {str_to_py_str(item.description)}" - - closed_str = "" - if item.default_closed is True: - closed_str = f", default_closed=True" - - parent_str = "" - if parent is not None: - parent_str = f", parent = {panel_dict[parent]}" - - - self._write(f"{inner}{panel_var} = " - f"{ntp_nt.var}.interface.new_panel(" - f"{str_to_py_str(item.name)}{description_str}" - f"{closed_str}{parent_str})\n") - - # tooltip - if item.description != "": - description = str_to_py_str(item.description) - self._write( - (f"{inner}{panel_var}.description = {description}\n")) - - panel_dict[item] = panel_var - - if len(item.interface_items) > 0: - _process_items(item) - - self._write("\n") - - _process_items(None) + self._process_items(inner, None, panel_dict, items_processed, ntp_nt) self._write("\n") - def _group_io_settings(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: - """ - Set the settings for group input and output sockets - - Parameters: - node (bpy.types.Node) : group input/output node - inner (str): indentation string - io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating - input and output settings for - """ - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, io, ntp_node_tree) - def _set_input_defaults(self, node: bpy.types.Node, inner: str, node_var: str) -> None: """ From 20009675c368d79293b67a15231e455e61cc9616 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:32:44 -0600 Subject: [PATCH 62/72] fix: use variable creation function for nt interface item vars --- ntp_operator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ntp_operator.py b/ntp_operator.py index 516dd85..1875a56 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -474,7 +474,7 @@ def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, ntp_nt: NTP_NodeTree) -> None: self._write(f"{inner}#Socket {socket.name}\n") # initialization - socket_var = clean_string(socket.name) + "_socket" + socket_var = self._create_var(socket.name + "_socket") name = str_to_py_str(socket.name) in_out_enum = enum_to_py_str(socket.in_out) @@ -549,7 +549,7 @@ def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, parent: NodeTreeInterfacePanel, ntp_nt: NTP_NodeTree): self._write(f"{inner}#Panel {panel.name}\n") - panel_var = clean_string(panel.name) + "_panel" + panel_var = self._create_var(panel.name + "_panel") panel_dict[panel] = panel_var description_str = "" From f0467dde7b3e240cd9f155c68d2c37eb852008aa Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 14 Jan 2024 22:44:28 -0600 Subject: [PATCH 63/72] doc: added comments to new tree interface functions --- ntp_operator.py | 57 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/ntp_operator.py b/ntp_operator.py index 1875a56..8f33e3e 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -6,10 +6,12 @@ if bpy.app.version < (4, 0, 0): from bpy.types import NodeSocketInterface else: - from bpy.types import NodeTreeInterfaceItem, NodeTreeInterfacePanel, NodeTreeInterfaceSocket + from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceSocket + from bpy.types import NodeTreeInterfaceItem import os from typing import TextIO +import shutil from .ntp_node_tree import NTP_NodeTree from .utils import * @@ -179,11 +181,15 @@ def dfs(nt: NodeTree) -> None: return result - def _process_group_node_tree(self, node: Node, node_var: str, inner: str ) -> None: """ Processes node tree of group node if one is present + + Parameters: + node (Node): the group node + node_var (str): variable for the group node + inner (str): indentation """ node_tree = node.node_tree if node_tree is None: @@ -438,6 +444,8 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, """ Set a node tree input/output's default properties if they exist + Helper function to _create_socket() + Parameters: socket_interface (NodeTreeInterfaceSocket): socket interface associated with the input/output @@ -472,6 +480,20 @@ def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, parent: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel, str], ntp_nt: NTP_NodeTree) -> None: + """ + Initialize a new tree socket + + Helper function to _process_items() + + Parameters: + inner (str): indentation string + socket (NodeTreeInterfaceSocket): the socket to recreate + parent (NodeTreeInterfacePanel): parent panel of the socket + (possibly None) + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + ntp_nt (NTP_NodeTree): owner of the socket + """ + self._write(f"{inner}#Socket {socket.name}\n") # initialization socket_var = self._create_var(socket.name + "_socket") @@ -547,6 +569,22 @@ def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel], items_processed: set[NodeTreeInterfacePanel], parent: NodeTreeInterfacePanel, ntp_nt: NTP_NodeTree): + """ + Initialize a new tree panel and its subitems + + Helper function to _process_items() + + Parameters: + inner (str): indentation string + panel (NodeTreeInterfacePanel): the panel to recreate + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + items_processed (set[NodeTreeInterfacePanel]): set of already + processed items, so none are done twice + parent (NodeTreeInterfacePanel): parent panel of the socket + (possibly None) + ntp_nt (NTP_NodeTree): owner of the socket + """ + self._write(f"{inner}#Panel {panel.name}\n") panel_var = self._create_var(panel.name + "_panel") @@ -587,6 +625,21 @@ def _process_items(self, inner: str, parent: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel], items_processed: set[NodeTreeInterfacePanel], ntp_nt: NTP_NodeTree) -> None: + """ + Recursive function to process all node tree interface items in a + given layer + + Helper function to _tree_interface_settings() + + Parameters: + inner (str): indentation string + parent (NodeTreeInterfacePanel): parent panel of the layer + (possibly None to signify the base) + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + items_processed (set[NodeTreeInterfacePanel]): set of already + processed items, so none are done twice + ntp_nt (NTP_NodeTree): owner of the socket + """ if parent is None: items = ntp_nt.node_tree.interface.items_tree else: From 9da1d108e79267ed877e88a8dca787a1ff51c390 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 21 Jan 2024 15:59:47 -0600 Subject: [PATCH 64/72] refactor: moved indent logic to write function, cleaned up functions --- compositor/operator.py | 165 ++++++------ geometry/operator.py | 186 +++++++------- material/operator.py | 101 ++++---- ntp_operator.py | 567 +++++++++++++++++++---------------------- utils.py | 22 +- 5 files changed, 489 insertions(+), 552 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 1a03a96..0ce02e6 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -1,6 +1,6 @@ import bpy -from bpy.types import Node +from bpy.types import Node, CompositorNodeColorBalance, CompositorNodeTree from ..ntp_operator import NTP_Operator from ..ntp_node_tree import NTP_NodeTree @@ -37,82 +37,102 @@ def __init__(self): def _create_scene(self, indent: str): #TODO: wrap in more general unique name util function - self._write(f"{indent}# Generate unique scene name\n") - self._write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") - self._write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") - self._write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - self._write(f"{indent}\ti = 1\n") - self._write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - self._write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - self._write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - self._write(f"{indent}\t\ti += 1\n\n") - - self._write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") - self._write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") - self._write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") - self._write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") - - def _initialize_compositor_node_tree(self, outer, ntp_nt, nt_name): + self._write(f"# Generate unique scene name", indent) + self._write(f"{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}", + indent) + self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR}", indent) + self._write(f"if bpy.data.scenes.get({END_NAME_VAR}) != None:", indent) + + indent2 = f"{indent}\t" + self._write(f"i = 1", indent2) + self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"", + indent2) + self._write(f"while bpy.data.scenes.get({END_NAME_VAR}) != None:", + indent2) + + indent3 = f"{indent}\t\t" + self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"", indent3) + self._write(f"i += 1\n", indent3) + + self._write(f"{SCENE_VAR} = bpy.context.window.scene.copy()\n", indent) + self._write(f"{SCENE_VAR}.name = {END_NAME_VAR}", indent) + self._write(f"{SCENE_VAR}.use_fake_user = True", indent) + self._write(f"bpy.context.window.scene = {SCENE_VAR}", indent) + + def _initialize_compositor_node_tree(self, ntp_nt, nt_name): #initialize node group - self._write(f"{outer}#initialize {nt_name} node group\n") - self._write(f"{outer}def {ntp_nt.var}_node_group():\n") + self._write(f"#initialize {nt_name} node group", self._outer) + self._write(f"def {ntp_nt.var}_node_group():", self._outer) - inner = f"{outer}\t" if ntp_nt.node_tree == self._base_node_tree: - self._write(f"{inner}{ntp_nt.var} = {SCENE_VAR}.node_tree\n") - self._write(f"{inner}#start with a clean node tree\n") - self._write(f"{inner}for node in {ntp_nt.var}.nodes:\n") - self._write(f"{inner}\t{ntp_nt.var}.nodes.remove(node)\n") + self._write(f"{ntp_nt.var} = {SCENE_VAR}.node_tree") + self._write(f"#start with a clean node tree") + self._write(f"for node in {ntp_nt.var}.nodes:") + self._write(f"\t{ntp_nt.var}.nodes.remove(node)") else: - self._write((f"{inner}{ntp_nt.var}" - f"= bpy.data.node_groups.new(" + self._write((f"{ntp_nt.var} = bpy.data.node_groups.new(" f"type = \'CompositorNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - self._write("\n") + f"name = {str_to_py_str(nt_name)})")) + self._write("") + + def _set_color_balance_settings(self, node: CompositorNodeColorBalance + ) -> None: + """ + Sets the color balance settings so we only set the active variables, + preventing conflict + + node (CompositorNodeColorBalance): the color balance node + """ + if node.correction_method == 'LIFT_GAMMA_GAIN': + lst = [("correction_method", ST.ENUM), + ("gain", ST.COLOR), + ("gamma", ST.COLOR), + ("lift", ST.COLOR)] + else: + lst = [("correction_method", ST.ENUM), + ("offset", ST.COLOR), + ("offset_basis", ST.FLOAT), + ("power", ST.COLOR), + ("slope", ST.COLOR)] + + self._settings['CompositorNodeColorBalance'] = lst - def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str): - node_var: str = self._create_node(node, inner, ntp_nt.var) + def _process_node(self, node: Node, ntp_nt: NTP_NodeTree): + """ + Create node and set settings, defaults, and cosmetics + + Parameters: + node (Node): node to process + ntp_nt (NTP_NodeTree): the node tree that node belongs to + """ + node_var: str = self._create_node(node, ntp_nt.var) if node.bl_idname == 'CompositorNodeColorBalance': - if node.correction_method == 'LIFT_GAMMA_GAIN': - lst = [("correction_method", ST.ENUM), - ("gain", ST.COLOR), - ("gamma", ST.COLOR), - ("lift", ST.COLOR)] - else: - lst = [("correction_method", ST.ENUM), - ("offset", ST.COLOR), - ("offset_basis", ST.FLOAT), - ("power", ST.COLOR), - ("slope", ST.COLOR)] - - self._settings['CompositorNodeColorBalance'] = lst - - self._set_settings_defaults(node, inner, node_var) - self._hide_hidden_sockets(node, inner, node_var) + self._set_color_balance_settings(node) + + self._set_settings_defaults(node) + self._hide_hidden_sockets(node) if node.bl_idname == 'CompositorNodeGroup': - self._process_group_node_tree(node, node_var, inner) + self._process_group_node_tree(node) if bpy.app.version < (4, 0, 0): if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: - self._group_io_settings(node, inner, "input", ntp_nt) + self._group_io_settings(node, "input", ntp_nt) ntp_nt.inputs_set = True elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: - self._group_io_settings(node, inner, "output", ntp_nt) + self._group_io_settings(node, "output", ntp_nt) ntp_nt.outputs_set = True - self._set_socket_defaults(node, node_var, inner) + self._set_socket_defaults(node) - def _process_node_tree(self, node_tree, level): + def _process_node_tree(self, node_tree: CompositorNodeTree): """ Generates a Python function to recreate a compositor node tree Parameters: - node_tree (NodeTree): node tree to be recreated - level (int): number of tabs to use for each line - + node_tree (CompositorNodeTree): node tree to be recreated """ if node_tree == self._base_node_tree: nt_var = self._create_var(self.compositor_name) @@ -121,30 +141,29 @@ def _process_node_tree(self, node_tree, level): nt_var = self._create_var(node_tree.name) nt_name = node_tree.name - outer, inner = make_indents(level) + self._node_tree_vars[node_tree] = nt_var ntp_nt = NTP_NodeTree(node_tree, nt_var) - self._initialize_compositor_node_tree(outer, ntp_nt, nt_name) + self._initialize_compositor_node_tree(ntp_nt, nt_name) if bpy.app.version >= (4, 0, 0): - self._tree_interface_settings(inner, ntp_nt) + self._tree_interface_settings(ntp_nt) #initialize nodes - self._write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"#initialize {nt_var} nodes") for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner) + self._process_node(node, ntp_nt) - self._set_parents(node_tree, inner) - self._set_locations(node_tree, inner) - self._set_dimensions(node_tree, inner) + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) - self._init_links(node_tree, inner, nt_var) + self._init_links(node_tree) - self._write(f"{inner}return {nt_var}\n") + self._write(f"return {nt_var}\n") - self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") - self._node_trees[node_tree] = nt_var + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def execute(self, context): #find node group to replicate @@ -164,6 +183,9 @@ def execute(self, context): comp_var = clean_string(self.compositor_name) if self.mode == 'ADDON': + self._outer = "\t\t" + self._inner = "\t\t\t" + self._setup_addon_directories(context, comp_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") @@ -172,7 +194,7 @@ def execute(self, context): self._class_name = clean_string(self.compositor_name, lower=False) self._init_operator(comp_var, self.compositor_name) - self._write("\tdef execute(self, context):\n") + self._write("def execute(self, context):", "\t") else: self._file = StringIO("") @@ -181,19 +203,14 @@ def execute(self, context): self._create_scene("\t\t") elif self.mode == 'SCRIPT': self._create_scene("") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 node_trees_to_process = self._topological_sort(self._base_node_tree) for node_tree in node_trees_to_process: - self._process_node_tree(node_tree, level) + self._process_node_tree(node_tree) if self.mode == 'ADDON': - self._write("\t\treturn {'FINISHED'}\n\n") + self._write("return {'FINISHED'}\n", self._outer) self._create_menu_func() self._create_register_func() diff --git a/geometry/operator.py b/geometry/operator.py index 9698229..3a1fc95 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -36,8 +36,7 @@ def __init__(self): self._settings = geo_node_settings if bpy.app.version >= (3, 6, 0): - def _process_zone_output_node(self, node: GeometryNode, inner: str, - node_var: str) -> None: + def _process_zone_output_node(self, node: GeometryNode) -> None: is_sim = False if node.bl_idname == 'GeometryNodeSimulationOutput': items = "state_items" @@ -48,65 +47,69 @@ def _process_zone_output_node(self, node: GeometryNode, inner: str, self.report({'WARNING'}, f"NodeToPython: {node.bl_idname} is " f"not recognized as a valid zone output") - self._write(f"{inner}# Remove generated {items}\n") - self._write(f"{inner}for item in {node_var}.{items}:\n") - self._write(f"{inner}\t{node_var}.{items}.remove(item)\n") + node_var = self._node_vars[node] + + self._write(f"# Remove generated {items}") + self._write(f"for item in {node_var}.{items}:") + self._write(f"\t{node_var}.{items}.remove(item)") for i, item in enumerate(getattr(node, items)): socket_type = enum_to_py_str(item.socket_type) name = str_to_py_str(item.name) - self._write(f"{inner}# Create item {name}\n") - self._write(f"{inner}{node_var}.{items}.new" - f"({socket_type}, {name})\n") + self._write(f"# Create item {name}") + self._write(f"{node_var}.{items}.new" + f"({socket_type}, {name})") if is_sim: item_var = f"{node_var}.{items}[{i}]" - attr_domain = enum_to_py_str(item.attribute_domain) - self._write((f"{inner}{item_var}.attribute_domain = " - f"{attr_domain}\n")) + ad = enum_to_py_str(item.attribute_domain) + self._write(f"{item_var}.attribute_domain = {ad}") + + def _process_node(self, node: Node, ntp_nt: NTP_GeoNodeTree) -> None: + """ + Create node and set settings, defaults, and cosmetics - def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, - inner: str) -> None: - #create node - node_var: str = self._create_node(node, inner, ntp_node_tree.var) - self._set_settings_defaults(node, inner, node_var) + Parameters: + node (Node): node to process + ntp_nt (NTP_NodeTree): the node tree that node belongs to + """ + node_var: str = self._create_node(node, ntp_nt.var) + self._set_settings_defaults(node) if bpy.app.version < (4, 0, 0): - if node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) - ntp_node_tree.inputs_set = True + if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: + self._group_io_settings(node, "input", ntp_nt) + ntp_nt.inputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) - ntp_node_tree.outputs_set = True + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: + self._group_io_settings(node, "output", ntp_nt) + ntp_nt.outputs_set = True if node.bl_idname == 'GeometryNodeGroup': - self._process_group_node_tree(node, node_var, inner) + self._process_group_node_tree(node) elif node.bl_idname == 'GeometryNodeSimulationInput': - ntp_node_tree.sim_inputs.append(node) + ntp_nt.sim_inputs.append(node) elif node.bl_idname == 'GeometryNodeSimulationOutput': - self._process_zone_output_node(node, inner, node_var) + self._process_zone_output_node(node) elif node.bl_idname == 'GeometryNodeRepeatInput': - ntp_node_tree.repeat_inputs.append(node) + ntp_nt.repeat_inputs.append(node) elif node.bl_idname == 'GeometryNodeRepeatOutput': - self._process_zone_output_node(node, inner, node_var) + self._process_zone_output_node(node) - self._hide_hidden_sockets(node, inner, node_var) + self._hide_hidden_sockets(node) if node.bl_idname not in {'GeometryNodeSimulationInput', 'GeometryNodeRepeatInput'}: - self._set_socket_defaults(node, node_var, inner) + self._set_socket_defaults(node) if bpy.app.version >= (3, 6, 0): - def _process_zones(self, zone_inputs: list[GeometryNode], - inner: str) -> None: + def _process_zones(self, zone_inputs: list[GeometryNode]) -> None: """ Recreates a zone zone_inputs (list[GeometryNodeSimulationInput]): list of simulation input nodes - inner (str): identation string """ for zone_input in zone_inputs: zone_output = zone_input.paired_output @@ -114,45 +117,26 @@ def _process_zones(self, zone_inputs: list[GeometryNode], zone_input_var = self._node_vars[zone_input] zone_output_var = self._node_vars[zone_output] - self._write(f"{inner}#Process zone input {zone_input.name}\n") - self._write((f"{inner}{zone_input_var}.pair_with_output" - f"({zone_output_var})\n")) + self._write(f"#Process zone input {zone_input.name}") + self._write(f"{zone_input_var}.pair_with_output" + f"({zone_output_var})") #must set defaults after paired with output - self._set_socket_defaults(zone_input, zone_input_var, inner) - self._set_socket_defaults(zone_output, zone_output_var, inner) - self._write("\n") - - def _process_node_tree(self, node_tree: GeometryNodeTree, - level: int) -> None: - """ - Generates a Python function to recreate a node tree - - Parameters: - node_tree (GeometryNodeTree): geometry node tree to be recreated - level (int): number of tabs to use for each line, used with - node groups within node groups and script/add-on differences - """ - - nt_var = self._create_var(node_tree.name) - outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? - # Eventually these should go away anyways, and level of indentation depends just on the mode + self._set_socket_defaults(zone_input) + self._set_socket_defaults(zone_output) + self._write("") - #initialize node group - self._write(f"{outer}#initialize {nt_var} node group\n") - self._write(f"{outer}def {nt_var}_node_group():\n") - self._write((f"{inner}{nt_var} = bpy.data.node_groups.new(" - f"type = \'GeometryNodeTree\', " - f"name = {str_to_py_str(node_tree.name)})\n")) - self._write("\n") - - if bpy.app.version >= (4, 0, 0): + if bpy.app.version >= (4, 0, 0): + def _set_geo_tree_properties(self, node_tree: GeometryNodeTree) -> None: is_mod = node_tree.is_modifier is_tool = node_tree.is_tool + + nt_var = self._node_tree_vars[node_tree] + if is_mod: - self._write(f"{inner}{nt_var}.is_modifier = True\n") + self._write(f"{nt_var}.is_modifier = True") if is_tool: - self._write(f"{inner}{nt_var}.is_tool = True\n") + self._write(f"{nt_var}.is_tool = True") tool_flags = ["is_mode_edit", "is_mode_sculpt", @@ -161,52 +145,70 @@ def _process_node_tree(self, node_tree: GeometryNodeTree, "is_type_point_cloud"] for flag in tool_flags: - self._write(f"{inner}{nt_var}.{flag} = " - f"{getattr(node_tree, flag)}\n") - self._write("\n") + self._write(f"{nt_var}.{flag} = {getattr(node_tree, flag)}") + self._write("") + + def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (GeometryNodeTree): geometry node tree to be recreated + """ + + nt_var = self._create_var(node_tree.name) + self._node_tree_vars[node_tree] = nt_var + + #initialize node group + self._write(f"#initialize {nt_var} node group", self._outer) + self._write(f"def {nt_var}_node_group():", self._outer) + self._write(f"{nt_var} = bpy.data.node_groups.new(" + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n") + + if bpy.app.version >= (4, 0, 0): + self._set_geo_tree_properties(node_tree) #initialize nodes - self._write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"#initialize {nt_var} nodes") ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) if bpy.app.version >= (4, 0, 0): - self._tree_interface_settings(inner, ntp_nt) + self._tree_interface_settings(ntp_nt) for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner) + self._process_node(node, ntp_nt) if bpy.app.version >= (3, 6, 0): - self._process_zones(ntp_nt.sim_inputs, inner) + self._process_zones(ntp_nt.sim_inputs) if bpy.app.version >= (4, 0, 0): - self._process_zones(ntp_nt.repeat_inputs, inner) + self._process_zones(ntp_nt.repeat_inputs) #set look of nodes - self._set_parents(node_tree, inner) - self._set_locations(node_tree, inner) - self._set_dimensions(node_tree, inner) + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) #create connections - self._init_links(node_tree, inner, nt_var) + self._init_links(node_tree) - self._write(f"{inner}return {nt_var}\n") + self._write(f"return {nt_var}\n") #create node group - self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") - - self._node_trees[node_tree] = nt_var + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): #get object - self._write(f"\t\tname = bpy.context.object.name\n") - self._write(f"\t\tobj = bpy.data.objects[name]\n") + self._write(f"name = bpy.context.object.name", self._outer) + self._write(f"obj = bpy.data.objects[name]", self._outer) #set modifier to the one we just created mod_name = str_to_py_str(nt.name) - self._write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')\n")) - self._write(f"\t\tmod.node_group = {nt_var}\n") + self._write(f"mod = obj.modifiers.new(name = {mod_name}, " + f"type = 'NODES')", self._outer) + self._write(f"mod.node_group = {nt_var}", self._outer) def execute(self, context): @@ -217,6 +219,9 @@ def execute(self, context): nt_var = clean_string(nt.name) if self.mode == 'ADDON': + self._outer = "\t\t" + self._inner = "\t\t\t" + self._setup_addon_directories(context, nt_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") @@ -224,23 +229,18 @@ def execute(self, context): self._create_header(nt.name) self._class_name = clean_string(nt.name, lower = False) self._init_operator(nt_var, nt.name) - self._write("\tdef execute(self, context):\n") + self._write("def execute(self, context):", "\t") else: self._file = StringIO("") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 node_trees_to_process = self._topological_sort(nt) for node_tree in node_trees_to_process: - self._process_node_tree(node_tree, level) + self._process_node_tree(node_tree) if self.mode == 'ADDON': self._apply_modifier(nt, nt_var) - self._write("\t\treturn {'FINISHED'}\n\n") + self._write("return {'FINISHED'}\n", self._outer) self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/material/operator.py b/material/operator.py index 72bd339..a253238 100644 --- a/material/operator.py +++ b/material/operator.py @@ -24,64 +24,61 @@ def __init__(self): self._settings = shader_node_settings def _create_material(self, indent: str): - self._write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" - f"name = {str_to_py_str(self.material_name)})\n")) - self._write(f"{indent}{MAT_VAR}.use_nodes = True\n") + self._write(f"{MAT_VAR} = bpy.data.materials.new(" + f"name = {str_to_py_str(self.material_name)})", indent) + self._write(f"{MAT_VAR}.use_nodes = True", indent) - def _initialize_shader_node_tree(self, outer: str, ntp_node_tree: NTP_NodeTree, nt_name: str) -> None: + def _initialize_shader_node_tree(self, ntp_node_tree: NTP_NodeTree, + nt_name: str) -> None: """ Initialize the shader node group Parameters: - outer (str): indentation level ntp_node_tree (NTP_NodeTree): node tree to be generated and variable to use nt_name (str): name to use for the node tree """ - self._write(f"{outer}#initialize {nt_name} node group\n") - self._write(f"{outer}def {ntp_node_tree.var}_node_group():\n") + self._write(f"#initialize {nt_name} node group", self._outer) + self._write(f"def {ntp_node_tree.var}_node_group():\n", self._outer) - inner = f"{outer}\t" if ntp_node_tree.node_tree == self._base_node_tree: - self._write(f"{inner}{ntp_node_tree.var} = {MAT_VAR}.node_tree\n") - self._write(f"{inner}#start with a clean node tree\n") - self._write(f"{inner}for node in {ntp_node_tree.var}.nodes:\n") - self._write(f"{inner}\t{ntp_node_tree.var}.nodes.remove(node)\n") + self._write(f"{ntp_node_tree.var} = {MAT_VAR}.node_tree") + self._write(f"#start with a clean node tree") + self._write(f"for node in {ntp_node_tree.var}.nodes:") + self._write(f"\t{ntp_node_tree.var}.nodes.remove(node)") else: - self._write((f"{inner}{ntp_node_tree.var} = bpy.data.node_groups.new(" + self._write((f"{ntp_node_tree.var} = bpy.data.node_groups.new(" f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - self._write("\n") + f"name = {str_to_py_str(nt_name)})")) + self._write("") - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str) -> None: + def _process_node(self, node: Node, ntp_nt: NTP_NodeTree) -> None: """ - Creates a node and sets settings, inputs, outputs, and cosmetics + Create node and set settings, defaults, and cosmetics Parameters: node (Node): node to process - ntp_node_tree (NTP_NodeTree): node tree the node belongs to, and - variable to use - inner + ntp_nt (NTP_NodeTree): the node tree that node belongs to """ - node_var: str = self._create_node(node, inner, ntp_node_tree.var) - self._set_settings_defaults(node, inner, node_var) + node_var: str = self._create_node(node, ntp_nt.var) + self._set_settings_defaults(node) if bpy.app.version < (4, 0, 0): - if node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) - ntp_node_tree.inputs_set = True + if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: + self._group_io_settings(node, "input", ntp_nt) + ntp_nt.inputs_set = True - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) - ntp_node_tree.outputs_set = True + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: + self._group_io_settings(node, "output", ntp_nt) + ntp_nt.outputs_set = True if node.bl_idname == 'ShaderNodeGroup': - self._process_group_node_tree(node, node_var, inner) + self._process_group_node_tree(node) - self._hide_hidden_sockets(node, inner, node_var) - self._set_socket_defaults(node, node_var, inner) + self._hide_hidden_sockets(node) + self._set_socket_defaults(node) - def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: + def _process_node_tree(self, node_tree: ShaderNodeTree) -> None: """ Generates a Python function to recreate a node tree @@ -98,32 +95,30 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: nt_var = self._create_var(node_tree.name) nt_name = node_tree.name - outer, inner = make_indents(level) + self._node_tree_vars[node_tree] = nt_var ntp_nt = NTP_NodeTree(node_tree, nt_var) - self._initialize_shader_node_tree(outer, ntp_nt, nt_name) + self._initialize_shader_node_tree(ntp_nt, nt_name) if bpy.app.version >= (4, 0, 0): - self._tree_interface_settings(inner, ntp_nt) + self._tree_interface_settings(ntp_nt) #initialize nodes - self._write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"#initialize {nt_var} nodes") for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner) - - self._set_parents(node_tree, inner) - self._set_locations(node_tree, inner) - self._set_dimensions(node_tree, inner) + self._process_node(node, ntp_nt) - self._init_links(node_tree, inner, nt_var) + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) - self._write(f"{inner}return {nt_var}\n") + self._init_links(node_tree) - self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") + self._write(f"return {nt_var}\n") - self._node_trees[node_tree] = nt_var + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def execute(self, context): @@ -138,6 +133,9 @@ def execute(self, context): mat_var = clean_string(self.material_name) if self.mode == 'ADDON': + self._outer = "\t\t" + self._inner = "\t\t\t" + self._setup_addon_directories(context, mat_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") @@ -146,27 +144,22 @@ def execute(self, context): self._class_name = clean_string(self.material_name, lower=False) self._init_operator(mat_var, self.material_name) - self._write("\tdef execute(self, context):\n") + self._write("def execute(self, context):", "\t") else: self._file = StringIO("") if self.mode == 'ADDON': self._create_material("\t\t") elif self.mode == 'SCRIPT': - self._create_material("") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 + self._create_material("") node_trees_to_process = self._topological_sort(self._base_node_tree) for node_tree in node_trees_to_process: - self._process_node_tree(node_tree, level) + self._process_node_tree(node_tree) if self.mode == 'ADDON': - self._write("\t\treturn {'FINISHED'}\n\n") + self._write("return {'FINISHED'}", self._outer) self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/ntp_operator.py b/ntp_operator.py index 8f33e3e..a4b4181 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -66,11 +66,16 @@ def __init__(self): # Class named for the generated operator self._class_name: str = None + # Indentation to use for the default write function + self._outer: str = "" + self._inner: str = "\t" + + # Base node tree we're converting self._base_node_tree: NodeTree = None # Dictionary to keep track of node tree->variable name pairs - self._node_trees: dict[NodeTree, str] = {} + self._node_tree_vars: dict[NodeTree, str] = {} # Dictionary to keep track of node->variable name pairs self._node_vars: dict[Node, str] = {} @@ -81,8 +86,10 @@ def __init__(self): # Dictionary used for setting node properties self._settings: dict[str, list[(str, ST)]] = {} - def _write(self, string: str): - self._file.write(string) + def _write(self, string: str, indent: str = None): + if indent is None: + indent = self._inner + self._file.write(f"{indent}{string}\n") def _setup_addon_directories(self, context: Context, nt_var: str) -> None: """ @@ -111,19 +118,17 @@ def _create_header(self, name: str) -> None: name (str): name of the add-on """ - self._write("bl_info = {\n") - self._write(f"\t\"name\" : \"{name}\",\n") - self._write("\t\"author\" : \"Node To Python\",\n") - self._write("\t\"version\" : (1, 0, 0),\n") - self._write(f"\t\"blender\" : {bpy.app.version},\n") - self._write("\t\"location\" : \"Object\",\n") # TODO - self._write("\t\"category\" : \"Node\"\n") - self._write("}\n") - self._write("\n") - self._write("import bpy\n") - self._write("import mathutils\n") - self._write("import os\n") - self._write("\n") + self._write("bl_info = {", "") + self._write(f"\t\"name\" : \"{name}\",", "") + self._write("\t\"author\" : \"Node To Python\",", "") + self._write("\t\"version\" : (1, 0, 0),", "") + self._write(f"\t\"blender\" : {bpy.app.version},", "") + self._write("\t\"location\" : \"Object\",", "") # TODO + self._write("\t\"category\" : \"Node\"", "") + self._write("}\n", "") + self._write("import bpy", "") + self._write("import mathutils", "") + self._write("import os\n", "") def _init_operator(self, idname: str, label: str) -> None: """ @@ -135,11 +140,11 @@ def _init_operator(self, idname: str, label: str) -> None: idname (str): name for the operator label (str): appearence inside Blender """ - self._write(f"class {self._class_name}(bpy.types.Operator):\n") - self._write(f"\tbl_idname = \"object.{idname}\"\n") - self._write(f"\tbl_label = \"{label}\"\n") - self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - self._write("\n") + self._write(f"class {self._class_name}(bpy.types.Operator):", "") + self._write(f"\tbl_idname = \"object.{idname}\"", "") + self._write(f"\tbl_label = \"{label}\"", "") + self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}", "") + self._write("") def _topological_sort(self, node_tree: NodeTree) -> list[NodeTree]: """ @@ -181,22 +186,20 @@ def dfs(nt: NodeTree) -> None: return result - def _process_group_node_tree(self, node: Node, node_var: str, inner: str - ) -> None: + def _process_group_node_tree(self, node: Node) -> None: """ Processes node tree of group node if one is present Parameters: node (Node): the group node - node_var (str): variable for the group node - inner (str): indentation """ node_tree = node.node_tree if node_tree is None: return - if node_tree in self._node_trees: - nt_var = self._node_trees[node_tree] - self._write((f"{inner}{node_var}.node_tree = {nt_var}\n")) + if node_tree in self._node_tree_vars: + nt_var = self._node_tree_vars[node_tree] + node_var = self._node_vars[node] + self._write(f"{node_var}.node_tree = {nt_var}") else: self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " f"wasn't properly initialized")) @@ -224,52 +227,49 @@ def _create_var(self, name: str) -> str: self._used_vars[var] = 0 return clean_name - def _create_node(self, node: Node, inner: str, node_tree_var: str) -> str: + def _create_node(self, node: Node, node_tree_var: str) -> str: """ Initializes a new node with location, dimension, and label info Parameters: node (bpy.types.Node): node to be copied - inner (str): indentation level for this logic node_tree_var (str): variable name for the node tree Returns: node_var (str): variable name for the node """ - self._write(f"{inner}#node {node.name}\n") + self._write(f"#node {node.name}") node_var = self._create_var(node.name) self._node_vars[node] = node_var - self._write((f"{inner}{node_var} " - f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) + idname = str_to_py_str(node.bl_idname) + self._write(f"{node_var} = {node_tree_var}.nodes.new({idname})") + # label if node.label: - self._write(f"{inner}{node_var}.label = \"{node.label}\"\n") + self._write(f"{node_var}.label = {str_to_py_str(node.label)}") # name - self._write(f"{inner}{node_var}.name = \"{node.name}\"\n") + self._write(f"{node_var}.name = {str_to_py_str(node.name)}") # color if node.use_custom_color: - self._write(f"{inner}{node_var}.use_custom_color = True\n") - self._write( - f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") + self._write(f"{node_var}.use_custom_color = True") + self._write(f"{node_var}.color = {vec3_to_py_str(node.color)}") # mute if node.mute: - self._write(f"{inner}{node_var}.mute = True\n") + self._write(f"{node_var}.mute = True") return node_var - def _set_settings_defaults(self, node: Node, inner: str, node_var: str - ) -> None: + def _set_settings_defaults(self, node: Node) -> None: """ Sets the defaults for any settings a node may have Parameters: node (bpy.types.Node): the node object we're copying settings from - inner (str): indentation node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname not in self._settings: @@ -278,6 +278,8 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str f"settings. Your Blender version may not be supported")) return + node_var = self._node_vars[node] + for (attr_name, type) in self._settings[node.bl_idname]: if not hasattr(node, attr_name): self.report({'WARNING'}, @@ -289,53 +291,51 @@ def _set_settings_defaults(self, node: Node, inner: str, node_var: str if attr is None: continue - setting_str = f"{inner}{node_var}.{attr_name}" + setting_str = f"{node_var}.{attr_name}" if type == ST.ENUM: if attr != '': - self._write(f"{setting_str} = {enum_to_py_str(attr)}\n") + self._write(f"{setting_str} = {enum_to_py_str(attr)}") elif type == ST.ENUM_SET: - self._write(f"{setting_str} = {attr}\n") + self._write(f"{setting_str} = {attr}") elif type == ST.STRING: - self._write(f"{setting_str} = {str_to_py_str(attr)}\n") + self._write(f"{setting_str} = {str_to_py_str(attr)}") elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: - self._write(f"{setting_str} = {attr}\n") + self._write(f"{setting_str} = {attr}") elif type == ST.VEC1: - self._write(f"{setting_str} = {vec1_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec1_to_py_str(attr)}") elif type == ST.VEC2: - self._write(f"{setting_str} = {vec2_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec2_to_py_str(attr)}") elif type == ST.VEC3: - self._write(f"{setting_str} = {vec3_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec3_to_py_str(attr)}") elif type == ST.VEC4: - self._write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec4_to_py_str(attr)}") elif type == ST.COLOR: - self._write(f"{setting_str} = {color_to_py_str(attr)}\n") + self._write(f"{setting_str} = {color_to_py_str(attr)}") elif type == ST.MATERIAL: name = str_to_py_str(attr.name) - self._write((f"{inner}if {name} in bpy.data.materials:\n")) - self._write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.materials[{name}]\n")) + self._write((f"if {name} in bpy.data.materials:")) + self._write((f"\t{node_var}.{attr_name} = " + f"bpy.data.materials[{name}]")) elif type == ST.OBJECT: name = str_to_py_str(attr.name) - self._write((f"{inner}if {name} in bpy.data.objects:\n")) - self._write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.objects[{name}]\n")) + self._write((f"if {name} in bpy.data.objects:")) + self._write((f"\t{node_var}.{attr_name} = " + f"bpy.data.objects[{name}]")) elif type == ST.COLOR_RAMP: - self._color_ramp_settings(node, inner, node_var, attr_name) + self._color_ramp_settings(node, attr_name) elif type == ST.CURVE_MAPPING: - self._curve_mapping_settings(node, inner, node_var, attr_name) + self._curve_mapping_settings(node, attr_name) elif type == ST.IMAGE: if self._addon_dir is not None and attr is not None: if attr.source in {'FILE', 'GENERATED', 'TILED'}: self._save_image(attr) - self._load_image( - attr, inner, f"{node_var}.{attr_name}") + self._load_image(attr, f"{node_var}.{attr_name}") elif type == ST.IMAGE_USER: - self._image_user_settings( - attr, inner, f"{node_var}.{attr_name}") + self._image_user_settings(attr, f"{node_var}.{attr_name}") if bpy.app.version < (4, 0, 0): def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, - inner: str, socket_var: str) -> None: + socket_var: str) -> None: """ Set a node group input/output's default properties if they exist Helper function to _group_io_settings() @@ -343,7 +343,6 @@ def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, Parameters: socket_interface (NodeSocketInterface): socket interface associated with the input/output - inner (str): indentation string socket_var (str): variable name for the socket """ if socket_interface.type not in self.default_sockets_v3: @@ -355,18 +354,18 @@ def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, dv = vec3_to_py_str(socket_interface.default_value) else: dv = socket_interface.default_value - self._write(f"{inner}{socket_var}.default_value = {dv}\n") + self._write(f"{socket_var}.default_value = {dv}") # min value if hasattr(socket_interface, "min_value"): min_val = socket_interface.min_value - self._write(f"{inner}{socket_var}.min_value = {min_val}\n") + self._write(f"{socket_var}.min_value = {min_val}") # max value if hasattr(socket_interface, "min_value"): max_val = socket_interface.max_value - self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) + self._write(f"{socket_var}.max_value = {max_val}") - def _group_io_settings(self, node: bpy.types.Node, inner: str, + def _group_io_settings(self, node: Node, io: str, # TODO: convert to enum ntp_node_tree: NTP_NodeTree) -> None: """ @@ -374,7 +373,6 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, Parameters: node (bpy.types.Node) : group input/output node - inner (str): indentation string io (str): whether we're generating the input or output settings node_tree_var (str): variable name of the generated node tree node_tree (bpy.types.NodeTree): node tree that we're generating @@ -390,57 +388,50 @@ def _group_io_settings(self, node: bpy.types.Node, inner: str, io_sockets = node.inputs io_socket_interfaces = node_tree.outputs - self._write(f"{inner}#{node_tree_var} {io}s\n") + self._write(f"#{node_tree_var} {io}s") for i, inout in enumerate(io_sockets): if inout.bl_idname == 'NodeSocketVirtual': continue - self._write(f"{inner}#{io} {inout.name}\n") + self._write(f"#{io} {inout.name}") idname = enum_to_py_str(inout.bl_idname) name = str_to_py_str(inout.name) - self._write( - f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") + self._write(f"{node_tree_var}.{io}s.new({idname}, {name})") socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - self._set_group_socket_defaults(socket_interface, inner, - socket_var) + self._set_group_socket_default(socket_interface, socket_var) # default attribute name if hasattr(socket_interface, "default_attribute_name"): if socket_interface.default_attribute_name != "": - dan = str_to_py_str( - socket_interface.default_attribute_name) - self._write((f"{inner}{socket_var}" - f".default_attribute_name = {dan}\n")) + dan = str_to_py_str(socket_interface.default_attribute_name) + self._write(f"{socket_var}.default_attribute_name = {dan}") # attribute domain if hasattr(socket_interface, "attribute_domain"): ad = enum_to_py_str(socket_interface.attribute_domain) - self._write( - f"{inner}{socket_var}.attribute_domain = {ad}\n") + self._write(f"{socket_var}.attribute_domain = {ad}") # tooltip if socket_interface.description != "": description = str_to_py_str(socket_interface.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) + self._write(f"{socket_var}.description = {description}") # hide_value if socket_interface.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") + self._write(f"{socket_var}.hide_value = True") # hide in modifier if hasattr(socket_interface, "hide_in_modifier"): if socket_interface.hide_in_modifier is True: - self._write( - f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write(f"{socket_var}.hide_in_modifier = True") - self._write("\n") - self._write("\n") + self._write("") + self._write("") elif bpy.app.version >= (4, 0, 0): def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, - inner: str, socket_var: str) -> None: + socket_var: str) -> None: """ Set a node tree input/output's default properties if they exist @@ -449,7 +440,6 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, Parameters: socket_interface (NodeTreeInterfaceSocket): socket interface associated with the input/output - inner (str): indentation string socket_var (str): variable name for the socket """ if type(socket_interface) in self.nondefault_sockets_v4: @@ -465,18 +455,18 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, dv = array_to_py_str(dv) elif type(dv) == str: dv = str_to_py_str(dv) - self._write(f"{inner}{socket_var}.default_value = {dv}\n") + self._write(f"{socket_var}.default_value = {dv}") # min value if hasattr(socket_interface, "min_value"): min_val = socket_interface.min_value - self._write(f"{inner}{socket_var}.min_value = {min_val}\n") + self._write(f"{socket_var}.min_value = {min_val}") # max value if hasattr(socket_interface, "min_value"): max_val = socket_interface.max_value - self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) + self._write(f"{socket_var}.max_value = {max_val}") - def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, + def _create_socket(self, socket: NodeTreeInterfaceSocket, parent: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel, str], ntp_nt: NTP_NodeTree) -> None: @@ -486,7 +476,6 @@ def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, Helper function to _process_items() Parameters: - inner (str): indentation string socket (NodeTreeInterfaceSocket): the socket to recreate parent (NodeTreeInterfacePanel): parent panel of the socket (possibly None) @@ -494,7 +483,7 @@ def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, ntp_nt (NTP_NodeTree): owner of the socket """ - self._write(f"{inner}#Socket {socket.name}\n") + self._write(f"#Socket {socket.name}") # initialization socket_var = self._create_var(socket.name + "_socket") name = str_to_py_str(socket.name) @@ -519,53 +508,49 @@ def _create_socket(self, inner: str, socket: NodeTreeInterfaceSocket, else: optional_parent_str = f", parent = {panel_dict[parent]}" - self._write(f"{inner}{socket_var} = " + self._write(f"{socket_var} = " f"{ntp_nt.var}.interface.new_socket(" f"name = {name}, in_out={in_out_enum}, " f"socket_type = {socket_type}" - f"{optional_parent_str})\n") + f"{optional_parent_str})") # subtype if hasattr(socket, "subtype"): subtype = enum_to_py_str(socket.subtype) - self._write(f"{inner}{socket_var}.subtype = {subtype}\n") + self._write(f"{socket_var}.subtype = {subtype}") - self._set_tree_socket_defaults(socket, inner, socket_var) + self._set_tree_socket_defaults(socket, socket_var) # default attribute name if socket.default_attribute_name != "": dan = str_to_py_str( socket.default_attribute_name) - self._write( - (f"{inner}{socket_var}.default_attribute_name = {dan}\n")) + self._write(f"{socket_var}.default_attribute_name = {dan}") # attribute domain ad = enum_to_py_str(socket.attribute_domain) - self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + self._write(f"{socket_var}.attribute_domain = {ad}") # hide_value if socket.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") + self._write(f"{socket_var}.hide_value = True") # hide in modifier if socket.hide_in_modifier is True: - self._write( - f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write(f"{socket_var}.hide_in_modifier = True") # force non field if socket.force_non_field is True: - self._write( - f"{inner}{socket_var}.force_non_field = True\n") + self._write(f"{socket_var}.force_non_field = True") # tooltip if socket.description != "": description = str_to_py_str(socket.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) + self._write(f"{socket_var}.description = {description}") - self._write("\n") + self._write("") - def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, + def _create_panel(self, panel: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel], items_processed: set[NodeTreeInterfacePanel], parent: NodeTreeInterfacePanel, ntp_nt: NTP_NodeTree): @@ -575,7 +560,6 @@ def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, Helper function to _process_items() Parameters: - inner (str): indentation string panel (NodeTreeInterfacePanel): the panel to recreate panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable items_processed (set[NodeTreeInterfacePanel]): set of already @@ -585,7 +569,7 @@ def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, ntp_nt (NTP_NodeTree): owner of the socket """ - self._write(f"{inner}#Panel {panel.name}\n") + self._write(f"#Panel {panel.name}") panel_var = self._create_var(panel.name + "_panel") panel_dict[panel] = panel_var @@ -603,25 +587,24 @@ def _create_panel(self, inner: str, panel: NodeTreeInterfacePanel, parent_str = f", parent = {panel_dict[parent]}" - self._write(f"{inner}{panel_var} = " + self._write(f"{panel_var} = " f"{ntp_nt.var}.interface.new_panel(" f"{str_to_py_str(panel.name)}{description_str}" - f"{closed_str}{parent_str})\n") + f"{closed_str}{parent_str})") # tooltip if panel.description != "": description = str_to_py_str(panel.description) - self._write( - (f"{inner}{panel_var}.description = {description}\n")) + self._write(f"{panel_var}.description = {description}") panel_dict[panel] = panel_var if len(panel.interface_items) > 0: - self._process_items(inner, panel, panel_dict, items_processed, ntp_nt) + self._process_items(panel, panel_dict, items_processed, ntp_nt) - self._write("\n") + self._write("") - def _process_items(self, inner: str, parent: NodeTreeInterfacePanel, + def _process_items(self, parent: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel], items_processed: set[NodeTreeInterfacePanel], ntp_nt: NTP_NodeTree) -> None: @@ -632,7 +615,6 @@ def _process_items(self, inner: str, parent: NodeTreeInterfacePanel, Helper function to _tree_interface_settings() Parameters: - inner (str): indentation string parent (NodeTreeInterfacePanel): parent panel of the layer (possibly None to signify the base) panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable @@ -653,48 +635,41 @@ def _process_items(self, inner: str, parent: NodeTreeInterfacePanel, items_processed.add(item) - print(item.name, items_processed) - if item.item_type == 'SOCKET': - self._create_socket(inner, item, parent, panel_dict, ntp_nt) + self._create_socket(item, parent, panel_dict, ntp_nt) elif item.item_type == 'PANEL': - self._create_panel(inner, item, panel_dict, items_processed, + self._create_panel(item, panel_dict, items_processed, parent, ntp_nt) - def _tree_interface_settings(self, inner: str, ntp_nt: NTP_NodeTree - ) -> None: + def _tree_interface_settings(self, ntp_nt: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets Parameters: - inner (str): indentation string ntp_nt (NTP_NodeTree): the node tree to set the interface for """ - self._write(f"{inner}#{ntp_nt.var} interface\n") + self._write(f"#{ntp_nt.var} interface") panel_dict: dict[NodeTreeInterfacePanel, str] = {} items_processed: set[NodeTreeInterfaceItem] = set() - self._process_items(inner, None, panel_dict, items_processed, ntp_nt) + self._process_items(None, panel_dict, items_processed, ntp_nt) - self._write("\n") + self._write("") - def _set_input_defaults(self, node: bpy.types.Node, inner: str, - node_var: str) -> None: + def _set_input_defaults(self, node: Node) -> None: """ Sets defaults for input sockets Parameters: - node (bpy.types.Node): node we're setting inputs for - inner (str): indentation - node_var (str): variable name we're using for the copied node - addon_dir (str): directory of the add-on, for if we need to save other - objects for the add-on + node (Node): node we're setting inputs for """ if node.bl_idname == 'NodeReroute': return + node_var = self._node_vars[node] + for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults and not input.is_linked: # TODO: this could be cleaner @@ -721,48 +696,42 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, img = input.default_value if img is not None and self._addon_dir != None: # write in a better way self._save_image(img) - self._load_image( - img, inner, f"{socket_var}.default_value") + self._load_image(img, f"{socket_var}.default_value") default_val = None # materials elif input.bl_idname == 'NodeSocketMaterial': - self._in_file_inputs(input, inner, socket_var, "materials") + self._in_file_inputs(input, socket_var, "materials") default_val = None # collections elif input.bl_idname == 'NodeSocketCollection': - self._in_file_inputs( - input, inner, socket_var, "collections") + self._in_file_inputs(input, socket_var, "collections") default_val = None # objects elif input.bl_idname == 'NodeSocketObject': - self._in_file_inputs(input, inner, socket_var, "objects") + self._in_file_inputs(input, socket_var, "objects") default_val = None # textures elif input.bl_idname == 'NodeSocketTexture': - self._in_file_inputs(input, inner, socket_var, "textures") + self._in_file_inputs(input, socket_var, "textures") default_val = None else: default_val = input.default_value if default_val is not None: - self._write(f"{inner}#{input.identifier}\n") - self._write((f"{inner}{socket_var}.default_value" - f" = {default_val}\n")) - self._write("\n") + self._write(f"#{input.identifier}") + self._write(f"{socket_var}.default_value = {default_val}") + self._write("") - def _set_output_defaults(self, node: bpy.types.Node, - inner: str, node_var: str) -> None: + def _set_output_defaults(self, node: Node) -> None: """ Some output sockets need default values set. It's rather annoying Parameters: node (bpy.types.Node): node for the output we're setting - inner (str): indentation string - node_var (str): variable name for the node we're setting output defaults for """ # TODO: probably should define elsewhere output_default_nodes = {'ShaderNodeValue', @@ -775,24 +744,22 @@ def _set_output_defaults(self, node: bpy.types.Node, if node.bl_idname not in output_default_nodes: return + node_var = self._node_vars[node] + dv = node.outputs[0].default_value if node.bl_idname in {'ShaderNodeRGB', 'CompositorNodeRGB'}: dv = vec4_to_py_str(list(dv)) if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}: dv = vec3_to_py_str(dv) - self._write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) + self._write(f"{node_var}.outputs[0].default_value = {dv}") - def _in_file_inputs(self, input: bpy.types.NodeSocket, - inner: str, - socket_var: str, - type: str - ) -> None: + def _in_file_inputs(self, input: bpy.types.NodeSocket, socket_var: str, + type: str) -> None: """ Sets inputs for a node input if one already exists in the blend file Parameters: input (bpy.types.NodeSocket): input socket we're setting the value for - inner (str): indentation string socket_var (str): variable name we're using for the socket type (str): from what section of bpy.data to pull the default value from """ @@ -800,21 +767,22 @@ def _in_file_inputs(self, input: bpy.types.NodeSocket, if input.default_value is None: return name = str_to_py_str(input.default_value.name) - self._write(f"{inner}if {name} in bpy.data.{type}:\n") - self._write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) - - def _color_ramp_settings(self, node: bpy.types.Node, - inner: str, - node_var: str, - color_ramp_name: str) -> None: + self._write(f"if {name} in bpy.data.{type}:") + self._write(f"\t{socket_var}.default_value = bpy.data.{type}[{name}]") + + def _set_socket_defaults(self, node: Node): + """ + Set input and output socket defaults + """ + self._set_input_defaults(node) + self._set_output_defaults(node) + + def _color_ramp_settings(self, node: Node, color_ramp_name: str) -> None: """ Replicate a color ramp node Parameters - node (bpy.types.Node): node object we're copying settings from - inner (str): indentation - node_var (str): name of the variable we're using for the color ramp + node (Node): node object we're copying settings from color_ramp_name (str): name of the color ramp to be copied """ @@ -823,52 +791,48 @@ def _color_ramp_settings(self, node: bpy.types.Node, raise ValueError( f"No color ramp named \"{color_ramp_name}\" found") + node_var = self._node_vars[node] + # settings - ramp_str = f"{inner}{node_var}.{color_ramp_name}" + ramp_str = f"{node_var}.{color_ramp_name}" + #color mode color_mode = enum_to_py_str(color_ramp.color_mode) - self._write(f"{ramp_str}.color_mode = {color_mode}\n") + self._write(f"{ramp_str}.color_mode = {color_mode}") + #hue interpolation hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) - self._write((f"{ramp_str}.hue_interpolation = " - f"{hue_interpolation}\n")) + self._write(f"{ramp_str}.hue_interpolation = {hue_interpolation}") + + #interpolation interpolation = enum_to_py_str(color_ramp.interpolation) - self._write((f"{ramp_str}.interpolation " - f"= {interpolation}\n")) - self._write("\n") + self._write(f"{ramp_str}.interpolation = {interpolation}") + self._write("") # key points - self._write(f"{inner}#initialize color ramp elements\n") + self._write(f"#initialize color ramp elements") self._write((f"{ramp_str}.elements.remove" - f"({ramp_str}.elements[0])\n")) + f"({ramp_str}.elements[0])")) for i, element in enumerate(color_ramp.elements): element_var = f"{node_var}_cre_{i}" if i == 0: - self._write(f"{inner}{element_var} = " - f"{ramp_str}.elements[{i}]\n") - self._write( - f"{inner}{element_var}.position = {element.position}\n") + self._write(f"{element_var} = {ramp_str}.elements[{i}]") + self._write(f"{element_var}.position = {element.position}") else: - self._write((f"{inner}{element_var} = " - f"{ramp_str}.elements" - f".new({element.position})\n")) + self._write(f"{element_var} = {ramp_str}.elements" + f".new({element.position})") - self._write((f"{inner}{element_var}.alpha = " - f"{element.alpha}\n")) + self._write(f"{element_var}.alpha = {element.alpha}") color_str = vec4_to_py_str(element.color) - self._write((f"{inner}{element_var}.color = {color_str}\n\n")) + self._write(f"{element_var}.color = {color_str}\n") - def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, - node_var: str, curve_mapping_name: str - ) -> None: + def _curve_mapping_settings(self, node: Node, + curve_mapping_name: str) -> None: """ Sets defaults for Float, Vector, and Color curves Parameters: - node (bpy.types.Node): curve node we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): variable name for the add-on's curve node + node (Node): curve node we're copying settings from curve_mapping_name (str): name of the curve mapping to be set """ @@ -877,73 +841,74 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " f"in node \"{node.bl_idname}\"")) + node_var = self._node_vars[node] + # mapping settings - self._write(f"{inner}#mapping settings\n") - mapping_var = f"{inner}{node_var}.{curve_mapping_name}" + self._write(f"#mapping settings") + mapping_var = f"{node_var}.{curve_mapping_name}" # extend extend = enum_to_py_str(mapping.extend) - self._write(f"{mapping_var}.extend = {extend}\n") + self._write(f"{mapping_var}.extend = {extend}") # tone tone = enum_to_py_str(mapping.tone) - self._write(f"{mapping_var}.tone = {tone}\n") + self._write(f"{mapping_var}.tone = {tone}") # black level b_lvl_str = vec3_to_py_str(mapping.black_level) - self._write((f"{mapping_var}.black_level = {b_lvl_str}\n")) + self._write(f"{mapping_var}.black_level = {b_lvl_str}") # white level w_lvl_str = vec3_to_py_str(mapping.white_level) - self._write((f"{mapping_var}.white_level = {w_lvl_str}\n")) + self._write(f"{mapping_var}.white_level = {w_lvl_str}") # minima and maxima min_x = mapping.clip_min_x - self._write(f"{mapping_var}.clip_min_x = {min_x}\n") + self._write(f"{mapping_var}.clip_min_x = {min_x}") min_y = mapping.clip_min_y - self._write(f"{mapping_var}.clip_min_y = {min_y}\n") + self._write(f"{mapping_var}.clip_min_y = {min_y}") max_x = mapping.clip_max_x - self._write(f"{mapping_var}.clip_max_x = {max_x}\n") + self._write(f"{mapping_var}.clip_max_x = {max_x}") max_y = mapping.clip_max_y - self._write(f"{mapping_var}.clip_max_y = {max_y}\n") + self._write(f"{mapping_var}.clip_max_y = {max_y}") # use_clip use_clip = mapping.use_clip - self._write(f"{mapping_var}.use_clip = {use_clip}\n") + self._write(f"{mapping_var}.use_clip = {use_clip}") # create curves for i, curve in enumerate(mapping.curves): # TODO: curve function - self._write(f"{inner}#curve {i}\n") + self._write(f"#curve {i}") curve_i = f"{node_var}_curve_{i}" - self._write((f"{inner}{curve_i} = " - f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) + self._write(f"{curve_i} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]") # Remove default points when CurveMap is initialized with more than # two points (just CompositorNodeHueCorrect) if (node.bl_idname == 'CompositorNodeHueCorrect'): - self._write((f"{inner}for i in range" - f"(len({curve_i}.points.values()) - 1, 1, -1):\n")) + self._write((f"for i in range" + f"(len({curve_i}.points.values()) - 1, 1, -1):")) self._write( - f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") + f"\t{curve_i}.points.remove({curve_i}.points[i])") for j, point in enumerate(curve.points): # TODO: point function - point_j = f"{inner}{curve_i}_point_{j}" + point_j = f"{curve_i}_point_{j}" loc = point.location loc_str = f"{loc[0]}, {loc[1]}" if j < 2: - self._write(f"{point_j} = {curve_i}.points[{j}]\n") - self._write(f"{point_j}.location = ({loc_str})\n") + self._write(f"{point_j} = {curve_i}.points[{j}]") + self._write(f"{point_j}.location = ({loc_str})") else: - self._write( - (f"{point_j} = {curve_i}.points.new({loc_str})\n")) + self._write(f"{point_j} = {curve_i}.points.new({loc_str})") handle = enum_to_py_str(point.handle_type) - self._write(f"{point_j}.handle_type = {handle}\n") + self._write(f"{point_j}.handle_type = {handle}") # update curve - self._write(f"{inner}#update curve after changes\n") - self._write(f"{mapping_var}.update()\n") + self._write(f"#update curve after changes") + self._write(f"{mapping_var}.update()") def _save_image(self, img: bpy.types.Image) -> None: """ @@ -967,16 +932,12 @@ def _save_image(self, img: bpy.types.Image) -> None: if not os.path.exists(img_path): img.save_render(img_path) - def _load_image(self, img: bpy.types.Image, - inner: str, - img_var: str - ) -> None: + def _load_image(self, img: bpy.types.Image, img_var: str) -> None: """ Loads an image from the add-on into a blend file and assigns it Parameters: img (bpy.types.Image): Blender image from the original node group - inner (str): indentation string img_var (str): variable name to be used for the image """ @@ -986,40 +947,37 @@ def _load_image(self, img: bpy.types.Image, img_str = img_to_py_str(img) # TODO: convert to special variables - self._write(f"{inner}#load image {img_str}\n") - self._write((f"{inner}base_dir = " - f"os.path.dirname(os.path.abspath(__file__))\n")) - self._write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " - f"\"{img_str}\")\n")) - self._write((f"{inner}{img_var} = bpy.data.images.load" - f"(image_path, check_existing = True)\n")) + self._write(f"#load image {img_str}") + self._write(f"base_dir = " + f"os.path.dirname(os.path.abspath(__file__))") + self._write(f"image_path = " + f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " + f"\"{img_str}\")") + self._write(f"{img_var} = bpy.data.images.load" + f"(image_path, check_existing = True)") # copy image settings - self._write(f"{inner}#set image settings\n") + self._write(f"#set image settings") # source source = enum_to_py_str(img.source) - self._write(f"{inner}{img_var}.source = {source}\n") + self._write(f"{img_var}.source = {source}") # color space settings color_space = enum_to_py_str(img.colorspace_settings.name) - self._write( - f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") + self._write(f"{img_var}.colorspace_settings.name = {color_space}") # alpha mode alpha_mode = enum_to_py_str(img.alpha_mode) - self._write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") + self._write(f"{img_var}.alpha_mode = {alpha_mode}") def _image_user_settings(self, img_user: bpy.types.ImageUser, - inner: str, img_user_var: str) -> None: """ Replicate the image user of an image node Parameters img_usr (bpy.types.ImageUser): image user to be copied - inner (str): indentation img_usr_var (str): variable name for the generated image user """ @@ -1027,76 +985,71 @@ def _image_user_settings(self, img_user: bpy.types.ImageUser, "frame_start", "tile", "use_auto_refresh", "use_cyclic"] for img_usr_attr in img_usr_attrs: - self._write((f"{inner}{img_user_var}.{img_usr_attr} = " - f"{getattr(img_user, img_usr_attr)}\n")) + self._write(f"{img_user_var}.{img_usr_attr} = " + f"{getattr(img_user, img_usr_attr)}") - def _set_parents(self, node_tree: bpy.types.NodeTree, - inner: str) -> None: + def _set_parents(self, node_tree: NodeTree) -> None: """ Sets parents for all nodes, mostly used to put nodes in frames Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - inner (str): indentation string + node_tree (NodeTree): node tree we're obtaining nodes from """ parent_comment = False for node in node_tree.nodes: if node is not None and node.parent is not None: if not parent_comment: - self._write(f"{inner}#Set parents\n") + self._write(f"#Set parents") parent_comment = True node_var = self._node_vars[node] parent_var = self._node_vars[node.parent] - self._write(f"{inner}{node_var}.parent = {parent_var}\n") - self._write("\n") + self._write(f"{node_var}.parent = {parent_var}") + self._write("") - def _set_locations(self, node_tree: bpy.types.NodeTree, inner: str) -> None: + def _set_locations(self, node_tree: NodeTree) -> None: """ Set locations for all nodes Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - inner (str): indentation string + node_tree (NodeTree): node tree we're obtaining nodes from """ - self._write(f"{inner}#Set locations\n") + self._write(f"#Set locations") for node in node_tree.nodes: node_var = self._node_vars[node] - self._write((f"{inner}{node_var}.location " - f"= ({node.location.x}, {node.location.y})\n")) - self._write("\n") + self._write(f"{node_var}.location " + f"= ({node.location.x}, {node.location.y})") + self._write("") - def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, - ) -> None: + def _set_dimensions(self, node_tree: NodeTree) -> None: """ Set dimensions for all nodes Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - inner (str): indentation string + node_tree (NodeTree): node tree we're obtaining nodes from """ - self._write(f"{inner}#Set dimensions\n") + self._write(f"#Set dimensions") for node in node_tree.nodes: node_var = self._node_vars[node] - self._write((f"{inner}{node_var}.width, {node_var}.height " - f"= {node.width}, {node.height}\n")) - self._write("\n") + self._write(f"{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}") + self._write("") - def _init_links(self, node_tree: bpy.types.NodeTree, - inner: str, - node_tree_var: str) -> None: + def _init_links(self, node_tree: NodeTree) -> None: """ Create all the links between nodes Parameters: - node_tree (bpy.types.NodeTree): node tree we're copying - inner (str): indentation - node_tree_var (str): variable name we're using for the copied node tree + node_tree (NodeTree): node tree to copy, with variable """ - if node_tree.links: - self._write(f"{inner}#initialize {node_tree_var} links\n") - for link in node_tree.links: + nt_var = self._node_tree_vars[node_tree] + + links = node_tree.links + if links: + self._write(f"#initialize {nt_var} links") + + for link in links: in_node_var = self._node_vars[link.from_node] input_socket = link.from_socket @@ -1120,66 +1073,60 @@ def _init_links(self, node_tree: bpy.types.NodeTree, output_idx = i break - self._write((f"{inner}#{in_node_var}.{input_socket.name} " - f"-> {out_node_var}.{output_socket.name}\n")) - self._write((f"{inner}{node_tree_var}.links.new({in_node_var}" - f".outputs[{input_idx}], " - f"{out_node_var}.inputs[{output_idx}])\n")) + self._write(f"#{in_node_var}.{input_socket.name} " + f"-> {out_node_var}.{output_socket.name}") + self._write(f"{nt_var}.links.new({in_node_var}" + f".outputs[{input_idx}], " + f"{out_node_var}.inputs[{output_idx}])") - def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, - node_var: str) -> None: + def _hide_hidden_sockets(self, node: Node) -> None: """ Hide hidden sockets Parameters: - node (bpy.types.Node): node object we're copying socket settings from - inner (str): indentation string - node_var (str): name of the variable we're using for this node + node (Node): node object we're copying socket settings from """ + node_var = self._node_vars[node] + for i, socket in enumerate(node.inputs): if socket.hide is True: - self._write(f"{inner}{node_var}.inputs[{i}].hide = True\n") + self._write(f"{node_var}.inputs[{i}].hide = True") for i, socket in enumerate(node.outputs): if socket.hide is True: - self._write(f"{inner}{node_var}.outputs[{i}].hide = True\n") - - def _set_socket_defaults(self, node: Node, node_var: str, inner: str): - self._set_input_defaults(node, inner, node_var) - self._set_output_defaults(node, inner, node_var) + self._write(f"{node_var}.outputs[{i}].hide = True") def _create_menu_func(self) -> None: """ Creates the menu function """ - - self._write("def menu_func(self, context):\n") - self._write(f"\tself.layout.operator({self._class_name}.bl_idname)\n") - self._write("\n") + self._write("def menu_func(self, context):", "") + self._write(f"self.layout.operator({self._class_name}.bl_idname)", "\t") + self._write("") def _create_register_func(self) -> None: """ Creates the register function """ - self._write("def register():\n") - self._write(f"\tbpy.utils.register_class({self._class_name})\n") - self._write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - self._write("\n") + self._write("def register():", "") + self._write(f"bpy.utils.register_class({self._class_name})", "\t") + self._write("bpy.types.VIEW3D_MT_object.append(menu_func)", "\t") + self._write("") def _create_unregister_func(self) -> None: """ Creates the unregister function """ - self._write("def unregister():\n") - self._write(f"\tbpy.utils.unregister_class({self._class_name})\n") - self._write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") - self._write("\n") + self._write("def unregister():", "") + self._write(f"bpy.utils.unregister_class({self._class_name})", "\t") + self._write("bpy.types.VIEW3D_MT_object.remove(menu_func)", "\t") + self._write("") def _create_main_func(self) -> None: """ Creates the main function """ - self._write("if __name__ == \"__main__\":\n") - self._write("\tregister()") + self._write("if __name__ == \"__main__\":", "") + self._write("register()", "\t") def _zip_addon(self) -> None: """ @@ -1189,11 +1136,11 @@ def _zip_addon(self) -> None: shutil.rmtree(self._zip_dir) # ABSTRACT - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str) -> None: + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree) -> None: return # ABSTRACT - def _process_node_tree(self, node_tree: NodeTree, level: int) -> None: + def _process_node_tree(self, node_tree: NodeTree) -> None: return def _report_finished(self, object: str): diff --git a/utils.py b/utils.py index 5f587ce..bc0f131 100644 --- a/utils.py +++ b/utils.py @@ -191,24 +191,4 @@ def img_to_py_str(img : bpy.types.Image) -> str: """ name = img.name.split('.', 1)[0] format = img.file_format.lower() - return f"{name}.{format}" - -#TODO: reconsider node tree definitions within node tree definitions -def make_indents(level: int) -> Tuple[str, str]: - """ - Returns strings with the correct number of indentations - given the level in the function. - - Node groups need processed recursively, - so there can sometimes be functions in functions. - - Parameters: - level (int): base number of indentations need - - Returns: - outer (str): a basic level of indentation for a node group. - inner (str): a level of indentation beyond outer - """ - outer = "\t"*level - inner = "\t"*(level + 1) - return outer, inner \ No newline at end of file + return f"{name}.{format}" \ No newline at end of file From 5590f5ea274d33206932e0bdd99ccc6246cf1ffb Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 21 Jan 2024 16:19:47 -0600 Subject: [PATCH 65/72] refactor: group nodes now just use ST.NODE_TREE --- compositor/node_settings.py | 2 ++ compositor/operator.py | 5 +---- geometry/node_settings.py | 2 ++ geometry/operator.py | 8 +++----- material/node_settings.py | 2 ++ material/operator.py | 5 +---- ntp_operator.py | 39 ++++++++++++++++++++----------------- utils.py | 1 + 8 files changed, 33 insertions(+), 31 deletions(-) diff --git a/compositor/node_settings.py b/compositor/node_settings.py index f2780a5..e4ae8ac 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -455,6 +455,8 @@ # MISC + 'CompositorNodeGroup' : [("node_tree", ST.NODE_TREE)], + 'NodeFrame' : [("label_size", ST.INT), ("shrink", ST.BOOL), ("text", ST.TEXT)], diff --git a/compositor/operator.py b/compositor/operator.py index 0ce02e6..b1daada 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -113,9 +113,6 @@ def _process_node(self, node: Node, ntp_nt: NTP_NodeTree): self._set_settings_defaults(node) self._hide_hidden_sockets(node) - if node.bl_idname == 'CompositorNodeGroup': - self._process_group_node_tree(node) - if bpy.app.version < (4, 0, 0): if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: self._group_io_settings(node, "input", ntp_nt) @@ -185,7 +182,7 @@ def execute(self, context): if self.mode == 'ADDON': self._outer = "\t\t" self._inner = "\t\t\t" - + self._setup_addon_directories(context, comp_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") diff --git a/geometry/node_settings.py b/geometry/node_settings.py index a1e5011..d637180 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -459,6 +459,8 @@ 'FunctionNodeQuaternionToRotation' : [], # MISC + 'GeometryNodeGroup' : [("node_tree", ST.NODE_TREE)], + 'NodeFrame' : [("label_size", ST.INT), ("shrink", ST.BOOL), ("text", ST.TEXT)], diff --git a/geometry/operator.py b/geometry/operator.py index 3a1fc95..2f51c3a 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -84,10 +84,7 @@ def _process_node(self, node: Node, ntp_nt: NTP_GeoNodeTree) -> None: self._group_io_settings(node, "output", ntp_nt) ntp_nt.outputs_set = True - if node.bl_idname == 'GeometryNodeGroup': - self._process_group_node_tree(node) - - elif node.bl_idname == 'GeometryNodeSimulationInput': + if node.bl_idname == 'GeometryNodeSimulationInput': ntp_nt.sim_inputs.append(node) elif node.bl_idname == 'GeometryNodeSimulationOutput': @@ -101,7 +98,8 @@ def _process_node(self, node: Node, ntp_nt: NTP_GeoNodeTree) -> None: self._hide_hidden_sockets(node) - if node.bl_idname not in {'GeometryNodeSimulationInput', 'GeometryNodeRepeatInput'}: + if node.bl_idname not in {'GeometryNodeSimulationInput', + 'GeometryNodeRepeatInput'}: self._set_socket_defaults(node) if bpy.app.version >= (3, 6, 0): diff --git a/material/node_settings.py b/material/node_settings.py index 0858187..04ae778 100644 --- a/material/node_settings.py +++ b/material/node_settings.py @@ -263,6 +263,8 @@ ("use_auto_update", ST.BOOL)], # MISC + 'ShaderNodeGroup' : [('node_tree', ST.NODE_TREE)], + 'NodeFrame' : [("label_size", ST.INT), ("shrink", ST.BOOL), ("text", ST.TEXT)], diff --git a/material/operator.py b/material/operator.py index a253238..97bc342 100644 --- a/material/operator.py +++ b/material/operator.py @@ -72,9 +72,6 @@ def _process_node(self, node: Node, ntp_nt: NTP_NodeTree) -> None: self._group_io_settings(node, "output", ntp_nt) ntp_nt.outputs_set = True - if node.bl_idname == 'ShaderNodeGroup': - self._process_group_node_tree(node) - self._hide_hidden_sockets(node) self._set_socket_defaults(node) @@ -135,7 +132,7 @@ def execute(self, context): if self.mode == 'ADDON': self._outer = "\t\t" self._inner = "\t\t\t" - + self._setup_addon_directories(context, mat_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") diff --git a/ntp_operator.py b/ntp_operator.py index a4b4181..1330df6 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -186,24 +186,6 @@ def dfs(nt: NodeTree) -> None: return result - def _process_group_node_tree(self, node: Node) -> None: - """ - Processes node tree of group node if one is present - - Parameters: - node (Node): the group node - """ - node_tree = node.node_tree - if node_tree is None: - return - if node_tree in self._node_tree_vars: - nt_var = self._node_tree_vars[node_tree] - node_var = self._node_vars[node] - self._write(f"{node_var}.node_tree = {nt_var}") - else: - self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " - f"wasn't properly initialized")) - def _create_var(self, name: str) -> str: """ Creates a unique variable name for a node tree @@ -325,6 +307,8 @@ def _set_settings_defaults(self, node: Node) -> None: self._color_ramp_settings(node, attr_name) elif type == ST.CURVE_MAPPING: self._curve_mapping_settings(node, attr_name) + elif type == ST.NODE_TREE: + self._node_tree_settings(node, attr_name) elif type == ST.IMAGE: if self._addon_dir is not None and attr is not None: if attr.source in {'FILE', 'GENERATED', 'TILED'}: @@ -910,6 +894,25 @@ def _curve_mapping_settings(self, node: Node, self._write(f"#update curve after changes") self._write(f"{mapping_var}.update()") + def _node_tree_settings(self, node: Node, attr_name: str) -> None: + """ + Processes node tree of group node if one is present + + Parameters: + node (Node): the group node + attr_name (str): name of the node tree attribute + """ + node_tree = getattr(node, attr_name) + if node_tree is None: + return + if node_tree in self._node_tree_vars: + nt_var = self._node_tree_vars[node_tree] + node_var = self._node_vars[node] + self._write(f"{node_var}.{attr_name} = {nt_var}") + else: + self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " + f"wasn't properly initialized")) + def _save_image(self, img: bpy.types.Image) -> None: """ Saves an image to an image directory of the add-on diff --git a/utils.py b/utils.py index bc0f131..ec5d04d 100644 --- a/utils.py +++ b/utils.py @@ -34,6 +34,7 @@ class ST(Enum): # Special settings COLOR_RAMP = auto() CURVE_MAPPING = auto() + NODE_TREE = auto() # Asset Library MATERIAL = auto() # Handle with asset library From eca90aae44bfc7c93acbf4713685304d54e59efd Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sun, 21 Jan 2024 22:56:15 -0600 Subject: [PATCH 66/72] fix: typo --- ntp_operator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ntp_operator.py b/ntp_operator.py index 1330df6..ab46e40 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -383,7 +383,7 @@ def _group_io_settings(self, node: Node, socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - self._set_group_socket_default(socket_interface, socket_var) + self._set_group_socket_defaults(socket_interface, socket_var) # default attribute name if hasattr(socket_interface, "default_attribute_name"): From d846b8a59b7963c568f96c37925b5381e93d726c Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 22 Jan 2024 10:54:38 -0600 Subject: [PATCH 67/72] fix: introduced NTP reserved names to prevent possible node name conflicts --- compositor/operator.py | 42 +++++++++------- geometry/operator.py | 24 ++++++--- material/operator.py | 8 ++- ntp_operator.py | 111 ++++++++++++++++++++++++++--------------- utils.py | 2 - 5 files changed, 117 insertions(+), 70 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index b1daada..41efd3b 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -8,11 +8,13 @@ from io import StringIO from .node_settings import compositor_node_settings -SCENE_VAR = "scene" -BASE_NAME_VAR = "base_name" -END_NAME_VAR = "end_name" +SCENE = "scene" +BASE_NAME = "base_name" +END_NAME = "end_name" +INDEX = "i" +NODE = "node" -ntp_vars = {SCENE_VAR, BASE_NAME_VAR, END_NAME_VAR} +comp_op_reserved_names = {SCENE, BASE_NAME, END_NAME, INDEX, NODE} class NTPCompositorOperator(NTP_Operator): bl_idname = "node.ntp_compositor" @@ -33,31 +35,33 @@ class NTPCompositorOperator(NTP_Operator): def __init__(self): super().__init__() self._settings = compositor_node_settings + for name in comp_op_reserved_names: + self._used_vars[name] = 1 def _create_scene(self, indent: str): #TODO: wrap in more general unique name util function self._write(f"# Generate unique scene name", indent) - self._write(f"{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}", + self._write(f"{BASE_NAME} = {str_to_py_str(self.compositor_name)}", indent) - self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR}", indent) - self._write(f"if bpy.data.scenes.get({END_NAME_VAR}) != None:", indent) + self._write(f"{END_NAME} = {BASE_NAME}", indent) + self._write(f"if bpy.data.scenes.get({END_NAME}) != None:", indent) indent2 = f"{indent}\t" - self._write(f"i = 1", indent2) - self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"", + self._write(f"{INDEX} = 1", indent2) + self._write(f"{END_NAME} = {BASE_NAME} + f\".{{i:03d}}\"", indent2) - self._write(f"while bpy.data.scenes.get({END_NAME_VAR}) != None:", + self._write(f"while bpy.data.scenes.get({END_NAME}) != None:", indent2) indent3 = f"{indent}\t\t" - self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"", indent3) - self._write(f"i += 1\n", indent3) + self._write(f"{END_NAME} = {BASE_NAME} + f\".{{{INDEX}:03d}}\"", indent3) + self._write(f"{INDEX} += 1\n", indent3) - self._write(f"{SCENE_VAR} = bpy.context.window.scene.copy()\n", indent) - self._write(f"{SCENE_VAR}.name = {END_NAME_VAR}", indent) - self._write(f"{SCENE_VAR}.use_fake_user = True", indent) - self._write(f"bpy.context.window.scene = {SCENE_VAR}", indent) + self._write(f"{SCENE} = bpy.context.window.scene.copy()\n", indent) + self._write(f"{SCENE}.name = {END_NAME}", indent) + self._write(f"{SCENE}.use_fake_user = True", indent) + self._write(f"bpy.context.window.scene = {SCENE}", indent) def _initialize_compositor_node_tree(self, ntp_nt, nt_name): #initialize node group @@ -65,10 +69,10 @@ def _initialize_compositor_node_tree(self, ntp_nt, nt_name): self._write(f"def {ntp_nt.var}_node_group():", self._outer) if ntp_nt.node_tree == self._base_node_tree: - self._write(f"{ntp_nt.var} = {SCENE_VAR}.node_tree") + self._write(f"{ntp_nt.var} = {SCENE}.node_tree") self._write(f"#start with a clean node tree") - self._write(f"for node in {ntp_nt.var}.nodes:") - self._write(f"\t{ntp_nt.var}.nodes.remove(node)") + self._write(f"for {NODE} in {ntp_nt.var}.nodes:") + self._write(f"\t{ntp_nt.var}.nodes.remove({NODE})") else: self._write((f"{ntp_nt.var} = bpy.data.node_groups.new(" f"type = \'CompositorNodeTree\', " diff --git a/geometry/operator.py b/geometry/operator.py index 2f51c3a..1688518 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -16,6 +16,15 @@ from .node_tree import NTP_GeoNodeTree from .node_settings import geo_node_settings +ITEM = "item" +OBJECT_NAME = "name" +OBJECT = "obj" +MODIFIER = "mod" +geo_op_reserved_names = {ITEM, + OBJECT_NAME, + OBJECT, + MODIFIER} + class NTPGeoNodesOperator(NTP_Operator): bl_idname = "node.ntp_geo_nodes" bl_label = "Geo Nodes to Python" @@ -34,6 +43,8 @@ class NTPGeoNodesOperator(NTP_Operator): def __init__(self): super().__init__() self._settings = geo_node_settings + for name in geo_op_reserved_names: + self._used_vars[name] = 1 if bpy.app.version >= (3, 6, 0): def _process_zone_output_node(self, node: GeometryNode) -> None: @@ -50,15 +61,14 @@ def _process_zone_output_node(self, node: GeometryNode) -> None: node_var = self._node_vars[node] self._write(f"# Remove generated {items}") - self._write(f"for item in {node_var}.{items}:") + self._write(f"for {ITEM} in {node_var}.{items}:") self._write(f"\t{node_var}.{items}.remove(item)") for i, item in enumerate(getattr(node, items)): socket_type = enum_to_py_str(item.socket_type) name = str_to_py_str(item.name) self._write(f"# Create item {name}") - self._write(f"{node_var}.{items}.new" - f"({socket_type}, {name})") + self._write(f"{node_var}.{items}.new({socket_type}, {name})") if is_sim: item_var = f"{node_var}.{items}[{i}]" ad = enum_to_py_str(item.attribute_domain) @@ -199,14 +209,14 @@ def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): #get object - self._write(f"name = bpy.context.object.name", self._outer) - self._write(f"obj = bpy.data.objects[name]", self._outer) + self._write(f"{OBJECT_NAME} = bpy.context.object.name", self._outer) + self._write(f"{OBJECT} = bpy.data.objects[{OBJECT_NAME}]", self._outer) #set modifier to the one we just created mod_name = str_to_py_str(nt.name) - self._write(f"mod = obj.modifiers.new(name = {mod_name}, " + self._write(f"{MODIFIER} = obj.modifiers.new(name = {mod_name}, " f"type = 'NODES')", self._outer) - self._write(f"mod.node_group = {nt_var}", self._outer) + self._write(f"{MODIFIER}.node_group = {nt_var}", self._outer) def execute(self, context): diff --git a/material/operator.py b/material/operator.py index 97bc342..16755ec 100644 --- a/material/operator.py +++ b/material/operator.py @@ -10,6 +10,8 @@ from .node_settings import shader_node_settings MAT_VAR = "mat" +NODE = "node" +shader_op_reserved_names = {MAT_VAR, NODE} class NTPMaterialOperator(NTP_Operator): bl_idname = "node.ntp_material" @@ -22,6 +24,8 @@ class NTPMaterialOperator(NTP_Operator): def __init__(self): super().__init__() self._settings = shader_node_settings + for name in shader_op_reserved_names: + self._used_vars[name] = 1 def _create_material(self, indent: str): self._write(f"{MAT_VAR} = bpy.data.materials.new(" @@ -44,8 +48,8 @@ def _initialize_shader_node_tree(self, ntp_node_tree: NTP_NodeTree, if ntp_node_tree.node_tree == self._base_node_tree: self._write(f"{ntp_node_tree.var} = {MAT_VAR}.node_tree") self._write(f"#start with a clean node tree") - self._write(f"for node in {ntp_node_tree.var}.nodes:") - self._write(f"\t{ntp_node_tree.var}.nodes.remove(node)") + self._write(f"for {NODE} in {ntp_node_tree.var}.nodes:") + self._write(f"\t{ntp_node_tree.var}.nodes.remove({NODE})") else: self._write((f"{ntp_node_tree.var} = bpy.data.node_groups.new(" f"type = \'ShaderNodeTree\', " diff --git a/ntp_operator.py b/ntp_operator.py index ab46e40..1c42262 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -16,6 +16,14 @@ from .ntp_node_tree import NTP_NodeTree from .utils import * +IMAGE_DIR_NAME = "imgs" +IMAGE_PATH = "image_path" +BASE_DIR = "base_dir" + +reserved_names = {IMAGE_DIR_NAME, + IMAGE_PATH, + BASE_DIR + } class NTP_Operator(Operator): """ @@ -70,7 +78,6 @@ def __init__(self): self._outer: str = "" self._inner: str = "\t" - # Base node tree we're converting self._base_node_tree: NodeTree = None @@ -86,6 +93,9 @@ def __init__(self): # Dictionary used for setting node properties self._settings: dict[str, list[(str, ST)]] = {} + for name in reserved_names: + self._used_vars[name] = 1 + def _write(self, string: str, indent: str = None): if indent is None: indent = self._inner @@ -192,8 +202,6 @@ def _create_var(self, name: str) -> str: Parameters: name (str): basic string we'd like to create the variable name out of - used_vars (dict[str, int]): dictionary containing variable names and - usage counts Returns: clean_name (str): variable name for the node tree @@ -296,13 +304,11 @@ def _set_settings_defaults(self, node: Node) -> None: elif type == ST.MATERIAL: name = str_to_py_str(attr.name) self._write((f"if {name} in bpy.data.materials:")) - self._write((f"\t{node_var}.{attr_name} = " - f"bpy.data.materials[{name}]")) + self._write((f"\t{setting_str} = bpy.data.materials[{name}]")) elif type == ST.OBJECT: name = str_to_py_str(attr.name) self._write((f"if {name} in bpy.data.objects:")) - self._write((f"\t{node_var}.{attr_name} = " - f"bpy.data.objects[{name}]")) + self._write((f"\t{setting_str} = bpy.data.objects[{name}]")) elif type == ST.COLOR_RAMP: self._color_ramp_settings(node, attr_name) elif type == ST.CURVE_MAPPING: @@ -798,7 +804,7 @@ def _color_ramp_settings(self, node: Node, color_ramp_name: str) -> None: self._write((f"{ramp_str}.elements.remove" f"({ramp_str}.elements[0])")) for i, element in enumerate(color_ramp.elements): - element_var = f"{node_var}_cre_{i}" + element_var = self._create_var(f"{node_var}_cre_{i}") if i == 0: self._write(f"{element_var} = {ramp_str}.elements[{i}]") self._write(f"{element_var}.position = {element.position}") @@ -861,39 +867,64 @@ def _curve_mapping_settings(self, node: Node, # create curves for i, curve in enumerate(mapping.curves): - # TODO: curve function - self._write(f"#curve {i}") - curve_i = f"{node_var}_curve_{i}" - self._write(f"{curve_i} = " - f"{node_var}.{curve_mapping_name}.curves[{i}]") - - # Remove default points when CurveMap is initialized with more than - # two points (just CompositorNodeHueCorrect) - if (node.bl_idname == 'CompositorNodeHueCorrect'): - self._write((f"for i in range" - f"(len({curve_i}.points.values()) - 1, 1, -1):")) - self._write( - f"\t{curve_i}.points.remove({curve_i}.points[i])") - - for j, point in enumerate(curve.points): - # TODO: point function - point_j = f"{curve_i}_point_{j}" - - loc = point.location - loc_str = f"{loc[0]}, {loc[1]}" - if j < 2: - self._write(f"{point_j} = {curve_i}.points[{j}]") - self._write(f"{point_j}.location = ({loc_str})") - else: - self._write(f"{point_j} = {curve_i}.points.new({loc_str})") - - handle = enum_to_py_str(point.handle_type) - self._write(f"{point_j}.handle_type = {handle}") + self._create_curve_map(node, i, curve, curve_mapping_name) # update curve self._write(f"#update curve after changes") self._write(f"{mapping_var}.update()") + def _create_curve_map(self, node: Node, i: int, curve: bpy.types.CurveMap, + curve_mapping_name: str) -> None: + """ + Helper function to create the ith curve of a node's curve mapping + + Parameters: + node (Node): the node with a curve mapping + i (int): index of the CurveMap within the mapping + curve (bpy.types.CurveMap): the curve map to recreate + curve_mapping_name (str): attribute name of the recreated curve mapping + """ + node_var = self._node_vars[node] + + self._write(f"#curve {i}") + curve_i_var = self._create_var(f"{node_var}_curve_{i}") + self._write(f"{curve_i_var} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]") + + # Remove default points when CurveMap is initialized with more than + # two points (just CompositorNodeHueCorrect) + if (node.bl_idname == 'CompositorNodeHueCorrect'): + self._write(f"for i in range" + f"(len({curve_i_var}.points.values()) - 1, 1, -1):") + self._write(f"\t{curve_i_var}.points.remove(" + f"{curve_i_var}.points[i])") + + for j, point in enumerate(curve.points): + self._create_curve_map_point(j, point, curve_i_var) + + def _create_curve_map_point(self, j: int, point: bpy.types.CurveMapPoint, + curve_i_var: str) -> None: + """ + Helper function to recreate a curve map point + + Parameters: + j (int): index of the point within the curve map + point (CurveMapPoint): point to recreate + curve_i_var (str): variable name of the point's curve map + """ + point_j_var = self._create_var(f"{curve_i_var}_point_{j}") + + loc = point.location + loc_str = f"{loc[0]}, {loc[1]}" + if j < 2: + self._write(f"{point_j_var} = {curve_i_var}.points[{j}]") + self._write(f"{point_j_var}.location = ({loc_str})") + else: + self._write(f"{point_j_var} = {curve_i_var}.points.new({loc_str})") + + handle = enum_to_py_str(point.handle_type) + self._write(f"{point_j_var}.handle_type = {handle}") + def _node_tree_settings(self, node: Node, attr_name: str) -> None: """ Processes node tree of group node if one is present @@ -951,13 +982,13 @@ def _load_image(self, img: bpy.types.Image, img_var: str) -> None: # TODO: convert to special variables self._write(f"#load image {img_str}") - self._write(f"base_dir = " + self._write(f"{BASE_DIR} = " f"os.path.dirname(os.path.abspath(__file__))") - self._write(f"image_path = " - f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " + self._write(f"{IMAGE_PATH} = " + f"os.path.join({BASE_DIR}, \"{IMAGE_DIR_NAME}\", " f"\"{img_str}\")") self._write(f"{img_var} = bpy.data.images.load" - f"(image_path, check_existing = True)") + f"({IMAGE_PATH}, check_existing = True)") # copy image settings self._write(f"#set image settings") diff --git a/utils.py b/utils.py index ec5d04d..5b3cb5d 100644 --- a/utils.py +++ b/utils.py @@ -7,8 +7,6 @@ import re from typing import Tuple -IMAGE_DIR_NAME = "imgs" - #node input sockets that are messy to set default values for dont_set_defaults = {'NodeSocketGeometry', 'NodeSocketShader', From 63f73778febc7742b69baa89fb0fd39e1f60a942 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:19:48 -0600 Subject: [PATCH 68/72] fix: add don't allow index param globally --- compositor/operator.py | 3 +-- ntp_operator.py | 12 +++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 41efd3b..907a203 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -11,10 +11,9 @@ SCENE = "scene" BASE_NAME = "base_name" END_NAME = "end_name" -INDEX = "i" NODE = "node" -comp_op_reserved_names = {SCENE, BASE_NAME, END_NAME, INDEX, NODE} +comp_op_reserved_names = {SCENE, BASE_NAME, END_NAME, NODE} class NTPCompositorOperator(NTP_Operator): bl_idname = "node.ntp_compositor" diff --git a/ntp_operator.py b/ntp_operator.py index 1c42262..4d11379 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -16,11 +16,14 @@ from .ntp_node_tree import NTP_NodeTree from .utils import * +INDEX = "i" IMAGE_DIR_NAME = "imgs" IMAGE_PATH = "image_path" BASE_DIR = "base_dir" -reserved_names = {IMAGE_DIR_NAME, +reserved_names = { + INDEX, + IMAGE_DIR_NAME, IMAGE_PATH, BASE_DIR } @@ -778,8 +781,7 @@ def _color_ramp_settings(self, node: Node, color_ramp_name: str) -> None: color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name) if not color_ramp: - raise ValueError( - f"No color ramp named \"{color_ramp_name}\" found") + raise ValueError(f"No color ramp named \"{color_ramp_name}\" found") node_var = self._node_vars[node] @@ -894,10 +896,10 @@ def _create_curve_map(self, node: Node, i: int, curve: bpy.types.CurveMap, # Remove default points when CurveMap is initialized with more than # two points (just CompositorNodeHueCorrect) if (node.bl_idname == 'CompositorNodeHueCorrect'): - self._write(f"for i in range" + self._write(f"for {INDEX} in range" f"(len({curve_i_var}.points.values()) - 1, 1, -1):") self._write(f"\t{curve_i_var}.points.remove(" - f"{curve_i_var}.points[i])") + f"{curve_i_var}.points[{INDEX}])") for j, point in enumerate(curve.points): self._create_curve_map_point(j, point, curve_i_var) From fcf74a521954c9d76d7b948f63b52dd0367afc8a Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 22 Jan 2024 11:24:46 -0600 Subject: [PATCH 69/72] fix: indexing now matches other repeat variables --- compositor/operator.py | 4 ++-- geometry/operator.py | 2 +- material/operator.py | 2 +- ntp_operator.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/compositor/operator.py b/compositor/operator.py index 907a203..8078912 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -2,7 +2,7 @@ from bpy.types import Node, CompositorNodeColorBalance, CompositorNodeTree -from ..ntp_operator import NTP_Operator +from ..ntp_operator import NTP_Operator, INDEX from ..ntp_node_tree import NTP_NodeTree from ..utils import * from io import StringIO @@ -35,7 +35,7 @@ def __init__(self): super().__init__() self._settings = compositor_node_settings for name in comp_op_reserved_names: - self._used_vars[name] = 1 + self._used_vars[name] = 0 def _create_scene(self, indent: str): diff --git a/geometry/operator.py b/geometry/operator.py index 1688518..a230ac8 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -44,7 +44,7 @@ def __init__(self): super().__init__() self._settings = geo_node_settings for name in geo_op_reserved_names: - self._used_vars[name] = 1 + self._used_vars[name] = 0 if bpy.app.version >= (3, 6, 0): def _process_zone_output_node(self, node: GeometryNode) -> None: diff --git a/material/operator.py b/material/operator.py index 16755ec..485b530 100644 --- a/material/operator.py +++ b/material/operator.py @@ -25,7 +25,7 @@ def __init__(self): super().__init__() self._settings = shader_node_settings for name in shader_op_reserved_names: - self._used_vars[name] = 1 + self._used_vars[name] = 0 def _create_material(self, indent: str): self._write(f"{MAT_VAR} = bpy.data.materials.new(" diff --git a/ntp_operator.py b/ntp_operator.py index 4d11379..1532450 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -97,7 +97,7 @@ def __init__(self): self._settings: dict[str, list[(str, ST)]] = {} for name in reserved_names: - self._used_vars[name] = 1 + self._used_vars[name] = 0 def _write(self, string: str, indent: str = None): if indent is None: From 50729ab73f75ca584b2b93db3dfdaad2f7e526c3 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:34:28 -0600 Subject: [PATCH 70/72] cleanup: minor import tweaks --- geometry/operator.py | 7 ------- ntp_operator.py | 11 +++++------ 2 files changed, 5 insertions(+), 13 deletions(-) diff --git a/geometry/operator.py b/geometry/operator.py index a230ac8..9733a15 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -2,13 +2,6 @@ from bpy.types import GeometryNode, GeometryNodeTree from bpy.types import Node -if bpy.app.version >= (3, 6, 0): - from bpy.types import GeometryNodeSimulationInput - from bpy.types import GeometryNodeSimulationOutput -if bpy.app.version >= (4, 0, 0): - from bpy.types import GeometryNodeRepeatInput - from bpy.types import GeometryNodeRepeatOutput - from io import StringIO from ..ntp_operator import NTP_Operator diff --git a/ntp_operator.py b/ntp_operator.py index 1532450..a7b1862 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -225,7 +225,7 @@ def _create_node(self, node: Node, node_tree_var: str) -> str: Initializes a new node with location, dimension, and label info Parameters: - node (bpy.types.Node): node to be copied + node (Node): node to be copied node_tree_var (str): variable name for the node tree Returns: node_var (str): variable name for the node @@ -262,7 +262,7 @@ def _set_settings_defaults(self, node: Node) -> None: Sets the defaults for any settings a node may have Parameters: - node (bpy.types.Node): the node object we're copying settings from + node (Node): the node object we're copying settings from node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname not in self._settings: @@ -365,10 +365,9 @@ def _group_io_settings(self, node: Node, Set the settings for group input and output sockets Parameters: - node (bpy.types.Node) : group input/output node + node (Node) : group input/output node io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating + ntp_node_tree (NTP_NodeTree): node tree that we're generating input and output settings for """ node_tree_var = ntp_node_tree.var @@ -724,7 +723,7 @@ def _set_output_defaults(self, node: Node) -> None: Some output sockets need default values set. It's rather annoying Parameters: - node (bpy.types.Node): node for the output we're setting + node (Node): node for the output we're setting """ # TODO: probably should define elsewhere output_default_nodes = {'ShaderNodeValue', From af1711fdccb1c039032629586e80031ef2ea7ddd Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Mon, 22 Jan 2024 18:37:37 -0600 Subject: [PATCH 71/72] feat: node settings now have version numbers --- compositor/node_settings.py | 935 +++++++++++++++++++++--------------- compositor/operator.py | 22 +- geometry/node_settings.py | 673 +++++++++++++++++--------- geometry/operator.py | 4 +- material/node_settings.py | 545 +++++++++++++-------- material/operator.py | 4 +- ntp_operator.py | 59 ++- utils.py | 16 +- 8 files changed, 1366 insertions(+), 892 deletions(-) diff --git a/compositor/node_settings.py b/compositor/node_settings.py index e4ae8ac..8034b30 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -1,447 +1,580 @@ -from ..utils import ST +from ..utils import ST, NTPNodeSetting -compositor_node_settings : dict[str, list[(str, ST)]] = { +compositor_node_settings : dict[str, list[NTPNodeSetting]] = { # INPUT - 'CompositorNodeBokehImage' : [("angle", ST.FLOAT), - ("catadioptric", ST.FLOAT), - ("flaps", ST.INT), - ("rounding", ST.FLOAT), - ("shift", ST.FLOAT)], - - 'CompositorNodeImage' : [("frame_duration", ST.INT), - ("frame_offset", ST.INT), - ("frame_start", ST.INT), - ("image", ST.IMAGE), - ("layer", ST.ENUM), - ("use_auto_refresh", ST.BOOL), - ("use_cyclic", ST.BOOL), - ("use_straight_alpha_output", ST.BOOL), - ("view", ST.ENUM)], - - 'CompositorNodeMask' : [("mask", ST.MASK), - ("motion_blur_samples", ST.INT), - ("motion_blur_shutter", ST.FLOAT), - ("size_source", ST.ENUM), - ("size_x", ST.INT), - ("size_y", ST.INT), - ("use_feather", ST.BOOL), - ("use_motion_blur", ST.BOOL)], - - 'CompositorNodeMovieClip' : [("clip", ST.MOVIE_CLIP)], - - 'CompositorNodeTexture' : [("node_output", ST.INT), #TODO: ?? - ("texture", ST.TEXTURE)], + 'CompositorNodeBokehImage' : [ + NTPNodeSetting("angle", ST.FLOAT), + NTPNodeSetting("catadioptric", ST.FLOAT), + NTPNodeSetting("flaps", ST.INT), + NTPNodeSetting("rounding", ST.FLOAT), + NTPNodeSetting("shift", ST.FLOAT) + ], + + 'CompositorNodeImage' : [ + NTPNodeSetting("frame_duration", ST.INT), + NTPNodeSetting("frame_offset", ST.INT), + NTPNodeSetting("frame_start", ST.INT), + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("layer", ST.ENUM), + NTPNodeSetting("use_auto_refresh", ST.BOOL), + NTPNodeSetting("use_cyclic", ST.BOOL), + NTPNodeSetting("use_straight_alpha_output", ST.BOOL), + NTPNodeSetting("view", ST.ENUM) + ], + + 'CompositorNodeMask' : [ + NTPNodeSetting("mask", ST.MASK), + NTPNodeSetting("motion_blur_samples", ST.INT), + NTPNodeSetting("motion_blur_shutter", ST.FLOAT), + NTPNodeSetting("size_source", ST.ENUM), + NTPNodeSetting("size_x", ST.INT), + NTPNodeSetting("size_y", ST.INT), + NTPNodeSetting("use_feather", ST.BOOL), + NTPNodeSetting("use_motion_blur", ST.BOOL) + ], + + 'CompositorNodeMovieClip' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP) + ], + + 'CompositorNodeTexture' : [ + NTPNodeSetting("node_output", ST.INT), #TODO: ?? + NTPNodeSetting("texture", ST.TEXTURE) + ], # Input > Constant - - 'CompositorNodeRGB' : [], - 'CompositorNodeValue' : [], + 'CompositorNodeRGB' : [], + 'CompositorNodeValue' : [], # Input > Scene + 'CompositorNodeRLayers' : [ + NTPNodeSetting("layer", ST.ENUM), + NTPNodeSetting("scene", ST.SCENE) + ], - 'CompositorNodeRLayers' : [("layer", ST.ENUM), - ("scene", ST.SCENE)], - - 'CompositorNodeSceneTime' : [], + 'CompositorNodeSceneTime' : [], - 'CompositorNodeTime' : [("curve", ST.CURVE_MAPPING), - ("frame_end", ST.INT), - ("frame_start", ST.INT)], + 'CompositorNodeTime' : [ + NTPNodeSetting("curve", ST.CURVE_MAPPING), + NTPNodeSetting("frame_end", ST.INT), + NTPNodeSetting("frame_start", ST.INT) + ], # OUTPUT - 'CompositorNodeComposite' : [("use_alpha", ST.BOOL)], - - 'CompositorNodeOutputFile' : [("active_input_index", ST.INT), #TODO: probably not right at all - - ("base_path", ST.STRING), - ("file_slots", ST.FILE_SLOTS), - ("format", ST.IMAGE_FORMAT_SETTINGS), - ("layer_slots", ST.LAYER_SLOTS)], - - 'CompositorNodeViewer' : [("center_x", ST.FLOAT), - ("center_y", ST.FLOAT), - ("tile_order", ST.ENUM), - ("use_alpha", ST.BOOL)], - - 'CompositorNodeSplitViewer' : [("axis", ST.ENUM), - ("factor", ST.INT)], + 'CompositorNodeComposite' : [ + NTPNodeSetting("use_alpha", ST.BOOL) + ], + + 'CompositorNodeOutputFile' : [ + NTPNodeSetting("active_input_index", ST.INT), #TODO: probably not right at all + NTPNodeSetting("base_path", ST.STRING), + NTPNodeSetting("file_slots", ST.FILE_SLOTS), + NTPNodeSetting("format", ST.IMAGE_FORMAT_SETTINGS), + NTPNodeSetting("layer_slots", ST.LAYER_SLOTS) + ], + + 'CompositorNodeViewer' : [ + NTPNodeSetting("center_x", ST.FLOAT), + NTPNodeSetting("center_y", ST.FLOAT), + NTPNodeSetting("tile_order", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL) + ], + + 'CompositorNodeSplitViewer' : [ + NTPNodeSetting("axis", ST.ENUM), + NTPNodeSetting("factor", ST.INT) + ], # COLOR - 'CompositorNodePremulKey' : [("mapping", ST.ENUM)], - - 'CompositorNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + 'CompositorNodePremulKey' : [ + NTPNodeSetting("mapping", ST.ENUM) + ], - 'CompositorNodeConvertColorSpace' : [("from_color_space", ST.ENUM), - ("to_color_space", ST.ENUM)], + 'CompositorNodeValToRGB' : [ + NTPNodeSetting("color_ramp", ST.COLOR_RAMP) + ], + 'CompositorNodeConvertColorSpace' : [ + NTPNodeSetting("from_color_space", ST.ENUM, min_version=(3, 1, 0)), + NTPNodeSetting("to_color_space", ST.ENUM, min_version=(3, 1, 0)) + ], - 'CompositorNodeSetAlpha' : [("mode", ST.ENUM)], + 'CompositorNodeSetAlpha' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'CompositorNodeInvert' : [("invert_alpha", ST.BOOL), - ("invert_rgb", ST.BOOL)], - - 'CompositorNodeRGBToBW' : [], + 'CompositorNodeInvert' : [ + NTPNodeSetting("invert_alpha", ST.BOOL), + NTPNodeSetting("invert_rgb", ST.BOOL) + ], + 'CompositorNodeRGBToBW' : [], # Color > Adjust - 'CompositorNodeBrightContrast' : [("use_premultiply", ST.BOOL)], - - 'CompositorNodeColorBalance' : [("correction_method", ST.ENUM), - ("gain", ST.COLOR), - ("gamma", ST.COLOR), - ("lift", ST.COLOR), - ("offset", ST.COLOR), - ("offset_basis", ST.FLOAT), - ("power", ST.COLOR), - ("slope", ST.COLOR)], - - 'CompositorNodeColorCorrection' : [("red", ST.BOOL), - ("green", ST.BOOL), - ("blue", ST.BOOL), - #master - ("master_saturation", ST.FLOAT), - ("master_contrast", ST.FLOAT), - ("master_gamma", ST.FLOAT), - ("master_gain", ST.FLOAT), - ("master_lift", ST.FLOAT), - #highlights - ("highlights_saturation", ST.FLOAT), - ("highlights_contrast", ST.FLOAT), - ("highlights_gamma", ST.FLOAT), - ("highlights_gain", ST.FLOAT), - ("highlights_lift", ST.FLOAT), - #midtones - ("midtones_saturation", ST.FLOAT), - ("midtones_contrast", ST.FLOAT), - ("midtones_gamma", ST.FLOAT), - ("midtones_gain", ST.FLOAT), - ("midtones_lift", ST.FLOAT), - #shadows - ("shadows_saturation", ST.FLOAT), - ("shadows_contrast", ST.FLOAT), - ("shadows_gamma", ST.FLOAT), - ("shadows_gain", ST.FLOAT), - ("shadows_lift", ST.FLOAT), - #midtones location - ("midtones_start", ST.FLOAT), - ("midtones_end", ST.FLOAT)], - - 'CompositorNodeExposure' : [], - 'CompositorNodeGamma' : [], - - 'CompositorNodeHueCorrect' : [("mapping", ST.CURVE_MAPPING)], - - 'CompositorNodeHueSat' : [], - - 'CompositorNodeCurveRGB' : [("mapping", ST.CURVE_MAPPING)], - - 'CompositorNodeTonemap' : [("adaptation", ST.FLOAT), - ("contrast", ST.FLOAT), - ("correction", ST.FLOAT), - ("gamma", ST.FLOAT), - ("intensity", ST.FLOAT), - ("key", ST.FLOAT), - ("offset", ST.FLOAT), - ("tonemap_type", ST.ENUM)], + 'CompositorNodeBrightContrast' : [ + NTPNodeSetting("use_premultiply", ST.BOOL) + ], + + 'CompositorNodeColorBalance' : [ + NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("gain", ST.COLOR), + NTPNodeSetting("gamma", ST.COLOR), + NTPNodeSetting("lift", ST.COLOR), + NTPNodeSetting("offset", ST.COLOR), + NTPNodeSetting("offset_basis", ST.FLOAT), + NTPNodeSetting("power", ST.COLOR), + NTPNodeSetting("slope", ST.COLOR) + ], + + 'CompositorNodeColorCorrection' : [ + NTPNodeSetting("red", ST.BOOL), + NTPNodeSetting("green", ST.BOOL), + NTPNodeSetting("blue", ST.BOOL), + #master + NTPNodeSetting("master_saturation", ST.FLOAT), + NTPNodeSetting("master_contrast", ST.FLOAT), + NTPNodeSetting("master_gamma", ST.FLOAT), + NTPNodeSetting("master_gain", ST.FLOAT), + NTPNodeSetting("master_lift", ST.FLOAT), + #highlights + NTPNodeSetting("highlights_saturation", ST.FLOAT), + NTPNodeSetting("highlights_contrast", ST.FLOAT), + NTPNodeSetting("highlights_gamma", ST.FLOAT), + NTPNodeSetting("highlights_gain", ST.FLOAT), + NTPNodeSetting("highlights_lift", ST.FLOAT), + #midtones + NTPNodeSetting("midtones_saturation", ST.FLOAT), + NTPNodeSetting("midtones_contrast", ST.FLOAT), + NTPNodeSetting("midtones_gamma", ST.FLOAT), + NTPNodeSetting("midtones_gain", ST.FLOAT), + NTPNodeSetting("midtones_lift", ST.FLOAT), + #shadows + NTPNodeSetting("shadows_saturation", ST.FLOAT), + NTPNodeSetting("shadows_contrast", ST.FLOAT), + NTPNodeSetting("shadows_gamma", ST.FLOAT), + NTPNodeSetting("shadows_gain", ST.FLOAT), + NTPNodeSetting("shadows_lift", ST.FLOAT), + #midtones location + NTPNodeSetting("midtones_start", ST.FLOAT), + NTPNodeSetting("midtones_end", ST.FLOAT) + ], + + 'CompositorNodeExposure' : [], + 'CompositorNodeGamma' : [], + + 'CompositorNodeHueCorrect' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'CompositorNodeHueSat' : [], + + 'CompositorNodeCurveRGB' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'CompositorNodeTonemap' : [ + NTPNodeSetting("adaptation", ST.FLOAT), + NTPNodeSetting("contrast", ST.FLOAT), + NTPNodeSetting("correction", ST.FLOAT), + NTPNodeSetting("gamma", ST.FLOAT), + NTPNodeSetting("intensity", ST.FLOAT), + NTPNodeSetting("key", ST.FLOAT), + NTPNodeSetting("offset", ST.FLOAT), + NTPNodeSetting("tonemap_type", ST.ENUM) + ], # Color > Mix - 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), - ("use_premultiply", ST.BOOL)], - - 'CompositorNodeCombineColor' : [("mode", ST.ENUM), - ("ycc_mode", ST.ENUM)], + 'CompositorNodeAlphaOver' : [ + NTPNodeSetting("premul", ST.FLOAT), + NTPNodeSetting("use_premultiply", ST.BOOL) + ], - 'CompositorNodeSeparateColor' : [("mode", ST.ENUM), - ("ycc_mode", ST.ENUM)], + 'CompositorNodeCombineColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)), + NTPNodeSetting("ycc_mode", ST.ENUM, min_version = (3, 3, 0)) + ], - 'CompositorNodeMixRGB' : [("blend_type", ST.ENUM), - ("use_alpha", ST.BOOL), - ("use_clamp", ST.BOOL)], #TODO: what is update() method for? + 'CompositorNodeSeparateColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)), + NTPNodeSetting("ycc_mode", ST.ENUM, min_version = (3, 3, 0)) + ], - 'CompositorNodeZcombine' : [("use_alpha", ST.BOOL), - ("use_antialias_z", ST.BOOL)], + 'CompositorNodeMixRGB' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + 'CompositorNodeZcombine' : [ + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_antialias_z", ST.BOOL) + ], # FILTER - 'CompositorNodeAntiAliasing' : [("contrast_limit", ST.FLOAT), - ("corner_rounding", ST.FLOAT), - ("threshold", ST.FLOAT)], + 'CompositorNodeAntiAliasing' : [ + NTPNodeSetting("contrast_limit", ST.FLOAT), + NTPNodeSetting("corner_rounding", ST.FLOAT), + NTPNodeSetting("threshold", ST.FLOAT) + ], - 'CompositorNodeDenoise' : [("prefilter", ST.ENUM), - ("use_hdr", ST.BOOL)], - - 'CompositorNodeDespeckle' : [("threshold", ST.FLOAT), - ("threshold_neighbor", ST.FLOAT)], - - - 'CompositorNodeDilateErode' : [("distance", ST.INT), - ("edge", ST.FLOAT), - ("falloff", ST.ENUM), - ("mode", ST.ENUM)], - - 'CompositorNodeInpaint' : [("distance", ST.INT)], - - - 'CompositorNodeFilter' : [("filter_type", ST.ENUM)], - - 'CompositorNodeGlare' : [("angle_offset", ST.FLOAT), - ("color_modulation", ST.FLOAT), - ("fade", ST.FLOAT), - ("glare_type", ST.ENUM), - ("iterations", ST.INT), - ("mix", ST.FLOAT), - ("quality", ST.ENUM), - ("size", ST.INT), - ("streaks", ST.INT), - ("threshold", ST.FLOAT), - ("use_rotate_45", ST.BOOL)], + 'CompositorNodeDenoise' : [ + NTPNodeSetting("prefilter", ST.ENUM), + NTPNodeSetting("use_hdr", ST.BOOL) + ], + + 'CompositorNodeDespeckle' : [ + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("threshold_neighbor", ST.FLOAT) + ], + + 'CompositorNodeDilateErode' : [ + NTPNodeSetting("distance", ST.INT), + NTPNodeSetting("edge", ST.FLOAT), + NTPNodeSetting("falloff", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM) + ], + + 'CompositorNodeInpaint' : [ + NTPNodeSetting("distance", ST.INT) + ], + + 'CompositorNodeFilter' : [ + NTPNodeSetting("filter_type", ST.ENUM) + ], + + 'CompositorNodeGlare' : [ + NTPNodeSetting("angle_offset", ST.FLOAT), + NTPNodeSetting("color_modulation", ST.FLOAT), + NTPNodeSetting("fade", ST.FLOAT), + NTPNodeSetting("glare_type", ST.ENUM), + NTPNodeSetting("iterations", ST.INT), + NTPNodeSetting("mix", ST.FLOAT), + NTPNodeSetting("quality", ST.ENUM), + NTPNodeSetting("size", ST.INT), + NTPNodeSetting("streaks", ST.INT), + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("use_rotate_45", ST.BOOL) + ], - 'CompositorNodeKuwahara' : [("eccentricity", ST.FLOAT), - ("sharpness", ST.FLOAT), - ("size", ST.INT), - ("uniformity", ST.INT), - ("variation", ST.ENUM)], - - 'CompositorNodePixelate' : [], - 'CompositorNodePosterize' : [], - - 'CompositorNodeSunBeams' : [("ray_length", ST.FLOAT), - ("source", ST.VEC2)], + 'CompositorNodeKuwahara' : [ + NTPNodeSetting("eccentricity", ST.FLOAT, min_version = (4, 0, 0)), + NTPNodeSetting("sharpness", ST.FLOAT, min_version = (4, 0, 0)), + NTPNodeSetting("size", ST.INT, min_version = (4, 0, 0)), + NTPNodeSetting("uniformity", ST.INT, min_version = (4, 0, 0)), + NTPNodeSetting("variation", ST.ENUM, min_version = (4, 0, 0)) + ], + + 'CompositorNodePixelate' : [], + 'CompositorNodePosterize' : [], + + 'CompositorNodeSunBeams' : [ + NTPNodeSetting("ray_length", ST.FLOAT), + NTPNodeSetting("source", ST.VEC2) + ], # Filter > Blur - 'CompositorNodeBilateralblur' : [("iterations", ST.INT), - ("sigma_color", ST.FLOAT), - ("sigma_space", ST.FLOAT)], - - 'CompositorNodeBlur' : [("aspect_correction", ST.ENUM), - ("factor", ST.FLOAT), - ("factor_x", ST.FLOAT), - ("factor_y", ST.FLOAT), - ("filter_type", ST.ENUM), - ("size_x", ST.INT), - ("size_y", ST.INT), - ("use_bokeh", ST.BOOL), - ("use_extended_bounds", ST.BOOL), - ("use_gamma_correction", ST.BOOL), - ("use_relative", ST.BOOL), - ("use_variable_size", ST.BOOL)], - - 'CompositorNodeBokehBlur' : [("blur_max", ST.FLOAT), - ("use_extended_bounds", ST.BOOL), - ("use_variable_size", ST.BOOL)], - - 'CompositorNodeDefocus' : [("angle", ST.FLOAT), - ("blur_max", ST.FLOAT), - ("bokeh", ST.ENUM), - ("f_stop", ST.FLOAT), - ("scene", ST.SCENE), - ("threshold", ST.FLOAT), - ("use_gamma_correction", ST.BOOL), - ("use_preview", ST.BOOL), - ("use_zbuffer", ST.BOOL), - ("z_scale", ST.FLOAT)], - - 'CompositorNodeDBlur' : [("angle", ST.FLOAT), - ("center_x", ST.FLOAT), - ("center_y", ST.FLOAT), - ("distance", ST.FLOAT), - ("iterations", ST.INT), - ("spin", ST.FLOAT), - ("zoom", ST.FLOAT)], + 'CompositorNodeBilateralblur' : [ + NTPNodeSetting("iterations", ST.INT), + NTPNodeSetting("sigma_color", ST.FLOAT), + NTPNodeSetting("sigma_space", ST.FLOAT) + ], + + 'CompositorNodeBlur' : [ + NTPNodeSetting("aspect_correction", ST.ENUM), + NTPNodeSetting("factor", ST.FLOAT), + NTPNodeSetting("factor_x", ST.FLOAT), + NTPNodeSetting("factor_y", ST.FLOAT), + NTPNodeSetting("filter_type", ST.ENUM), + NTPNodeSetting("size_x", ST.INT), + NTPNodeSetting("size_y", ST.INT), + NTPNodeSetting("use_bokeh", ST.BOOL), + NTPNodeSetting("use_extended_bounds", ST.BOOL), + NTPNodeSetting("use_gamma_correction", ST.BOOL), + NTPNodeSetting("use_relative", ST.BOOL), + NTPNodeSetting("use_variable_size", ST.BOOL) + ], + + 'CompositorNodeBokehBlur' : [ + NTPNodeSetting("blur_max", ST.FLOAT), + NTPNodeSetting("use_extended_bounds", ST.BOOL), + NTPNodeSetting("use_variable_size", ST.BOOL) + ], + + 'CompositorNodeDefocus' : [ + NTPNodeSetting("angle", ST.FLOAT), + NTPNodeSetting("blur_max", ST.FLOAT), + NTPNodeSetting("bokeh", ST.ENUM), + NTPNodeSetting("f_stop", ST.FLOAT), + NTPNodeSetting("scene", ST.SCENE), + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("use_gamma_correction", ST.BOOL), + NTPNodeSetting("use_preview", ST.BOOL), + NTPNodeSetting("use_zbuffer", ST.BOOL), + NTPNodeSetting("z_scale", ST.FLOAT) + ], + + 'CompositorNodeDBlur' : [ + NTPNodeSetting("angle", ST.FLOAT), + NTPNodeSetting("center_x", ST.FLOAT), + NTPNodeSetting("center_y", ST.FLOAT), + NTPNodeSetting("distance", ST.FLOAT), + NTPNodeSetting("iterations", ST.INT), + NTPNodeSetting("spin", ST.FLOAT), + NTPNodeSetting("use_wrap", ST.BOOL, max_version = (3, 4, 0)), + NTPNodeSetting("zoom", ST.FLOAT) + ], - 'CompositorNodeVecBlur' : [("factor", ST.FLOAT), - ("samples", ST.INT), - ("speed_max", ST.INT), - ("speed_min", ST.INT), - ("use_curved", ST.BOOL)], + 'CompositorNodeVecBlur' : [ + NTPNodeSetting("factor", ST.FLOAT), + NTPNodeSetting("samples", ST.INT), + NTPNodeSetting("speed_max", ST.INT), + NTPNodeSetting("speed_min", ST.INT), + NTPNodeSetting("use_curved", ST.BOOL) + ], # KEYING - 'CompositorNodeChannelMatte' : [("color_space", ST.ENUM), - ("limit_channel", ST.ENUM), - ("limit_max", ST.FLOAT), - ("limit_method", ST.ENUM), - ("limit_min", ST.FLOAT), - ("matte_channel", ST.ENUM)], - - 'CompositorNodeChromaMatte' : [("gain", ST.FLOAT), - ("lift", ST.FLOAT), - ("shadow_adjust", ST.FLOAT), - ("threshold", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeColorMatte' : [("color_hue", ST.FLOAT), - ("color_saturation", ST.FLOAT), - ("color_value", ST.FLOAT)], - - 'CompositorNodeColorSpill' : [("channel", ST.ENUM), - ("limit_channel", ST.ENUM), - ("limit_method", ST.ENUM), - ("ratio", ST.FLOAT), - ("unspill_blue", ST.FLOAT), - ("unspill_green", ST.FLOAT), - ("unspill_red", ST.FLOAT), - ("use_unspill", ST.BOOL)], - - 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeDistanceMatte' : [("channel", ST.ENUM), - ("falloff", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeKeying' : [("blur_post", ST.INT), - ("blur_pre", ST.INT), - ("clip_black", ST.FLOAT), - ("clip_white", ST.FLOAT), - ("despill_balance", ST.FLOAT), - ("despill_factor", ST.FLOAT), - ("dilate_distance", ST.INT), - ("edge_kernel_radius", ST.INT), - ("edge_kernel_tolerance", ST.FLOAT), - ("feather_distance", ST.INT), - ("feather_falloff", ST.ENUM), - ("screen_balance", ST.FLOAT)], - - 'CompositorNodeKeyingScreen' : [("clip", ST.MOVIE_CLIP), - ("tracing_object", ST.STRING)], - - 'CompositorNodeLumaMatte' : [("limit_max", ST.FLOAT), - ("limit_min", ST.FLOAT)], + 'CompositorNodeChannelMatte' : [ + NTPNodeSetting("color_space", ST.ENUM), + NTPNodeSetting("limit_channel", ST.ENUM), + NTPNodeSetting("limit_max", ST.FLOAT), + NTPNodeSetting("limit_method", ST.ENUM), + NTPNodeSetting("limit_min", ST.FLOAT), + NTPNodeSetting("matte_channel", ST.ENUM) + ], + + 'CompositorNodeChromaMatte' : [ + NTPNodeSetting("gain", ST.FLOAT), + NTPNodeSetting("lift", ST.FLOAT), + NTPNodeSetting("shadow_adjust", ST.FLOAT), + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("tolerance", ST.FLOAT) + ], + + 'CompositorNodeColorMatte' : [ + NTPNodeSetting("color_hue", ST.FLOAT), + NTPNodeSetting("color_saturation", ST.FLOAT), + NTPNodeSetting("color_value", ST.FLOAT) + ], + + 'CompositorNodeColorSpill' : [ + NTPNodeSetting("channel", ST.ENUM), + NTPNodeSetting("limit_channel", ST.ENUM), + NTPNodeSetting("limit_method", ST.ENUM), + NTPNodeSetting("ratio", ST.FLOAT), + NTPNodeSetting("unspill_blue", ST.FLOAT), + NTPNodeSetting("unspill_green", ST.FLOAT), + NTPNodeSetting("unspill_red", ST.FLOAT), + NTPNodeSetting("use_unspill", ST.BOOL) + ], + + 'CompositorNodeDiffMatte' : [ + NTPNodeSetting("falloff", ST.FLOAT), + NTPNodeSetting("tolerance", ST.FLOAT) + ], + + 'CompositorNodeDistanceMatte' : [ + NTPNodeSetting("channel", ST.ENUM), + NTPNodeSetting("falloff", ST.FLOAT), + NTPNodeSetting("tolerance", ST.FLOAT) + ], + + 'CompositorNodeKeying' : [ + NTPNodeSetting("blur_post", ST.INT), + NTPNodeSetting("blur_pre", ST.INT), + NTPNodeSetting("clip_black", ST.FLOAT), + NTPNodeSetting("clip_white", ST.FLOAT), + NTPNodeSetting("despill_balance", ST.FLOAT), + NTPNodeSetting("despill_factor", ST.FLOAT), + NTPNodeSetting("dilate_distance", ST.INT), + NTPNodeSetting("edge_kernel_radius", ST.INT), + NTPNodeSetting("edge_kernel_tolerance", ST.FLOAT), + NTPNodeSetting("feather_distance", ST.INT), + NTPNodeSetting("feather_falloff", ST.ENUM), + NTPNodeSetting("screen_balance", ST.FLOAT) + ], + + 'CompositorNodeKeyingScreen' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("tracking_object", ST.STRING) + ], + + 'CompositorNodeLumaMatte' : [ + NTPNodeSetting("limit_max", ST.FLOAT), + NTPNodeSetting("limit_min", ST.FLOAT) + ], + # MASK - 'CompositorNodeCryptomatteV2' : [("add", ST.COLOR), - ("entries", ST.CRYPTOMATTE_ENTRIES), - ("frame_duration", ST.INT), - ("frame_offset", ST.INT), - ("frame_start", ST.INT), - ("image", ST.IMAGE), - ("layer", ST.ENUM), - ("layer_name", ST.ENUM), - ("matte_id", ST.STRING), - ("remove", ST.COLOR), - ("scene", ST.SCENE), - ("source", ST.ENUM), - ("use_auto_refresh", ST.BOOL), - ("use_cyclic", ST.BOOL), - ("view", ST.ENUM)], - - 'CompositorNodeCryptomatte' : [("add", ST.COLOR), #TODO: may need special handling - ("matte_id", ST.STRING), - ("remove", ST.COLOR)], - - - 'CompositorNodeBoxMask' : [("height", ST.FLOAT), - ("mask_type", ST.ENUM), - ("rotation", ST.FLOAT), - ("width", ST.FLOAT), - ("x", ST.FLOAT), - ("y", ST.FLOAT)], - - 'CompositorNodeEllipseMask' : [("height", ST.FLOAT), - ("mask_type", ST.ENUM), - ("rotation", ST.FLOAT), - ("width", ST.FLOAT), - ("x", ST.FLOAT), - ("y", ST.FLOAT)], - - - 'CompositorNodeDoubleEdgeMask' : [("edge_mode", ST.ENUM), - ("inner_mode", ST.ENUM)], - - 'CompositorNodeIDMask' : [("index", ST.INT), - ("use_antialiasing", ST.BOOL)], + 'CompositorNodeCryptomatteV2' : [ + NTPNodeSetting("add", ST.COLOR), + NTPNodeSetting("entries", ST.CRYPTOMATTE_ENTRIES), + NTPNodeSetting("frame_duration", ST.INT), + NTPNodeSetting("frame_offset", ST.INT), + NTPNodeSetting("frame_start", ST.INT), + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("layer", ST.ENUM), + NTPNodeSetting("layer_name", ST.ENUM), + NTPNodeSetting("matte_id", ST.STRING), + NTPNodeSetting("remove", ST.COLOR), + NTPNodeSetting("scene", ST.SCENE), + NTPNodeSetting("source", ST.ENUM), + NTPNodeSetting("use_auto_refresh", ST.BOOL), + NTPNodeSetting("use_cyclic", ST.BOOL), + NTPNodeSetting("view", ST.ENUM) + ], + + 'CompositorNodeCryptomatte' : [ + NTPNodeSetting("add", ST.COLOR), #TODO: may need special handling + NTPNodeSetting("matte_id", ST.STRING), + NTPNodeSetting("remove", ST.COLOR) + ], + + 'CompositorNodeBoxMask' : [ + NTPNodeSetting("height", ST.FLOAT), + NTPNodeSetting("mask_type", ST.ENUM), + NTPNodeSetting("rotation", ST.FLOAT), + NTPNodeSetting("width", ST.FLOAT), + NTPNodeSetting("x", ST.FLOAT), + NTPNodeSetting("y", ST.FLOAT) + ], + + 'CompositorNodeEllipseMask' : [ + NTPNodeSetting("height", ST.FLOAT), + NTPNodeSetting("mask_type", ST.ENUM), + NTPNodeSetting("rotation", ST.FLOAT), + NTPNodeSetting("width", ST.FLOAT), + NTPNodeSetting("x", ST.FLOAT), + NTPNodeSetting("y", ST.FLOAT) + ], + + 'CompositorNodeDoubleEdgeMask' : [ + NTPNodeSetting("edge_mode", ST.ENUM), + NTPNodeSetting("inner_mode", ST.ENUM) + ], + + 'CompositorNodeIDMask' : [ + NTPNodeSetting("index", ST.INT), + NTPNodeSetting("use_antialiasing", ST.BOOL) + ], # TRACKING - 'CompositorNodePlaneTrackDeform' : [("clip", ST.MOVIE_CLIP), - ("motion_blur_samples", ST.INT), - ("motion_blur_shutter", ST.FLOAT), - ("plane_track_name", ST.STRING), - ("tracking_object", ST.STRING), - ("use_motion_blur", ST.BOOL)], - - 'CompositorNodeStabilize' : [("clip", ST.MOVIE_CLIP), - ("filter_type", ST.ENUM), - ("invert", ST.BOOL)], - - 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), - ("frame_relative", ST.INT), - ("position", ST.ENUM), - ("track_name", ST.STRING), #TODO: probably not right - ("tracking_object", ST.STRING)], - + 'CompositorNodePlaneTrackDeform' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("motion_blur_samples", ST.INT), + NTPNodeSetting("motion_blur_shutter", ST.FLOAT), + NTPNodeSetting("plane_track_name", ST.STRING), + NTPNodeSetting("tracking_object", ST.STRING), + NTPNodeSetting("use_motion_blur", ST.BOOL) + ], + + 'CompositorNodeStabilize' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("filter_type", ST.ENUM), + NTPNodeSetting("invert", ST.BOOL) + ], + + 'CompositorNodeTrackPos' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("frame_relative", ST.INT), + NTPNodeSetting("position", ST.ENUM), + NTPNodeSetting("track_name", ST.STRING), #TODO: probably not right + NTPNodeSetting("tracking_object", ST.STRING) + ], # TRANSFORM - 'CompositorNodeRotate' : [("filter_type", ST.ENUM)], - - 'CompositorNodeScale' : [("frame_method", ST.ENUM), - ("offset_x", ST.FLOAT), - ("offset_y", ST.FLOAT), - ("space", ST.ENUM)], - - - 'CompositorNodeTransform' : [("filter_type", ST.ENUM)], - - 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), - ("wrap_axis", ST.ENUM)], - - - 'CompositorNodeCornerPin' : [], - - 'CompositorNodeCrop' : [("max_x", ST.INT), - ("max_y", ST.INT), - ("min_x", ST.INT), - ("min_y", ST.INT), - ("rel_max_x", ST.FLOAT), - ("rel_max_y", ST.FLOAT), - ("rel_min_x", ST.FLOAT), - ("rel_min_y", ST.FLOAT), - ("relative", ST.BOOL), - ("use_crop_size", ST.BOOL)], - - - 'CompositorNodeDisplace' : [], - - 'CompositorNodeFlip' : [("axis", ST.ENUM)], - - 'CompositorNodeMapUV' : [("alpha", ST.INT)], - - - 'CompositorNodeLensdist' : [("use_fit", ST.BOOL), - ("use_jitter", ST.BOOL), - ("use_projector", ST.BOOL)], - - 'CompositorNodeMovieDistortion' : [("clip", ST.MOVIE_CLIP), - ("distortion_type", ST.ENUM)], - + 'CompositorNodeRotate' : [ + NTPNodeSetting("filter_type", ST.ENUM) + ], + + 'CompositorNodeScale' : [ + NTPNodeSetting("frame_method", ST.ENUM), + NTPNodeSetting("offset_x", ST.FLOAT), + NTPNodeSetting("offset_y", ST.FLOAT), + NTPNodeSetting("space", ST.ENUM) + ], + + 'CompositorNodeTransform' : [ + NTPNodeSetting("filter_type", ST.ENUM) + ], + + 'CompositorNodeTranslate' : [ + NTPNodeSetting("use_relative", ST.BOOL), + NTPNodeSetting("wrap_axis", ST.ENUM) + ], + + 'CompositorNodeCornerPin' : [], + + 'CompositorNodeCrop' : [ + NTPNodeSetting("max_x", ST.INT), + NTPNodeSetting("max_y", ST.INT), + NTPNodeSetting("min_x", ST.INT), + NTPNodeSetting("min_y", ST.INT), + NTPNodeSetting("rel_max_x", ST.FLOAT), + NTPNodeSetting("rel_max_y", ST.FLOAT), + NTPNodeSetting("rel_min_x", ST.FLOAT), + NTPNodeSetting("rel_min_y", ST.FLOAT), + NTPNodeSetting("relative", ST.BOOL), + NTPNodeSetting("use_crop_size", ST.BOOL) + ], + + 'CompositorNodeDisplace' : [], + + 'CompositorNodeFlip' : [ + NTPNodeSetting("axis", ST.ENUM) + ], + + 'CompositorNodeMapUV' : [ + NTPNodeSetting("alpha", ST.INT) + ], + + 'CompositorNodeLensdist' : [ + NTPNodeSetting("use_fit", ST.BOOL), + NTPNodeSetting("use_jitter", ST.BOOL), + NTPNodeSetting("use_projector", ST.BOOL) + ], + + 'CompositorNodeMovieDistortion' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("distortion_type", ST.ENUM) + ], # UTILITIES - 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], - - 'CompositorNodeMapValue' : [("max", ST.VEC1), - ("min", ST.VEC1), - ("offset", ST.VEC1), - ("size", ST.VEC1), - ("use_max", ST.BOOL), - ("use_min", ST.BOOL)], - - 'CompositorNodeMath' : [("operation", ST.ENUM), - ("use_clamp", ST.BOOL)], - - - 'CompositorNodeLevels' : [("channel", ST.ENUM)], + 'CompositorNodeMapRange' : [ + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'CompositorNodeMapValue' : [ + NTPNodeSetting("max", ST.VEC1), + NTPNodeSetting("min", ST.VEC1), + NTPNodeSetting("offset", ST.VEC1), + NTPNodeSetting("size", ST.VEC1), + NTPNodeSetting("use_max", ST.BOOL), + NTPNodeSetting("use_min", ST.BOOL) + ], + + 'CompositorNodeMath' : [ + NTPNodeSetting("operation", ST.ENUM), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'CompositorNodeLevels' : [ + NTPNodeSetting("channel", ST.ENUM) + ], - 'CompositorNodeNormalize' : [], + 'CompositorNodeNormalize' : [], - 'CompositorNodeSwitch' : [("check", ST.BOOL)], + 'CompositorNodeSwitch' : [ + NTPNodeSetting("check", ST.BOOL) + ], 'CompositorNodeSwitchView' : [], @@ -451,19 +584,27 @@ 'CompositorNodeSeparateXYZ' : [], 'CompositorNodeNormal' : [], - 'CompositorNodeCurveVec' : [("mapping", ST.CURVE_MAPPING)], + 'CompositorNodeCurveVec' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], # MISC - 'CompositorNodeGroup' : [("node_tree", ST.NODE_TREE)], + 'CompositorNodeGroup' : [ + NTPNodeSetting("node_tree", ST.NODE_TREE) + ], - 'NodeFrame' : [("label_size", ST.INT), - ("shrink", ST.BOOL), - ("text", ST.TEXT)], + 'NodeFrame' : [ + NTPNodeSetting("label_size", ST.INT), + NTPNodeSetting("shrink", ST.BOOL), + NTPNodeSetting("text", ST.TEXT) + ], 'NodeGroupInput' : [], - 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + 'NodeGroupOutput' : [ + NTPNodeSetting("is_active_output", ST.BOOL) + ], 'NodeReroute' : [] } diff --git a/compositor/operator.py b/compositor/operator.py index 8078912..afe2d62 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -13,7 +13,7 @@ END_NAME = "end_name" NODE = "node" -comp_op_reserved_names = {SCENE, BASE_NAME, END_NAME, NODE} +COMP_OP_RESERVED_NAMES = {SCENE, BASE_NAME, END_NAME, NODE} class NTPCompositorOperator(NTP_Operator): bl_idname = "node.ntp_compositor" @@ -34,7 +34,7 @@ class NTPCompositorOperator(NTP_Operator): def __init__(self): super().__init__() self._settings = compositor_node_settings - for name in comp_op_reserved_names: + for name in COMP_OP_RESERVED_NAMES: self._used_vars[name] = 0 @@ -87,16 +87,16 @@ def _set_color_balance_settings(self, node: CompositorNodeColorBalance node (CompositorNodeColorBalance): the color balance node """ if node.correction_method == 'LIFT_GAMMA_GAIN': - lst = [("correction_method", ST.ENUM), - ("gain", ST.COLOR), - ("gamma", ST.COLOR), - ("lift", ST.COLOR)] + lst = [NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("gain", ST.COLOR), + NTPNodeSetting("gamma", ST.COLOR), + NTPNodeSetting("lift", ST.COLOR)] else: - lst = [("correction_method", ST.ENUM), - ("offset", ST.COLOR), - ("offset_basis", ST.FLOAT), - ("power", ST.COLOR), - ("slope", ST.COLOR)] + lst = [NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("offset", ST.COLOR), + NTPNodeSetting("offset_basis", ST.FLOAT), + NTPNodeSetting("power", ST.COLOR), + NTPNodeSetting("slope", ST.COLOR)] self._settings['CompositorNodeColorBalance'] = lst diff --git a/geometry/node_settings.py b/geometry/node_settings.py index d637180..ffa30c6 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -1,190 +1,271 @@ -from ..utils import ST +from ..utils import ST, NTPNodeSetting -geo_node_settings : dict[str, list[(str, ST)]] = { +geo_node_settings : dict[str, list[NTPNodeSetting]] = { # ATTRIBUTE - 'GeometryNodeAttributeStatistic' : [("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeAttributeStatistic' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM) + ], - 'GeometryNodeAttributeDomainSize' : [("component", ST.ENUM)], + 'GeometryNodeAttributeDomainSize' : [ + NTPNodeSetting("component", ST.ENUM, min_version = (3, 1, 0)) + ], - 'GeometryNodeBlurAttribute' : [("data_type", ST.ENUM)], + 'GeometryNodeBlurAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 5, 0)) + ], - 'GeometryNodeCaptureAttribute' : [("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeCaptureAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM) + ], - 'GeometryNodeRemoveAttribute' : [], + 'GeometryNodeRemoveAttribute' : [], - 'GeometryNodeStoreNamedAttribute' : [("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeStoreNamedAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 2, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 2, 0)) + ], - 'GeometryNodeAttributeTransfer' : [("data_type", ST.ENUM), - ("domain", ST.ENUM), - ("mapping", ST.ENUM)], + 'GeometryNodeAttributeTransfer' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM), + NTPNodeSetting("mapping", ST.ENUM) + ], # INPUT # Input > Constant - 'FunctionNodeInputBool' : [("boolean", ST.BOOL)], + 'FunctionNodeInputBool' : [ + NTPNodeSetting("boolean", ST.BOOL) + ], - 'FunctionNodeInputColor' : [("color", ST.VEC4)], + 'FunctionNodeInputColor' : [ + NTPNodeSetting("color", ST.VEC4) + ], - 'GeometryNodeInputImage' : [("image", ST.IMAGE)], + 'GeometryNodeInputImage' : [ + NTPNodeSetting("image", ST.IMAGE, min_version = (3, 5, 0)) + ], - 'FunctionNodeInputInt' : [("integer", ST.INT)], + 'FunctionNodeInputInt' : [ + NTPNodeSetting("integer", ST.INT) + ], - 'GeometryNodeInputMaterial' : [("material", ST.MATERIAL)], + 'GeometryNodeInputMaterial' : [ + NTPNodeSetting("material", ST.MATERIAL) + ], - 'FunctionNodeInputString' : [("string", ST.STRING)], + 'FunctionNodeInputString' : [ + NTPNodeSetting("string", ST.STRING) + ], - 'ShaderNodeValue' : [], + 'ShaderNodeValue' : [], - 'FunctionNodeInputVector' : [("vector", ST.VEC3)], + 'FunctionNodeInputVector' : [ + NTPNodeSetting("vector", ST.VEC3) + ], #Input > Group 'NodeGroupInput' : [], # Input > Scene - 'GeometryNodeTool3DCursor' : [], + 'GeometryNodeTool3DCursor' : [], - 'GeometryNodeCollectionInfo' : [("transform_space", ST.ENUM)], + 'GeometryNodeCollectionInfo' : [ + NTPNodeSetting("transform_space", ST.ENUM) + ], - 'GeometryNodeImageInfo' : [], - 'GeometryNodeIsViewport' : [], + 'GeometryNodeImageInfo' : [], + 'GeometryNodeIsViewport' : [], - 'GeometryNodeObjectInfo' : [("transform_space", ST.ENUM)], + 'GeometryNodeObjectInfo' : [ + NTPNodeSetting("transform_space", ST.ENUM) + ], - 'GeometryNodeSelfObject' : [], + 'GeometryNodeSelfObject' : [], 'GeometryNodeInputSceneTime' : [], # OUTPUT - 'GeometryNodeViewer' : [("data_type", ST.ENUM), - - ("domain", ST.ENUM)], + 'GeometryNodeViewer' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0)) + ], # GEOMETRY - 'GeometryNodeJoinGeometry' : [], + 'GeometryNodeJoinGeometry' : [], 'GeometryNodeGeometryToInstance' : [], # Geometry > Read - 'GeometryNodeInputID' : [], - 'GeometryNodeInputIndex' : [], + 'GeometryNodeInputID' : [], + 'GeometryNodeInputIndex' : [], - 'GeometryNodeInputNamedAttribute' : [("data_type", ST.ENUM)], + 'GeometryNodeInputNamedAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 2, 0)) + ], - 'GeometryNodeInputNormal' : [], - 'GeometryNodeInputPosition' : [], - 'GeometryNodeInputRadius' : [], - 'GeometryNodeToolSelection' : [], + 'GeometryNodeInputNormal' : [], + 'GeometryNodeInputPosition' : [], + 'GeometryNodeInputRadius' : [], + 'GeometryNodeToolSelection' : [], # Geometry > Sample - 'GeometryNodeProximity' : [("target_element", ST.ENUM)], + 'GeometryNodeProximity' : [ + NTPNodeSetting("target_element", ST.ENUM) + ], 'GeometryNodeIndexOfNearest' : [], - 'GeometryNodeRaycast' : [("data_type", ST.ENUM), - ("mapping", ST.ENUM)], + 'GeometryNodeRaycast' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("mapping", ST.ENUM) + ], - 'GeometryNodeSampleIndex' : [("clamp", ST.BOOL), - ("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeSampleIndex' : [ + NTPNodeSetting("clamp", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0)) + ], - 'GeometryNodeSampleNearest' : [("domain", ST.ENUM)], + 'GeometryNodeSampleNearest' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0)) + ], # Geometry > Write - 'GeometryNodeSetID' : [], - 'GeometryNodeSetPosition' : [], + 'GeometryNodeSetID' : [], + 'GeometryNodeSetPosition' : [], 'GeometryNodeToolSetSelection' : [], # Geometry > Operations - 'GeometryNodeBoundBox' : [], - 'GeometryNodeConvexHull' : [], + 'GeometryNodeBoundBox' : [], + 'GeometryNodeConvexHull' : [], - 'GeometryNodeDeleteGeometry' : [("domain", ST.ENUM), - ("mode", ST.ENUM)], + 'GeometryNodeDeleteGeometry' : [ + NTPNodeSetting("domain", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeDuplicateElements' : [("domain", ST.ENUM)], + 'GeometryNodeDuplicateElements' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 2, 0)) + ], - 'GeometryNodeMergeByDistance' : [("mode", ST.ENUM)], + 'GeometryNodeMergeByDistance' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)) + ], - 'GeometryNodeTransform' : [], + 'GeometryNodeTransform' : [], 'GeometryNodeSeparateComponents' : [], - 'GeometryNodeSeparateGeometry' : [("domain", ST.ENUM)], + 'GeometryNodeSeparateGeometry' : [ + NTPNodeSetting("domain", ST.ENUM) + ], # CURVE # Curve > Read 'GeometryNodeInputCurveHandlePositions' : [], - 'GeometryNodeCurveLength' : [], - 'GeometryNodeInputTangent' : [], - 'GeometryNodeInputCurveTilt' : [], - 'GeometryNodeCurveEndpointSelection' : [], + 'GeometryNodeCurveLength' : [], + 'GeometryNodeInputTangent' : [], + 'GeometryNodeInputCurveTilt' : [], + 'GeometryNodeCurveEndpointSelection' : [], - 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", ST.ENUM), - ("mode", ST.ENUM_SET)], + 'GeometryNodeCurveHandleTypeSelection' : [ + NTPNodeSetting("handle_type", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM_SET) + ], - 'GeometryNodeInputSplineCyclic' : [], - 'GeometryNodeSplineLength' : [], - 'GeometryNodeSplineParameter' : [], - 'GeometryNodeInputSplineResolution' : [], + 'GeometryNodeInputSplineCyclic' : [], + 'GeometryNodeSplineLength' : [], + 'GeometryNodeSplineParameter' : [], + 'GeometryNodeInputSplineResolution' : [], # Curve > Sample - 'GeometryNodeSampleCurve' : [("data_type", ST.ENUM), - ("mode", ST.ENUM), - ("use_all_curves", ST.BOOL)], + 'GeometryNodeSampleCurve' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("mode", ST.ENUM), + NTPNodeSetting("use_all_curves", ST.BOOL, min_version = (3, 4, 0)) + ], # Curve > Write - 'GeometryNodeSetCurveNormal' : [("mode", ST.ENUM)], + 'GeometryNodeSetCurveNormal' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 4, 0)) + ], - 'GeometryNodeSetCurveRadius' : [], - 'GeometryNodeSetCurveTilt' : [], + 'GeometryNodeSetCurveRadius' : [], + 'GeometryNodeSetCurveTilt' : [], - 'GeometryNodeSetCurveHandlePositions' : [("mode", ST.ENUM)], + 'GeometryNodeSetCurveHandlePositions' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeCurveSetHandles' : [("handle_type", ST.ENUM), - ("mode", ST.ENUM_SET)], + 'GeometryNodeCurveSetHandles' : [ + NTPNodeSetting("handle_type", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM_SET) + ], - 'GeometryNodeSetSplineCyclic' : [], - 'GeometryNodeSetSplineResolution' : [], + 'GeometryNodeSetSplineCyclic' : [], + 'GeometryNodeSetSplineResolution' : [], - 'GeometryNodeCurveSplineType' : [("spline_type", ST.ENUM)], + 'GeometryNodeCurveSplineType' : [ + NTPNodeSetting("spline_type", ST.ENUM) + ], # Curve > Operations - 'GeometryNodeCurveToMesh' : [], + 'GeometryNodeCurveToMesh' : [], - 'GeometryNodeCurveToPoints' : [("mode", ST.ENUM)], + 'GeometryNodeCurveToPoints' : [ + NTPNodeSetting("mode", ST.ENUM) + ], 'GeometryNodeDeformCurvesOnSurface' : [], - 'GeometryNodeFillCurve' : [("mode", ST.ENUM)], + 'GeometryNodeFillCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeFilletCurve' : [("mode", ST.ENUM)], + 'GeometryNodeFilletCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeInterpolateCurves' : [], + 'GeometryNodeInterpolateCurves' : [], - 'GeometryNodeResampleCurve' : [("mode", ST.ENUM)], + 'GeometryNodeResampleCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeReverseCurve' : [], - 'GeometryNodeSubdivideCurve' : [], + 'GeometryNodeReverseCurve' : [], + 'GeometryNodeSubdivideCurve' : [], - 'GeometryNodeTrimCurve' : [("mode", ST.ENUM)], + 'GeometryNodeTrimCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], # Curve > Primitives - 'GeometryNodeCurveArc' : [("mode", ST.ENUM)], + 'GeometryNodeCurveArc' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)) + ], - 'GeometryNodeCurvePrimitiveBezierSegment' : [("mode", ST.ENUM)], + 'GeometryNodeCurvePrimitiveBezierSegment' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeCurvePrimitiveCircle' : [("mode", ST.ENUM)], + 'GeometryNodeCurvePrimitiveCircle' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeCurvePrimitiveLine' : [("mode", ST.ENUM)], + 'GeometryNodeCurvePrimitiveLine' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeCurveSpiral' : [], - 'GeometryNodeCurveQuadraticBezier' : [], + 'GeometryNodeCurveSpiral' : [], + 'GeometryNodeCurveQuadraticBezier' : [], - 'GeometryNodeCurvePrimitiveQuadrilateral' : [("mode", ST.ENUM)], + 'GeometryNodeCurvePrimitiveQuadrilateral' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeCurveStar' : [], + 'GeometryNodeCurveStar' : [], # Curve > Topology 'GeometryNodeOffsetPointInCurve' : [], @@ -193,10 +274,13 @@ # INSTANCES - 'GeometryNodeInstanceOnPoints' : [], - 'GeometryNodeInstancesToPoints' : [], + 'GeometryNodeInstanceOnPoints' : [], + 'GeometryNodeInstancesToPoints' : [], - 'GeometryNodeRealizeInstances' : [("legacy_behavior", ST.BOOL)], + 'GeometryNodeRealizeInstances' : [ + NTPNodeSetting("legacy_behavior", ST.BOOL, min_version = (3, 1, 0), + max_version = (3, 6, 0)) + ], 'GeometryNodeRotateInstances' : [], 'GeometryNodeScaleInstances' : [], @@ -223,58 +307,86 @@ 'GeometryNodeInputMeshVertexNeighbors' : [], # Mesh > Sample - 'GeometryNodeSampleNearestSurface' : [("data_type", ST.ENUM)], + 'GeometryNodeSampleNearestSurface' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)) + ], - 'GeometryNodeSampleUVSurface' : [("data_type", ST.ENUM)], + 'GeometryNodeSampleUVSurface' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)) + ], # Mesh > Write 'GeometryNodeToolSetFaceSet' : [], - 'GeometryNodeSetShadeSmooth' : [("domain", ST.ENUM)], + 'GeometryNodeSetShadeSmooth' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (4, 0, 0)) + ], # Mesh > Operations 'GeometryNodeDualMesh' : [], 'GeometryNodeEdgePathsToCurves' : [], 'GeometryNodeEdgePathsToSelection' : [], - 'GeometryNodeExtrudeMesh' : [("mode", ST.ENUM)], + 'GeometryNodeExtrudeMesh' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)) + ], - 'GeometryNodeFlipFaces' : [], + 'GeometryNodeFlipFaces' : [], - 'GeometryNodeMeshBoolean' : [("operation", ST.ENUM)], + 'GeometryNodeMeshBoolean' : [ + NTPNodeSetting("operation", ST.ENUM) + ], - 'GeometryNodeMeshToCurve' : [], + 'GeometryNodeMeshToCurve' : [], - 'GeometryNodeMeshToPoints' : [("mode", ST.ENUM)], + 'GeometryNodeMeshToPoints' : [ + NTPNodeSetting("mode", ST.ENUM) + ], - 'GeometryNodeMeshToVolume' : [("resolution_mode", ST.ENUM)], + 'GeometryNodeMeshToVolume' : [ + NTPNodeSetting("resolution_mode", ST.ENUM, min_version = (3, 3, 0)) + ], - 'GeometryNodeScaleElements' : [("domain", ST.ENUM), - ("scale_mode", ST.ENUM)], + 'GeometryNodeScaleElements' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("scale_mode", ST.ENUM, min_version = (3, 1, 0)) + ], - 'GeometryNodeSplitEdges' : [], - 'GeometryNodeSubdivideMesh' : [], + 'GeometryNodeSplitEdges' : [], + 'GeometryNodeSubdivideMesh' : [], - 'GeometryNodeSubdivisionSurface' : [("boundary_smooth", ST.ENUM), - ("uv_smooth", ST.ENUM)], + 'GeometryNodeSubdivisionSurface' : [ + NTPNodeSetting("boundary_smooth", ST.ENUM), + NTPNodeSetting("uv_smooth", ST.ENUM) + ], - 'GeometryNodeTriangulate' : [("ngon_method", ST.ENUM), - ("quad_method", ST.ENUM)], + 'GeometryNodeTriangulate' : [ + NTPNodeSetting("ngon_method", ST.ENUM), + NTPNodeSetting("quad_method", ST.ENUM) + ], # Mesh > Primitives - 'GeometryNodeMeshCone' : [("fill_type", ST.ENUM)], + 'GeometryNodeMeshCone' : [ + NTPNodeSetting("fill_type", ST.ENUM) + ], - 'GeometryNodeMeshCube' : [], + 'GeometryNodeMeshCube' : [], - 'GeometryNodeMeshCylinder' : [("fill_type", ST.ENUM)], + 'GeometryNodeMeshCylinder' : [ + NTPNodeSetting("fill_type", ST.ENUM) + ], 'GeometryNodeMeshGrid' : [], 'GeometryNodeMeshIcoSphere' : [], - 'GeometryNodeMeshCircle' : [("fill_type", ST.ENUM)], + 'GeometryNodeMeshCircle' : [ + NTPNodeSetting("fill_type", ST.ENUM) + ], - 'GeometryNodeMeshLine' : [("count_mode", ST.ENUM), - ("mode", ST.ENUM)], + 'GeometryNodeMeshLine' : [ + NTPNodeSetting("count_mode", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM) + ], 'GeometryNodeMeshUVSphere' : [], @@ -290,27 +402,37 @@ # Mesh > UV 'GeometryNodeUVPackIslands' : [], - 'GeometryNodeUVUnwrap' : [("method", ST.ENUM)], + 'GeometryNodeUVUnwrap': [ + NTPNodeSetting("method", ST.ENUM, min_version = (3, 3, 0)) + ], # POINT - 'GeometryNodeDistributePointsInVolume' : [("mode", ST.ENUM)], + 'GeometryNodeDistributePointsInVolume' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 4, 0)) + ], - 'GeometryNodeDistributePointsOnFaces' : [("distribute_method", ST.ENUM), - ("use_legacy_normal", ST.BOOL)], + 'GeometryNodeDistributePointsOnFaces' : [ + NTPNodeSetting("distribute_method", ST.ENUM), + NTPNodeSetting("use_legacy_normal", ST.BOOL, min_version = (3, 5, 0)) + ], - 'GeometryNodePoints' : [], - 'GeometryNodePointsToCurves' : [], - 'GeometryNodePointsToVertices' : [], + 'GeometryNodePoints' : [], + 'GeometryNodePointsToCurves' : [], + 'GeometryNodePointsToVertices' : [], - 'GeometryNodePointsToVolume' : [("resolution_mode", ST.ENUM)], + 'GeometryNodePointsToVolume' : [ + NTPNodeSetting("resolution_mode", ST.ENUM) + ], - 'GeometryNodeSetPointRadius' : [], + 'GeometryNodeSetPointRadius' : [], # VOLUME 'GeometryNodeVolumeCube' : [], - 'GeometryNodeVolumeToMesh' : [("resolution_mode", ST.ENUM)], + 'GeometryNodeVolumeToMesh' : [ + NTPNodeSetting("resolution_mode", ST.ENUM) + ], # SIMULATION @@ -327,130 +449,203 @@ # TEXTURE - 'ShaderNodeTexBrick' : [("offset", ST.FLOAT), - ("offset_frequency", ST.INT), - ("squash", ST.FLOAT), - ("squash_frequency", ST.INT)], - - 'ShaderNodeTexChecker' : [], - - 'ShaderNodeTexGradient' : [("gradient_type", ST.ENUM)], - - 'GeometryNodeImageTexture' : [("extension", ST.ENUM), - ("interpolation", ST.ENUM)], - - 'ShaderNodeTexMagic' : [("turbulence_depth", ST.INT)], - - 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", ST.ENUM), - ("musgrave_type", ST.ENUM)], - - 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM)], - - 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), - ("feature", ST.ENUM), - ("voronoi_dimensions", ST.ENUM)], - - 'ShaderNodeTexWave' : [("bands_direction", ST.ENUM), - ("rings_direction", ST.ENUM), - ("wave_profile", ST.ENUM), - ("wave_type", ST.ENUM)], - - 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", ST.ENUM)], + 'ShaderNodeTexBrick' : [ + NTPNodeSetting("offset", ST.FLOAT), + NTPNodeSetting("offset_frequency", ST.INT), + NTPNodeSetting("squash", ST.FLOAT), + NTPNodeSetting("squash_frequency", ST.INT) + ], + + 'ShaderNodeTexChecker' : [], + + 'ShaderNodeTexGradient' : [ + NTPNodeSetting("gradient_type", ST.ENUM) + ], + + 'GeometryNodeImageTexture' : [ + NTPNodeSetting("extension", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("interpolation", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'ShaderNodeTexMagic' : [ + NTPNodeSetting("turbulence_depth", ST.INT) + ], + + 'ShaderNodeTexMusgrave' : [ + NTPNodeSetting("musgrave_dimensions", ST.ENUM), + NTPNodeSetting("musgrave_type", ST.ENUM) + ], + + 'ShaderNodeTexNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM) + ], + + 'ShaderNodeTexVoronoi' : [ + NTPNodeSetting("distance", ST.ENUM), + NTPNodeSetting("feature", ST.ENUM), + NTPNodeSetting("voronoi_dimensions", ST.ENUM) + ], + + 'ShaderNodeTexWave' : [ + NTPNodeSetting("bands_direction", ST.ENUM), + NTPNodeSetting("rings_direction", ST.ENUM), + NTPNodeSetting("wave_profile", ST.ENUM), + NTPNodeSetting("wave_type", ST.ENUM) + ], + + 'ShaderNodeTexWhiteNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM) + ], # UTILITIES - 'ShaderNodeMix' : [("blend_type", ST.ENUM), - ("clamp_factor", ST.BOOL), - ("clamp_result", ST.BOOL), - ("data_type", ST.ENUM), - ("factor_mode", ST.ENUM)], - - 'FunctionNodeRandomValue' : [("data_type", ST.ENUM)], - - 'GeometryNodeRepeatInput' : [], - 'GeometryNodeRepeatOutput' : [("inspection_index", ST.INT)], - - 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], + 'ShaderNodeMix' : [ + NTPNodeSetting("blend_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_factor", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_result", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("factor_mode", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'FunctionNodeRandomValue' : [ + NTPNodeSetting("data_type", ST.ENUM) + ], + + 'GeometryNodeRepeatInput' : [], + + 'GeometryNodeRepeatOutput' : [ + NTPNodeSetting("inspection_index", ST.INT, min_version = (4, 0, 0)) + ], + + 'GeometryNodeSwitch' : [ + NTPNodeSetting("input_type", ST.ENUM) + ], # Utilities > Color - 'ShaderNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], - - 'ShaderNodeRGBCurve' : [("mapping", ST.CURVE_MAPPING)], - - 'FunctionNodeCombineColor' : [("mode", ST.ENUM)], - - 'ShaderNodeMixRGB' : [("blend_type", ST.ENUM), - ("use_alpha", ST.BOOL), - ("use_clamp", ST.BOOL)], #legacy - - 'FunctionNodeSeparateColor' : [("mode", ST.ENUM)], + 'ShaderNodeValToRGB' : [ + NTPNodeSetting("color_ramp", ST.COLOR_RAMP) + ], + + 'ShaderNodeRGBCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'FunctionNodeCombineColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'ShaderNodeMixRGB' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], #legacy + + 'FunctionNodeSeparateColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], # Utilities > Text - 'GeometryNodeStringJoin' : [], - 'FunctionNodeReplaceString' : [], - 'FunctionNodeSliceString' : [], - 'FunctionNodeStringLength' : [], - - 'GeometryNodeStringToCurves' : [("align_x", ST.ENUM), - ("align_y", ST.ENUM), - ("font", ST.FONT), - ("overflow", ST.ENUM), - ("pivot_mode", ST.ENUM)], + 'GeometryNodeStringJoin' : [], + 'FunctionNodeReplaceString' : [], + 'FunctionNodeSliceString' : [], + 'FunctionNodeStringLength' : [], + + 'GeometryNodeStringToCurves' : [ + NTPNodeSetting("align_x", ST.ENUM), + NTPNodeSetting("align_y", ST.ENUM), + NTPNodeSetting("font", ST.FONT), + NTPNodeSetting("overflow", ST.ENUM), + NTPNodeSetting("pivot_mode", ST.ENUM, min_version = (3, 1, 0)) + ], 'FunctionNodeValueToString' : [], 'FunctionNodeInputSpecialCharacters' : [], # Utilities > Vector - 'ShaderNodeVectorCurve' : [("mapping", ST.CURVE_MAPPING)], + 'ShaderNodeVectorCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], - 'ShaderNodeVectorMath' : [("operation", ST.ENUM)], + 'ShaderNodeVectorMath' : [ + NTPNodeSetting("operation", ST.ENUM) + ], - 'ShaderNodeVectorRotate' : [("invert", ST.BOOL), - ("rotation_type", ST.ENUM)], + 'ShaderNodeVectorRotate' : [ + NTPNodeSetting("invert", ST.BOOL), + NTPNodeSetting("rotation_type", ST.ENUM) + ], - 'ShaderNodeCombineXYZ' : [], - 'ShaderNodeSeparateXYZ' : [], + 'ShaderNodeCombineXYZ' : [], + 'ShaderNodeSeparateXYZ' : [], # Utilities > Field - 'GeometryNodeAccumulateField' : [("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeAccumulateField' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM) + ], - 'GeometryNodeFieldAtIndex' : [("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeFieldAtIndex' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 1, 0)) + ], - 'GeometryNodeFieldOnDomain' : [("data_type", ST.ENUM), - ("domain", ST.ENUM)], + 'GeometryNodeFieldOnDomain' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 3, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 3, 0)) + ], # Utilities > Math - 'FunctionNodeBooleanMath' : [("operation", ST.ENUM)], - - 'ShaderNodeClamp' : [("clamp_type", ST.ENUM)], - - 'FunctionNodeCompare' : [("data_type", ST.ENUM), - ("mode", ST.ENUM), - ("operation", ST.ENUM)], - - 'ShaderNodeFloatCurve' : [("mapping", ST.CURVE_MAPPING)], - - 'FunctionNodeFloatToInt' : [("rounding_mode", ST.ENUM)], - - 'ShaderNodeMapRange' : [("clamp", ST.BOOL), - ("data_type", ST.ENUM), - ("interpolation_type", ST.ENUM)], - - 'ShaderNodeMath' : [("operation", ST.ENUM), - ("use_clamp", ST.BOOL)], + 'FunctionNodeBooleanMath' : [ + NTPNodeSetting("operation", ST.ENUM) + ], + + 'ShaderNodeClamp' : [ + NTPNodeSetting("clamp_type", ST.ENUM) + ], + + 'FunctionNodeCompare' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("operation", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'FunctionNodeCompareFloats' : [ + NTPNodeSetting("operation", ST.ENUM, max_version = (3, 0, 0)) + ], + + 'ShaderNodeFloatCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'FunctionNodeFloatToInt' : [ + NTPNodeSetting("rounding_mode", ST.ENUM) + ], + + 'ShaderNodeMapRange' : [ + NTPNodeSetting("clamp", ST.BOOL), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("interpolation_type", ST.ENUM) + ], + + 'ShaderNodeMath' : [ + NTPNodeSetting("operation", ST.ENUM), + NTPNodeSetting("use_clamp", ST.BOOL) + ], # Utilities > Rotation - 'FunctionNodeAlignEulerToVector' : [("axis", ST.ENUM), - ("pivot_axis", ST.ENUM)], - - 'FunctionNodeAxisAngleToRotation' : [], - 'FunctionNodeEulerToRotation' : [], - 'FunctionNodeInvertRotation' : [], + 'FunctionNodeAlignEulerToVector' : [ + NTPNodeSetting("axis", ST.ENUM), + NTPNodeSetting("pivot_axis", ST.ENUM) + ], + + 'FunctionNodeAxisAngleToRotation' : [], + 'FunctionNodeEulerToRotation' : [], + 'FunctionNodeInvertRotation' : [], - 'FunctionNodeRotateEuler' : [("space", ST.ENUM), - ("type", ST.ENUM)], + 'FunctionNodeRotateEuler' : [ + NTPNodeSetting("space", ST.ENUM), + NTPNodeSetting("type", ST.ENUM) + ], 'FunctionNodeRotateVector' : [], 'FunctionNodeRotationToAxisAngle' : [], @@ -459,15 +654,21 @@ 'FunctionNodeQuaternionToRotation' : [], # MISC - 'GeometryNodeGroup' : [("node_tree", ST.NODE_TREE)], + 'GeometryNodeGroup' : [ + NTPNodeSetting("node_tree", ST.NODE_TREE) + ], - 'NodeFrame' : [("label_size", ST.INT), - ("shrink", ST.BOOL), - ("text", ST.TEXT)], + 'NodeFrame' : [ + NTPNodeSetting("label_size", ST.INT), + NTPNodeSetting("shrink", ST.BOOL), + NTPNodeSetting("text", ST.TEXT) + ], 'NodeGroupInput' : [], - 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + 'NodeGroupOutput' : [ + NTPNodeSetting("is_active_output", ST.BOOL) + ], 'NodeReroute' : [] diff --git a/geometry/operator.py b/geometry/operator.py index 9733a15..d88d6cc 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -13,7 +13,7 @@ OBJECT_NAME = "name" OBJECT = "obj" MODIFIER = "mod" -geo_op_reserved_names = {ITEM, +GEO_OP_RESERVED_NAMES = {ITEM, OBJECT_NAME, OBJECT, MODIFIER} @@ -36,7 +36,7 @@ class NTPGeoNodesOperator(NTP_Operator): def __init__(self): super().__init__() self._settings = geo_node_settings - for name in geo_op_reserved_names: + for name in GEO_OP_RESERVED_NAMES: self._used_vars[name] = 0 if bpy.app.version >= (3, 6, 0): diff --git a/material/node_settings.py b/material/node_settings.py index 04ae778..3729d84 100644 --- a/material/node_settings.py +++ b/material/node_settings.py @@ -1,277 +1,396 @@ -from ..utils import ST +from ..utils import ST, NTPNodeSetting -shader_node_settings : dict[str, list[(str, ST)]] = { +shader_node_settings : dict[str, list[NTPNodeSetting]] = { # INPUT - 'ShaderNodeAmbientOcclusion' : [("inside", ST.BOOL), - ("only_local", ST.BOOL), - ("samples", ST.INT)], - - 'ShaderNodeAttribute' : [("attribute_name", ST.STRING), #TODO: separate attribute type? - ("attribute_type", ST.ENUM)], - - 'ShaderNodeBevel' : [("samples", ST.INT)], - - 'ShaderNodeCameraData' : [], - - 'ShaderNodeVertexColor' : [("layer_name", ST.STRING)], #TODO: separate color attribute type? - - 'ShaderNodeHairInfo' : [], - 'ShaderNodeFresnel' : [], - 'ShaderNodeNewGeometry' : [], - 'ShaderNodeLayerWeight' : [], - 'ShaderNodeLightPath' : [], - 'ShaderNodeObjectInfo' : [], - 'ShaderNodeParticleInfo' : [], - 'ShaderNodePointInfo' : [], - 'ShaderNodeRGB' : [], - - 'ShaderNodeTangent' : [("axis", ST.ENUM), - ("direction_type", ST.ENUM), - ("uv_map", ST.STRING)], #TODO: special UV Map type? - - 'ShaderNodeTexCoord' : [("from_instancer", ST.BOOL), - ("object", ST.OBJECT)], - - 'ShaderNodeUVAlongStroke' : [("use_tips", ST.BOOL)], - - 'ShaderNodeUVMap' : [("from_instancer", ST.BOOL), - ("uv_map", ST.STRING)], #TODO: see ShaderNodeTangent - - 'ShaderNodeValue' : [], - 'ShaderNodeVolumeInfo' : [], - - 'ShaderNodeWireframe' : [("use_pixel_size", ST.BOOL)], + 'ShaderNodeAmbientOcclusion' : [ + NTPNodeSetting("inside", ST.BOOL), + NTPNodeSetting("only_local", ST.BOOL), + NTPNodeSetting("samples", ST.INT) + ], + + 'ShaderNodeAttribute' : [ + NTPNodeSetting("attribute_name", ST.STRING), #TODO: separate attribute type? + NTPNodeSetting("attribute_type", ST.ENUM) + ], + + 'ShaderNodeBevel' : [ + NTPNodeSetting("samples", ST.INT) + ], + + 'ShaderNodeCameraData' : [], + + 'ShaderNodeVertexColor' : [ + NTPNodeSetting("layer_name", ST.STRING) #TODO: separate color attribute type? + ], + + 'ShaderNodeHairInfo' : [], + 'ShaderNodeFresnel' : [], + 'ShaderNodeNewGeometry' : [], + 'ShaderNodeLayerWeight' : [], + 'ShaderNodeLightPath' : [], + 'ShaderNodeObjectInfo' : [], + 'ShaderNodeParticleInfo' : [], + 'ShaderNodePointInfo' : [], + 'ShaderNodeRGB' : [], + + 'ShaderNodeTangent' : [ + NTPNodeSetting("axis", ST.ENUM), + NTPNodeSetting("direction_type", ST.ENUM), + NTPNodeSetting("uv_map", ST.STRING) #TODO: special UV Map type? + ], + + 'ShaderNodeTexCoord' : [ + NTPNodeSetting("from_instancer", ST.BOOL), + NTPNodeSetting("object", ST.OBJECT) + ], + + 'ShaderNodeUVAlongStroke' : [ + NTPNodeSetting("use_tips", ST.BOOL) + ], + + 'ShaderNodeUVMap' : [ + NTPNodeSetting("from_instancer", ST.BOOL), + NTPNodeSetting("uv_map", ST.STRING) + ], #TODO: see ShaderNodeTangent + + 'ShaderNodeValue' : [], + 'ShaderNodeVolumeInfo' : [], + + 'ShaderNodeWireframe' : [ + NTPNodeSetting("use_pixel_size", ST.BOOL) + ], # OUTPUT - 'ShaderNodeOutputAOV' : [("name", ST.STRING)], - - 'ShaderNodeOutputLight' : [("is_active_output", ST.BOOL), - ("target", ST.ENUM)], - - 'ShaderNodeOutputLineStyle' : [("blend_type", ST.ENUM), - ("is_active_output", ST.BOOL), - ("target", ST.ENUM), - ("use_alpha", ST.BOOL), - ("use_clamp", ST.BOOL)], - - 'ShaderNodeOutputMaterial' : [("is_active_output", ST.BOOL), - ("target", ST.ENUM)], - - 'ShaderNodeOutputWorld' : [("is_active_output", ST.BOOL), - ("target", ST.ENUM)], + 'ShaderNodeOutputAOV' : [ + NTPNodeSetting("name", ST.STRING) + ], + + 'ShaderNodeOutputLight' : [ + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM) + ], + + 'ShaderNodeOutputLineStyle' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'ShaderNodeOutputMaterial' : [ + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM) + ], + + 'ShaderNodeOutputWorld' : [ + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM) + ], # SHADER - 'ShaderNodeAddShader' : [], + 'ShaderNodeAddShader' : [], - 'ShaderNodeBsdfAnisotropic' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfAnisotropic' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], - 'ShaderNodeBackground' : [], - 'ShaderNodeBsdfDiffuse' : [], - 'ShaderNodeEmission' : [], + 'ShaderNodeBackground' : [], + 'ShaderNodeBsdfDiffuse' : [], + 'ShaderNodeEmission' : [], - 'ShaderNodeBsdfGlass' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfGlass' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], - 'ShaderNodeBsdfGlossy' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfGlossy' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], - 'ShaderNodeBsdfHair' : [("component", ST.ENUM)], + 'ShaderNodeBsdfHair' : [ + NTPNodeSetting("component", ST.ENUM) + ], - 'ShaderNodeHoldout' : [], - 'ShaderNodeMixShader' : [], + 'ShaderNodeHoldout' : [], + 'ShaderNodeMixShader' : [], - 'ShaderNodeBsdfPrincipled' : [("distribution", ST.ENUM), - ("subsurface_method", ST.ENUM)], + 'ShaderNodeBsdfPrincipled' : [ + NTPNodeSetting("distribution", ST.ENUM), + NTPNodeSetting("subsurface_method", ST.ENUM) + ], - 'ShaderNodeBsdfHairPrincipled' : [("model", ST.ENUM), - ("parametrization", ST.ENUM)], + 'ShaderNodeBsdfHairPrincipled' : [ + NTPNodeSetting("model", ST.ENUM), + NTPNodeSetting("parametrization", ST.ENUM) + ], - 'ShaderNodeVolumePrincipled' : [], + 'ShaderNodeVolumePrincipled' : [], - 'ShaderNodeBsdfRefraction' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfRefraction' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], - 'ShaderNodeBsdfSheen' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfSheen' : [ + NTPNodeSetting("distribution", ST.ENUM, min_version = (4, 0, 0)) + ], - 'ShaderNodeEeveeSpecular' : [], + 'ShaderNodeEeveeSpecular' : [], - 'ShaderNodeSubsurfaceScattering' : [("falloff", ST.ENUM)], + 'ShaderNodeSubsurfaceScattering' : [ + NTPNodeSetting("falloff", ST.ENUM) + ], - 'ShaderNodeBsdfToon' : [("component", ST.ENUM)], + 'ShaderNodeBsdfToon' : [ + NTPNodeSetting("component", ST.ENUM) + ], - 'ShaderNodeBsdfTranslucent' : [], - 'ShaderNodeBsdfTransparent' : [], - 'ShaderNodeBsdfVelvet' : [], - 'ShaderNodeVolumeAbsorption' : [], - 'ShaderNodeVolumeScatter' : [], + 'ShaderNodeBsdfTranslucent' : [], + 'ShaderNodeBsdfTransparent' : [], + 'ShaderNodeBsdfVelvet' : [], + 'ShaderNodeVolumeAbsorption' : [], + 'ShaderNodeVolumeScatter' : [], # TEXTURE - 'ShaderNodeTexBrick' : [("offset", ST.FLOAT), - ("offset_frequency", ST.INT), - ("squash", ST.FLOAT), - ("squash_frequency", ST.INT)], - - 'ShaderNodeTexChecker' : [], - - 'ShaderNodeTexEnvironment' : [("image", ST.IMAGE), - ("image_user", ST.IMAGE_USER), - ("interpolation", ST.ENUM), - ("projection", ST.ENUM)], - - 'ShaderNodeTexGradient' : [("gradient_type", ST.ENUM)], - - 'ShaderNodeTexIES' : [("filepath", ST.STRING), #TODO - ("ies", ST.TEXT), - ("mode", ST.ENUM)], - - 'ShaderNodeTexImage' : [("extension", ST.ENUM), - ("image", ST.IMAGE), - ("image_user", ST.IMAGE_USER), - ("interpolation", ST.ENUM), - ("projection", ST.ENUM), - ("projection_blend", ST.FLOAT)], - - 'ShaderNodeTexMagic' : [("turbulence_depth", ST.INT)], - - 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", ST.ENUM), - ("musgrave_type", ST.ENUM)], - - 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM), - ("normalize", ST.BOOL)], - - 'ShaderNodeTexPointDensity' : [("interpolation", ST.ENUM), - ("object", ST.OBJECT), - ("particle_color_source", ST.ENUM), - ("particle_system", ST.PARTICLE_SYSTEM), - ("point_source", ST.ENUM), - ("radius", ST.FLOAT), - ("resolution", ST.INT), - ("space", ST.ENUM), - ("vertex_attribute_name", ST.STRING), #TODO - ("vertex_color_source", ST.ENUM)], - - 'ShaderNodeTexSky' : [("air_density", ST.FLOAT), - ("altitude", ST.FLOAT), - ("dust_density", ST.FLOAT), - ("ground_albedo", ST.FLOAT), - ("ozone_density", ST.FLOAT), - ("sky_type", ST.ENUM), - ("sun_direction", ST.VEC3), - ("sun_disc", ST.BOOL), - ("sun_elevation", ST.FLOAT), - ("sun_intensity", ST.FLOAT), - ("sun_rotation", ST.FLOAT), - ("sun_size", ST.FLOAT), - ("turbidity", ST.FLOAT)], - - 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), - ("feature", ST.ENUM), - ("normalize", ST.BOOL), - ("voronoi_dimensions", ST.ENUM)], - - 'ShaderNodeTexWave' : [("bands_direction", ST.ENUM), - ("rings_direction", ST.ENUM), - ("wave_profile", ST.ENUM), - ("wave_type", ST.ENUM)], - - 'ShaderNodeTexWhiteNoise' : [("noise_dimensions", ST.ENUM)], + 'ShaderNodeTexBrick' : [ + NTPNodeSetting("offset", ST.FLOAT), + NTPNodeSetting("offset_frequency", ST.INT), + NTPNodeSetting("squash", ST.FLOAT), + NTPNodeSetting("squash_frequency", ST.INT) + ], + + 'ShaderNodeTexChecker' : [], + + 'ShaderNodeTexEnvironment' : [ + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("image_user", ST.IMAGE_USER), + NTPNodeSetting("interpolation", ST.ENUM), + NTPNodeSetting("projection", ST.ENUM) + ], + + 'ShaderNodeTexGradient' : [ + NTPNodeSetting("gradient_type", ST.ENUM) + ], + + 'ShaderNodeTexIES' : [ + NTPNodeSetting("filepath", ST.STRING), #TODO + NTPNodeSetting("ies", ST.TEXT), + NTPNodeSetting("mode", ST.ENUM) + ], + + 'ShaderNodeTexImage' : [ + NTPNodeSetting("extension", ST.ENUM), + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("image_user", ST.IMAGE_USER), + NTPNodeSetting("interpolation", ST.ENUM), + NTPNodeSetting("projection", ST.ENUM), + NTPNodeSetting("projection_blend", ST.FLOAT) + ], + + 'ShaderNodeTexMagic' : [ + NTPNodeSetting("turbulence_depth", ST.INT) + ], + + 'ShaderNodeTexMusgrave' : [ + NTPNodeSetting("musgrave_dimensions", ST.ENUM), + NTPNodeSetting("musgrave_type", ST.ENUM) + ], + + 'ShaderNodeTexNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM), + NTPNodeSetting("normalize", ST.BOOL, min_version = (4, 0, 0)) + ], + + 'ShaderNodeTexPointDensity' : [ + NTPNodeSetting("interpolation", ST.ENUM), + NTPNodeSetting("object", ST.OBJECT), + NTPNodeSetting("particle_color_source", ST.ENUM), + NTPNodeSetting("particle_system", ST.PARTICLE_SYSTEM), + NTPNodeSetting("point_source", ST.ENUM), + NTPNodeSetting("radius", ST.FLOAT), + NTPNodeSetting("resolution", ST.INT), + NTPNodeSetting("space", ST.ENUM), + NTPNodeSetting("vertex_attribute_name", ST.STRING), #TODO + NTPNodeSetting("vertex_color_source", ST.ENUM) + ], + + 'ShaderNodeTexSky' : [ + NTPNodeSetting("air_density", ST.FLOAT), + NTPNodeSetting("altitude", ST.FLOAT), + NTPNodeSetting("dust_density", ST.FLOAT), + NTPNodeSetting("ground_albedo", ST.FLOAT), + NTPNodeSetting("ozone_density", ST.FLOAT), + NTPNodeSetting("sky_type", ST.ENUM), + NTPNodeSetting("sun_direction", ST.VEC3), + NTPNodeSetting("sun_disc", ST.BOOL), + NTPNodeSetting("sun_elevation", ST.FLOAT), + NTPNodeSetting("sun_intensity", ST.FLOAT), + NTPNodeSetting("sun_rotation", ST.FLOAT), + NTPNodeSetting("sun_size", ST.FLOAT), + NTPNodeSetting("turbidity", ST.FLOAT) + ], + + 'ShaderNodeTexVoronoi' : [ + NTPNodeSetting("distance", ST.ENUM), + NTPNodeSetting("feature", ST.ENUM), + NTPNodeSetting("normalize", ST.BOOL, min_version = (4, 0, 0)), + NTPNodeSetting("voronoi_dimensions", ST.ENUM) + ], + + 'ShaderNodeTexWave' : [ + NTPNodeSetting("bands_direction", ST.ENUM), + NTPNodeSetting("rings_direction", ST.ENUM), + NTPNodeSetting("wave_profile", ST.ENUM), + NTPNodeSetting("wave_type", ST.ENUM) + ], + + 'ShaderNodeTexWhiteNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM) + ], # COLOR 'ShaderNodeBrightContrast' : [], - 'ShaderNodeGamma' : [], - 'ShaderNodeHueSaturation' : [], - 'ShaderNodeInvert' : [], - 'ShaderNodeLightFalloff' : [], - 'ShaderNodeMix' : [("blend_type", ST.ENUM), - ("clamp_factor", ST.BOOL), - ("clamp_result", ST.BOOL), - ("data_type", ST.ENUM), - ("factor_mode", ST.ENUM)], - - 'ShaderNodeRGBCurve' : [("mapping", ST.CURVE_MAPPING)], - - - # VECTOR - 'ShaderNodeBump' : [("invert", ST.BOOL)], - - 'ShaderNodeDisplacement' : [("space", ST.ENUM)], - - 'ShaderNodeMapping' : [("vector_type", ST.ENUM)], - - 'ShaderNodeNormal' : [], + 'ShaderNodeMix' : [ + NTPNodeSetting("blend_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_factor", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_result", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("factor_mode", ST.ENUM, min_version = (3, 4, 0)) + ], - 'ShaderNodeNormalMap' : [("space", ST.ENUM), - ("uv_map", ST.STRING)], #TODO + 'ShaderNodeMixRGB' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], - 'ShaderNodeVectorCurve' : [("mapping", ST.CURVE_MAPPING)], + 'ShaderNodeRGBCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], - 'ShaderNodeVectorDisplacement' : [("space", ST.ENUM)], - 'ShaderNodeVectorRotate' : [("invert", ST.BOOL), - ("rotation_type", ST.ENUM)], - - 'ShaderNodeVectorTransform' : [("convert_from", ST.ENUM), - ("convert_to", ST.ENUM), - ("vector_type", ST.ENUM)], + # VECTOR + 'ShaderNodeBump' : [ + NTPNodeSetting("invert", ST.BOOL) + ], + + 'ShaderNodeDisplacement' : [ + NTPNodeSetting("space", ST.ENUM) + ], + + 'ShaderNodeMapping' : [ + NTPNodeSetting("vector_type", ST.ENUM) + ], + + 'ShaderNodeNormal' : [], + + 'ShaderNodeNormalMap' : [ + NTPNodeSetting("space", ST.ENUM), + NTPNodeSetting("uv_map", ST.STRING) #TODO + ], + + 'ShaderNodeVectorCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'ShaderNodeVectorDisplacement' : [ + NTPNodeSetting("space", ST.ENUM) + ], + + 'ShaderNodeVectorRotate' : [ + NTPNodeSetting("invert", ST.BOOL), + NTPNodeSetting("rotation_type", ST.ENUM) + ], + + 'ShaderNodeVectorTransform' : [ + NTPNodeSetting("convert_from", ST.ENUM), + NTPNodeSetting("convert_to", ST.ENUM), + NTPNodeSetting("vector_type", ST.ENUM) + ], # CONVERTER - 'ShaderNodeBlackbody' : [], - - 'ShaderNodeClamp' : [("clamp_type", ST.ENUM)], + 'ShaderNodeBlackbody' : [], - 'ShaderNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + 'ShaderNodeClamp' : [ + NTPNodeSetting("clamp_type", ST.ENUM) + ], - 'ShaderNodeCombineColor' : [("mode", ST.ENUM)], + 'ShaderNodeValToRGB' : [ + NTPNodeSetting("color_ramp", ST.COLOR_RAMP) + ], - 'ShaderNodeCombineXYZ' : [], + 'ShaderNodeCombineColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], - 'ShaderNodeFloatCurve' : [("mapping", ST.CURVE_MAPPING)], + 'ShaderNodeCombineXYZ' : [], - 'ShaderNodeMapRange' : [("clamp", ST.BOOL), - ("data_type", ST.ENUM), - ("interpolation_type", ST.ENUM)], + 'ShaderNodeFloatCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], - 'ShaderNodeMath' : [("operation", ST.ENUM), - ("use_clamp", ST.BOOL)], + 'ShaderNodeMapRange' : [ + NTPNodeSetting("clamp", ST.BOOL), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("interpolation_type", ST.ENUM) + ], - 'ShaderNodeRGBToBW' : [], + 'ShaderNodeMath' : [ + NTPNodeSetting("operation", ST.ENUM), + NTPNodeSetting("use_clamp", ST.BOOL) + ], - 'ShaderNodeSeparateColor' : [("mode", ST.ENUM)], + 'ShaderNodeRGBToBW' : [], - 'ShaderNodeSeparateXYZ' : [], + 'ShaderNodeSeparateColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], - 'ShaderNodeShaderToRGB' : [], + 'ShaderNodeSeparateXYZ' : [], + 'ShaderNodeShaderToRGB' : [], - 'ShaderNodeVectorMath' : [("operation", ST.ENUM)], + 'ShaderNodeVectorMath' : [ + NTPNodeSetting("operation", ST.ENUM) + ], - 'ShaderNodeWavelength' : [], + 'ShaderNodeWavelength' : [], # SCRIPT - 'ShaderNodeScript' : [("bytecode", ST.STRING), #TODO: test all that - ("bytecode_hash", ST.STRING), - ("filepath", ST.STRING), - ("mode", ST.ENUM), - ("script", ST.TEXT), - ("use_auto_update", ST.BOOL)], + 'ShaderNodeScript' : [ + NTPNodeSetting("bytecode", ST.STRING), #TODO: test all that + NTPNodeSetting("bytecode_hash", ST.STRING), + NTPNodeSetting("filepath", ST.STRING), + NTPNodeSetting("mode", ST.ENUM), + NTPNodeSetting("script", ST.TEXT), + NTPNodeSetting("use_auto_update", ST.BOOL) + ], # MISC - 'ShaderNodeGroup' : [('node_tree', ST.NODE_TREE)], + 'ShaderNodeGroup' : [ + NTPNodeSetting('node_tree', ST.NODE_TREE) + ], - 'NodeFrame' : [("label_size", ST.INT), - ("shrink", ST.BOOL), - ("text", ST.TEXT)], + 'NodeFrame' : [ + NTPNodeSetting("label_size", ST.INT), + NTPNodeSetting("shrink", ST.BOOL), + NTPNodeSetting("text", ST.TEXT) + ], - 'NodeGroupInput' : [], + 'NodeGroupInput' : [], - 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + 'NodeGroupOutput' : [ + NTPNodeSetting("is_active_output", ST.BOOL) + ], - 'NodeReroute' : [] + 'NodeReroute' : [] } diff --git a/material/operator.py b/material/operator.py index 485b530..0cc8461 100644 --- a/material/operator.py +++ b/material/operator.py @@ -11,7 +11,7 @@ MAT_VAR = "mat" NODE = "node" -shader_op_reserved_names = {MAT_VAR, NODE} +SHADER_OP_RESERVED_NAMES = {MAT_VAR, NODE} class NTPMaterialOperator(NTP_Operator): bl_idname = "node.ntp_material" @@ -24,7 +24,7 @@ class NTPMaterialOperator(NTP_Operator): def __init__(self): super().__init__() self._settings = shader_node_settings - for name in shader_op_reserved_names: + for name in SHADER_OP_RESERVED_NAMES: self._used_vars[name] = 0 def _create_material(self, indent: str): diff --git a/ntp_operator.py b/ntp_operator.py index a7b1862..732985a 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -21,13 +21,18 @@ IMAGE_PATH = "image_path" BASE_DIR = "base_dir" -reserved_names = { +RESERVED_NAMES = { INDEX, IMAGE_DIR_NAME, IMAGE_PATH, BASE_DIR } +#node input sockets that are messy to set default values for +DONT_SET_DEFAULTS = {'NodeSocketGeometry', + 'NodeSocketShader', + 'NodeSocketVirtual'} + class NTP_Operator(Operator): """ "Abstract" base class for all NTP operators. Blender types and abstraction @@ -96,7 +101,7 @@ def __init__(self): # Dictionary used for setting node properties self._settings: dict[str, list[(str, ST)]] = {} - for name in reserved_names: + for name in RESERVED_NAMES: self._used_vars[name] = 0 def _write(self, string: str, indent: str = None): @@ -273,57 +278,63 @@ def _set_settings_defaults(self, node: Node) -> None: node_var = self._node_vars[node] - for (attr_name, type) in self._settings[node.bl_idname]: + for setting in self._settings[node.bl_idname]: + + attr_name = setting.name + st = setting.st + if not hasattr(node, attr_name): - self.report({'WARNING'}, - f"NodeToPython: Couldn't find attribute " - f"\"{attr_name}\" for node {node.name} of type " - f"{node.bl_idname}") + if (bpy.app.version >= setting.min_version and + bpy.app.version <= setting.max_version): + self.report({'WARNING'}, + f"NodeToPython: Couldn't find attribute " + f"\"{attr_name}\" for node {node.name} of type " + f"{node.bl_idname}") continue attr = getattr(node, attr_name, None) if attr is None: continue setting_str = f"{node_var}.{attr_name}" - if type == ST.ENUM: + if st == ST.ENUM: if attr != '': self._write(f"{setting_str} = {enum_to_py_str(attr)}") - elif type == ST.ENUM_SET: + elif st == ST.ENUM_SET: self._write(f"{setting_str} = {attr}") - elif type == ST.STRING: + elif st == ST.STRING: self._write(f"{setting_str} = {str_to_py_str(attr)}") - elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: + elif st == ST.BOOL or st == ST.INT or st == ST.FLOAT: self._write(f"{setting_str} = {attr}") - elif type == ST.VEC1: + elif st == ST.VEC1: self._write(f"{setting_str} = {vec1_to_py_str(attr)}") - elif type == ST.VEC2: + elif st == ST.VEC2: self._write(f"{setting_str} = {vec2_to_py_str(attr)}") - elif type == ST.VEC3: + elif st == ST.VEC3: self._write(f"{setting_str} = {vec3_to_py_str(attr)}") - elif type == ST.VEC4: + elif st == ST.VEC4: self._write(f"{setting_str} = {vec4_to_py_str(attr)}") - elif type == ST.COLOR: + elif st == ST.COLOR: self._write(f"{setting_str} = {color_to_py_str(attr)}") - elif type == ST.MATERIAL: + elif st == ST.MATERIAL: name = str_to_py_str(attr.name) self._write((f"if {name} in bpy.data.materials:")) self._write((f"\t{setting_str} = bpy.data.materials[{name}]")) - elif type == ST.OBJECT: + elif st == ST.OBJECT: name = str_to_py_str(attr.name) self._write((f"if {name} in bpy.data.objects:")) self._write((f"\t{setting_str} = bpy.data.objects[{name}]")) - elif type == ST.COLOR_RAMP: + elif st == ST.COLOR_RAMP: self._color_ramp_settings(node, attr_name) - elif type == ST.CURVE_MAPPING: + elif st == ST.CURVE_MAPPING: self._curve_mapping_settings(node, attr_name) - elif type == ST.NODE_TREE: + elif st == ST.NODE_TREE: self._node_tree_settings(node, attr_name) - elif type == ST.IMAGE: + elif st == ST.IMAGE: if self._addon_dir is not None and attr is not None: if attr.source in {'FILE', 'GENERATED', 'TILED'}: self._save_image(attr) self._load_image(attr, f"{node_var}.{attr_name}") - elif type == ST.IMAGE_USER: + elif st == ST.IMAGE_USER: self._image_user_settings(attr, f"{node_var}.{attr_name}") if bpy.app.version < (4, 0, 0): @@ -663,7 +674,7 @@ def _set_input_defaults(self, node: Node) -> None: node_var = self._node_vars[node] for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults and not input.is_linked: + if input.bl_idname not in DONT_SET_DEFAULTS and not input.is_linked: # TODO: this could be cleaner socket_var = f"{node_var}.inputs[{i}]" diff --git a/utils.py b/utils.py index 5b3cb5d..84fc729 100644 --- a/utils.py +++ b/utils.py @@ -5,12 +5,7 @@ from enum import Enum, auto import keyword import re -from typing import Tuple - -#node input sockets that are messy to set default values for -dont_set_defaults = {'NodeSocketGeometry', - 'NodeSocketShader', - 'NodeSocketVirtual'} +from typing import NamedTuple class ST(Enum): """ @@ -28,7 +23,7 @@ class ST(Enum): VEC3 = auto() VEC4 = auto() COLOR = auto() - + # Special settings COLOR_RAMP = auto() CURVE_MAPPING = auto() @@ -54,6 +49,13 @@ class ST(Enum): FILE_SLOTS = auto() LAYER_SLOTS = auto() #unimplemented +class NTPNodeSetting(NamedTuple): + name: str + st: ST + min_version: tuple = (3, 0, 0) + max_version: tuple = (4, 1, 0) + + def clean_string(string: str, lower: bool = True) -> str: """ Cleans up a string for use as a variable or file name From 8c6968f370ed310f80ef41e67541e69addac3721 Mon Sep 17 00:00:00 2001 From: BrendanParmer <51296046+BrendanParmer@users.noreply.github.com> Date: Sat, 27 Jan 2024 22:47:06 -0600 Subject: [PATCH 72/72] docs: update README for v3.0, code comments --- docs/README.md | 46 +++++++++++++++++++++------------------------- utils.py | 4 ++-- 2 files changed, 23 insertions(+), 27 deletions(-) diff --git a/docs/README.md b/docs/README.md index 1edd431..bc461c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,7 +15,7 @@ Blender's node-based editors are powerful, yet accessible tools, and I wanted to * interfacing with other parts of the software or properties of an object ## Supported Versions -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. +NodeToPython v3.0 is supported for Blender 3.0 - 4.0 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update. ## Installation 1. Download the `NodeToPython.zip` file from the [latest release](https://github.com/BrendanParmer/NodeToPython/releases) @@ -35,33 +35,29 @@ Select the node group you want code for, and you'll be prompted with a **Script* * Doesn't include `import bpy` line * 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 * **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. - -## Future -### v3.x -* Expansion to Compositing nodes -* New Blender 4.0 nodes and changes - -### Later -* Better asset handling -* Auto-set handle movies and image sequences -* Automatically format code to be PEP8 compliant -* Automatically detect the minimum version of Blender needed to run the add-on - -## Potential Issues -* As of version 2.2, the add-on will not set default values for - * Scripts - * IES files - * Filepaths - * UV maps -* This add-on doesn't currently set default values in Geometry Nodes modifiers, just the node groups themselves -* Currently when setting default values for the following, the add-on must be run in the same blend file as the node group was created in to set the default, otherwise it will just set it to `None`: + * The current default operator install location is in the Object menu + +## Future Plans +* Investigate drivers and keyframes +* A better default operator install location for generated add-ons +* A development repository with useful scripts and tests +* Better handling of more setting types, including + * Image sequences + * Movie clips * Materials * Objects - * Collections * Textures - -* In a future version, I plan on having the add-on adding all of the above to the Asset Library for reference -* 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)) + * Text objects + * Scenes + * Particle systems + * Fonts + * Masks + * Cryptomatte entries + * Image format settings + * File slots + * Layer slots +* Automatic detection of the minimum/maximum version of Blender compatible with a generated add-on +* Autoformatting of generated code ## Bug Reports and Suggestions diff --git a/utils.py b/utils.py index 84fc729..08d68fb 100644 --- a/utils.py +++ b/utils.py @@ -45,8 +45,8 @@ class ST(Enum): FONT = auto() #unimplemented MASK = auto() #unimplemented CRYPTOMATTE_ENTRIES = auto() #unimplemented - IMAGE_FORMAT_SETTINGS = auto() - FILE_SLOTS = auto() + IMAGE_FORMAT_SETTINGS = auto() #unimplemented + FILE_SLOTS = auto() #unimplemented LAYER_SLOTS = auto() #unimplemented class NTPNodeSetting(NamedTuple):