diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..94b05c4 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,3 @@ +* [Brendan Parmer](https://github.com/BrendanParmer) +* [Carlsu](https://github.com/carls3d) +* [atticus-lv](https://github.com/atticus-lv) \ No newline at end of file diff --git a/NodeToPython/__init__.py b/NodeToPython/__init__.py index 6782b87..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, 1), + "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 0d6cd63..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.1" +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 682517d..eb798fc 100644 --- a/NodeToPython/compositor/operator.py +++ b/NodeToPython/compositor/operator.py @@ -107,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)), @@ -116,6 +116,17 @@ 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) 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 5e1a7b7..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): @@ -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) 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 1b28435..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,10 +8,12 @@ from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceSocket from bpy.types import NodeTreeInterfaceItem +from bpy.types import bpy_prop_array + import datetime import os import shutil -from typing import TextIO +from typing import TextIO, Callable from .license_templates import license_templates from .ntp_node_tree import NTP_NodeTree @@ -66,6 +67,9 @@ class NTP_Operator(Operator): def __init__(self): super().__init__() + # Write functions after nodes are mostly initialized and linked up + self._write_after_links: list[Callable] = [] + # File (TextIO) or string (StringIO) the add-on/script is generated into self._file: TextIO = None @@ -332,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: @@ -434,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, @@ -553,11 +569,25 @@ def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, dv = socket_interface.default_value - if type(socket_interface) == bpy.types.NodeTreeInterfaceSocketColor: + if type(socket_interface) is bpy.types.NodeTreeInterfaceSocketMenu: + if dv == "": + self.report({'WARNING'}, + "NodeToPython: No menu found for socket " + f"{socket_interface.name}" + ) + return + + self._write_after_links.append( + lambda _socket_var=socket_var, _dv=enum_to_py_str(dv): ( + self._write(f"{_socket_var}.default_value = {_dv}") + ) + ) + return + elif 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) == 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) @@ -799,6 +829,8 @@ def _set_input_defaults(self, node: Node) -> None: #menu elif input.bl_idname == 'NodeSocketMenu': + if input.default_value == '': + continue default_val = enum_to_py_str(input.default_value) # images @@ -1249,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 @@ -1346,6 +1415,11 @@ def _init_links(self, node_tree: NodeTree) -> None: f".outputs[{input_idx}], " f"{out_node_var}.inputs[{output_idx}])") + for _func in self._write_after_links: + _func() + self._write_after_links = [] + + def _set_node_tree_properties(self, node_tree: NodeTree) -> None: nt_var = self._node_tree_vars[node_tree] @@ -1353,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: """ 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/docs/README.md b/docs/README.md index 938568f..8e8106d 100644 --- a/docs/README.md +++ b/docs/README.md @@ -18,6 +18,10 @@ Blender's node-based editors are powerful, yet accessible tools, and I wanted to NodeToPython v3.2.1 is supported for Blender 3.0 - 4.2 on Windows, macOS, and Linux. ## Installation +### Blender Extensions Platform +NodeToPython is now on the official [Blender Extensions Platform](https://extensions.blender.org/add-ons/node-to-python/)! See https://extensions.blender.org/about/ for installation instructions and more info. + +### GitHub 1. Download the `NodeToPython.zip` file from the [latest release](https://github.com/BrendanParmer/NodeToPython/releases) * If you download other options, you'll need to rename the zip and the first folder to "NodeToPython" so Blender can properly import the add-on 2. In Blender, navigate to `Edit > Preferences > Add-ons` @@ -53,4 +57,4 @@ This tool is provided under the MIT license and is intended for lawful use only. In particular, this tool generates code that requires the Blender Python API, which is licensed under the GNU General Public License (GPL). Code generated by this tool that incorporates or uses the Blender Python API must comply with the GPL requirements. For more details on the GPL, please see https://www.gnu.org/licenses/gpl-3.0.html. For more details on Blender's licensing, please see https://www.blender.org/about/license/ -**Disclaimer**: The authors of this tool are not legal professionals. This notice is provided for informational purposes only and should not be construed as legal advice. For specific legal advice related to the use of this tool and compliance with the GPL or other legal matters, please consult a qualified attorney. \ No newline at end of file +**Disclaimer**: The authors of this tool are not legal professionals. This notice is provided for informational purposes only and should not be construed as legal advice. For specific legal advice related to the use of this tool and compliance with the GPL or other legal matters, please consult a qualified attorney. 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,