diff --git a/NodeToPython/__init__.py b/NodeToPython/__init__.py index e37c738..efe61f0 100644 --- a/NodeToPython/__init__.py +++ b/NodeToPython/__init__.py @@ -2,7 +2,7 @@ "name": "Node to Python", "description": "Convert Blender node groups to a Python add-on!", "author": "Brendan Parmer", - "version": (3, 2, 2), + "version": (3, 3, 0), "blender": (3, 0, 0), "location": "Node", "category": "Node", diff --git a/NodeToPython/blender_manifest.toml b/NodeToPython/blender_manifest.toml index 8c63634..332fccb 100644 --- a/NodeToPython/blender_manifest.toml +++ b/NodeToPython/blender_manifest.toml @@ -1,7 +1,7 @@ schema_version = "1.0.0" id = "node_to_python" -version = "3.2.2" +version = "3.3.0" name = "Node To Python" tagline = "Turn node groups into Python code" maintainer = "Brendan Parmer " @@ -12,7 +12,7 @@ website = "https://github.com/BrendanParmer/NodeToPython" tags = ["Development", "Compositing", "Geometry Nodes", "Material", "Node"] blender_version_min = "4.2.0" -blender_version_max = "4.3.0" +blender_version_max = "4.4.0" license = [ "SPDX:MIT", diff --git a/NodeToPython/compositor/operator.py b/NodeToPython/compositor/operator.py index d16ebbc..eb798fc 100644 --- a/NodeToPython/compositor/operator.py +++ b/NodeToPython/compositor/operator.py @@ -31,45 +31,44 @@ def __init__(self): self._used_vars[name] = 0 - def _create_scene(self, indent: str): + def _create_scene(self, indent_level: int): #TODO: wrap in more general unique name util function - self._write(f"# Generate unique scene name", indent) + self._write(f"# Generate unique scene name", indent_level) self._write(f"{BASE_NAME} = {str_to_py_str(self.compositor_name)}", - indent) - self._write(f"{END_NAME} = {BASE_NAME}", indent) - self._write(f"if bpy.data.scenes.get({END_NAME}) != None:", indent) + indent_level) + self._write(f"{END_NAME} = {BASE_NAME}", indent_level) + self._write(f"if bpy.data.scenes.get({END_NAME}) != None:", indent_level) - indent2 = f"{indent}\t" - self._write(f"{INDEX} = 1", indent2) + self._write(f"{INDEX} = 1", indent_level + 1) self._write(f"{END_NAME} = {BASE_NAME} + f\".{{i:03d}}\"", - indent2) + indent_level + 1) self._write(f"while bpy.data.scenes.get({END_NAME}) != None:", - indent2) + indent_level + 1) - indent3 = f"{indent}\t\t" - self._write(f"{END_NAME} = {BASE_NAME} + f\".{{{INDEX}:03d}}\"", indent3) - self._write(f"{INDEX} += 1\n", indent3) + self._write(f"{END_NAME} = {BASE_NAME} + f\".{{{INDEX}:03d}}\"", + indent_level + 2) + self._write(f"{INDEX} += 1\n", indent_level + 2) - 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) + self._write(f"{SCENE} = bpy.context.window.scene.copy()\n", indent_level) + self._write(f"{SCENE}.name = {END_NAME}", indent_level) + self._write(f"{SCENE}.use_fake_user = True", indent_level) + self._write(f"bpy.context.window.scene = {SCENE}", indent_level) def _initialize_compositor_node_tree(self, ntp_nt, nt_name): #initialize node group - self._write(f"#initialize {nt_name} node group", self._outer) - self._write(f"def {ntp_nt.var}_node_group():", self._outer) + self._write(f"#initialize {nt_name} node group", self._outer_indent_level) + self._write(f"def {ntp_nt.var}_node_group():", self._outer_indent_level) if ntp_nt.node_tree == self._base_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"{ntp_nt.var}.nodes.remove({NODE})", self._inner_indent_level + 1) else: self._write((f"{ntp_nt.var} = bpy.data.node_groups.new(" f"type = \'CompositorNodeTree\', " f"name = {str_to_py_str(nt_name)})")) - self._write("") + self._write("", 0) # Compositor node tree settings #TODO: might be good to make this optional @@ -108,7 +107,7 @@ def _set_color_balance_settings(self, node: CompositorNodeColorBalance NTPNodeSetting("gamma", ST.COLOR, min_version_=(3, 5, 0)), NTPNodeSetting("lift", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("lift", ST.COLOR, min_version_=(3, 5, 0))] - else: + elif node.correction_method == 'OFFSET_POWER_SLOPE': lst = [NTPNodeSetting("correction_method", ST.ENUM), NTPNodeSetting("offset", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("offset", ST.COLOR, min_version_=(3, 5, 0)), @@ -117,11 +116,20 @@ def _set_color_balance_settings(self, node: CompositorNodeColorBalance NTPNodeSetting("power", ST.COLOR, min_version_=(3, 5, 0)), NTPNodeSetting("slope", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("slope", ST.COLOR, min_version_=(3, 5, 0))] + elif node.correction_method == 'WHITEPOINT': + lst = [NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("input_temperature", ST.FLOAT), + NTPNodeSetting("input_tint", ST.FLOAT), + NTPNodeSetting("output_temperature", ST.FLOAT), + NTPNodeSetting("output_tint", ST.FLOAT)] + else: + self.report({'ERROR'}, + f"Unknown color balance correction method " + f"{enum_to_py_str(node.correction_method)}") + return color_balance_info = self._node_infos['CompositorNodeColorBalance'] self._node_infos['CompositorNodeColorBalance'] = color_balance_info._replace(attributes_ = lst) - for setting in self._node_infos['CompositorNodeColorBalance'].attributes_: - print(setting.name_) def _process_node(self, node: Node, ntp_nt: NTP_NodeTree): """ @@ -191,7 +199,7 @@ def _process_node_tree(self, node_tree: CompositorNodeTree): self._write(f"return {nt_var}\n") #create node group - self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer_indent_level) def execute(self, context): if not self._setup_options(context.scene.ntp_options): @@ -214,8 +222,8 @@ 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._outer_indent_level = 2 + self._inner_indent_level = 3 if not self._setup_addon_directories(context, comp_var): return {'CANCELLED'} @@ -226,7 +234,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("def execute(self, context):", "\t") + self._write("def execute(self, context):", 1) else: self._file = StringIO("") if self._include_imports: @@ -234,9 +242,9 @@ def execute(self, context): if self.is_scene: if self._mode == 'ADDON': - self._create_scene("\t\t") + self._create_scene(2) elif self._mode == 'SCRIPT': - self._create_scene("") + self._create_scene(0) node_trees_to_process = self._topological_sort(self._base_node_tree) @@ -244,7 +252,7 @@ def execute(self, context): self._process_node_tree(node_tree) if self._mode == 'ADDON': - self._write("return {'FINISHED'}\n", self._outer) + self._write("return {'FINISHED'}\n", self._outer_indent_level) self._create_menu_func() self._create_register_func() diff --git a/NodeToPython/geometry/node_tree.py b/NodeToPython/geometry/node_tree.py index 73b3565..8129276 100644 --- a/NodeToPython/geometry/node_tree.py +++ b/NodeToPython/geometry/node_tree.py @@ -1,18 +1,24 @@ import bpy -from bpy.types import GeometryNodeTree +from bpy.types import GeometryNodeTree, GeometryNode if bpy.app.version >= (3, 6, 0): from bpy.types import GeometryNodeSimulationInput -if bpy.app.version > (4, 0, 0): +if bpy.app.version >= (4, 0, 0): from bpy.types import GeometryNodeRepeatInput +if bpy.app.version >= (4, 3, 0): + from bpy.types import GeometryNodeForeachGeometryElementInput + 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.zone_inputs: dict[list[GeometryNode]] = {} if bpy.app.version >= (3, 6, 0): - self.sim_inputs: list[GeometryNodeSimulationInput] = [] + self.zone_inputs["GeometryNodeSimulationInput"] = [] if bpy.app.version >= (4, 0, 0): - self.repeat_inputs: list[GeometryNodeRepeatInput] = [] + self.zone_inputs["GeometryNodeRepeatInput"] = [] + if bpy.app.version >= (4, 3, 0): + self.zone_inputs["GeometryNodeForeachGeometryElementInput"] = [] diff --git a/NodeToPython/geometry/operator.py b/NodeToPython/geometry/operator.py index 4cb8a8b..114f509 100644 --- a/NodeToPython/geometry/operator.py +++ b/NodeToPython/geometry/operator.py @@ -49,16 +49,12 @@ 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 == 'GeometryNodeSimulationInput': - ntp_nt.sim_inputs.append(node) - - elif node.bl_idname == 'GeometryNodeRepeatInput': - ntp_nt.repeat_inputs.append(node) + if node.bl_idname in ntp_nt.zone_inputs: + ntp_nt.zone_inputs[node.bl_idname].append(node) self._hide_hidden_sockets(node) - if node.bl_idname not in {'GeometryNodeSimulationInput', - 'GeometryNodeRepeatInput'}: + if node.bl_idname not in ntp_nt.zone_inputs: self._set_socket_defaults(node) if bpy.app.version >= (3, 6, 0): @@ -81,7 +77,7 @@ def _process_zones(self, zone_inputs: list[GeometryNode]) -> None: #must set defaults after paired with output self._set_socket_defaults(zone_input) self._set_socket_defaults(zone_output) - self._write("") + self._write("", 0) if bpy.app.version >= (4, 0, 0): def _set_geo_tree_properties(self, node_tree: GeometryNodeTree) -> None: @@ -105,7 +101,7 @@ def _set_geo_tree_properties(self, node_tree: GeometryNodeTree) -> None: for flag in tool_flags: if hasattr(node_tree, flag) is True: self._write(f"{nt_var}.{flag} = {getattr(node_tree, flag)}") - self._write("") + self._write("", 0) def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: """ @@ -119,8 +115,8 @@ def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: 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"#initialize {nt_var} node group", self._outer_indent_level) + self._write(f"def {nt_var}_node_group():", self._outer_indent_level) self._write(f"{nt_var} = bpy.data.node_groups.new(" f"type = \'GeometryNodeTree\', " f"name = {str_to_py_str(node_tree.name)})\n") @@ -139,10 +135,8 @@ def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: for node in node_tree.nodes: self._process_node(node, ntp_nt) - if bpy.app.version >= (3, 6, 0): - self._process_zones(ntp_nt.sim_inputs) - if bpy.app.version >= (4, 0, 0): - self._process_zones(ntp_nt.repeat_inputs) + for zone_list in ntp_nt.zone_inputs.values(): + self._process_zones(zone_list) #set look of nodes self._set_parents(node_tree) @@ -155,19 +149,19 @@ def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: self._write(f"return {nt_var}\n") #create node group - self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer_indent_level) def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): #get object - self._write(f"{OBJECT_NAME} = bpy.context.object.name", self._outer) - self._write(f"{OBJECT} = bpy.data.objects[{OBJECT_NAME}]", self._outer) + self._write(f"{OBJECT_NAME} = bpy.context.object.name", self._outer_indent_level) + self._write(f"{OBJECT} = bpy.data.objects[{OBJECT_NAME}]", self._outer_indent_level) #set modifier to the one we just created mod_name = str_to_py_str(nt.name) self._write(f"{MODIFIER} = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')", self._outer) - self._write(f"{MODIFIER}.node_group = {nt_var}", self._outer) + f"type = 'NODES')", self._outer_indent_level) + self._write(f"{MODIFIER}.node_group = {nt_var}", self._outer_indent_level) def execute(self, context): @@ -181,8 +175,8 @@ def execute(self, context): nt_var = clean_string(nt.name) if self._mode == 'ADDON': - self._outer = "\t\t" - self._inner = "\t\t\t" + self._outer_indent_level = 2 + self._inner_indent_level = 3 if not self._setup_addon_directories(context, nt_var): return {'CANCELLED'} @@ -192,7 +186,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._write("def execute(self, context):", "\t") + self._write("def execute(self, context):", 1) else: self._file = StringIO("") if self._include_imports: @@ -206,7 +200,7 @@ def execute(self, context): if self._mode == 'ADDON': self._apply_modifier(nt, nt_var) - self._write("return {'FINISHED'}\n", self._outer) + self._write("return {'FINISHED'}\n", self._outer_indent_level) self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/NodeToPython/node_settings.py b/NodeToPython/node_settings.py index b771df0..20e8e4e 100644 --- a/NodeToPython/node_settings.py +++ b/NodeToPython/node_settings.py @@ -9,6 +9,7 @@ class ST(Enum): COLOR = auto() ENUM = auto() ENUM_SET = auto() + EULER = auto() FLOAT = auto() INT = auto() STRING = auto() @@ -17,26 +18,28 @@ class ST(Enum): VEC3 = auto() VEC4 = auto() BAKE_ITEMS = auto() + CAPTURE_ATTRIBUTE_ITEMS = auto() COLOR_RAMP = auto() CURVE_MAPPING = auto() ENUM_DEFINITION = auto() + ENUM_ITEM = auto() + FOREACH_GEO_ELEMENT_GENERATION_ITEMS = auto() + FOREACH_GEO_ELEMENT_INPUT_ITEMS = auto() + FOREACH_GEO_ELEMENT_MAIN_ITEMS = auto() INDEX_SWITCH_ITEMS = auto() + MENU_SWITCH_ITEMS = auto() NODE_TREE = auto() REPEAT_OUTPUT_ITEMS = auto() SIM_OUTPUT_ITEMS = auto() IMAGE = auto() IMAGE_USER = auto() - CAPTURE_ATTRIBUTE_ITEMS = auto() CRYPTOMATTE_ENTRIES = auto() - ENUM_ITEM = auto() - EULER = auto() FILE_SLOTS = auto() FONT = auto() IMAGE_FORMAT_SETTINGS = auto() LAYER_SLOTS = auto() MASK = auto() MATERIAL = auto() - MENU_SWITCH_ITEMS = auto() MOVIE_CLIP = auto() OBJECT = auto() PARTICLE_SYSTEM = auto() @@ -48,12 +51,12 @@ class NTPNodeSetting(NamedTuple): name_: str st_: ST min_version_: tuple = (3, 0, 0) - max_version_: tuple = (4, 3, 0) + max_version_: tuple = (4, 4, 0) class NodeInfo(NamedTuple): attributes_: list[NTPNodeSetting] min_version_: tuple = (3, 0, 0) - max_version_: tuple = (4, 3, 0) + max_version_: tuple = (4, 4, 0) node_settings : dict[str, NodeInfo] = { 'CompositorNodeAlphaOver' : NodeInfo( @@ -161,11 +164,17 @@ class NodeInfo(NamedTuple): NTPNodeSetting("gain", ST.COLOR, min_version_=(3, 5, 0)), NTPNodeSetting("gamma", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("gamma", ST.COLOR, min_version_=(3, 5, 0)), + NTPNodeSetting("input_temperature", ST.FLOAT, min_version_=(4, 3, 0)), + NTPNodeSetting("input_tint", ST.FLOAT, min_version_=(4, 3, 0)), + NTPNodeSetting("input_whitepoint", ST.COLOR, min_version_=(4, 3, 0)), NTPNodeSetting("lift", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("lift", ST.COLOR, min_version_=(3, 5, 0)), NTPNodeSetting("offset", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("offset", ST.COLOR, min_version_=(3, 5, 0)), NTPNodeSetting("offset_basis", ST.FLOAT), + NTPNodeSetting("output_temperature", ST.FLOAT, min_version_=(4, 3, 0)), + NTPNodeSetting("output_tint", ST.FLOAT, min_version_=(4, 3, 0)), + NTPNodeSetting("output_whitepoint", ST.COLOR, min_version_=(4, 3, 0)), NTPNodeSetting("power", ST.VEC3, max_version_=(3, 5, 0)), NTPNodeSetting("power", ST.COLOR, min_version_=(3, 5, 0)), NTPNodeSetting("slope", ST.VEC3, max_version_=(3, 5, 0)), @@ -652,6 +661,7 @@ class NodeInfo(NamedTuple): NTPNodeSetting("file_slots", ST.FILE_SLOTS), NTPNodeSetting("format", ST.IMAGE_FORMAT_SETTINGS), NTPNodeSetting("layer_slots", ST.LAYER_SLOTS), + NTPNodeSetting("save_as_render", ST.BOOL, min_version_=(4, 3, 0)), ] ), @@ -961,6 +971,13 @@ class NodeInfo(NamedTuple): ] ), + 'FunctionNodeHashValue' : NodeInfo( + [ + NTPNodeSetting("data_type", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + 'FunctionNodeInputBool' : NodeInfo( [ NTPNodeSetting("boolean", ST.BOOL), @@ -1003,6 +1020,13 @@ class NodeInfo(NamedTuple): ] ), + 'FunctionNodeIntegerMath' : NodeInfo( + [ + NTPNodeSetting("operation", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + 'FunctionNodeInvertMatrix' : NodeInfo( [], min_version_ = (4, 2, 0) @@ -1018,6 +1042,11 @@ class NodeInfo(NamedTuple): max_version_ = (3, 2, 0) ), + 'FunctionNodeMatrixDeterminant' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + 'FunctionNodeMatrixMultiply' : NodeInfo( [], min_version_ = (4, 2, 0) @@ -1119,7 +1148,9 @@ class NodeInfo(NamedTuple): ), 'FunctionNodeValueToString' : NodeInfo( - [] + [ + NTPNodeSetting("data_type", ST.ENUM, min_version_=(4, 3, 0)), + ] ), 'GeometryNodeAccumulateField' : NodeInfo( @@ -1302,6 +1333,11 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeCurvesToGreasePencil' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + 'GeometryNodeCustomGroup' : NodeInfo( [ NTPNodeSetting("node_tree", ST.NODE_TREE), @@ -1423,6 +1459,25 @@ class NodeInfo(NamedTuple): min_version_ = (3, 1, 0) ), + 'GeometryNodeForeachGeometryElementInput' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + + 'GeometryNodeForeachGeometryElementOutput' : NodeInfo( + [ + NTPNodeSetting("active_generation_index", ST.INT), + NTPNodeSetting("active_input_index", ST.INT), + NTPNodeSetting("active_main_index", ST.INT), + NTPNodeSetting("domain", ST.ENUM), + NTPNodeSetting("generation_items", ST.FOREACH_GEO_ELEMENT_GENERATION_ITEMS), + NTPNodeSetting("input_items", ST.FOREACH_GEO_ELEMENT_INPUT_ITEMS), + NTPNodeSetting("inspection_index", ST.INT), + NTPNodeSetting("main_items", ST.FOREACH_GEO_ELEMENT_MAIN_ITEMS), + ], + min_version_ = (4, 3, 0) + ), + 'GeometryNodeGeometryToInstance' : NodeInfo( [], min_version_ = (3, 1, 0) @@ -1435,6 +1490,41 @@ class NodeInfo(NamedTuple): min_version_ = (4, 1, 0) ), + 'GeometryNodeGizmoDial' : NodeInfo( + [ + NTPNodeSetting("color_id", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + + 'GeometryNodeGizmoLinear' : NodeInfo( + [ + NTPNodeSetting("color_id", ST.ENUM), + NTPNodeSetting("draw_style", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + + 'GeometryNodeGizmoTransform' : NodeInfo( + [ + NTPNodeSetting("use_rotation_x", ST.BOOL), + NTPNodeSetting("use_rotation_y", ST.BOOL), + NTPNodeSetting("use_rotation_z", ST.BOOL), + NTPNodeSetting("use_scale_x", ST.BOOL), + NTPNodeSetting("use_scale_y", ST.BOOL), + NTPNodeSetting("use_scale_z", ST.BOOL), + NTPNodeSetting("use_translation_x", ST.BOOL), + NTPNodeSetting("use_translation_y", ST.BOOL), + NTPNodeSetting("use_translation_z", ST.BOOL), + ], + min_version_ = (4, 3, 0) + ), + + 'GeometryNodeGreasePencilToCurves' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + 'GeometryNodeGridToMesh' : NodeInfo( [], min_version_ = (4, 2, 0) @@ -1458,6 +1548,21 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeImportOBJ' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + + 'GeometryNodeImportPLY' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + + 'GeometryNodeImportSTL' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + 'GeometryNodeIndexOfNearest' : NodeInfo( [], min_version_ = (3, 6, 0) @@ -1976,6 +2081,13 @@ class NodeInfo(NamedTuple): min_version_ = (3, 1, 0) ), + 'GeometryNodeMergeLayers' : NodeInfo( + [ + NTPNodeSetting("mode", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + 'GeometryNodeMeshBoolean' : NodeInfo( [ NTPNodeSetting("operation", ST.ENUM), @@ -2305,6 +2417,11 @@ class NodeInfo(NamedTuple): [] ), + 'GeometryNodeSetGeometryName' : NodeInfo( + [], + min_version_ = (4, 3, 0) + ), + 'GeometryNodeSetID' : NodeInfo( [] ), @@ -2469,6 +2586,7 @@ class NodeInfo(NamedTuple): 'GeometryNodeToolSetSelection' : NodeInfo( [ NTPNodeSetting("domain", ST.ENUM), + NTPNodeSetting("selection_type", ST.ENUM, min_version_=(4, 3, 0)), ], min_version_ = (4, 0, 0) ), @@ -2536,6 +2654,13 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeWarning' : NodeInfo( + [ + NTPNodeSetting("warning_type", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + 'NodeFrame' : NodeInfo( [ NTPNodeSetting("label_size", ST.INT), @@ -2561,7 +2686,9 @@ class NodeInfo(NamedTuple): ), 'NodeReroute' : NodeInfo( - [] + [ + NTPNodeSetting("socket_idname", ST.STRING, min_version_=(4, 3, 0)), + ] ), 'ShaderNodeAddShader' : NodeInfo( @@ -2637,6 +2764,14 @@ class NodeInfo(NamedTuple): ] ), + 'ShaderNodeBsdfMetallic' : NodeInfo( + [ + NTPNodeSetting("distribution", ST.ENUM), + NTPNodeSetting("fresnel_type", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + 'ShaderNodeBsdfPrincipled' : NodeInfo( [ NTPNodeSetting("distribution", ST.ENUM), @@ -2989,6 +3124,13 @@ class NodeInfo(NamedTuple): ] ), + 'ShaderNodeTexGabor' : NodeInfo( + [ + NTPNodeSetting("gabor_type", ST.ENUM), + ], + min_version_ = (4, 3, 0) + ), + 'ShaderNodeTexGradient' : NodeInfo( [ NTPNodeSetting("gradient_type", ST.ENUM), @@ -3168,7 +3310,9 @@ class NodeInfo(NamedTuple): ), 'ShaderNodeVolumeScatter' : NodeInfo( - [] + [ + NTPNodeSetting("phase", ST.ENUM, min_version_=(4, 3, 0)), + ] ), 'ShaderNodeWavelength' : NodeInfo( diff --git a/NodeToPython/ntp_operator.py b/NodeToPython/ntp_operator.py index 767d6c3..5442a56 100644 --- a/NodeToPython/ntp_operator.py +++ b/NodeToPython/ntp_operator.py @@ -1,7 +1,6 @@ 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 @@ -9,6 +8,8 @@ from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceSocket from bpy.types import NodeTreeInterfaceItem +from bpy.types import bpy_prop_array + import datetime import os import shutil @@ -82,8 +83,8 @@ def __init__(self): self._class_name: str = None # Indentation to use for the default write function - self._outer: str = "" - self._inner: str = "\t" + self._outer_indent_level: int = 0 + self._inner_indent_level: int = 1 # Base node tree we're converting self._base_node_tree: NodeTree = None @@ -109,20 +110,34 @@ def __init__(self): # Set dimensions of generated nodes self._should_set_dimensions = True + # Indentation string (default four spaces) + self._indentation = " " + if bpy.app.version >= (3, 4, 0): # Set default values for hidden sockets self._set_unavailable_defaults = False - def _write(self, string: str, indent: str = None): - if indent is None: - indent = self._inner - self._file.write(f"{indent}{string}\n") + def _write(self, string: str, indent_level: int = None): + if indent_level is None: + indent_level = self._inner_indent_level + indent_str = indent_level * self._indentation + self._file.write(f"{indent_str}{string}\n") def _setup_options(self, options: NTPOptions) -> bool: # General self._mode = options.mode self._include_group_socket_values = options.include_group_socket_values self._should_set_dimensions = options.set_dimensions + + if options.indentation_type == 'SPACES_2': + self._indentation = " " + elif options.indentation_type == 'SPACES_4': + self._indentation = " " + elif options.indentation_type == 'SPACES_8': + self._indentation = " " + elif options.indentation_type == 'TABS': + self._indentation = "\t" + if bpy.app.version >= (3, 4, 0): self._set_unavailable_defaults = options.set_unavailable_defaults @@ -182,25 +197,25 @@ def _create_header(self, name: str) -> None: name (str): name of the add-on """ - self._write("bl_info = {", "") + self._write("bl_info = {", 0) self._name = name if self._name_override and self._name_override != "": self._name = self._name_override - self._write(f"\t\"name\" : {str_to_py_str(self._name)},", "") + self._write(f"\"name\" : {str_to_py_str(self._name)},", 1) if self._description and self._description != "": - self._write(f"\t\"description\" : {str_to_py_str(self._description)}," "") - self._write(f"\t\"author\" : {str_to_py_str(self._author_name)},", "") - self._write(f"\t\"version\" : {vec3_to_py_str(self._version)},", "") - self._write(f"\t\"blender\" : {bpy.app.version},", "") - self._write(f"\t\"location\" : {str_to_py_str(self._location)},", "") + self._write(f"\"description\" : {str_to_py_str(self._description)},", 1) + self._write(f"\"author\" : {str_to_py_str(self._author_name)},", 1) + self._write(f"\"version\" : {vec3_to_py_str(self._version)},", 1) + self._write(f"\"blender\" : {bpy.app.version},", 1) + self._write(f"\"location\" : {str_to_py_str(self._location)},", 1) category = self._category if category == "Custom": category = self._custom_category - self._write(f"\t\"category\" : {str_to_py_str(category)},", "") - self._write("}\n", "") - self._write("import bpy", "") - self._write("import mathutils", "") - self._write("import os\n", "") + self._write(f"\"category\" : {str_to_py_str(category)},", 1) + self._write("}\n", 0) + self._write("import bpy", 0) + self._write("import mathutils", 0) + self._write("import os\n", 0) def _init_operator(self, idname: str, label: str) -> None: """ @@ -213,11 +228,10 @@ def _init_operator(self, idname: str, label: str) -> None: label (str): appearence inside Blender """ self._idname = idname - self._write(f"class {self._class_name}(bpy.types.Operator):", "") - self._write(f"\tbl_idname = \"node.{idname}\"", "") - self._write(f"\tbl_label = {str_to_py_str(label)}", "") - self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}", "") - self._write("") + self._write(f"class {self._class_name}(bpy.types.Operator):", 0) + self._write(f"bl_idname = \"node.{idname}\"", 1) + self._write(f"bl_label = {str_to_py_str(label)}", 1) + self._write("bl_options = {\'REGISTER\', \'UNDO\'}\n", 1) def _topological_sort(self, node_tree: NodeTree) -> list[NodeTree]: """ @@ -322,6 +336,12 @@ def _create_node(self, node: Node, node_tree_var: str) -> str: # hide if node.hide: self._write(f"{node_var}.hide = True") + + # Warning propagation + if bpy.app.version >= (4, 3, 0): + if node.warning_propagation != 'ALL': + self._write(f"{node_var}.warning_propagation = " + f"{enum_to_py_str(node.warning_propagation)}") return node_var def _set_settings_defaults(self, node: Node) -> None: @@ -389,12 +409,14 @@ def _set_settings_defaults(self, node: Node) -> None: self._write(f"{setting_str} = {color_to_py_str(attr)}") 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}]")) + self._write(f"if {name} in bpy.data.materials:") + self._write(f"{setting_str} = bpy.data.materials[{name}]", + self._inner_indent_level + 1) 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}]")) + self._write(f"if {name} in bpy.data.objects:") + self._write(f"{setting_str} = bpy.data.objects[{name}]", + self._inner_indent_level + 1) elif st == ST.COLOR_RAMP: self._color_ramp_settings(node, attr_name) elif st == ST.CURVE_MAPPING: @@ -422,6 +444,12 @@ def _set_settings_defaults(self, node: Node) -> None: self._capture_attribute_items(attr, f"{node_var}.{attr_name}") elif st == ST.MENU_SWITCH_ITEMS: self._menu_switch_items(attr, f"{node_var}.{attr_name}") + elif st == ST.FOREACH_GEO_ELEMENT_GENERATION_ITEMS: + self._foreach_geo_element_generation_items(attr, f"{node_var}.{attr_name}") + elif st == ST.FOREACH_GEO_ELEMENT_INPUT_ITEMS: + self._foreach_geo_element_input_items(attr, f"{node_var}.{attr_name}") + elif st == ST.FOREACH_GEO_ELEMENT_MAIN_ITEMS: + self._foreach_geo_element_main_items(attr, f"{node_var}.{attr_name}") if bpy.app.version < (4, 0, 0): def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, @@ -518,8 +546,8 @@ def _group_io_settings(self, node: Node, if socket_interface.hide_in_modifier is True: self._write(f"{socket_var}.hide_in_modifier = True") - self._write("") - self._write("") + self._write("", 0) + self._write("", 0) elif bpy.app.version >= (4, 0, 0): def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, @@ -559,7 +587,7 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, 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: + elif type(dv) == bpy_prop_array: dv = array_to_py_str(dv) elif type(dv) == str: dv = str_to_py_str(dv) @@ -657,7 +685,7 @@ def _create_socket(self, socket: NodeTreeInterfaceSocket, description = str_to_py_str(socket.description) self._write(f"{socket_var}.description = {description}") - self._write("") + self._write("", 0) def _create_panel(self, panel: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel], @@ -706,7 +734,7 @@ def _create_panel(self, panel: NodeTreeInterfacePanel, if len(panel.interface_items) > 0: self._process_items(panel, panel_dict, items_processed, ntp_nt) - self._write("") + self._write("", 0) def _process_items(self, parent: NodeTreeInterfacePanel, panel_dict: dict[NodeTreeInterfacePanel], @@ -760,7 +788,7 @@ def _tree_interface_settings(self, ntp_nt: NTP_NodeTree) -> None: self._process_items(None, panel_dict, items_processed, ntp_nt) - self._write("") + self._write("", 0) def _set_input_defaults(self, node: Node) -> None: """ @@ -838,7 +866,7 @@ def _set_input_defaults(self, node: Node) -> None: if default_val is not None: self._write(f"#{input.identifier}") self._write(f"{socket_var}.default_value = {default_val}") - self._write("") + self._write("", 0) def _set_output_defaults(self, node: Node) -> None: """ @@ -882,7 +910,8 @@ def _in_file_inputs(self, input: bpy.types.NodeSocket, socket_var: str, return name = str_to_py_str(input.default_value.name) self._write(f"if {name} in bpy.data.{type}:") - self._write(f"\t{socket_var}.default_value = bpy.data.{type}[{name}]") + self._write(f"{socket_var}.default_value = bpy.data.{type}[{name}]", + self._inner_indent_level + 1) def _set_socket_defaults(self, node: Node): """ @@ -920,7 +949,7 @@ def _color_ramp_settings(self, node: Node, color_ramp_name: str) -> None: #interpolation interpolation = enum_to_py_str(color_ramp.interpolation) self._write(f"{ramp_str}.interpolation = {interpolation}") - self._write("") + self._write("", 0) # key points self._write(f"#initialize color ramp elements") @@ -1019,8 +1048,9 @@ def _create_curve_map(self, node: Node, i: int, curve: bpy.types.CurveMap, if (node.bl_idname == 'CompositorNodeHueCorrect'): 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[{INDEX}])") + self._write(f"{curve_i_var}.points.remove(" + f"{curve_i_var}.points[{INDEX}])", + self._inner_indent_level + 1) for j, point in enumerate(curve.points): self._create_curve_map_point(j, point, curve_i_var) @@ -1115,8 +1145,8 @@ def _load_image(self, img: bpy.types.Image, img_var: str) -> None: 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}\")") + f"os.path.join({BASE_DIR}, {str_to_py_str(IMAGE_DIR_NAME)}, " + f"{str_to_py_str(img_str)})") self._write(f"{img_var} = bpy.data.images.load" f"({IMAGE_PATH}, check_existing = True)") @@ -1251,6 +1281,43 @@ def _menu_switch_items(self, menu_switch_items: bpy.types.NodeMenuSwitchItems, m desc_str = str_to_py_str(item.description) self._write(f"{menu_switch_items_str}[{i}].description = {desc_str}") + if bpy.app.version >= (4, 3, 0): + def _foreach_geo_element_generation_items(self, + generation_items: bpy.types.NodeGeometryForeachGeometryElementGenerationItems, + generation_items_str: str + ) -> None: + self._write(f"{generation_items_str}.clear()") + for i, item in enumerate(generation_items): + socket_type = enum_to_py_str(item.socket_type) + name_str = str_to_py_str(item.name) + self._write(f"{generation_items_str}.new({socket_type}, {name_str})") + + item_str = f"{generation_items_str}[{i}]" + + ad = enum_to_py_str(item.domain) + self._write(f"{item_str}.domain = {ad}") + + def _foreach_geo_element_input_items(self, + input_items: bpy.types.NodeGeometryForeachGeometryElementInputItems, + input_items_str: str + ) -> None: + self._write(f"{input_items_str}.clear()") + for i, item in enumerate(input_items): + socket_type = enum_to_py_str(item.socket_type) + name_str = str_to_py_str(item.name) + self._write(f"{input_items_str}.new({socket_type}, {name_str})") + + def _foreach_geo_element_main_items(self, + main_items: bpy.types.NodeGeometryForeachGeometryElementMainItems, + main_items_str: str + ) -> None: + self._write(f"{main_items_str}.clear()") + for i, item in enumerate(main_items): + socket_type = enum_to_py_str(item.socket_type) + name_str = str_to_py_str(item.name) + self._write(f"{main_items_str}.new({socket_type}, {name_str})") + + def _set_parents(self, node_tree: NodeTree) -> None: """ Sets parents for all nodes, mostly used to put nodes in frames @@ -1267,7 +1334,7 @@ def _set_parents(self, node_tree: NodeTree) -> None: node_var = self._node_vars[node] parent_var = self._node_vars[node.parent] self._write(f"{node_var}.parent = {parent_var}") - self._write("") + self._write("", 0) def _set_locations(self, node_tree: NodeTree) -> None: """ @@ -1282,7 +1349,7 @@ def _set_locations(self, node_tree: NodeTree) -> None: node_var = self._node_vars[node] self._write(f"{node_var}.location " f"= ({node.location.x}, {node.location.y})") - self._write("") + self._write("", 0) def _set_dimensions(self, node_tree: NodeTree) -> None: """ @@ -1299,7 +1366,7 @@ def _set_dimensions(self, node_tree: NodeTree) -> None: node_var = self._node_vars[node] self._write(f"{node_var}.width, {node_var}.height " f"= {node.width}, {node.height}") - self._write("") + self._write("", 0) def _init_links(self, node_tree: NodeTree) -> None: """ @@ -1360,7 +1427,11 @@ def _set_node_tree_properties(self, node_tree: NodeTree) -> None: color_tag_str = enum_to_py_str(node_tree.color_tag) self._write(f"{nt_var}.color_tag = {color_tag_str}") desc_str = str_to_py_str(node_tree.description) - self._write(f"{nt_var}.description = {desc_str}\n") + self._write(f"{nt_var}.description = {desc_str}") + if bpy.app.version >= (4, 3, 0): + default_width = node_tree.default_group_node_width + self._write(f"{nt_var}.default_group_node_width = {default_width}") + self._write("\n") def _hide_hidden_sockets(self, node: Node) -> None: """ @@ -1382,34 +1453,31 @@ def _create_menu_func(self) -> None: """ Creates the menu function """ - self._write("def menu_func(self, context):", "") - self._write(f"self.layout.operator({self._class_name}.bl_idname)", "\t") - self._write("") + self._write("def menu_func(self, context):", 0) + self._write(f"self.layout.operator({self._class_name}.bl_idname)\n", 1) def _create_register_func(self) -> None: """ Creates the register function """ - self._write("def register():", "") - self._write(f"bpy.utils.register_class({self._class_name})", "\t") - self._write(f"bpy.types.{self._menu_id}.append(menu_func)", "\t") - self._write("") + self._write("def register():", 0) + self._write(f"bpy.utils.register_class({self._class_name})", 1) + self._write(f"bpy.types.{self._menu_id}.append(menu_func)\n", 1) def _create_unregister_func(self) -> None: """ Creates the unregister function """ - self._write("def unregister():", "") - self._write(f"bpy.utils.unregister_class({self._class_name})", "\t") - self._write(f"bpy.types.{self._menu_id}.remove(menu_func)", "\t") - self._write("") + self._write("def unregister():", 0) + self._write(f"bpy.utils.unregister_class({self._class_name})", 1) + self._write(f"bpy.types.{self._menu_id}.remove(menu_func)\n", 1) def _create_main_func(self) -> None: """ Creates the main function """ - self._write("if __name__ == \"__main__\":", "") - self._write("register()", "\t") + self._write("if __name__ == \"__main__\":", 0) + self._write("register()", 1) def _create_license(self) -> None: if not self._should_create_license: diff --git a/NodeToPython/options.py b/NodeToPython/options.py index 3ea4bff..a5df210 100644 --- a/NodeToPython/options.py +++ b/NodeToPython/options.py @@ -22,6 +22,19 @@ class NTPOptions(bpy.types.PropertyGroup): description = "Set dimensions of generated nodes", default = True ) + + indentation_type: bpy.props.EnumProperty( + name="Indentation Type", + description="Whitespace to use for each indentation block", + items = [ + ('SPACES_2', "2 Spaces", ""), + ('SPACES_4', "4 Spaces", ""), + ('SPACES_8', "8 Spaces", ""), + ('TABS', "Tabs", "") + ], + default = 'SPACES_4' + ) + if bpy.app.version >= (3, 4, 0): set_unavailable_defaults : bpy.props.BoolProperty( name = "Set unavailable defaults", @@ -92,9 +105,14 @@ class NTPOptions(bpy.types.PropertyGroup): ('SPDX:MPL-2.0', "Mozilla Public License 2.0", ""), ('SPDX:Pixar', "Pixar License", ""), ('SPDX:Zlib', "Zlib License", ""), - ('OTHER', "Other", "") + ('OTHER', "Other", "User is responsible for including the license " + "and adding it to the manifest.\n" + "Please note that by using the Blender Python " + "API your add-on must comply with the GNU GPL. " + "See https://www.blender.org/about/license/ for " + "more details") ], - default = 'OTHER' + default = 'SPDX:GPL-3.0-or-later' ) should_create_license: bpy.props.BoolProperty( name="Create License", @@ -163,7 +181,8 @@ def draw(self, context): option_list = [ "mode", "include_group_socket_values", - "set_dimensions" + "set_dimensions", + "indentation_type" ] if bpy.app.version >= (3, 4, 0): option_list.append("set_unavailable_defaults") diff --git a/NodeToPython/shader/operator.py b/NodeToPython/shader/operator.py index b614f38..5d5ff6f 100644 --- a/NodeToPython/shader/operator.py +++ b/NodeToPython/shader/operator.py @@ -27,10 +27,10 @@ def __init__(self): for name in SHADER_OP_RESERVED_NAMES: self._used_vars[name] = 0 - def _create_material(self, indent: str): + def _create_material(self, indent_level: int): 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) + f"name = {str_to_py_str(self.material_name)})", indent_level) + self._write(f"{MAT_VAR}.use_nodes = True", indent_level) def _initialize_shader_node_tree(self, ntp_node_tree: NTP_NodeTree, nt_name: str) -> None: @@ -42,19 +42,19 @@ def _initialize_shader_node_tree(self, ntp_node_tree: NTP_NodeTree, variable to use nt_name (str): name to use for the node tree """ - self._write(f"#initialize {nt_name} node group", self._outer) - self._write(f"def {ntp_node_tree.var}_node_group():\n", self._outer) + self._write(f"#initialize {nt_name} node group", self._outer_indent_level) + self._write(f"def {ntp_node_tree.var}_node_group():\n", self._outer_indent_level) 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"{ntp_node_tree.var}.nodes.remove({NODE})", self._inner_indent_level + 1) else: self._write((f"{ntp_node_tree.var} = bpy.data.node_groups.new(" f"type = \'ShaderNodeTree\', " f"name = {str_to_py_str(nt_name)})")) - self._write("") + self._write("", 0) def _process_node(self, node: Node, ntp_nt: NTP_NodeTree) -> None: """ @@ -124,7 +124,7 @@ def _process_node_tree(self, node_tree: ShaderNodeTree) -> None: self._write(f"return {nt_var}\n") #create node group - self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer_indent_level) def execute(self, context): @@ -142,8 +142,8 @@ 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._outer_indent_level = 2 + self._inner_indent_level = 3 if not self._setup_addon_directories(context, mat_var): return {'CANCELLED'} @@ -154,16 +154,16 @@ def execute(self, context): self._class_name = clean_string(self.material_name, lower=False) self._init_operator(mat_var, self.material_name) - self._write("def execute(self, context):", "\t") + self._write("def execute(self, context):", 1) else: self._file = StringIO("") if self._include_imports: self._file.write("import bpy, mathutils\n\n") if self._mode == 'ADDON': - self._create_material("\t\t") + self._create_material(2) elif self._mode == 'SCRIPT': - self._create_material("") + self._create_material(0) node_trees_to_process = self._topological_sort(self._base_node_tree) @@ -171,7 +171,7 @@ def execute(self, context): self._process_node_tree(node_tree) if self._mode == 'ADDON': - self._write("return {'FINISHED'}", self._outer) + self._write("return {'FINISHED'}", self._outer_indent_level) self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/NodeToPython/utils.py b/NodeToPython/utils.py index d411290..5f84414 100644 --- a/NodeToPython/utils.py +++ b/NodeToPython/utils.py @@ -1,7 +1,9 @@ import bpy -from bpy_types import bpy_types import mathutils + +from bpy.types import bpy_prop_array + import keyword import re @@ -106,7 +108,7 @@ 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: +def array_to_py_str(array: bpy_prop_array) -> str: """ Converts a bpy_prop_array into a string diff --git a/tools/node_settings_generator/parse_nodes.py b/tools/node_settings_generator/parse_nodes.py index ab42e73..9465f59 100644 --- a/tools/node_settings_generator/parse_nodes.py +++ b/tools/node_settings_generator/parse_nodes.py @@ -97,17 +97,17 @@ def download_file(filepath: str, version: Version, local_path: str) -> bool: if not os.path.exists(os.path.dirname(local_path)): os.makedirs(os.path.dirname(local_path)) - while True: + NUM_TRIES = 10 + for i in range(NUM_TRIES): try: with urllib.request.urlopen(req) as response: with open(local_path, 'wb') as file: file.write(response.read()) break - except urllib.error.HTTPError as e: - if e.code == 429: - time.sleep(1.0) - else: - raise + except Exception as e: + if i == NUM_TRIES - 1: + raise e + time.sleep(1.0) print(f"Downloaded {file_url} to {local_path}") return True @@ -121,14 +121,18 @@ def get_subclasses(current: str, parent: str, root_path: str, if not os.path.exists(current_path): download_file(relative_path, version, current_path) + if os.path.getsize(current_path) == 0: + download_file(relative_path, version, current_path) + with open(current_path, "r") as current_file: current_html = current_file.read() soup = BeautifulSoup(current_html, "html.parser") - sections = soup.find_all(id=f"{current.lower()}-{parent.lower()}") + main_id = f"{current.lower()}-{parent.lower()}" + sections = soup.find_all(id=main_id) if not sections: - raise ValueError(f"{version.tuple_str()} {current}: Couldn't find main section") + raise ValueError(f"{version.tuple_str()} {current}: Couldn't find main section with id {main_id}") section = sections[0] paragraphs = section.find_all("p") diff --git a/tools/node_settings_generator/types_utils.py b/tools/node_settings_generator/types_utils.py index 5bf5201..1d385c9 100644 --- a/tools/node_settings_generator/types_utils.py +++ b/tools/node_settings_generator/types_utils.py @@ -9,6 +9,7 @@ class ST(Enum): COLOR = auto() ENUM = auto() ENUM_SET = auto() + EULER = auto() FLOAT = auto() INT = auto() STRING = auto() @@ -18,31 +19,33 @@ class ST(Enum): VEC4 = auto() # Special settings - BAKE_ITEMS = auto() - COLOR_RAMP = auto() - CURVE_MAPPING = auto() - ENUM_DEFINITION = auto() - INDEX_SWITCH_ITEMS = auto() - NODE_TREE = auto() - REPEAT_OUTPUT_ITEMS = auto() - SIM_OUTPUT_ITEMS = auto() + BAKE_ITEMS = auto() + CAPTURE_ATTRIBUTE_ITEMS = auto() + COLOR_RAMP = auto() + CURVE_MAPPING = auto() + ENUM_DEFINITION = auto() + ENUM_ITEM = auto() + FOREACH_GEO_ELEMENT_GENERATION_ITEMS = auto() + FOREACH_GEO_ELEMENT_INPUT_ITEMS = auto() + FOREACH_GEO_ELEMENT_MAIN_ITEMS = auto() + INDEX_SWITCH_ITEMS = auto() + MENU_SWITCH_ITEMS = auto() + NODE_TREE = auto() + REPEAT_OUTPUT_ITEMS = auto() + SIM_OUTPUT_ITEMS = auto() # Image IMAGE = auto() #needs refactor IMAGE_USER = auto() #needs refactor # Currently unimplemented - CAPTURE_ATTRIBUTE_ITEMS = auto() #TODO NTP v3.2 CRYPTOMATTE_ENTRIES = auto() - ENUM_ITEM = auto() #TODO NTP v3.2 - EULER = auto() #TODO NTP v3.2 FILE_SLOTS = auto() FONT = auto() IMAGE_FORMAT_SETTINGS = auto() LAYER_SLOTS = auto() MASK = auto() MATERIAL = auto() #TODO asset library - MENU_SWITCH_ITEMS = auto() #TODO NTP v3.2 MOVIE_CLIP = auto() OBJECT = auto() #TODO asset library PARTICLE_SYSTEM = auto() @@ -59,6 +62,9 @@ class ST(Enum): ST.CURVE_MAPPING, ST.ENUM_DEFINITION, ST.FILE_SLOTS, + ST.FOREACH_GEO_ELEMENT_GENERATION_ITEMS, + ST.FOREACH_GEO_ELEMENT_INPUT_ITEMS, + ST.FOREACH_GEO_ELEMENT_MAIN_ITEMS, ST.IMAGE_FORMAT_SETTINGS, ST.IMAGE_USER, ST.INDEX_SWITCH_ITEMS, @@ -100,6 +106,9 @@ class ST(Enum): "NodeEnumItem" : ST.ENUM_ITEM, "NodeGeometryBakeItems" : ST.BAKE_ITEMS, "NodeGeometryCaptureAttributeItems" : ST.CAPTURE_ATTRIBUTE_ITEMS, + "NodeGeometryForeachGeometryElementGenerationItems": ST.FOREACH_GEO_ELEMENT_GENERATION_ITEMS, + "NodeGeometryForeachGeometryElementInputItems" : ST.FOREACH_GEO_ELEMENT_INPUT_ITEMS, + "NodeGeometryForeachGeometryElementMainItems": ST.FOREACH_GEO_ELEMENT_MAIN_ITEMS, "NodeGeometryRepeatOutputItems" : ST.REPEAT_OUTPUT_ITEMS, "NodeGeometrySimulationOutputItems" : ST.SIM_OUTPUT_ITEMS, "NodeIndexSwitchItems" : ST.INDEX_SWITCH_ITEMS,