diff --git a/NodeToPython/blender_manifest.toml b/NodeToPython/blender_manifest.toml index ed9e910..e673134 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 = "4.1.1" +version = "4.2.0" name = "Node To Python" tagline = "Turn node groups into Python code" maintainer = "Brendan Parmer " diff --git a/NodeToPython/export/geometry/exporter.py b/NodeToPython/export/geometry/exporter.py index fc73db2..c4971be 100644 --- a/NodeToPython/export/geometry/exporter.py +++ b/NodeToPython/export/geometry/exporter.py @@ -7,13 +7,7 @@ from .node_tree import NTP_GeoNodeTree, NTP_NodeTree -OBJECT_NAME = "name" -OBJECT = "obj" -MODIFIER = "mod" GEO_OP_RESERVED_NAMES = { - OBJECT_NAME, - OBJECT, - MODIFIER } class GeometryNodesExporter(NodeTreeExporter): diff --git a/NodeToPython/export/node_settings.py b/NodeToPython/export/node_settings.py index ec510f9..ca9424d 100644 --- a/NodeToPython/export/node_settings.py +++ b/NodeToPython/export/node_settings.py @@ -13,6 +13,7 @@ class ST(Enum): FLOAT = auto() INT = auto() STRING = auto() + VEC = auto() VEC1 = auto() VEC2 = auto() VEC3 = auto() @@ -21,6 +22,7 @@ class ST(Enum): CAPTURE_ATTRIBUTE_ITEMS = auto() CLOSURE_INPUT_ITEMS = auto() CLOSURE_OUTPUT_ITEMS = auto() + CLOSURE_TO_LIST_ITEMS = auto() COLOR_MANAGED_DISPLAY_SETTINGS = auto() COLOR_MANAGED_VIEW_SETTINGS = auto() COLOR_RAMP = auto() @@ -28,18 +30,20 @@ class ST(Enum): COMPOSITOR_FILE_OUTPUT_ITEMS = auto() CURVE_MAPPING = auto() ENUM_DEFINITION = auto() - ENUM_ITEM = auto() EVALUATE_CLOSURE_INPUT_ITEMS = auto() EVALUATE_CLOSURE_OUTPUT_ITEMS = auto() FIELD_TO_GRID_ITEMS = auto() + FIELD_TO_LIST_ITEMS = auto() FOREACH_GEO_ELEMENT_GENERATION_ITEMS = auto() FOREACH_GEO_ELEMENT_INPUT_ITEMS = auto() FOREACH_GEO_ELEMENT_MAIN_ITEMS = auto() FORMAT_STRING_ITEMS = auto() GEOMETRY_VIEWER_ITEMS = auto() INDEX_SWITCH_ITEMS = auto() + MENU_INPUT = auto() MENU_SWITCH_ITEMS = auto() NODE_TREE = auto() + RAYCAST_ATTR_ITEMS = auto() REPEAT_OUTPUT_ITEMS = auto() SEPARATE_BUNDLE_ITEMS = auto() SIM_OUTPUT_ITEMS = auto() @@ -64,12 +68,12 @@ class NTPNodeSetting(NamedTuple): name_: str st_: ST min_version_: tuple = (4, 2, 0) - max_version_: tuple = (5, 2, 0) + max_version_: tuple = (5, 3, 0) class NodeInfo(NamedTuple): attributes_: list[NTPNodeSetting] min_version_: tuple = (4, 2, 0) - max_version_: tuple = (5, 2, 0) + max_version_: tuple = (5, 3, 0) node_settings : dict[str, NodeInfo] = { 'CompositorNodeAlphaOver' : NodeInfo( @@ -95,6 +99,11 @@ class NodeInfo(NamedTuple): ] ), + 'CompositorNodeBlankImage' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'CompositorNodeBlur' : NodeInfo( [ NTPNodeSetting("aspect_correction", ST.ENUM, max_version_=(4, 5, 0)), @@ -702,6 +711,7 @@ class NodeInfo(NamedTuple): NTPNodeSetting("format", ST.IMAGE_FORMAT_SETTINGS), NTPNodeSetting("layer_slots", ST.LAYER_SLOTS, max_version_=(5, 0, 0)), NTPNodeSetting("save_as_render", ST.BOOL, min_version_=(4, 3, 0)), + NTPNodeSetting("use_file_extension", ST.BOOL, min_version_=(5, 2, 0)), ] ), @@ -835,6 +845,11 @@ class NodeInfo(NamedTuple): ] ), + 'CompositorNodeStringToImage' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'CompositorNodeSunBeams' : NodeInfo( [ NTPNodeSetting("ray_length", ST.FLOAT, max_version_=(4, 5, 0)), @@ -1040,7 +1055,8 @@ class NodeInfo(NamedTuple): 'FunctionNodeInputColor' : NodeInfo( [ - NTPNodeSetting("value", ST.VEC4), + NTPNodeSetting("value", ST.VEC4, max_version_=(5, 2, 0)), + NTPNodeSetting("value", ST.VEC, min_version_=(5, 2, 0)), ] ), @@ -1050,6 +1066,23 @@ class NodeInfo(NamedTuple): ] ), + 'FunctionNodeInputIntVector' : NodeInfo( + [ + # NOTE: in order to export correctly, the dimensions + # must be set before the vector. + NTPNodeSetting("vector_dimensions", ST.INT), + NTPNodeSetting("vector", ST.VEC), + ], + min_version_ = (5, 2, 0) + ), + + 'FunctionNodeInputMenu' : NodeInfo( + [ + NTPNodeSetting("value", ST.MENU_INPUT), + ], + min_version_ = (5, 2, 0) + ), + 'FunctionNodeInputRotation' : NodeInfo( [ NTPNodeSetting("rotation_euler", ST.EULER), @@ -1068,7 +1101,11 @@ class NodeInfo(NamedTuple): 'FunctionNodeInputVector' : NodeInfo( [ - NTPNodeSetting("vector", ST.VEC3), + NTPNodeSetting("vector", ST.VEC3, max_version_=(5, 2, 0)), + # NOTE: in order to export correctly, the dimensions + # must be set before the vector. + NTPNodeSetting("vector_dimensions", ST.INT, min_version_=(5, 2, 0)), + NTPNodeSetting("vector", ST.VEC, min_version_=(5, 2, 0)), ] ), @@ -1126,6 +1163,11 @@ class NodeInfo(NamedTuple): [] ), + 'FunctionNodeReverseString' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'FunctionNodeRotateEuler' : NodeInfo( [ NTPNodeSetting("rotation_type", ST.ENUM), @@ -1169,10 +1211,20 @@ class NodeInfo(NamedTuple): [] ), + 'FunctionNodeSetStringCase' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'FunctionNodeSliceString' : NodeInfo( [] ), + 'FunctionNodeSplitString' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'FunctionNodeStringLength' : NodeInfo( [] ), @@ -1196,6 +1248,11 @@ class NodeInfo(NamedTuple): [] ), + 'FunctionNodeTrimString' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'FunctionNodeValueToString' : NodeInfo( [ NTPNodeSetting("data_type", ST.ENUM, min_version_=(4, 3, 0)), @@ -1209,6 +1266,11 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeApplySimulatedData' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeAttributeDomainSize' : NodeInfo( [ NTPNodeSetting("component", ST.ENUM), @@ -1274,6 +1336,29 @@ class NodeInfo(NamedTuple): max_version_ = (5, 0, 0) ), + 'GeometryNodeClosureToList' : NodeInfo( + [ + NTPNodeSetting("active_index", ST.INT), + NTPNodeSetting("list_items", ST.CLOSURE_TO_LIST_ITEMS), + ], + min_version_ = (5, 2, 0) + ), + + 'GeometryNodeClusterByConnected' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + + 'GeometryNodeClusterByDistance' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + + 'GeometryNodeCollectionChildren' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeCollectionInfo' : NodeInfo( [ NTPNodeSetting("transform_space", ST.ENUM), @@ -1525,6 +1610,7 @@ class NodeInfo(NamedTuple): 'GeometryNodeFieldToList' : NodeInfo( [ NTPNodeSetting("active_index", ST.INT), + NTPNodeSetting("list_items", ST.FIELD_TO_LIST_ITEMS), ], min_version_ = (5, 1, 0) ), @@ -1549,6 +1635,13 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeFilterList' : NodeInfo( + [ + NTPNodeSetting("socket_type", ST.ENUM), + ], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeFlipFaces' : NodeInfo( [] ), @@ -1576,11 +1669,21 @@ class NodeInfo(NamedTuple): [] ), + 'GeometryNodeGetAttributeNames' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeGetGeometryBundle' : NodeInfo( [], min_version_ = (5, 1, 0) ), + 'GeometryNodeGetGeometryComponent' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeGetNamedGrid' : NodeInfo( [ NTPNodeSetting("data_type", ST.ENUM), @@ -1790,6 +1893,13 @@ class NodeInfo(NamedTuple): [] ), + 'GeometryNodeInputFont' : NodeInfo( + [ + NTPNodeSetting("font", ST.FONT), + ], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeInputID' : NodeInfo( [] ), @@ -1809,6 +1919,11 @@ class NodeInfo(NamedTuple): min_version_ = (4, 5, 0) ), + 'GeometryNodeInputInstanceReference' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeInputInstanceRotation' : NodeInfo( [] ), @@ -1974,7 +2089,6 @@ class NodeInfo(NamedTuple): 'GeometryNodeMenuSwitch' : NodeInfo( [ NTPNodeSetting("active_index", ST.INT), - NTPNodeSetting("active_item", ST.ENUM_ITEM), NTPNodeSetting("data_type", ST.ENUM), NTPNodeSetting("enum_items", ST.MENU_SWITCH_ITEMS), ] @@ -1993,6 +2107,16 @@ class NodeInfo(NamedTuple): min_version_ = (4, 3, 0) ), + 'GeometryNodeMergePoints' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + + 'GeometryNodeMeshBevel' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeMeshBoolean' : NodeInfo( [ NTPNodeSetting("operation", ST.ENUM), @@ -2136,6 +2260,11 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeRenameAttribute' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeRepeatInput' : NodeInfo( [] ), @@ -2244,6 +2373,11 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeSampleSoundFrequencies' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeSampleUVSurface' : NodeInfo( [ NTPNodeSetting("data_type", ST.ENUM), @@ -2370,6 +2504,16 @@ class NodeInfo(NamedTuple): min_version_ = (4, 5, 0) ), + 'GeometryNodeSetNURBSOrder' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + + 'GeometryNodeSetNURBSWeight' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeSetPointRadius' : NodeInfo( [] ), @@ -2409,6 +2553,13 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeSortList' : NodeInfo( + [ + NTPNodeSetting("socket_type", ST.ENUM), + ], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeSplineLength' : NodeInfo( [] ), @@ -2475,6 +2626,11 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeTagFilter' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeTool3DCursor' : NodeInfo( [] ), @@ -2508,6 +2664,11 @@ class NodeInfo(NamedTuple): ] ), + 'GeometryNodeTransferAttributes' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'GeometryNodeTransform' : NodeInfo( [ NTPNodeSetting("mode", ST.ENUM, max_version_=(5, 0, 0)), @@ -2581,6 +2742,11 @@ class NodeInfo(NamedTuple): min_version_ = (4, 3, 0) ), + 'GeometryNodeXPBDSolver' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'NodeClosureInput' : NodeInfo( [], min_version_ = (5, 0, 0) @@ -2640,6 +2806,11 @@ class NodeInfo(NamedTuple): min_version_ = (5, 1, 0) ), + 'NodeGetNestedBundlePaths' : NodeInfo( + [], + min_version_ = (5, 2, 0) + ), + 'NodeGroup' : NodeInfo( [ NTPNodeSetting("node_tree", ST.NODE_TREE), @@ -2656,6 +2827,14 @@ class NodeInfo(NamedTuple): ] ), + 'NodeImplicitConversion' : NodeInfo( + [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("socket_idname", ST.STRING), + ], + min_version_ = (5, 2, 0) + ), + 'NodeJoinBundle' : NodeInfo( [], min_version_ = (5, 0, 0) @@ -2952,6 +3131,7 @@ class NodeInfo(NamedTuple): 'ShaderNodeNormalMap' : NodeInfo( [ + NTPNodeSetting("base", ST.ENUM, min_version_=(5, 2, 0)), NTPNodeSetting("convention", ST.ENUM, min_version_=(5, 1, 0)), NTPNodeSetting("space", ST.ENUM), NTPNodeSetting("uv_map", ST.STRING), @@ -3030,7 +3210,9 @@ class NodeInfo(NamedTuple): 'ShaderNodeRaycast' : NodeInfo( [ + NTPNodeSetting("active_index", ST.INT, min_version_=(5, 2, 0)), NTPNodeSetting("only_local", ST.BOOL), + NTPNodeSetting("sample_attribute_items", ST.RAYCAST_ATTR_ITEMS, min_version_=(5, 2, 0)), ], min_version_ = (5, 1, 0) ), @@ -3188,7 +3370,8 @@ class NodeInfo(NamedTuple): NTPNodeSetting("ground_albedo", ST.FLOAT), NTPNodeSetting("ozone_density", ST.FLOAT), NTPNodeSetting("sky_type", ST.ENUM), - NTPNodeSetting("sun_direction", ST.VEC3), + NTPNodeSetting("sun_direction", ST.VEC3, max_version_=(5, 2, 0)), + NTPNodeSetting("sun_direction", ST.VEC, min_version_=(5, 2, 0)), NTPNodeSetting("sun_disc", ST.BOOL), NTPNodeSetting("sun_elevation", ST.FLOAT), NTPNodeSetting("sun_intensity", ST.FLOAT), diff --git a/NodeToPython/export/node_tree_exporter.py b/NodeToPython/export/node_tree_exporter.py index 3fb8de4..8c6d295 100644 --- a/NodeToPython/export/node_tree_exporter.py +++ b/NodeToPython/export/node_tree_exporter.py @@ -56,6 +56,9 @@ if bpy.app.version >= (5, 1, 0): NO_DEFAULT_SOCKETS.add(bpy.types.NodeTreeInterfaceSocketFont) +if bpy.app.version >= (5, 2, 0): + NO_DEFAULT_SOCKETS.add(bpy.types.NodeTreeInterfaceSocketSound) + #node input sockets that are messy to set default values for DONT_SET_DEFAULTS = { 'NodeSocketGeometry', @@ -632,6 +635,9 @@ def _process_node(self, node: bpy.types.Node, ntp_nt: NTP_NodeTree) -> None: node_var: str = self._create_node(node, ntp_nt._var) self._set_settings_defaults(node) + if bpy.app.version >= (5, 2, 0): + self._set_panel_states(node) + if node.bl_idname in ntp_nt._zone_inputs: ntp_nt._zone_inputs[node.bl_idname].append(node) @@ -739,6 +745,8 @@ def _set_settings_defaults(self, node: bpy.types.Node) -> None: self._write(f"{setting_str} = {str_to_py_str(attr)}") elif st == ST.BOOL or st == ST.INT or st == ST.FLOAT: self._write(f"{setting_str} = {attr}") + elif st == ST.VEC: + self._write(f"{setting_str} = {vec_to_py_str(attr, len(attr))}") elif st == ST.VEC1: self._write(f"{setting_str} = {vec1_to_py_str(attr)}") elif st == ST.VEC2: @@ -819,6 +827,14 @@ def _set_settings_defaults(self, node: bpy.types.Node) -> None: self._combine_bundle_items(attr, setting_str) elif st == ST.SEPARATE_BUNDLE_ITEMS: self._separate_bundle_items(attr, setting_str) + elif st == ST.CLOSURE_TO_LIST_ITEMS: + self._closure_to_list_items(attr, setting_str) + elif st == ST.FIELD_TO_LIST_ITEMS: + self._field_to_list_items(attr, setting_str) + elif st == ST.RAYCAST_ATTR_ITEMS: + self._raycast_attr_items(attr, setting_str) + elif st == ST.MENU_INPUT: + self._menu_input(attr, setting_str) def _set_if_in_blend_file(self, attr, setting_str: str, data_type: str ) -> None: @@ -1429,6 +1445,67 @@ def _color_managed_view_settings(self, look_str = enum_to_py_str(view_settings.look) self._write(f"{view_settings_str}.look = {look_str}") + if bpy.app.version >= (5, 2, 0): + def _closure_to_list_items(self, + closure_to_list_items: bpy.types.GeometryNodeClosureToListItems, + closure_to_list_items_str: str + ) -> None: + self._write(f"{closure_to_list_items_str}.clear()") + for i, item in enumerate(closure_to_list_items): + socket_type = enum_to_py_str(item.socket_type) + name_str = str_to_py_str(item.name) + self._write((f"{closure_to_list_items_str}.new(" + f"{socket_type}, {name_str})")) + + item_str = f"{closure_to_list_items_str}[{i}]" + + structure_type = enum_to_py_str(item.structure_type) + self._write(f"{item_str}.structure_type = {structure_type}") + + def _field_to_list_items(self, + field_to_list_items: bpy.types.GeometryNodeFieldToListItems, + field_to_list_items_str: str + ) -> None: + self._write(f"{field_to_list_items_str}.clear()") + for i, item in enumerate(field_to_list_items): + socket_type = enum_to_py_str(item.socket_type) + name_str = str_to_py_str(item.name) + self._write((f"{field_to_list_items_str}.new(" + f"{socket_type}, {name_str})")) + + def _raycast_attr_items(self, + raycast_attr_items: bpy.types.NodeRaycastSampleAttributeItems, + raycast_attr_items_str: str + ) -> None: + if raycast_attr_items is None: + return + + self._write(f"{raycast_attr_items_str}.clear()") + for i, item in enumerate(raycast_attr_items): + socket_type = enum_to_py_str( + data_type_to_socket_type[item.data_type] + ) + name_str = str_to_py_str(item.name) + self._write((f"{raycast_attr_items_str}.new(" + f"{socket_type}, {name_str})")) + + item_str = f"{raycast_attr_items_str}[{i}]" + + data_type = enum_to_py_str(item.data_type) + self._write(f"{item_str}.data_type = {data_type}") + + def _menu_input(self, + menu_input: str, + menu_input_str: str + ) -> None: + if menu_input != "": + self._write_after_links.append( + lambda _menu_input_str=menu_input_str, + _menu_input_value = str_to_py_str(menu_input): + self._write(f"{_menu_input_str} = {_menu_input_value}") + ) + + def _hide_hidden_sockets(self, node: bpy.types.Node) -> None: """ Hide hidden sockets @@ -1540,6 +1617,10 @@ def _set_input_defaults(self, node: bpy.types.Node) -> None: self._in_file_inputs(input, socket_var, "fonts") default_val = None + elif input.bl_idname == 'NodeSocketSound': + self._in_file_inputs(input, socket_var, "sounds") + default_val = None + else: default_val = getattr(input, "default_value") @@ -1595,6 +1676,14 @@ def _in_file_inputs(self, input: bpy.types.NodeSocket, socket_var: str, self._write(f"{socket_var}.default_value = bpy.data.{type}[{name}]", self._operator._inner_indent_level + 1) + if bpy.app.version >= (5, 2, 0): + def _set_panel_states(self, node: bpy.types.Node): + node_var = self._node_vars[node] + for i, panel_state in enumerate(node.panel_states): + pstate_var = f"{node_var}.panel_states[{i}]" + self._write(f"{pstate_var}.is_collapsed = " + f"{panel_state.is_collapsed}") + def _process_zones(self, zone_input_list: list[bpy.types.Node]) -> None: """ Recreates a zone @@ -1625,7 +1714,7 @@ def _get_node_var( ) -> str: nt_var = self._node_tree_vars[node_tree] return f"{nt_var}.nodes[{str_to_py_str(node.name)}]" - + def _set_parents(self, node_tree: bpy.types.NodeTree) -> None: """ Sets parents for all nodes, mostly used to put nodes in frames diff --git a/NodeToPython/export/ntp_operator.py b/NodeToPython/export/ntp_operator.py index d93eec6..caadd2b 100644 --- a/NodeToPython/export/ntp_operator.py +++ b/NodeToPython/export/ntp_operator.py @@ -27,7 +27,7 @@ } MIN_BLENDER_VERSION = (4, 2, 0) -MAX_BLENDER_VERSION = (5, 2, 0) +MAX_BLENDER_VERSION = (5, 3, 0) class NodeTreeInfo(): def __init__(self): diff --git a/NodeToPython/export/utils.py b/NodeToPython/export/utils.py index 2536572..4ac1cb2 100644 --- a/NodeToPython/export/utils.py +++ b/NodeToPython/export/utils.py @@ -174,4 +174,21 @@ 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}" \ No newline at end of file + return f"{name}.{format}" + +data_type_to_socket_type : dict[str, str] = { + 'FLOAT' : 'FLOAT', + 'INT' : 'INT', + 'BOOLEAN' : 'BOOLEAN', + 'FLOAT_VECTOR' : 'VECTOR', + 'FLOAT_COLOR' : 'RGBA', + 'QUATERNION' : 'ROTATION', + 'FLOAT4X4' : 'MATRIX', + 'STRING' : 'STRING', + 'INT8' : 'INT', + 'INT16_2D' : 'INT_VECTOR', + 'INT32_2D' : 'INT_VECTOR', + 'FLOAT2' : 'VECTOR', + 'FLOAT4' : 'VECTOR', + 'BYTE_COLOR' : 'COLOR' +} \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 572a404..b53e94c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,7 +12,7 @@ Node To Python automatically handles node layout, default values, subgroups, nam Blender's node-based editors are powerful, yet accessible tools, and I wanted to make scripting them easier for add-on creators. Combining Python with node based setups allows you to do things that would otherwise be tedious or impossible. ## Supported Versions -NodeToPython v4.1.1 is supported for Blender 4.2 - 5.1 on Windows, macOS, and Linux +NodeToPython v4.2.0 is supported for Blender 4.2 - 5.2 on Windows, macOS, and Linux * Some work is required to update NodeToPython for each Blender version, so experimental versions may not work properly and may even crash. * New nodes, updated settings, and new node tree features require development to support. * NodeToPython updates for new Blender versions usually doesn't start until the Beta phase of the release cycle @@ -44,7 +44,7 @@ In the options panel, select either **Script** or **Add-on**. When submitting an issue, please include -* Your version of Blender (4.2 - 5.1) +* Your version of Blender (4.2 - 5.2) * Your operating system (Windows, MacOS, or Linux) * Steps to reproduce the issue or a description of what you were trying to accomplish. Providing a test blend file is especially helpful diff --git a/tools/node_settings_generator/parse_nodes.py b/tools/node_settings_generator/parse_nodes.py index 562d547..3baa4ea 100644 --- a/tools/node_settings_generator/parse_nodes.py +++ b/tools/node_settings_generator/parse_nodes.py @@ -1,5 +1,5 @@ import argparse -from bs4 import BeautifulSoup +from bs4 import BeautifulSoup, SoupStrainer from threading import Thread, Lock from io import TextIOWrapper import os @@ -39,7 +39,7 @@ class NodeInfo(NamedTuple): BLENDER_3_MAX_VERSION = 6 BLENDER_4_MAX_VERSION = 5 -BLENDER_5_MAX_VERSION = 1 +BLENDER_5_MAX_VERSION = 2 NTP_MIN_VERSION = Version(4, 2) @@ -66,6 +66,8 @@ def process_attr(attr, section, node: str, version: Version) -> None: f"was marked deprecated, returning\n") return + readonly = "readonly" in str(attr) + # Get type type_section = attr.find("dd", class_="field-odd") if not type_section: @@ -80,14 +82,20 @@ def process_attr(attr, section, node: str, version: Version) -> None: else: types_dict[first_word].add(type_text) - ntp_type = types_utils.get_NTP_type(type_text) + ntp_type = types_utils.get_NTP_type(type_text, readonly) if ntp_type is None: # Read-only attribute, don't add to attribute list log(f"WARNING: {version.tuple_str()} {node}.{name}'s " f"type is being ignored:\n\t{type_text.strip()}\n") return + + # Unfortunately needs handled spearately, + # node tree creation fails unless value is written after linking + if node == 'FunctionNodeInputMenu' and name == "value": + ntp_type = types_utils.ST.MENU_INPUT ntp_setting = NTPNodeSetting(name, ntp_type) + with mutex: if ntp_setting not in nodes_dict[node].attributes_: nodes_dict[node].attributes_[ntp_setting] = [] @@ -134,6 +142,58 @@ def download_file(filepath: str, version: Version, local_path: str) -> bool: print(f"Downloaded {file_url} to {local_path}") return True +def get_subclass_links( + current: str, + parent: str, + version: Version, + soup: BeautifulSoup +) -> list: + main_id = f"{current.lower()}-{parent.lower()}" + sections = soup.find_all(id=main_id) + if not sections: + raise ValueError(f"{version.tuple_str()} {current}: " + f"Couldn't find main section with id {main_id}") + + section = sections[0] + + if version < (5, 2): + paragraphs = section.find_all("p") + if len(paragraphs) < 2: + raise ValueError(f"{version.tuple_str()} {current}: " + f"Couldn't find subclass section") + + subclasses_paragraph = paragraphs[1] + if not subclasses_paragraph.text.strip().startswith("subclasses —"): + # No subclasses for this type + process_node(current, section, version) + return [] + + subclass_anchors = subclasses_paragraph.find_all("a") + if not subclass_anchors: + raise ValueError(f"{version.tuple_str()} {current} " + f"No anchors in subclasses paragraph") + + subclass_types = [anchor.get("title") for anchor in subclass_anchors] + return subclass_types + else: + subclass_section_divs = soup.find_all("div", class_="toctree-wrapper compound") + if not subclass_section_divs or len(subclass_section_divs) < 1: + process_node(current, section, version) + return [] + subclass_section = subclass_section_divs[0] + all_links = subclass_section.find_all("ul") + if not all_links or len(all_links) < 1: + raise ValueError(f"{version.tuple_str()} {current}: " + f"Expected one subclass list") + links = all_links[0] + subclass_anchors = links.find_all("a") + if not subclass_anchors: + raise ValueError(f"{version.tuple_str()} {current} " + f"No anchors in subclasses list") + + subclass_types = [f"bpy.types.{anchor.contents[0].split('(')[0]}" + for anchor in subclass_anchors] + return subclass_types def get_subclasses(current: str, parent: str, root_path: str, version: Version) -> None: @@ -149,32 +209,13 @@ def get_subclasses(current: str, parent: str, root_path: str, with open(current_path, "r") as current_file: current_html = current_file.read() - soup = BeautifulSoup(current_html, "html.parser") - main_id = f"{current.lower()}-{parent.lower()}" - sections = soup.find_all(id=main_id) - if not sections: - raise ValueError(f"{version.tuple_str()} {current}: " - f"Couldn't find main section with id {main_id}") - - section = sections[0] - paragraphs = section.find_all("p") - if len(paragraphs) < 2: - raise ValueError(f"{version.tuple_str()} {current}: " - f"Couldn't find subclass section") + only_main = SoupStrainer(id=main_id) - subclasses_paragraph = paragraphs[1] - if not subclasses_paragraph.text.strip().startswith("subclasses —"): - # No subclasses for this type - process_node(current, section, version) - return + soup = BeautifulSoup(current_html, "html.parser", parse_only=only_main) - subclass_anchors = subclasses_paragraph.find_all("a") - if not subclass_anchors: - raise ValueError(f"{version.tuple_str()} {current} " - f"No anchors in subclasses paragraph") + subclass_types = get_subclass_links(current, parent, version, soup) - subclass_types = [anchor.get("title") for anchor in subclass_anchors] threads: list[Thread] = [] for type in subclass_types: if not type: diff --git a/tools/node_settings_generator/types_utils.py b/tools/node_settings_generator/types_utils.py index eb57ac2..025985a 100644 --- a/tools/node_settings_generator/types_utils.py +++ b/tools/node_settings_generator/types_utils.py @@ -13,6 +13,7 @@ class ST(Enum): FLOAT = auto() INT = auto() STRING = auto() + VEC = auto() VEC1 = auto() VEC2 = auto() VEC3 = auto() @@ -23,6 +24,7 @@ class ST(Enum): CAPTURE_ATTRIBUTE_ITEMS = auto() CLOSURE_INPUT_ITEMS = auto() CLOSURE_OUTPUT_ITEMS = auto() + CLOSURE_TO_LIST_ITEMS = auto() COLOR_MANAGED_DISPLAY_SETTINGS = auto() COLOR_MANAGED_VIEW_SETTINGS = auto() COLOR_RAMP = auto() @@ -30,18 +32,20 @@ class ST(Enum): COMPOSITOR_FILE_OUTPUT_ITEMS = auto() CURVE_MAPPING = auto() ENUM_DEFINITION = auto() - ENUM_ITEM = auto() EVALUATE_CLOSURE_INPUT_ITEMS = auto() EVALUATE_CLOSURE_OUTPUT_ITEMS = auto() FIELD_TO_GRID_ITEMS = auto() + FIELD_TO_LIST_ITEMS = auto() FOREACH_GEO_ELEMENT_GENERATION_ITEMS = auto() FOREACH_GEO_ELEMENT_INPUT_ITEMS = auto() FOREACH_GEO_ELEMENT_MAIN_ITEMS = auto() FORMAT_STRING_ITEMS = auto() GEOMETRY_VIEWER_ITEMS = auto() INDEX_SWITCH_ITEMS = auto() + MENU_INPUT = auto() MENU_SWITCH_ITEMS = auto() NODE_TREE = auto() + RAYCAST_ATTR_ITEMS = auto() REPEAT_OUTPUT_ITEMS = auto() SEPARATE_BUNDLE_ITEMS = auto() SIM_OUTPUT_ITEMS = auto() @@ -72,6 +76,7 @@ class ST(Enum): ST.CAPTURE_ATTRIBUTE_ITEMS, ST.CLOSURE_INPUT_ITEMS, ST.CLOSURE_OUTPUT_ITEMS, + ST.CLOSURE_TO_LIST_ITEMS, ST.COLOR_MANAGED_DISPLAY_SETTINGS, ST.COLOR_MANAGED_VIEW_SETTINGS, ST.COLOR_RAMP, @@ -83,6 +88,7 @@ class ST(Enum): ST.EVALUATE_CLOSURE_INPUT_ITEMS, ST.EVALUATE_CLOSURE_OUTPUT_ITEMS, ST.FIELD_TO_GRID_ITEMS, + ST.FIELD_TO_LIST_ITEMS, ST.FILE_SLOTS, ST.FOREACH_GEO_ELEMENT_GENERATION_ITEMS, ST.FOREACH_GEO_ELEMENT_INPUT_ITEMS, @@ -94,6 +100,7 @@ class ST(Enum): ST.INDEX_SWITCH_ITEMS, ST.LAYER_SLOTS, ST.MENU_SWITCH_ITEMS, + ST.RAYCAST_ATTR_ITEMS, ST.REPEAT_OUTPUT_ITEMS, ST.SEPARATE_BUNDLE_ITEMS, ST.SIM_OUTPUT_ITEMS @@ -101,7 +108,11 @@ class ST(Enum): doc_to_NTP_type_dict : dict[str, ST | None] = { "" : None, + "bpy_prop_array[int]" : ST.VEC, + "bpy_prop_array[float]" : ST.VEC, "bpy_prop_collection of CryptomatteEntry": ST.CRYPTOMATTE_ENTRIES, + "bpy_prop_collection[CryptomatteEntry]" : ST.CRYPTOMATTE_ENTRIES, + "bool" : ST.BOOL, "boolean" : ST.BOOL, "Collection" : ST.COLLECTION, "ColorManagedDisplaySettings" : ST.COLOR_MANAGED_DISPLAY_SETTINGS, @@ -118,15 +129,20 @@ class ST(Enum): "float array of 2" : ST.VEC2, "float array of 3" : ST.VEC3, "float array of 4" : ST.VEC4, + "GeometryNodeClosureToListItems" : ST.CLOSURE_TO_LIST_ITEMS, "GeometryNodeFieldToGridItems" : ST.FIELD_TO_GRID_ITEMS, + "GeometryNodeFieldToListItem" : None, # Always read-only + "GeometryNodeFieldToListItems" : ST.FIELD_TO_LIST_ITEMS, "Image" : ST.IMAGE, "ImageFormatSettings" : ST.IMAGE_FORMAT_SETTINGS, "ImageUser" : ST.IMAGE_USER, "int" : ST.INT, + "Literal" : ST.ENUM, "Mask" : ST.MASK, "Material" : ST.MATERIAL, "mathutils.Color" : ST.COLOR, - "mathutils.Euler" : ST.EULER, #TODO + "mathutils.Euler" : ST.EULER, + "mathutils.Vector" : ST.VEC, "mathutils.Vector of 3" : ST.VEC3, "MovieClip" : ST.MOVIE_CLIP, "Node" : None, # (<4.2) Always used with zone inputs, need to make sure @@ -136,7 +152,7 @@ class ST(Enum): "NodeCombineBundleItems" : ST.COMBINE_BUNDLE_ITEMS, "NodeCompositorFileOutputItems" : ST.COMPOSITOR_FILE_OUTPUT_ITEMS, "NodeEnumDefinition" : ST.ENUM_DEFINITION, - "NodeEnumItem" : ST.ENUM_ITEM, + "NodeEnumItem" : None, #Always read-only "NodeEvaluateClosureInputItems" : ST.EVALUATE_CLOSURE_OUTPUT_ITEMS, "NodeEvaluateClosureOutputItems" : ST.EVALUATE_CLOSURE_OUTPUT_ITEMS, "NodeFunctionFormatStringItems" : ST.FORMAT_STRING_ITEMS, @@ -150,6 +166,7 @@ class ST(Enum): "NodeGeometryViewerItems" : ST.GEOMETRY_VIEWER_ITEMS, "NodeIndexSwitchItems" : ST.INDEX_SWITCH_ITEMS, "NodeMenuSwitchItems" : ST.MENU_SWITCH_ITEMS, + "NodeRaycastSampleAttributeItems" : ST.RAYCAST_ATTR_ITEMS, "NodeSeparateBundleItems" : ST.SEPARATE_BUNDLE_ITEMS, "NodeTree" : ST.NODE_TREE, "Object" : ST.OBJECT, @@ -157,15 +174,18 @@ class ST(Enum): "PropertyGroup" : None, #Always read-only "RepeatItem" : None, #Always set with index "Scene" : ST.SCENE, + "set[Literal" : ST.ENUM_SET, "SimulationStateItem" : None, #Always set with index + "str" : ST.STRING, "string" : ST.STRING, "TexMapping" : None, #Always read-only "Text" : ST.TEXT, + "TextboxState" : None, # Always read-only "Texture" : ST.TEXTURE, "VectorFont" : ST.FONT } -def get_NTP_type(type_str: str) -> ST | None: +def get_NTP_type(type_str: str, readonly: bool) -> ST | None: """ Time complexity isn't great, might be able to optimize with a trie or similar data structure @@ -180,7 +200,7 @@ def get_NTP_type(type_str: str) -> ST | None: result = doc_to_NTP_type_dict[longest_prefix] - is_readonly = "read" in type_str + is_readonly = ("read" in type_str) or readonly if is_readonly and result not in READ_ONLY_TYPES: return None else: