diff --git a/compositor/node_settings.py b/compositor/node_settings.py index 781eee6..e4ae8ac 100644 --- a/compositor/node_settings.py +++ b/compositor/node_settings.py @@ -29,28 +29,25 @@ 'CompositorNodeMovieClip' : [("clip", ST.MOVIE_CLIP)], - 'CompositorNodeRLayers' : [("layer", ST.ENUM), - ("scene", ST.SCENE)], + 'CompositorNodeTexture' : [("node_output", ST.INT), #TODO: ?? + ("texture", ST.TEXTURE)], + + # Input > Constant 'CompositorNodeRGB' : [], + 'CompositorNodeValue' : [], - 'CompositorNodeSceneTime' : [], + # Input > Scene - 'CompositorNodeTexture' : [("node_output", ST.INT), #TODO: ?? - ("texture", ST.TEXTURE)], + 'CompositorNodeRLayers' : [("layer", ST.ENUM), + ("scene", ST.SCENE)], + + 'CompositorNodeSceneTime' : [], 'CompositorNodeTime' : [("curve", ST.CURVE_MAPPING), ("frame_end", ST.INT), ("frame_start", ST.INT)], - 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), - ("frame_relative", ST.INT), - ("position", ST.ENUM), - ("track_name", ST.STRING), #TODO: probably not right - ("tracking_object", ST.STRING)], - - 'CompositorNodeValue' : [], - # OUTPUT 'CompositorNodeComposite' : [("use_alpha", ST.BOOL)], @@ -62,21 +59,33 @@ ("format", ST.IMAGE_FORMAT_SETTINGS), ("layer_slots", ST.LAYER_SLOTS)], - 'CompositorNodeLevels' : [("channel", ST.ENUM)], - - 'CompositorNodeSplitViewer' : [("axis", ST.ENUM), - ("factor", ST.INT)], - 'CompositorNodeViewer' : [("center_x", ST.FLOAT), ("center_y", ST.FLOAT), ("tile_order", ST.ENUM), ("use_alpha", ST.BOOL)], + 'CompositorNodeSplitViewer' : [("axis", ST.ENUM), + ("factor", ST.INT)], + # COLOR - 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), - ("use_premultiply", ST.BOOL)], + 'CompositorNodePremulKey' : [("mapping", ST.ENUM)], + 'CompositorNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + + 'CompositorNodeConvertColorSpace' : [("from_color_space", ST.ENUM), + ("to_color_space", ST.ENUM)], + + + 'CompositorNodeSetAlpha' : [("mode", ST.ENUM)], + + 'CompositorNodeInvert' : [("invert_alpha", ST.BOOL), + ("invert_rgb", ST.BOOL)], + + 'CompositorNodeRGBToBW' : [], + + + # Color > Adjust 'CompositorNodeBrightContrast' : [("use_premultiply", ST.BOOL)], 'CompositorNodeColorBalance' : [("correction_method", ST.ENUM), @@ -120,22 +129,12 @@ ("midtones_end", ST.FLOAT)], 'CompositorNodeExposure' : [], - 'CompositorNodeGamma' : [], 'CompositorNodeHueCorrect' : [("mapping", ST.CURVE_MAPPING)], 'CompositorNodeHueSat' : [], - 'CompositorNodeInvert' : [("invert_alpha", ST.BOOL), - ("invert_rgb", ST.BOOL)], - - 'CompositorNodeMixRGB' : [("blend_type", ST.ENUM), - ("use_alpha", ST.BOOL), - ("use_clamp", ST.BOOL)], #TODO: what is update() method for? - - 'CompositorNodePosterize' : [], - 'CompositorNodeCurveRGB' : [("mapping", ST.CURVE_MAPPING)], 'CompositorNodeTonemap' : [("adaptation", ST.FLOAT), @@ -147,46 +146,73 @@ ("offset", ST.FLOAT), ("tonemap_type", ST.ENUM)], - 'CompositorNodeZcombine' : [("use_alpha", ST.BOOL), - ("use_antialias_z", ST.BOOL)], + + # Color > Mix + 'CompositorNodeAlphaOver' : [("premul", ST.FLOAT), + ("use_premultiply", ST.BOOL)], + 'CompositorNodeCombineColor' : [("mode", ST.ENUM), + ("ycc_mode", ST.ENUM)], - # CONVERTER - 'CompositorNodePremulKey' : [("mapping", ST.ENUM)], + 'CompositorNodeSeparateColor' : [("mode", ST.ENUM), + ("ycc_mode", ST.ENUM)], - 'CompositorNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], + 'CompositorNodeMixRGB' : [("blend_type", ST.ENUM), + ("use_alpha", ST.BOOL), + ("use_clamp", ST.BOOL)], #TODO: what is update() method for? - 'CompositorNodeConvertColorSpace' : [("from_color_space", ST.ENUM), - ("to_color_space", ST.ENUM)], + 'CompositorNodeZcombine' : [("use_alpha", ST.BOOL), + ("use_antialias_z", ST.BOOL)], - 'CompositorNodeCombineColor' : [("mode", ST.ENUM), - ("ycc_mode", ST.ENUM)], - 'CompositorNodeCombineXYZ' : [], - 'CompositorNodeIDMask' : [("index", ST.INT), - ("use_antialiasing", ST.BOOL)], + # FILTER + 'CompositorNodeAntiAliasing' : [("contrast_limit", ST.FLOAT), + ("corner_rounding", ST.FLOAT), + ("threshold", ST.FLOAT)], + + 'CompositorNodeDenoise' : [("prefilter", ST.ENUM), + ("use_hdr", ST.BOOL)], - 'CompositorNodeMath' : [("operation", ST.ENUM), - ("use_clamp", ST.BOOL)], + 'CompositorNodeDespeckle' : [("threshold", ST.FLOAT), + ("threshold_neighbor", ST.FLOAT)], - 'CompositorNodeRGBToBW' : [], - 'CompositorNodeSeparateColor' : [("mode", ST.ENUM), - ("ycc_mode", ST.ENUM)], + 'CompositorNodeDilateErode' : [("distance", ST.INT), + ("edge", ST.FLOAT), + ("falloff", ST.ENUM), + ("mode", ST.ENUM)], - 'CompositorNodeSeparateXYZ' : [], + 'CompositorNodeInpaint' : [("distance", ST.INT)], - 'CompositorNodeSetAlpha' : [("mode", ST.ENUM)], - 'CompositorNodeSwitchView' : [], + 'CompositorNodeFilter' : [("filter_type", ST.ENUM)], + 'CompositorNodeGlare' : [("angle_offset", ST.FLOAT), + ("color_modulation", ST.FLOAT), + ("fade", ST.FLOAT), + ("glare_type", ST.ENUM), + ("iterations", ST.INT), + ("mix", ST.FLOAT), + ("quality", ST.ENUM), + ("size", ST.INT), + ("streaks", ST.INT), + ("threshold", ST.FLOAT), + ("use_rotate_45", ST.BOOL)], + + 'CompositorNodeKuwahara' : [("eccentricity", ST.FLOAT), + ("sharpness", ST.FLOAT), + ("size", ST.INT), + ("uniformity", ST.INT), + ("variation", ST.ENUM)], - # FILTER - 'CompositorNodeAntiAliasing' : [("contrast_limit", ST.FLOAT), - ("corner_rounding", ST.FLOAT), - ("threshold", ST.FLOAT)], + 'CompositorNodePixelate' : [], + 'CompositorNodePosterize' : [], + + 'CompositorNodeSunBeams' : [("ray_length", ST.FLOAT), + ("source", ST.VEC2)], + # Filter > Blur 'CompositorNodeBilateralblur' : [("iterations", ST.INT), ("sigma_color", ST.FLOAT), ("sigma_space", ST.FLOAT)], @@ -219,14 +245,6 @@ ("use_zbuffer", ST.BOOL), ("z_scale", ST.FLOAT)], - 'CompositorNodeDespeckle' : [("threshold", ST.FLOAT), - ("threshold_neighbor", ST.FLOAT)], - - 'CompositorNodeDilateErode' : [("distance", ST.INT), - ("edge", ST.FLOAT), - ("falloff", ST.ENUM), - ("mode", ST.ENUM)], - 'CompositorNodeDBlur' : [("angle", ST.FLOAT), ("center_x", ST.FLOAT), ("center_y", ST.FLOAT), @@ -234,93 +252,73 @@ ("iterations", ST.INT), ("spin", ST.FLOAT), ("zoom", ST.FLOAT)], - - 'CompositorNodeFilter' : [("filter_type", ST.ENUM)], - - 'CompositorNodeGlare' : [("angle_offset", ST.FLOAT), - ("color_modulation", ST.FLOAT), - ("fade", ST.FLOAT), - ("glare_type", ST.ENUM), - ("iterations", ST.INT), - ("mix", ST.FLOAT), - ("quality", ST.ENUM), - ("size", ST.INT), - ("streaks", ST.INT), - ("threshold", ST.FLOAT), - ("use_rotate_45", ST.BOOL)], - - 'CompositorNodeInpaint' : [("distance", ST.INT)], - - 'CompositorNodePixelate' : [], - - 'CompositorNodeSunBeams' : [("ray_length", ST.FLOAT), - ("source", ST.VEC2)], - + 'CompositorNodeVecBlur' : [("factor", ST.FLOAT), ("samples", ST.INT), ("speed_max", ST.INT), ("speed_min", ST.INT), ("use_curved", ST.BOOL)], - - # VECTOR - 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], - - 'CompositorNodeMapValue' : [("max", ST.VEC1), - ("min", ST.VEC1), - ("offset", ST.VEC1), - ("size", ST.VEC1), - ("use_max", ST.BOOL), - ("use_min", ST.BOOL)], - - 'CompositorNodeNormal' : [], - - 'CompositorNodeNormalize' : [], - - 'CompositorNodeCurveVec' : [("mapping", ST.CURVE_MAPPING)], - - - # MATTE - 'CompositorNodeBoxMask' : [("height", ST.FLOAT), - ("mask_type", ST.ENUM), - ("rotation", ST.FLOAT), - ("width", ST.FLOAT), - ("x", ST.FLOAT), - ("y", ST.FLOAT)], - - 'CompositorNodeChannelMatte' : [("color_space", ST.ENUM), - ("limit_channel", ST.ENUM), - ("limit_max", ST.FLOAT), - ("limit_method", ST.ENUM), - ("limit_min", ST.FLOAT), - ("matte_channel", ST.ENUM)], - - 'CompositorNodeChromaMatte' : [("gain", ST.FLOAT), - ("lift", ST.FLOAT), + + # KEYING + 'CompositorNodeChannelMatte' : [("color_space", ST.ENUM), + ("limit_channel", ST.ENUM), + ("limit_max", ST.FLOAT), + ("limit_method", ST.ENUM), + ("limit_min", ST.FLOAT), + ("matte_channel", ST.ENUM)], + + 'CompositorNodeChromaMatte' : [("gain", ST.FLOAT), + ("lift", ST.FLOAT), ("shadow_adjust", ST.FLOAT), - ("threshold", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeColorMatte' : [("color_hue", ST.FLOAT), - ("color_saturation", ST.FLOAT), - ("color_value", ST.FLOAT)], - - 'CompositorNodeColorSpill' : [("channel", ST.ENUM), - ("limit_channel", ST.ENUM), - ("limit_method", ST.ENUM), - ("ratio", ST.FLOAT), - ("unspill_blue", ST.FLOAT), - ("unspill_green", ST.FLOAT), - ("unspill_red", ST.FLOAT), - ("use_unspill", ST.BOOL)], - + ("threshold", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeColorMatte' : [("color_hue", ST.FLOAT), + ("color_saturation", ST.FLOAT), + ("color_value", ST.FLOAT)], + + 'CompositorNodeColorSpill' : [("channel", ST.ENUM), + ("limit_channel", ST.ENUM), + ("limit_method", ST.ENUM), + ("ratio", ST.FLOAT), + ("unspill_blue", ST.FLOAT), + ("unspill_green", ST.FLOAT), + ("unspill_red", ST.FLOAT), + ("use_unspill", ST.BOOL)], + + 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeDistanceMatte' : [("channel", ST.ENUM), + ("falloff", ST.FLOAT), + ("tolerance", ST.FLOAT)], + + 'CompositorNodeKeying' : [("blur_post", ST.INT), + ("blur_pre", ST.INT), + ("clip_black", ST.FLOAT), + ("clip_white", ST.FLOAT), + ("despill_balance", ST.FLOAT), + ("despill_factor", ST.FLOAT), + ("dilate_distance", ST.INT), + ("edge_kernel_radius", ST.INT), + ("edge_kernel_tolerance", ST.FLOAT), + ("feather_distance", ST.INT), + ("feather_falloff", ST.ENUM), + ("screen_balance", ST.FLOAT)], + + 'CompositorNodeKeyingScreen' : [("clip", ST.MOVIE_CLIP), + ("tracing_object", ST.STRING)], + + 'CompositorNodeLumaMatte' : [("limit_max", ST.FLOAT), + ("limit_min", ST.FLOAT)], + + # MASK 'CompositorNodeCryptomatteV2' : [("add", ST.COLOR), ("entries", ST.CRYPTOMATTE_ENTRIES), ("frame_duration", ST.INT), ("frame_offset", ST.INT), ("frame_start", ST.INT), - #("has_layers", ST.BOOL), #TODO: readonly? - #("has_views", ST.BOOL), #TODO: readonly? ("image", ST.IMAGE), ("layer", ST.ENUM), ("layer_name", ST.ENUM), @@ -336,15 +334,13 @@ ("matte_id", ST.STRING), ("remove", ST.COLOR)], - 'CompositorNodeDiffMatte' : [("falloff", ST.FLOAT), - ("tolerance", ST.FLOAT)], - 'CompositorNodeDistanceMatte' : [("channel", ST.ENUM), - ("falloff", ST.FLOAT), - ("tolerance", ST.FLOAT)], - - 'CompositorNodeDoubleEdgeMask' : [("edge_mode", ST.ENUM), - ("inner_mode", ST.ENUM)], + 'CompositorNodeBoxMask' : [("height", ST.FLOAT), + ("mask_type", ST.ENUM), + ("rotation", ST.FLOAT), + ("width", ST.FLOAT), + ("x", ST.FLOAT), + ("y", ST.FLOAT)], 'CompositorNodeEllipseMask' : [("height", ST.FLOAT), ("mask_type", ST.ENUM), @@ -353,27 +349,49 @@ ("x", ST.FLOAT), ("y", ST.FLOAT)], - 'CompositorNodeKeying' : [("blur_post", ST.INT), - ("blur_pre", ST.INT), - ("clip_black", ST.FLOAT), - ("clip_white", ST.FLOAT), - ("despill_balance", ST.FLOAT), - ("despill_factor", ST.FLOAT), - ("dilate_distance", ST.INT), - ("edge_kernel_radius", ST.INT), - ("edge_kernel_tolerance", ST.FLOAT), - ("feather_distance", ST.INT), - ("feather_falloff", ST.ENUM), - ("screen_balance", ST.FLOAT)], - 'CompositorNodeKeyingScreen' : [("clip", ST.MOVIE_CLIP), - ("tracing_object", ST.STRING)], + 'CompositorNodeDoubleEdgeMask' : [("edge_mode", ST.ENUM), + ("inner_mode", ST.ENUM)], - 'CompositorNodeLumaMatte' : [("limit_max", ST.FLOAT), - ("limit_min", ST.FLOAT)], + 'CompositorNodeIDMask' : [("index", ST.INT), + ("use_antialiasing", ST.BOOL)], + + + # TRACKING + 'CompositorNodePlaneTrackDeform' : [("clip", ST.MOVIE_CLIP), + ("motion_blur_samples", ST.INT), + ("motion_blur_shutter", ST.FLOAT), + ("plane_track_name", ST.STRING), + ("tracking_object", ST.STRING), + ("use_motion_blur", ST.BOOL)], + + 'CompositorNodeStabilize' : [("clip", ST.MOVIE_CLIP), + ("filter_type", ST.ENUM), + ("invert", ST.BOOL)], + + 'CompositorNodeTrackPos' : [("clip", ST.MOVIE_CLIP), + ("frame_relative", ST.INT), + ("position", ST.ENUM), + ("track_name", ST.STRING), #TODO: probably not right + ("tracking_object", ST.STRING)], + + + + # TRANSFORM + 'CompositorNodeRotate' : [("filter_type", ST.ENUM)], + + 'CompositorNodeScale' : [("frame_method", ST.ENUM), + ("offset_x", ST.FLOAT), + ("offset_y", ST.FLOAT), + ("space", ST.ENUM)], + + + 'CompositorNodeTransform' : [("filter_type", ST.ENUM)], + + 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), + ("wrap_axis", ST.ENUM)], - # DISTORT 'CompositorNodeCornerPin' : [], 'CompositorNodeCrop' : [("max_x", ST.INT), @@ -387,50 +405,65 @@ ("relative", ST.BOOL), ("use_crop_size", ST.BOOL)], + 'CompositorNodeDisplace' : [], 'CompositorNodeFlip' : [("axis", ST.ENUM)], + 'CompositorNodeMapUV' : [("alpha", ST.INT)], + + 'CompositorNodeLensdist' : [("use_fit", ST.BOOL), ("use_jitter", ST.BOOL), ("use_projector", ST.BOOL)], - 'CompositorNodeMapUV' : [("alpha", ST.INT)], - 'CompositorNodeMovieDistortion' : [("clip", ST.MOVIE_CLIP), ("distortion_type", ST.ENUM)], - 'CompositorNodePlaneTrackDeform' : [("clip", ST.MOVIE_CLIP), - ("motion_blur_samples", ST.INT), - ("motion_blur_shutter", ST.FLOAT), - ("plane_track_name", ST.STRING), - ("tracking_object", ST.STRING), - ("use_motion_blur", ST.BOOL)], - 'CompositorNodeRotate' : [("filter_type", ST.ENUM)], - 'CompositorNodeScale' : [("frame_method", ST.ENUM), - ("offset_x", ST.FLOAT), - ("offset_y", ST.FLOAT), - ("space", ST.ENUM)], + # UTILITIES + 'CompositorNodeMapRange' : [("use_clamp", ST.BOOL)], - 'CompositorNodeStabilize' : [("clip", ST.MOVIE_CLIP), - ("filter_type", ST.ENUM), - ("invert", ST.BOOL)], + 'CompositorNodeMapValue' : [("max", ST.VEC1), + ("min", ST.VEC1), + ("offset", ST.VEC1), + ("size", ST.VEC1), + ("use_max", ST.BOOL), + ("use_min", ST.BOOL)], - 'CompositorNodeTransform' : [("filter_type", ST.ENUM)], + 'CompositorNodeMath' : [("operation", ST.ENUM), + ("use_clamp", ST.BOOL)], + + + 'CompositorNodeLevels' : [("channel", ST.ENUM)], + + 'CompositorNodeNormalize' : [], - 'CompositorNodeTranslate' : [("use_relative", ST.BOOL), - ("wrap_axis", ST.ENUM)], + + 'CompositorNodeSwitch' : [("check", ST.BOOL)], + + 'CompositorNodeSwitchView' : [], - # LAYOUT - 'CompositorNodeSwitch' : [("check", ST.BOOL)], + # VECTOR + 'CompositorNodeCombineXYZ' : [], + 'CompositorNodeSeparateXYZ' : [], + 'CompositorNodeNormal' : [], + + 'CompositorNodeCurveVec' : [("mapping", ST.CURVE_MAPPING)], # MISC - 'NodeFrame' : [], - 'NodeGroupInput' : [], - 'NodeGroupOutput' : [], - 'NodeReroute' : [] + 'CompositorNodeGroup' : [("node_tree", ST.NODE_TREE)], + + 'NodeFrame' : [("label_size", ST.INT), + ("shrink", ST.BOOL), + ("text", ST.TEXT)], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + + 'NodeReroute' : [] } diff --git a/compositor/operator.py b/compositor/operator.py index b521e77..b1daada 100644 --- a/compositor/operator.py +++ b/compositor/operator.py @@ -1,6 +1,6 @@ import bpy -from bpy.types import Node +from bpy.types import Node, CompositorNodeColorBalance, CompositorNodeTree from ..ntp_operator import NTP_Operator from ..ntp_node_tree import NTP_NodeTree @@ -37,125 +37,139 @@ def __init__(self): def _create_scene(self, indent: str): #TODO: wrap in more general unique name util function - self._write(f"{indent}# Generate unique scene name\n") - self._write(f"{indent}{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}\n") - self._write(f"{indent}{END_NAME_VAR} = {BASE_NAME_VAR}\n") - self._write(f"{indent}if bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - self._write(f"{indent}\ti = 1\n") - self._write(f"{indent}\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - self._write(f"{indent}\twhile bpy.data.scenes.get({END_NAME_VAR}) != None:\n") - self._write(f"{indent}\t\t{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"\n") - self._write(f"{indent}\t\ti += 1\n\n") - - self._write(f"{indent}{SCENE_VAR} = bpy.context.window.scene.copy()\n\n") - self._write(f"{indent}{SCENE_VAR}.name = {END_NAME_VAR}\n") - self._write(f"{indent}{SCENE_VAR}.use_fake_user = True\n") - self._write(f"{indent}bpy.context.window.scene = {SCENE_VAR}\n") - - def _initialize_compositor_node_tree(self, outer, nt_var, level, inner, nt_name): - #initialize node group - self._write(f"{outer}#initialize {nt_var} node group\n") - self._write(f"{outer}def {nt_var}_node_group():\n") - - if self._is_outermost_node_group(level): #outermost node group - self._write(f"{inner}{nt_var} = {SCENE_VAR}.node_tree\n") - self._write(f"{inner}#start with a clean node tree\n") - self._write(f"{inner}for node in {nt_var}.nodes:\n") - self._write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + self._write(f"# Generate unique scene name", indent) + self._write(f"{BASE_NAME_VAR} = {str_to_py_str(self.compositor_name)}", + indent) + self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR}", indent) + self._write(f"if bpy.data.scenes.get({END_NAME_VAR}) != None:", indent) + + indent2 = f"{indent}\t" + self._write(f"i = 1", indent2) + self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"", + indent2) + self._write(f"while bpy.data.scenes.get({END_NAME_VAR}) != None:", + indent2) + + indent3 = f"{indent}\t\t" + self._write(f"{END_NAME_VAR} = {BASE_NAME_VAR} + f\".{{i:03d}}\"", indent3) + self._write(f"i += 1\n", indent3) + + self._write(f"{SCENE_VAR} = bpy.context.window.scene.copy()\n", indent) + self._write(f"{SCENE_VAR}.name = {END_NAME_VAR}", indent) + self._write(f"{SCENE_VAR}.use_fake_user = True", indent) + self._write(f"bpy.context.window.scene = {SCENE_VAR}", indent) + + def _initialize_compositor_node_tree(self, ntp_nt, nt_name): + #initialize node group + self._write(f"#initialize {nt_name} node group", self._outer) + self._write(f"def {ntp_nt.var}_node_group():", self._outer) + + if ntp_nt.node_tree == self._base_node_tree: + self._write(f"{ntp_nt.var} = {SCENE_VAR}.node_tree") + self._write(f"#start with a clean node tree") + self._write(f"for node in {ntp_nt.var}.nodes:") + self._write(f"\t{ntp_nt.var}.nodes.remove(node)") else: - self._write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" + self._write((f"{ntp_nt.var} = bpy.data.node_groups.new(" f"type = \'CompositorNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - self._write("\n") - - def _process_node(self, node: Node, ntp_nt: NTP_NodeTree, inner: str, level: int): - if node.bl_idname == 'CompositorNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in self._node_trees: - self._process_comp_node_group(node_nt, level + 1, self._node_vars, - self._used_vars) - self._node_trees.add(node_nt) - - node_var: str = self._create_node(node, inner, ntp_nt.var) + f"name = {str_to_py_str(nt_name)})")) + self._write("") + + def _set_color_balance_settings(self, node: CompositorNodeColorBalance + ) -> None: + """ + Sets the color balance settings so we only set the active variables, + preventing conflict + + node (CompositorNodeColorBalance): the color balance node + """ + if node.correction_method == 'LIFT_GAMMA_GAIN': + lst = [("correction_method", ST.ENUM), + ("gain", ST.COLOR), + ("gamma", ST.COLOR), + ("lift", ST.COLOR)] + else: + lst = [("correction_method", ST.ENUM), + ("offset", ST.COLOR), + ("offset_basis", ST.FLOAT), + ("power", ST.COLOR), + ("slope", ST.COLOR)] + + self._settings['CompositorNodeColorBalance'] = lst + + def _process_node(self, node: Node, ntp_nt: NTP_NodeTree): + """ + Create node and set settings, defaults, and cosmetics + + Parameters: + node (Node): node to process + ntp_nt (NTP_NodeTree): the node tree that node belongs to + """ + node_var: str = self._create_node(node, ntp_nt.var) if node.bl_idname == 'CompositorNodeColorBalance': - if node.correction_method == 'LIFT_GAMMA_GAIN': - lst = [("correction_method", ST.ENUM), - ("gain", ST.COLOR), - ("gamma", ST.COLOR), - ("lift", ST.COLOR)] - else: - lst = [("correction_method", ST.ENUM), - ("offset", ST.COLOR), - ("offset_basis", ST.FLOAT), - ("power", ST.COLOR), - ("slope", ST.COLOR)] - - self._settings['CompositorNodeColorBalance'] = lst - - self._set_settings_defaults(node, inner, node_var) - self._hide_hidden_sockets(node, inner, node_var) - - if node.bl_idname == 'CompositorNodeGroup': - if node.node_tree is not None: - self._write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - self._group_io_settings(node, inner, "input", ntp_nt) - inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - self._group_io_settings(node, inner, "output", ntp_nt) - outputs_set = True - - self._set_socket_defaults(node, node_var, inner) + self._set_color_balance_settings(node) + + self._set_settings_defaults(node) + self._hide_hidden_sockets(node) + + if bpy.app.version < (4, 0, 0): + if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: + self._group_io_settings(node, "input", ntp_nt) + ntp_nt.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: + self._group_io_settings(node, "output", ntp_nt) + ntp_nt.outputs_set = True + + self._set_socket_defaults(node) - def _process_node_tree(self, node_tree, level): + def _process_node_tree(self, node_tree: CompositorNodeTree): """ Generates a Python function to recreate a compositor node tree Parameters: - node_tree (NodeTree): node tree to be recreated - level (int): number of tabs to use for each line - + node_tree (CompositorNodeTree): node tree to be recreated """ - if self._is_outermost_node_group(level): + if node_tree == self._base_node_tree: nt_var = self._create_var(self.compositor_name) nt_name = self.compositor_name else: nt_var = self._create_var(node_tree.name) nt_name = node_tree.name - outer, inner = make_indents(level) + self._node_tree_vars[node_tree] = nt_var - self._initialize_compositor_node_tree(outer, nt_var, level, inner, nt_name) - ntp_nt = NTP_NodeTree(node_tree, nt_var) + self._initialize_compositor_node_tree(ntp_nt, nt_name) + + if bpy.app.version >= (4, 0, 0): + self._tree_interface_settings(ntp_nt) #initialize nodes - self._write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"#initialize {nt_var} nodes") for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) + self._process_node(node, ntp_nt) - self._set_parents(node_tree, inner) - self._set_locations(node_tree, inner) - self._set_dimensions(node_tree, inner) + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) - self._init_links(node_tree, inner, nt_var) + self._init_links(node_tree) - self._write(f"\n{outer}{nt_var}_node_group()\n\n") + self._write(f"return {nt_var}\n") + + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def execute(self, context): #find node group to replicate if self.is_scene: - nt = bpy.data.scenes[self.compositor_name].node_tree + self._base_node_tree = bpy.data.scenes[self.compositor_name].node_tree else: - nt = bpy.data.node_groups[self.compositor_name] + self._base_node_tree = bpy.data.node_groups[self.compositor_name] - if nt is None: + if self._base_node_tree is None: #shouldn't happen self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " "valid compositor node tree. Is Use Nodes " @@ -166,6 +180,9 @@ def execute(self, context): comp_var = clean_string(self.compositor_name) if self.mode == 'ADDON': + self._outer = "\t\t" + self._inner = "\t\t\t" + self._setup_addon_directories(context, comp_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") @@ -174,7 +191,7 @@ def execute(self, context): self._class_name = clean_string(self.compositor_name, lower=False) self._init_operator(comp_var, self.compositor_name) - self._write("\tdef execute(self, context):\n") + self._write("def execute(self, context):", "\t") else: self._file = StringIO("") @@ -183,15 +200,14 @@ def execute(self, context): self._create_scene("\t\t") elif self.mode == 'SCRIPT': self._create_scene("") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - self._process_node_tree(nt, level) + + node_trees_to_process = self._topological_sort(self._base_node_tree) + + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree) if self.mode == 'ADDON': - self._write("\t\treturn {'FINISHED'}\n\n") + self._write("return {'FINISHED'}\n", self._outer) self._create_menu_func() self._create_register_func() diff --git a/geometry/node_settings.py b/geometry/node_settings.py index 50d645e..d637180 100644 --- a/geometry/node_settings.py +++ b/geometry/node_settings.py @@ -43,16 +43,16 @@ 'NodeGroupInput' : [], # Input > Scene + 'GeometryNodeTool3DCursor' : [], + 'GeometryNodeCollectionInfo' : [("transform_space", ST.ENUM)], 'GeometryNodeImageInfo' : [], - 'GeometryNodeIsViewport' : [], 'GeometryNodeObjectInfo' : [("transform_space", ST.ENUM)], 'GeometryNodeSelfObject' : [], - 'GeometryNodeInputSceneTime' : [], @@ -64,21 +64,18 @@ # GEOMETRY 'GeometryNodeJoinGeometry' : [], - 'GeometryNodeGeometryToInstance' : [], # Geometry > Read 'GeometryNodeInputID' : [], - 'GeometryNodeInputIndex' : [], 'GeometryNodeInputNamedAttribute' : [("data_type", ST.ENUM)], 'GeometryNodeInputNormal' : [], - 'GeometryNodeInputPosition' : [], - 'GeometryNodeInputRadius' : [], + 'GeometryNodeToolSelection' : [], # Geometry > Sample 'GeometryNodeProximity' : [("target_element", ST.ENUM)], @@ -95,13 +92,12 @@ 'GeometryNodeSampleNearest' : [("domain", ST.ENUM)], # Geometry > Write - 'GeometryNodeSetID' : [], - - 'GeometryNodeSetPosition' : [], + 'GeometryNodeSetID' : [], + 'GeometryNodeSetPosition' : [], + 'GeometryNodeToolSetSelection' : [], # Geometry > Operations 'GeometryNodeBoundBox' : [], - 'GeometryNodeConvexHull' : [], 'GeometryNodeDeleteGeometry' : [("domain", ST.ENUM), @@ -112,7 +108,6 @@ 'GeometryNodeMergeByDistance' : [("mode", ST.ENUM)], 'GeometryNodeTransform' : [], - 'GeometryNodeSeparateComponents' : [], 'GeometryNodeSeparateGeometry' : [("domain", ST.ENUM)], @@ -121,24 +116,17 @@ # CURVE # Curve > Read 'GeometryNodeInputCurveHandlePositions' : [], - 'GeometryNodeCurveLength' : [], - 'GeometryNodeInputTangent' : [], - 'GeometryNodeInputCurveTilt' : [], - 'GeometryNodeCurveEndpointSelection' : [], 'GeometryNodeCurveHandleTypeSelection' : [("handle_type", ST.ENUM), ("mode", ST.ENUM_SET)], 'GeometryNodeInputSplineCyclic' : [], - 'GeometryNodeSplineLength' : [], - 'GeometryNodeSplineParameter' : [], - 'GeometryNodeInputSplineResolution' : [], # Curve > Sample @@ -150,7 +138,6 @@ 'GeometryNodeSetCurveNormal' : [("mode", ST.ENUM)], 'GeometryNodeSetCurveRadius' : [], - 'GeometryNodeSetCurveTilt' : [], 'GeometryNodeSetCurveHandlePositions' : [("mode", ST.ENUM)], @@ -159,7 +146,6 @@ ("mode", ST.ENUM_SET)], 'GeometryNodeSetSplineCyclic' : [], - 'GeometryNodeSetSplineResolution' : [], 'GeometryNodeCurveSplineType' : [("spline_type", ST.ENUM)], @@ -180,7 +166,6 @@ 'GeometryNodeResampleCurve' : [("mode", ST.ENUM)], 'GeometryNodeReverseCurve' : [], - 'GeometryNodeSubdivideCurve' : [], 'GeometryNodeTrimCurve' : [("mode", ST.ENUM)], @@ -195,7 +180,6 @@ 'GeometryNodeCurvePrimitiveLine' : [("mode", ST.ENUM)], 'GeometryNodeCurveSpiral' : [], - 'GeometryNodeCurveQuadraticBezier' : [], 'GeometryNodeCurvePrimitiveQuadrilateral' : [("mode", ST.ENUM)], @@ -204,54 +188,38 @@ # Curve > Topology 'GeometryNodeOffsetPointInCurve' : [], - 'GeometryNodeCurveOfPoint' : [], - 'GeometryNodePointsOfCurve' : [], # INSTANCES 'GeometryNodeInstanceOnPoints' : [], - 'GeometryNodeInstancesToPoints' : [], 'GeometryNodeRealizeInstances' : [("legacy_behavior", ST.BOOL)], 'GeometryNodeRotateInstances' : [], - 'GeometryNodeScaleInstances' : [], - 'GeometryNodeTranslateInstances' : [], - 'GeometryNodeInputInstanceRotation' : [], - 'GeometryNodeInputInstanceScale' : [], # MESH # Mesh > Read 'GeometryNodeInputMeshEdgeAngle' : [], - 'GeometryNodeInputMeshEdgeNeighbors' : [], - 'GeometryNodeInputMeshEdgeVertices' : [], - 'GeometryNodeEdgesToFaceGroups' : [], - 'GeometryNodeInputMeshFaceArea' : [], - 'GeometryNodeInputMeshFaceNeighbors' : [], - + 'GeometryNodeToolFaceSet' : [], 'GeometryNodeMeshFaceSetBoundaries' : [], - 'GeometryNodeInputMeshFaceIsPlanar' : [], - 'GeometryNodeInputShadeSmooth' : [], - + 'GeometryNodeInputEdgeSmooth' : [], 'GeometryNodeInputMeshIsland' : [], - 'GeometryNodeInputShortestEdgePaths' : [], - 'GeometryNodeInputMeshVertexNeighbors' : [], # Mesh > Sample @@ -260,13 +228,13 @@ 'GeometryNodeSampleUVSurface' : [("data_type", ST.ENUM)], # Mesh > Write - 'GeometryNodeSetShadeSmooth' : [], + 'GeometryNodeToolSetFaceSet' : [], + + 'GeometryNodeSetShadeSmooth' : [("domain", ST.ENUM)], # Mesh > Operations 'GeometryNodeDualMesh' : [], - 'GeometryNodeEdgePathsToCurves' : [], - 'GeometryNodeEdgePathsToSelection' : [], 'GeometryNodeExtrudeMesh' : [("mode", ST.ENUM)], @@ -285,7 +253,6 @@ ("scale_mode", ST.ENUM)], 'GeometryNodeSplitEdges' : [], - 'GeometryNodeSubdivideMesh' : [], 'GeometryNodeSubdivisionSurface' : [("boundary_smooth", ST.ENUM), @@ -302,7 +269,6 @@ 'GeometryNodeMeshCylinder' : [("fill_type", ST.ENUM)], 'GeometryNodeMeshGrid' : [], - 'GeometryNodeMeshIcoSphere' : [], 'GeometryNodeMeshCircle' : [("fill_type", ST.ENUM)], @@ -314,17 +280,11 @@ # Mesh > Topology 'GeometryNodeCornersOfFace' : [], - 'GeometryNodeCornersOfVertex' : [], - 'GeometryNodeEdgesOfCorner' : [], - 'GeometryNodeEdgesOfVertex' : [], - 'GeometryNodeFaceOfCorner' : [], - 'GeometryNodeOffsetCornerInFace' : [], - 'GeometryNodeVertexOfCorner' : [], # Mesh > UV @@ -340,7 +300,7 @@ ("use_legacy_normal", ST.BOOL)], 'GeometryNodePoints' : [], - + 'GeometryNodePointsToCurves' : [], 'GeometryNodePointsToVertices' : [], 'GeometryNodePointsToVolume' : [("resolution_mode", ST.ENUM)], @@ -350,25 +310,19 @@ # VOLUME 'GeometryNodeVolumeCube' : [], - 'GeometryNodeVolumeToMesh' : [("resolution_mode", ST.ENUM)], # SIMULATION 'GeometryNodeSimulationInput' : [], - 'GeometryNodeSimulationOutput' : [], # MATERIAL 'GeometryNodeReplaceMaterial' : [], - 'GeometryNodeInputMaterialIndex' : [], - 'GeometryNodeMaterialSelection' : [], - 'GeometryNodeSetMaterial' : [], - 'GeometryNodeSetMaterialIndex' : [], @@ -413,7 +367,10 @@ 'FunctionNodeRandomValue' : [("data_type", ST.ENUM)], - 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], + 'GeometryNodeRepeatInput' : [], + 'GeometryNodeRepeatOutput' : [("inspection_index", ST.INT)], + + 'GeometryNodeSwitch' : [("input_type", ST.ENUM)], # Utilities > Color 'ShaderNodeValToRGB' : [("color_ramp", ST.COLOR_RAMP)], @@ -430,11 +387,8 @@ # Utilities > Text 'GeometryNodeStringJoin' : [], - 'FunctionNodeReplaceString' : [], - 'FunctionNodeSliceString' : [], - 'FunctionNodeStringLength' : [], 'GeometryNodeStringToCurves' : [("align_x", ST.ENUM), @@ -444,7 +398,6 @@ ("pivot_mode", ST.ENUM)], 'FunctionNodeValueToString' : [], - 'FunctionNodeInputSpecialCharacters' : [], # Utilities > Vector @@ -456,7 +409,6 @@ ("rotation_type", ST.ENUM)], 'ShaderNodeCombineXYZ' : [], - 'ShaderNodeSeparateXYZ' : [], # Utilities > Field @@ -490,16 +442,33 @@ ("use_clamp", ST.BOOL)], # Utilities > Rotation - 'FunctionNodeAlignEulerToVector' : [("axis", ST.ENUM), - ("pivot_axis", ST.ENUM)], + 'FunctionNodeAlignEulerToVector' : [("axis", ST.ENUM), + ("pivot_axis", ST.ENUM)], + + 'FunctionNodeAxisAngleToRotation' : [], + 'FunctionNodeEulerToRotation' : [], + 'FunctionNodeInvertRotation' : [], - 'FunctionNodeRotateEuler' : [("space", ST.ENUM), - ("type", ST.ENUM)], + 'FunctionNodeRotateEuler' : [("space", ST.ENUM), + ("type", ST.ENUM)], + + 'FunctionNodeRotateVector' : [], + 'FunctionNodeRotationToAxisAngle' : [], + 'FunctionNodeRotationToEuler' : [], + 'FunctionNodeRotationToQuaternion' : [], + 'FunctionNodeQuaternionToRotation' : [], # MISC - 'NodeFrame' : [], - 'NodeGroupInput' : [], - 'NodeGroupOutput' : [], - 'NodeReroute' : [] + 'GeometryNodeGroup' : [("node_tree", ST.NODE_TREE)], + + 'NodeFrame' : [("label_size", ST.INT), + ("shrink", ST.BOOL), + ("text", ST.TEXT)], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + + 'NodeReroute' : [] } \ No newline at end of file diff --git a/geometry/node_tree.py b/geometry/node_tree.py index 64be218..73b3565 100644 --- a/geometry/node_tree.py +++ b/geometry/node_tree.py @@ -1,8 +1,18 @@ -from bpy.types import GeometryNodeTree, GeometryNodeSimulationInput +import bpy +from bpy.types import GeometryNodeTree + +if bpy.app.version >= (3, 6, 0): + from bpy.types import GeometryNodeSimulationInput + +if bpy.app.version > (4, 0, 0): + from bpy.types import GeometryNodeRepeatInput from ..ntp_node_tree import NTP_NodeTree class NTP_GeoNodeTree(NTP_NodeTree): def __init__(self, node_tree: GeometryNodeTree, var: str): super().__init__(node_tree, var) - self.sim_inputs: list[GeometryNodeSimulationInput] = [] + if bpy.app.version >= (3, 6, 0): + self.sim_inputs: list[GeometryNodeSimulationInput] = [] + if bpy.app.version >= (4, 0, 0): + self.repeat_inputs: list[GeometryNodeRepeatInput] = [] diff --git a/geometry/operator.py b/geometry/operator.py index 90f45a9..2f51c3a 100644 --- a/geometry/operator.py +++ b/geometry/operator.py @@ -1,9 +1,14 @@ import bpy -from bpy.types import GeometryNodeSimulationInput -from bpy.types import GeometryNodeSimulationOutput -from bpy.types import GeometryNodeTree +from bpy.types import GeometryNode, GeometryNodeTree from bpy.types import Node +if bpy.app.version >= (3, 6, 0): + from bpy.types import GeometryNodeSimulationInput + from bpy.types import GeometryNodeSimulationOutput +if bpy.app.version >= (4, 0, 0): + from bpy.types import GeometryNodeRepeatInput + from bpy.types import GeometryNodeRepeatOutput + from io import StringIO from ..ntp_operator import NTP_Operator @@ -30,133 +35,178 @@ def __init__(self): super().__init__() self._settings = geo_node_settings - def _process_sim_output_node(self, node: GeometryNodeSimulationOutput, - inner: str, node_var: str) -> None: - self._write(f"{inner}# Remove generated sim state items\n") - self._write(f"{inner}for item in {node_var}.state_items:\n") - self._write(f"{inner}\t{node_var}.state_items.remove(item)\n") - - for i, si in enumerate(node.state_items): - socket_type = enum_to_py_str(si.socket_type) - name = str_to_py_str(si.name) - self._write(f"{inner}#create SSI {name}\n") - self._write((f"{inner}{node_var}.state_items.new" - f"({socket_type}, {name})\n")) - si_var = f"{node_var}.state_items[{i}]" - attr_domain = enum_to_py_str(si.attribute_domain) - self._write((f"{inner}{si_var}.attribute_domain = {attr_domain}\n")) - - def _process_node(self, node: Node, ntp_node_tree: NTP_GeoNodeTree, - inner: str, level: int) -> None: - #create node - node_var: str = self._create_node(node, inner, ntp_node_tree.var) - self._set_settings_defaults(node, inner, node_var) - if node.bl_idname == 'GeometryNodeGroup': - self._process_group_node_tree(node, node_var, level, inner) - - elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) - ntp_node_tree.inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) - ntp_node_tree.outputs_set = True - - elif node.bl_idname == 'GeometryNodeSimulationInput': - ntp_node_tree.sim_inputs.append(node) + if bpy.app.version >= (3, 6, 0): + def _process_zone_output_node(self, node: GeometryNode) -> None: + is_sim = False + if node.bl_idname == 'GeometryNodeSimulationOutput': + items = "state_items" + is_sim = True + elif node.bl_idname == 'GeometryNodeRepeatOutput': + items = "repeat_items" + else: + self.report({'WARNING'}, f"NodeToPython: {node.bl_idname} is " + f"not recognized as a valid zone output") + + node_var = self._node_vars[node] + + self._write(f"# Remove generated {items}") + self._write(f"for item in {node_var}.{items}:") + self._write(f"\t{node_var}.{items}.remove(item)") + + for i, item in enumerate(getattr(node, items)): + socket_type = enum_to_py_str(item.socket_type) + name = str_to_py_str(item.name) + self._write(f"# Create item {name}") + self._write(f"{node_var}.{items}.new" + f"({socket_type}, {name})") + if is_sim: + item_var = f"{node_var}.{items}[{i}]" + ad = enum_to_py_str(item.attribute_domain) + self._write(f"{item_var}.attribute_domain = {ad}") + + def _process_node(self, node: Node, ntp_nt: NTP_GeoNodeTree) -> None: + """ + Create node and set settings, defaults, and cosmetics - elif node.bl_idname == 'GeometryNodeSimulationOutput': - self._process_sim_output_node(node, inner, node_var) - - self._hide_hidden_sockets(node, inner, node_var) + Parameters: + node (Node): node to process + ntp_nt (NTP_NodeTree): the node tree that node belongs to + """ + node_var: str = self._create_node(node, ntp_nt.var) + self._set_settings_defaults(node) - if node.bl_idname != 'GeometryNodeSimulationInput': - self._set_socket_defaults(node, node_var, inner) + if bpy.app.version < (4, 0, 0): + if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: + self._group_io_settings(node, "input", ntp_nt) + ntp_nt.inputs_set = True + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: + self._group_io_settings(node, "output", ntp_nt) + ntp_nt.outputs_set = True - def _process_sim_zones(self, sim_inputs: list[GeometryNodeSimulationInput], - inner: str) -> None: - """ - Recreate simulation zones - sim_inputs (list[GeometryNodeSimulationInput]): list of - simulation input nodes - inner (str): identation string - """ - for sim_input in sim_inputs: - sim_output = sim_input.paired_output - - sim_input_var = self._node_vars[sim_input] - sim_output_var = self._node_vars[sim_output] - self._write((f"{inner}{sim_input_var}.pair_with_output" - f"({sim_output_var})\n")) + if node.bl_idname == 'GeometryNodeSimulationInput': + ntp_nt.sim_inputs.append(node) - #must set defaults after paired with output - self._set_socket_defaults(sim_input, sim_input_var, inner) - self._set_socket_defaults(sim_output, sim_output_var, inner) + elif node.bl_idname == 'GeometryNodeSimulationOutput': + self._process_zone_output_node(node) + elif node.bl_idname == 'GeometryNodeRepeatInput': + ntp_nt.repeat_inputs.append(node) + + elif node.bl_idname == 'GeometryNodeRepeatOutput': + self._process_zone_output_node(node) + + self._hide_hidden_sockets(node) + + if node.bl_idname not in {'GeometryNodeSimulationInput', + 'GeometryNodeRepeatInput'}: + self._set_socket_defaults(node) + + if bpy.app.version >= (3, 6, 0): + def _process_zones(self, zone_inputs: list[GeometryNode]) -> None: + """ + Recreates a zone + zone_inputs (list[GeometryNodeSimulationInput]): list of + simulation input nodes + """ + for zone_input in zone_inputs: + zone_output = zone_input.paired_output + + zone_input_var = self._node_vars[zone_input] + zone_output_var = self._node_vars[zone_output] + + self._write(f"#Process zone input {zone_input.name}") + self._write(f"{zone_input_var}.pair_with_output" + f"({zone_output_var})") + + #must set defaults after paired with output + self._set_socket_defaults(zone_input) + self._set_socket_defaults(zone_output) + self._write("") + + if bpy.app.version >= (4, 0, 0): + def _set_geo_tree_properties(self, node_tree: GeometryNodeTree) -> None: + is_mod = node_tree.is_modifier + is_tool = node_tree.is_tool + + nt_var = self._node_tree_vars[node_tree] + + if is_mod: + self._write(f"{nt_var}.is_modifier = True") + if is_tool: + self._write(f"{nt_var}.is_tool = True") + + tool_flags = ["is_mode_edit", + "is_mode_sculpt", + "is_type_curve", + "is_type_mesh", + "is_type_point_cloud"] + + for flag in tool_flags: + self._write(f"{nt_var}.{flag} = {getattr(node_tree, flag)}") + self._write("") - def _process_node_tree(self, node_tree: GeometryNodeTree, - level: int) -> None: + def _process_node_tree(self, node_tree: GeometryNodeTree) -> None: """ Generates a Python function to recreate a node tree Parameters: node_tree (GeometryNodeTree): geometry node tree to be recreated - level (int): number of tabs to use for each line, used with - node groups within node groups and script/add-on differences """ - nt_var = self._create_var(node_tree.name) - outer, inner = make_indents(level) #TODO: put in NTP_NodeTree class? - # Eventually these should go away anyways, and level of indentation depends just on the mode + nt_var = self._create_var(node_tree.name) + self._node_tree_vars[node_tree] = nt_var #initialize node group - self._write(f"{outer}#initialize {nt_var} node group\n") - self._write(f"{outer}def {nt_var}_node_group():\n") - self._write((f"{inner}{nt_var} = bpy.data.node_groups.new(" - f"type = \'GeometryNodeTree\', " - f"name = {str_to_py_str(node_tree.name)})\n")) - self._write("\n") + self._write(f"#initialize {nt_var} node group", self._outer) + self._write(f"def {nt_var}_node_group():", self._outer) + self._write(f"{nt_var} = bpy.data.node_groups.new(" + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n") if bpy.app.version >= (4, 0, 0): - self._write(f"{inner}{nt_var}.is_modifier = {node_tree.is_modifier}\n") - self._write(f"{inner}{nt_var}.is_tool = {node_tree.is_tool}\n") - + self._set_geo_tree_properties(node_tree) + #initialize nodes - self._write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"#initialize {nt_var} nodes") ntp_nt = NTP_GeoNodeTree(node_tree, nt_var) + if bpy.app.version >= (4, 0, 0): + self._tree_interface_settings(ntp_nt) + for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) + 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) - self._process_sim_zones(ntp_nt.sim_inputs, inner) - #set look of nodes - self._set_parents(node_tree, inner) - self._set_locations(node_tree, inner) - self._set_dimensions(node_tree, inner) + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) #create connections - self._init_links(node_tree, inner, nt_var) + self._init_links(node_tree) - self._write(f"{inner}return {nt_var}\n") + self._write(f"return {nt_var}\n") #create node group - self._write(f"\n{outer}{nt_var} = {nt_var}_node_group()\n\n") - return self._used_vars + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) def _apply_modifier(self, nt: GeometryNodeTree, nt_var: str): #get object - self._write(f"\t\tname = bpy.context.object.name\n") - self._write(f"\t\tobj = bpy.data.objects[name]\n") + self._write(f"name = bpy.context.object.name", self._outer) + self._write(f"obj = bpy.data.objects[name]", self._outer) #set modifier to the one we just created mod_name = str_to_py_str(nt.name) - self._write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')\n")) - self._write(f"\t\tmod.node_group = {nt_var}\n") + self._write(f"mod = obj.modifiers.new(name = {mod_name}, " + f"type = 'NODES')", self._outer) + self._write(f"mod.node_group = {nt_var}", self._outer) def execute(self, context): @@ -167,6 +217,9 @@ def execute(self, context): nt_var = clean_string(nt.name) if self.mode == 'ADDON': + self._outer = "\t\t" + self._inner = "\t\t\t" + self._setup_addon_directories(context, nt_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") @@ -174,19 +227,18 @@ def execute(self, context): self._create_header(nt.name) self._class_name = clean_string(nt.name, lower = False) self._init_operator(nt_var, nt.name) - self._write("\tdef execute(self, context):\n") + self._write("def execute(self, context):", "\t") else: self._file = StringIO("") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - self._process_node_tree(nt, level) + + node_trees_to_process = self._topological_sort(nt) + + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree) if self.mode == 'ADDON': self._apply_modifier(nt, nt_var) - self._write("\t\treturn {'FINISHED'}\n\n") + self._write("return {'FINISHED'}\n", self._outer) self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/material/node_settings.py b/material/node_settings.py index c6b339e..04ae778 100644 --- a/material/node_settings.py +++ b/material/node_settings.py @@ -16,21 +16,13 @@ 'ShaderNodeVertexColor' : [("layer_name", ST.STRING)], #TODO: separate color attribute type? 'ShaderNodeHairInfo' : [], - 'ShaderNodeFresnel' : [], - 'ShaderNodeNewGeometry' : [], - 'ShaderNodeLayerWeight' : [], - 'ShaderNodeLightPath' : [], - 'ShaderNodeObjectInfo' : [], - 'ShaderNodeParticleInfo' : [], - 'ShaderNodePointInfo' : [], - 'ShaderNodeRGB' : [], 'ShaderNodeTangent' : [("axis", ST.ENUM), @@ -46,7 +38,6 @@ ("uv_map", ST.STRING)], #TODO: see ShaderNodeTangent 'ShaderNodeValue' : [], - 'ShaderNodeVolumeInfo' : [], 'ShaderNodeWireframe' : [("use_pixel_size", ST.BOOL)], @@ -77,9 +68,7 @@ 'ShaderNodeBsdfAnisotropic' : [("distribution", ST.ENUM)], 'ShaderNodeBackground' : [], - 'ShaderNodeBsdfDiffuse' : [], - 'ShaderNodeEmission' : [], 'ShaderNodeBsdfGlass' : [("distribution", ST.ENUM)], @@ -89,18 +78,20 @@ 'ShaderNodeBsdfHair' : [("component", ST.ENUM)], 'ShaderNodeHoldout' : [], - 'ShaderNodeMixShader' : [], 'ShaderNodeBsdfPrincipled' : [("distribution", ST.ENUM), ("subsurface_method", ST.ENUM)], - 'ShaderNodeBsdfHairPrincipled' : [("parametrization", ST.ENUM)], + 'ShaderNodeBsdfHairPrincipled' : [("model", ST.ENUM), + ("parametrization", ST.ENUM)], 'ShaderNodeVolumePrincipled' : [], 'ShaderNodeBsdfRefraction' : [("distribution", ST.ENUM)], + 'ShaderNodeBsdfSheen' : [("distribution", ST.ENUM)], + 'ShaderNodeEeveeSpecular' : [], 'ShaderNodeSubsurfaceScattering' : [("falloff", ST.ENUM)], @@ -108,13 +99,9 @@ 'ShaderNodeBsdfToon' : [("component", ST.ENUM)], 'ShaderNodeBsdfTranslucent' : [], - 'ShaderNodeBsdfTransparent' : [], - 'ShaderNodeBsdfVelvet' : [], - 'ShaderNodeVolumeAbsorption' : [], - 'ShaderNodeVolumeScatter' : [], @@ -149,7 +136,8 @@ 'ShaderNodeTexMusgrave' : [("musgrave_dimensions", ST.ENUM), ("musgrave_type", ST.ENUM)], - 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM)], + 'ShaderNodeTexNoise' : [("noise_dimensions", ST.ENUM), + ("normalize", ST.BOOL)], 'ShaderNodeTexPointDensity' : [("interpolation", ST.ENUM), ("object", ST.OBJECT), @@ -178,6 +166,7 @@ 'ShaderNodeTexVoronoi' : [("distance", ST.ENUM), ("feature", ST.ENUM), + ("normalize", ST.BOOL), ("voronoi_dimensions", ST.ENUM)], 'ShaderNodeTexWave' : [("bands_direction", ST.ENUM), @@ -274,8 +263,15 @@ ("use_auto_update", ST.BOOL)], # MISC - 'NodeFrame' : [], - 'NodeGroupInput' : [], - 'NodeGroupOutput' : [], - 'NodeReroute' : [] + 'ShaderNodeGroup' : [('node_tree', ST.NODE_TREE)], + + 'NodeFrame' : [("label_size", ST.INT), + ("shrink", ST.BOOL), + ("text", ST.TEXT)], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [("is_active_output", ST.BOOL)], + + 'NodeReroute' : [] } diff --git a/material/operator.py b/material/operator.py index cc84f24..97bc342 100644 --- a/material/operator.py +++ b/material/operator.py @@ -24,45 +24,58 @@ def __init__(self): self._settings = shader_node_settings def _create_material(self, indent: str): - self._write((f"{indent}{MAT_VAR} = bpy.data.materials.new(" - f"name = {str_to_py_str(self.material_name)})\n")) - self._write(f"{indent}{MAT_VAR}.use_nodes = True\n") - - def _initialize_shader_node_tree(self, outer, nt_var, level, inner, nt_name): - #initialize node group - self._write(f"{outer}#initialize {nt_var} node group\n") - self._write(f"{outer}def {nt_var}_node_group():\n") - - if self._is_outermost_node_group(level): - self._write(f"{inner}{nt_var} = {MAT_VAR}.node_tree\n") - self._write(f"{inner}#start with a clean node tree\n") - self._write(f"{inner}for node in {nt_var}.nodes:\n") - self._write(f"{inner}\t{nt_var}.nodes.remove(node)\n") + self._write(f"{MAT_VAR} = bpy.data.materials.new(" + f"name = {str_to_py_str(self.material_name)})", indent) + self._write(f"{MAT_VAR}.use_nodes = True", indent) + + def _initialize_shader_node_tree(self, ntp_node_tree: NTP_NodeTree, + nt_name: str) -> None: + """ + Initialize the shader node group + + Parameters: + ntp_node_tree (NTP_NodeTree): node tree to be generated and + variable to use + nt_name (str): name to use for the node tree + """ + self._write(f"#initialize {nt_name} node group", self._outer) + self._write(f"def {ntp_node_tree.var}_node_group():\n", self._outer) + + 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)") else: - self._write((f"{inner}{nt_var} = bpy.data.node_groups.new(" + self._write((f"{ntp_node_tree.var} = bpy.data.node_groups.new(" f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - self._write("\n") - - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, level: int) -> None: - #create node - node_var: str = self._create_node(node, inner, ntp_node_tree.var) - self._set_settings_defaults(node, inner, node_var) - - if node.bl_idname == 'ShaderNodeGroup': - self._process_group_node_tree(node, node_var, level, inner) - elif node.bl_idname == 'NodeGroupInput' and not ntp_node_tree.inputs_set: - self._group_io_settings(node, inner, "input", ntp_node_tree) - ntp_node_tree.inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not ntp_node_tree.outputs_set: - self._group_io_settings(node, inner, "output", ntp_node_tree) - ntp_node_tree.outputs_set = True - - self._hide_hidden_sockets(node, inner, node_var) - self._set_socket_defaults(node, node_var, inner) - - def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: + f"name = {str_to_py_str(nt_name)})")) + self._write("") + + def _process_node(self, node: Node, ntp_nt: NTP_NodeTree) -> None: + """ + Create node and set settings, defaults, and cosmetics + + Parameters: + node (Node): node to process + ntp_nt (NTP_NodeTree): the node tree that node belongs to + """ + node_var: str = self._create_node(node, ntp_nt.var) + self._set_settings_defaults(node) + + if bpy.app.version < (4, 0, 0): + if node.bl_idname == 'NodeGroupInput' and not ntp_nt.inputs_set: + self._group_io_settings(node, "input", ntp_nt) + ntp_nt.inputs_set = True + + elif node.bl_idname == 'NodeGroupOutput' and not ntp_nt.outputs_set: + self._group_io_settings(node, "output", ntp_nt) + ntp_nt.outputs_set = True + + self._hide_hidden_sockets(node) + self._set_socket_defaults(node) + + def _process_node_tree(self, node_tree: ShaderNodeTree) -> None: """ Generates a Python function to recreate a node tree @@ -72,39 +85,43 @@ def _process_node_tree(self, node_tree: ShaderNodeTree, level: int) -> None: node groups within node groups and script/add-on differences """ - if self._is_outermost_node_group(level): + if node_tree == self._base_node_tree: nt_var = self._create_var(self.material_name) nt_name = self.material_name #TODO: this is probably overcomplicating things if we move to a harder material vs shader node tree difference else: nt_var = self._create_var(node_tree.name) nt_name = node_tree.name - outer, inner = make_indents(level) - - self._initialize_shader_node_tree(outer, nt_var, level, inner, nt_name) + self._node_tree_vars[node_tree] = nt_var ntp_nt = NTP_NodeTree(node_tree, nt_var) + self._initialize_shader_node_tree(ntp_nt, nt_name) + + if bpy.app.version >= (4, 0, 0): + self._tree_interface_settings(ntp_nt) + #initialize nodes - self._write(f"{inner}#initialize {nt_var} nodes\n") + self._write(f"#initialize {nt_var} nodes") for node in node_tree.nodes: - self._process_node(node, ntp_nt, inner, level) + self._process_node(node, ntp_nt) - self._set_parents(node_tree, inner) - self._set_locations(node_tree, inner) - self._set_dimensions(node_tree, inner) + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) - self._init_links(node_tree, inner, nt_var) + self._init_links(node_tree) - self._write(f"{inner}return {nt_var}\n") + self._write(f"return {nt_var}\n") - self._write(f"\n{outer}{nt_var}_node_group()\n\n") + self._write(f"{nt_var} = {nt_var}_node_group()\n", self._outer) + def execute(self, context): #find node group to replicate - nt = bpy.data.materials[self.material_name].node_tree - if nt is None: + self._base_node_tree = bpy.data.materials[self.material_name].node_tree + if self._base_node_tree is None: self.report({'ERROR'}, ("NodeToPython: This doesn't seem to be a " "valid material. Is Use Nodes selected?")) return {'CANCELLED'} @@ -113,6 +130,9 @@ def execute(self, context): mat_var = clean_string(self.material_name) if self.mode == 'ADDON': + self._outer = "\t\t" + self._inner = "\t\t\t" + self._setup_addon_directories(context, mat_var) self._file = open(f"{self._addon_dir}/__init__.py", "w") @@ -121,24 +141,22 @@ def execute(self, context): self._class_name = clean_string(self.material_name, lower=False) self._init_operator(mat_var, self.material_name) - self._write("\tdef execute(self, context):\n") + self._write("def execute(self, context):", "\t") else: self._file = StringIO("") if self.mode == 'ADDON': self._create_material("\t\t") elif self.mode == 'SCRIPT': - self._create_material("") - + self._create_material("") + + node_trees_to_process = self._topological_sort(self._base_node_tree) - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - self._process_node_tree(nt, level) + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree) if self.mode == 'ADDON': - self._write("\t\treturn {'FINISHED'}\n\n") + self._write("return {'FINISHED'}", self._outer) self._create_menu_func() self._create_register_func() self._create_unregister_func() diff --git a/ntp_node_tree.py b/ntp_node_tree.py index 5469ba1..a551d01 100644 --- a/ntp_node_tree.py +++ b/ntp_node_tree.py @@ -1,4 +1,5 @@ from bpy.types import NodeTree +import bpy class NTP_NodeTree: def __init__(self, node_tree: NodeTree, var: str): @@ -8,7 +9,8 @@ def __init__(self, node_tree: NodeTree, var: str): # The variable named for the regenerated node tree self.var: str = var - # Keep track of if we need to set the default values for the node - # tree inputs and outputs - self.inputs_set: bool = False - self.outputs_set: bool = False \ No newline at end of file + if bpy.app.version < (4, 0, 0): + # Keep track of if we need to set the default values for the node + # tree inputs and outputs + self.inputs_set: bool = False + self.outputs_set: bool = False \ No newline at end of file diff --git a/ntp_operator.py b/ntp_operator.py index e7da207..ab46e40 100644 --- a/ntp_operator.py +++ b/ntp_operator.py @@ -1,12 +1,17 @@ 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 +else: + from bpy.types import NodeTreeInterfacePanel, NodeTreeInterfaceSocket + from bpy.types import NodeTreeInterfaceItem import os from typing import TextIO +import shutil from .ntp_node_tree import NTP_NodeTree from .utils import * @@ -29,7 +34,7 @@ class NTP_Operator(Operator): ] ) - #node tree input sockets that have default properties + # node tree input sockets that have default properties if bpy.app.version < (4, 0, 0): default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} else: @@ -61,8 +66,16 @@ def __init__(self): # Class named for the generated operator self._class_name: str = None - # Set to keep track of already created node trees - self._node_trees: set[NodeTree] = set() + # Indentation to use for the default write function + self._outer: str = "" + self._inner: str = "\t" + + + # Base node tree we're converting + self._base_node_tree: NodeTree = None + + # Dictionary to keep track of node tree->variable name pairs + self._node_tree_vars: dict[NodeTree, str] = {} # Dictionary to keep track of node->variable name pairs self._node_vars: dict[Node, str] = {} @@ -73,8 +86,10 @@ def __init__(self): # Dictionary used for setting node properties self._settings: dict[str, list[(str, ST)]] = {} - def _write(self, string: str): - self._file.write(string) + def _write(self, string: str, indent: str = None): + if indent is None: + indent = self._inner + self._file.write(f"{indent}{string}\n") def _setup_addon_directories(self, context: Context, nt_var: str) -> None: """ @@ -103,19 +118,17 @@ def _create_header(self, name: str) -> None: name (str): name of the add-on """ - self._write("bl_info = {\n") - self._write(f"\t\"name\" : \"{name}\",\n") - self._write("\t\"author\" : \"Node To Python\",\n") - self._write("\t\"version\" : (1, 0, 0),\n") - self._write(f"\t\"blender\" : {bpy.app.version},\n") - self._write("\t\"location\" : \"Object\",\n") # TODO - self._write("\t\"category\" : \"Node\"\n") - self._write("}\n") - self._write("\n") - self._write("import bpy\n") - self._write("import mathutils\n") - self._write("import os\n") - self._write("\n") + self._write("bl_info = {", "") + self._write(f"\t\"name\" : \"{name}\",", "") + self._write("\t\"author\" : \"Node To Python\",", "") + self._write("\t\"version\" : (1, 0, 0),", "") + self._write(f"\t\"blender\" : {bpy.app.version},", "") + self._write("\t\"location\" : \"Object\",", "") # TODO + self._write("\t\"category\" : \"Node\"", "") + self._write("}\n", "") + self._write("import bpy", "") + self._write("import mathutils", "") + self._write("import os\n", "") def _init_operator(self, idname: str, label: str) -> None: """ @@ -127,31 +140,51 @@ def _init_operator(self, idname: str, label: str) -> None: idname (str): name for the operator label (str): appearence inside Blender """ - self._write(f"class {self._class_name}(bpy.types.Operator):\n") - self._write(f"\tbl_idname = \"object.{idname}\"\n") - self._write(f"\tbl_label = \"{label}\"\n") - self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - self._write("\n") - - def _is_outermost_node_group(self, level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - - def _process_group_node_tree(self, node: Node, node_var: str, level: int, - inner: str) -> None: + self._write(f"class {self._class_name}(bpy.types.Operator):", "") + self._write(f"\tbl_idname = \"object.{idname}\"", "") + self._write(f"\tbl_label = \"{label}\"", "") + self._write("\tbl_options = {\'REGISTER\', \'UNDO\'}", "") + self._write("") + + def _topological_sort(self, node_tree: NodeTree) -> list[NodeTree]: """ - Processes node tree of group node if one is present + Perform a topological sort on the node graph to determine dependencies + and which node groups need processed first + + Parameters: + node_tree (NodeTree): the base node tree to convert + + Returns: + (list[NodeTree]): the node trees in order of processing """ - node_tree = node.node_tree - if node_tree is not None: - if node_tree not in self._node_trees: - self._process_node_tree(node_tree, level + 1) - self._node_trees.add(node_tree) - self._write((f"{inner}{node_var}.node_tree = bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) + if isinstance(node_tree, bpy.types.CompositorNodeTree): + group_node_type = 'CompositorNodeGroup' + elif isinstance(node_tree, bpy.types.GeometryNodeTree): + group_node_type = 'GeometryNodeGroup' + elif isinstance(node_tree, bpy.types.ShaderNodeTree): + group_node_type = 'ShaderNodeGroup' + + visited = set() + result: list[NodeTree] = [] + + def dfs(nt: NodeTree) -> None: + """ + Helper function to perform depth-first search on a NodeTree + + Parameters: + nt (NodeTree): current node tree in the dependency graph + """ + if nt not in visited: + visited.add(nt) + for group_node in [node for node in nt.nodes + if node.bl_idname == group_node_type]: + if group_node.node_tree not in visited: + dfs(group_node.node_tree) + result.append(nt) + + dfs(node_tree) + + return result def _create_var(self, name: str) -> str: """ @@ -159,7 +192,8 @@ def _create_var(self, name: str) -> str: Parameters: name (str): basic string we'd like to create the variable name out of - used_vars (dict[str, int]): dictionary containing variable names and usage counts + used_vars (dict[str, int]): dictionary containing variable names and + usage counts Returns: clean_name (str): variable name for the node tree @@ -175,118 +209,124 @@ def _create_var(self, name: str) -> str: self._used_vars[var] = 0 return clean_name - def _create_node(self, node: Node, inner: str, node_tree_var: str) -> str: + def _create_node(self, node: Node, node_tree_var: str) -> str: """ Initializes a new node with location, dimension, and label info Parameters: node (bpy.types.Node): node to be copied - inner (str): indentation level for this logic node_tree_var (str): variable name for the node tree Returns: node_var (str): variable name for the node """ - self._write(f"{inner}#node {node.name}\n") + self._write(f"#node {node.name}") node_var = self._create_var(node.name) self._node_vars[node] = node_var - self._write((f"{inner}{node_var} " - f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) + idname = str_to_py_str(node.bl_idname) + self._write(f"{node_var} = {node_tree_var}.nodes.new({idname})") + # label if node.label: - self._write(f"{inner}{node_var}.label = \"{node.label}\"\n") + self._write(f"{node_var}.label = {str_to_py_str(node.label)}") # name - self._write(f"{inner}{node_var}.name = \"{node.name}\"\n") + self._write(f"{node_var}.name = {str_to_py_str(node.name)}") # color if node.use_custom_color: - self._write(f"{inner}{node_var}.use_custom_color = True\n") - self._write( - f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") + self._write(f"{node_var}.use_custom_color = True") + self._write(f"{node_var}.color = {vec3_to_py_str(node.color)}") # mute if node.mute: - self._write(f"{inner}{node_var}.mute = True\n") + self._write(f"{node_var}.mute = True") return node_var - def _set_settings_defaults(self, node: Node, inner: str, node_var: str - ) -> None: + def _set_settings_defaults(self, node: Node) -> None: """ Sets the defaults for any settings a node may have Parameters: node (bpy.types.Node): the node object we're copying settings from - inner (str): indentation node_var (str): name of the variable we're using for the node in our add-on """ if node.bl_idname not in self._settings: - print((f"NodeToPython: couldn't find {node.bl_idname} in settings." - f"Your Blender version may not be supported")) + self.report({'WARNING'}, + (f"NodeToPython: couldn't find {node.bl_idname} in " + f"settings. Your Blender version may not be supported")) return + node_var = self._node_vars[node] + for (attr_name, type) in self._settings[node.bl_idname]: + if not hasattr(node, attr_name): + self.report({'WARNING'}, + f"NodeToPython: Couldn't find attribute " + f"\"{attr_name}\" for node {node.name} of type " + f"{node.bl_idname}") + continue attr = getattr(node, attr_name, None) if attr is None: - print(f"\"{node_var}.{attr_name}\" not found") continue - setting_str = f"{inner}{node_var}.{attr_name}" + + setting_str = f"{node_var}.{attr_name}" if type == ST.ENUM: if attr != '': - self._write(f"{setting_str} = {enum_to_py_str(attr)}\n") + self._write(f"{setting_str} = {enum_to_py_str(attr)}") elif type == ST.ENUM_SET: - self._write(f"{setting_str} = {attr}\n") + self._write(f"{setting_str} = {attr}") elif type == ST.STRING: - self._write(f"{setting_str} = {str_to_py_str(attr)}\n") + self._write(f"{setting_str} = {str_to_py_str(attr)}") elif type == ST.BOOL or type == ST.INT or type == ST.FLOAT: - self._write(f"{setting_str} = {attr}\n") + self._write(f"{setting_str} = {attr}") elif type == ST.VEC1: - self._write(f"{setting_str} = {vec1_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec1_to_py_str(attr)}") elif type == ST.VEC2: - self._write(f"{setting_str} = {vec2_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec2_to_py_str(attr)}") elif type == ST.VEC3: - self._write(f"{setting_str} = {vec3_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec3_to_py_str(attr)}") elif type == ST.VEC4: - self._write(f"{setting_str} = {vec4_to_py_str(attr)}\n") + self._write(f"{setting_str} = {vec4_to_py_str(attr)}") elif type == ST.COLOR: - self._write(f"{setting_str} = {color_to_py_str(attr)}\n") + self._write(f"{setting_str} = {color_to_py_str(attr)}") elif type == ST.MATERIAL: name = str_to_py_str(attr.name) - self._write((f"{inner}if {name} in bpy.data.materials:\n")) - self._write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.materials[{name}]\n")) + self._write((f"if {name} in bpy.data.materials:")) + self._write((f"\t{node_var}.{attr_name} = " + f"bpy.data.materials[{name}]")) elif type == ST.OBJECT: name = str_to_py_str(attr.name) - self._write((f"{inner}if {name} in bpy.data.objects:\n")) - self._write((f"{inner}\t{node_var}.{attr_name} = " - f"bpy.data.objects[{name}]\n")) + self._write((f"if {name} in bpy.data.objects:")) + self._write((f"\t{node_var}.{attr_name} = " + f"bpy.data.objects[{name}]")) elif type == ST.COLOR_RAMP: - self._color_ramp_settings(node, inner, node_var, attr_name) + self._color_ramp_settings(node, attr_name) elif type == ST.CURVE_MAPPING: - self._curve_mapping_settings(node, inner, node_var, attr_name) + self._curve_mapping_settings(node, attr_name) + elif type == ST.NODE_TREE: + self._node_tree_settings(node, attr_name) elif type == ST.IMAGE: if self._addon_dir is not None and attr is not None: if attr.source in {'FILE', 'GENERATED', 'TILED'}: self._save_image(attr) - self._load_image( - attr, inner, f"{node_var}.{attr_name}") + self._load_image(attr, f"{node_var}.{attr_name}") elif type == ST.IMAGE_USER: - self._image_user_settings( - attr, inner, f"{node_var}.{attr_name}") + self._image_user_settings(attr, f"{node_var}.{attr_name}") if bpy.app.version < (4, 0, 0): - def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, - inner: str, socket_var: str) -> None: + def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, + socket_var: str) -> None: """ Set a node group input/output's default properties if they exist + Helper function to _group_io_settings() Parameters: socket_interface (NodeSocketInterface): socket interface associated with the input/output - inner (str): indentation string socket_var (str): variable name for the socket """ if socket_interface.type not in self.default_sockets_v3: @@ -298,26 +338,25 @@ def _set_group_socket_default_v3(self, socket_interface: NodeSocketInterface, dv = vec3_to_py_str(socket_interface.default_value) else: dv = socket_interface.default_value - self._write(f"{inner}{socket_var}.default_value = {dv}\n") + self._write(f"{socket_var}.default_value = {dv}") # min value if hasattr(socket_interface, "min_value"): min_val = socket_interface.min_value - self._write(f"{inner}{socket_var}.min_value = {min_val}\n") + self._write(f"{socket_var}.min_value = {min_val}") # max value if hasattr(socket_interface, "min_value"): max_val = socket_interface.max_value - self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) + self._write(f"{socket_var}.max_value = {max_val}") - def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + def _group_io_settings(self, node: Node, + io: str, # TODO: convert to enum + ntp_node_tree: NTP_NodeTree) -> None: """ Set the settings for group input and output sockets Parameters: node (bpy.types.Node) : group input/output node - inner (str): indentation string io (str): whether we're generating the input or output settings node_tree_var (str): variable name of the generated node tree node_tree (bpy.types.NodeTree): node tree that we're generating @@ -333,63 +372,58 @@ def _group_io_settings_v3(self, node: bpy.types.Node, inner: str, io_sockets = node.inputs io_socket_interfaces = node_tree.outputs - self._write(f"{inner}#{node_tree_var} {io}s\n") + self._write(f"#{node_tree_var} {io}s") for i, inout in enumerate(io_sockets): if inout.bl_idname == 'NodeSocketVirtual': continue - self._write(f"{inner}#{io} {inout.name}\n") + self._write(f"#{io} {inout.name}") idname = enum_to_py_str(inout.bl_idname) name = str_to_py_str(inout.name) - self._write( - f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") + self._write(f"{node_tree_var}.{io}s.new({idname}, {name})") socket_interface = io_socket_interfaces[i] socket_var = f"{node_tree_var}.{io}s[{i}]" - self._set_group_socket_default_v3(socket_interface, inner, - socket_var) + self._set_group_socket_defaults(socket_interface, socket_var) # default attribute name if hasattr(socket_interface, "default_attribute_name"): if socket_interface.default_attribute_name != "": - dan = str_to_py_str( - socket_interface.default_attribute_name) - self._write((f"{inner}{socket_var}" - f".default_attribute_name = {dan}\n")) + dan = str_to_py_str(socket_interface.default_attribute_name) + self._write(f"{socket_var}.default_attribute_name = {dan}") # attribute domain if hasattr(socket_interface, "attribute_domain"): ad = enum_to_py_str(socket_interface.attribute_domain) - self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + self._write(f"{socket_var}.attribute_domain = {ad}") # tooltip if socket_interface.description != "": description = str_to_py_str(socket_interface.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) + self._write(f"{socket_var}.description = {description}") # hide_value if socket_interface.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") + self._write(f"{socket_var}.hide_value = True") # hide in modifier if hasattr(socket_interface, "hide_in_modifier"): if socket_interface.hide_in_modifier is True: - self._write( - f"{inner}{socket_var}.hide_in_modifier = True\n") + self._write(f"{socket_var}.hide_in_modifier = True") + + self._write("") + self._write("") - self._write("\n") - self._write("\n") - elif bpy.app.version >= (4, 0, 0): - def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInterfaceSocket, - inner: str, socket_var: str) -> None: + def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, + socket_var: str) -> None: """ - Set a node group input/output's default properties if they exist + Set a node tree input/output's default properties if they exist + + Helper function to _create_socket() Parameters: socket_interface (NodeTreeInterfaceSocket): socket interface associated with the input/output - inner (str): indentation string socket_var (str): variable name for the socket """ if type(socket_interface) in self.nondefault_sockets_v4: @@ -401,160 +435,225 @@ def _set_group_socket_default_v4(self, socket_interface: bpy.types.NodeTreeInter dv = vec4_to_py_str(dv) elif type(dv) in {mathutils.Vector, mathutils.Euler}: dv = vec3_to_py_str(dv) + elif type(dv) == bpy_types.bpy_prop_array: + dv = array_to_py_str(dv) elif type(dv) == str: dv = str_to_py_str(dv) - self._write(f"{inner}{socket_var}.default_value = {dv}\n") + self._write(f"{socket_var}.default_value = {dv}") # min value if hasattr(socket_interface, "min_value"): min_val = socket_interface.min_value - self._write(f"{inner}{socket_var}.min_value = {min_val}\n") + self._write(f"{socket_var}.min_value = {min_val}") # max value if hasattr(socket_interface, "min_value"): max_val = socket_interface.max_value - self._write((f"{inner}{socket_var}.max_value = {max_val}\n")) + self._write(f"{socket_var}.max_value = {max_val}") - def _group_io_settings_v4(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: + def _create_socket(self, socket: NodeTreeInterfaceSocket, + parent: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel, str], + ntp_nt: NTP_NodeTree) -> None: """ - Set the settings for group input and output sockets + Initialize a new tree socket + + Helper function to _process_items() Parameters: - node (bpy.types.Node) : group input/output node - inner (str): indentation string - io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating - input and output settings for + socket (NodeTreeInterfaceSocket): the socket to recreate + parent (NodeTreeInterfacePanel): parent panel of the socket + (possibly None) + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + ntp_nt (NTP_NodeTree): owner of the socket """ - node_tree_var = ntp_node_tree.var - node_tree = ntp_node_tree.node_tree - if io == "input": - io_sockets = node.outputs # Might be removeable, - # think we can get all the info from the inouts - # from the socket interfaces, need to double check. - # If so, then we can just run these at the initialization - # of the node tree, meaning we can clean up the clunky - # Group Input/Group Output node reliance, two calls - # Should be pretty easy to add in panels afterwards, - # looks like those are tied fairly close to the new socket - # system - items_tree = node_tree.interface.items_tree - io_socket_interfaces = [item for item in items_tree - if item.item_type == 'SOCKET' - and item.in_out == 'INPUT'] + self._write(f"#Socket {socket.name}") + # initialization + socket_var = self._create_var(socket.name + "_socket") + name = str_to_py_str(socket.name) + in_out_enum = enum_to_py_str(socket.in_out) + + socket_type = enum_to_py_str(socket.bl_socket_idname) + """ + I might be missing something, but the Python API's set up a bit + weird here now. The new socket initialization only accepts types + from a list of basic ones, but there doesn't seem to be a way of + retrieving just this basic type without the subtype information. + """ + if 'Float' in socket_type: + socket_type = enum_to_py_str('NodeSocketFloat') + elif 'Int' in socket_type: + socket_type = enum_to_py_str('NodeSocketInt') + elif 'Vector' in socket_type: + socket_type = enum_to_py_str('NodeSocketVector') + + if parent is None: + optional_parent_str = "" else: - io_sockets = node.inputs - items_tree = node_tree.interface.items_tree - io_socket_interfaces = [item for item in items_tree - if item.item_type == 'SOCKET' - and item.in_out == 'OUTPUT'] - - self._write(f"{inner}#{node_tree_var} {io}s\n") - for i, socket_interface in enumerate(io_socket_interfaces): - self._write(f"{inner}#{io} {socket_interface.name}\n") + optional_parent_str = f", parent = {panel_dict[parent]}" + + self._write(f"{socket_var} = " + f"{ntp_nt.var}.interface.new_socket(" + f"name = {name}, in_out={in_out_enum}, " + f"socket_type = {socket_type}" + f"{optional_parent_str})") + + # subtype + if hasattr(socket, "subtype"): + subtype = enum_to_py_str(socket.subtype) + self._write(f"{socket_var}.subtype = {subtype}") + + self._set_tree_socket_defaults(socket, socket_var) + + # default attribute name + if socket.default_attribute_name != "": + dan = str_to_py_str( + socket.default_attribute_name) + self._write(f"{socket_var}.default_attribute_name = {dan}") + + # attribute domain + ad = enum_to_py_str(socket.attribute_domain) + self._write(f"{socket_var}.attribute_domain = {ad}") + + # hide_value + if socket.hide_value is True: + self._write(f"{socket_var}.hide_value = True") + + # hide in modifier + if socket.hide_in_modifier is True: + self._write(f"{socket_var}.hide_in_modifier = True") + + # force non field + if socket.force_non_field is True: + self._write(f"{socket_var}.force_non_field = True") - socket_interface: bpy.types.NodeTreeInterfaceSocket = io_socket_interfaces[i] - - #initialization - socket_var = clean_string(socket_interface.name) + "_socket" - name = str_to_py_str(socket_interface.name) - in_out_enum = enum_to_py_str(socket_interface.in_out) - - socket_type = enum_to_py_str(socket_interface.bl_socket_idname) - """ - I might be missing something, but the Python API's set up a bit - weird here now. The new socket initialization only accepts types - from a list of basic ones, but there doesn't seem to be a way of - retrieving just this basic typewithout the subtype information. - """ - if 'Float' in socket_type: - socket_type = enum_to_py_str('NodeSocketFloat') - elif 'Int' in socket_type: - socket_type = enum_to_py_str('NodeSocketInt') - elif 'Vector' in socket_type: - socket_type = enum_to_py_str('NodeSocketVector') - + # tooltip + if socket.description != "": + description = str_to_py_str(socket.description) + self._write(f"{socket_var}.description = {description}") - self._write(f"{inner}{socket_var} = " - f"{node_tree_var}.interface.new_socket(" - f"name = {name}, in_out={in_out_enum}, " - f"socket_type = {socket_type})\n") + self._write("") - #subtype - if hasattr(socket_interface, "subtype"): - subtype = enum_to_py_str(socket_interface.subtype) - self._write(f"{inner}{socket_var}.subtype = {subtype}\n") + def _create_panel(self, panel: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel], + items_processed: set[NodeTreeInterfacePanel], + parent: NodeTreeInterfacePanel, ntp_nt: NTP_NodeTree): + """ + Initialize a new tree panel and its subitems - self._set_group_socket_default_v4(socket_interface, inner, - socket_var) + Helper function to _process_items() - # default attribute name - if socket_interface.default_attribute_name != "": - dan = str_to_py_str(socket_interface.default_attribute_name) - self._write((f"{inner}{socket_var}.default_attribute_name = {dan}\n")) + Parameters: + panel (NodeTreeInterfacePanel): the panel to recreate + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + items_processed (set[NodeTreeInterfacePanel]): set of already + processed items, so none are done twice + parent (NodeTreeInterfacePanel): parent panel of the socket + (possibly None) + ntp_nt (NTP_NodeTree): owner of the socket + """ - # attribute domain - ad = enum_to_py_str(socket_interface.attribute_domain) - self._write(f"{inner}{socket_var}.attribute_domain = {ad}\n") + self._write(f"#Panel {panel.name}") + + panel_var = self._create_var(panel.name + "_panel") + panel_dict[panel] = panel_var + + description_str = "" + if panel.description != "": + description_str = f", description = {str_to_py_str(panel.description)}" + + closed_str = "" + if panel.default_closed is True: + closed_str = f", default_closed=True" + + parent_str = "" + if parent is not None: + parent_str = f", parent = {panel_dict[parent]}" + + + self._write(f"{panel_var} = " + f"{ntp_nt.var}.interface.new_panel(" + f"{str_to_py_str(panel.name)}{description_str}" + f"{closed_str}{parent_str})") # tooltip - if socket_interface.description != "": - description = str_to_py_str(socket_interface.description) - self._write( - (f"{inner}{socket_var}.description = {description}\n")) + if panel.description != "": + description = str_to_py_str(panel.description) + self._write(f"{panel_var}.description = {description}") - # hide_value - if socket_interface.hide_value is True: - self._write(f"{inner}{socket_var}.hide_value = True\n") + panel_dict[panel] = panel_var - # hide in modifier - if socket_interface.hide_in_modifier is True: - self._write(f"{inner}{socket_var}.hide_in_modifier = True\n") + if len(panel.interface_items) > 0: + self._process_items(panel, panel_dict, items_processed, ntp_nt) + + self._write("") - #force non field - if socket_interface.force_non_field is True: - self._write(f"{inner}{socket_var}.force_non_field = True\n") + def _process_items(self, parent: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel], + items_processed: set[NodeTreeInterfacePanel], + ntp_nt: NTP_NodeTree) -> None: + """ + Recursive function to process all node tree interface items in a + given layer - self._write("\n") - self._write("\n") + Helper function to _tree_interface_settings() - def _group_io_settings(self, node: bpy.types.Node, inner: str, - io: str, # TODO: convert to enum - ntp_node_tree: NTP_NodeTree) -> None: - """ - Set the settings for group input and output sockets + Parameters: + parent (NodeTreeInterfacePanel): parent panel of the layer + (possibly None to signify the base) + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + items_processed (set[NodeTreeInterfacePanel]): set of already + processed items, so none are done twice + ntp_nt (NTP_NodeTree): owner of the socket + """ + if parent is None: + items = ntp_nt.node_tree.interface.items_tree + else: + items = parent.interface_items - Parameters: - node (bpy.types.Node) : group input/output node - inner (str): indentation string - io (str): whether we're generating the input or output settings - node_tree_var (str): variable name of the generated node tree - node_tree (bpy.types.NodeTree): node tree that we're generating - input and output settings for - """ - if bpy.app.version < (4, 0, 0): - self._group_io_settings_v3(node, inner, io, ntp_node_tree) - else: - self._group_io_settings_v4(node, inner, io, ntp_node_tree) + for item in items: + if item.parent.index != -1 and item.parent not in panel_dict: + continue # child of panel not processed yet + if item in items_processed: + continue + + items_processed.add(item) - def _set_input_defaults(self, node: bpy.types.Node, inner: str, - node_var: str) -> None: + if item.item_type == 'SOCKET': + self._create_socket(item, parent, panel_dict, ntp_nt) + + elif item.item_type == 'PANEL': + self._create_panel(item, panel_dict, items_processed, + parent, ntp_nt) + + def _tree_interface_settings(self, ntp_nt: NTP_NodeTree) -> None: + """ + Set the settings for group input and output sockets + + Parameters: + ntp_nt (NTP_NodeTree): the node tree to set the interface for + """ + + self._write(f"#{ntp_nt.var} interface") + panel_dict: dict[NodeTreeInterfacePanel, str] = {} + items_processed: set[NodeTreeInterfaceItem] = set() + + self._process_items(None, panel_dict, items_processed, ntp_nt) + + self._write("") + + def _set_input_defaults(self, node: Node) -> None: """ Sets defaults for input sockets Parameters: - node (bpy.types.Node): node we're setting inputs for - inner (str): indentation - node_var (str): variable name we're using for the copied node - addon_dir (str): directory of the add-on, for if we need to save other - objects for the add-on + node (Node): node we're setting inputs for """ if node.bl_idname == 'NodeReroute': return + node_var = self._node_vars[node] + for i, input in enumerate(node.inputs): if input.bl_idname not in dont_set_defaults and not input.is_linked: # TODO: this could be cleaner @@ -568,10 +667,10 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, elif "Vector" in input.bl_idname: default_val = vec3_to_py_str(input.default_value) - #rotation types + # rotation types elif input.bl_idname == 'NodeSocketRotation': default_val = vec3_to_py_str(input.default_value) - + # strings elif input.bl_idname == 'NodeSocketString': default_val = str_to_py_str(input.default_value) @@ -581,48 +680,42 @@ def _set_input_defaults(self, node: bpy.types.Node, inner: str, img = input.default_value if img is not None and self._addon_dir != None: # write in a better way self._save_image(img) - self._load_image( - img, inner, f"{socket_var}.default_value") + self._load_image(img, f"{socket_var}.default_value") default_val = None # materials elif input.bl_idname == 'NodeSocketMaterial': - self._in_file_inputs(input, inner, socket_var, "materials") + self._in_file_inputs(input, socket_var, "materials") default_val = None # collections elif input.bl_idname == 'NodeSocketCollection': - self._in_file_inputs( - input, inner, socket_var, "collections") + self._in_file_inputs(input, socket_var, "collections") default_val = None # objects elif input.bl_idname == 'NodeSocketObject': - self._in_file_inputs(input, inner, socket_var, "objects") + self._in_file_inputs(input, socket_var, "objects") default_val = None # textures elif input.bl_idname == 'NodeSocketTexture': - self._in_file_inputs(input, inner, socket_var, "textures") + self._in_file_inputs(input, socket_var, "textures") default_val = None else: default_val = input.default_value if default_val is not None: - self._write(f"{inner}#{input.identifier}\n") - self._write((f"{inner}{socket_var}.default_value" - f" = {default_val}\n")) - self._write("\n") + self._write(f"#{input.identifier}") + self._write(f"{socket_var}.default_value = {default_val}") + self._write("") - def _set_output_defaults(self, node: bpy.types.Node, - inner: str, node_var: str) -> None: + def _set_output_defaults(self, node: Node) -> None: """ Some output sockets need default values set. It's rather annoying Parameters: node (bpy.types.Node): node for the output we're setting - inner (str): indentation string - node_var (str): variable name for the node we're setting output defaults for """ # TODO: probably should define elsewhere output_default_nodes = {'ShaderNodeValue', @@ -635,24 +728,22 @@ def _set_output_defaults(self, node: bpy.types.Node, if node.bl_idname not in output_default_nodes: return + node_var = self._node_vars[node] + dv = node.outputs[0].default_value if node.bl_idname in {'ShaderNodeRGB', 'CompositorNodeRGB'}: dv = vec4_to_py_str(list(dv)) if node.bl_idname in {'ShaderNodeNormal', 'CompositorNodeNormal'}: dv = vec3_to_py_str(dv) - self._write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) + self._write(f"{node_var}.outputs[0].default_value = {dv}") - def _in_file_inputs(self, input: bpy.types.NodeSocket, - inner: str, - socket_var: str, - type: str - ) -> None: + def _in_file_inputs(self, input: bpy.types.NodeSocket, socket_var: str, + type: str) -> None: """ Sets inputs for a node input if one already exists in the blend file Parameters: input (bpy.types.NodeSocket): input socket we're setting the value for - inner (str): indentation string socket_var (str): variable name we're using for the socket type (str): from what section of bpy.data to pull the default value from """ @@ -660,21 +751,22 @@ def _in_file_inputs(self, input: bpy.types.NodeSocket, if input.default_value is None: return name = str_to_py_str(input.default_value.name) - self._write(f"{inner}if {name} in bpy.data.{type}:\n") - self._write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) - - def _color_ramp_settings(self, node: bpy.types.Node, - inner: str, - node_var: str, - color_ramp_name: str) -> None: + self._write(f"if {name} in bpy.data.{type}:") + self._write(f"\t{socket_var}.default_value = bpy.data.{type}[{name}]") + + def _set_socket_defaults(self, node: Node): + """ + Set input and output socket defaults + """ + self._set_input_defaults(node) + self._set_output_defaults(node) + + def _color_ramp_settings(self, node: Node, color_ramp_name: str) -> None: """ Replicate a color ramp node Parameters - node (bpy.types.Node): node object we're copying settings from - inner (str): indentation - node_var (str): name of the variable we're using for the color ramp + node (Node): node object we're copying settings from color_ramp_name (str): name of the color ramp to be copied """ @@ -683,52 +775,48 @@ def _color_ramp_settings(self, node: bpy.types.Node, raise ValueError( f"No color ramp named \"{color_ramp_name}\" found") + node_var = self._node_vars[node] + # settings - ramp_str = f"{inner}{node_var}.{color_ramp_name}" + ramp_str = f"{node_var}.{color_ramp_name}" + #color mode color_mode = enum_to_py_str(color_ramp.color_mode) - self._write(f"{ramp_str}.color_mode = {color_mode}\n") + self._write(f"{ramp_str}.color_mode = {color_mode}") + #hue interpolation hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) - self._write((f"{ramp_str}.hue_interpolation = " - f"{hue_interpolation}\n")) + self._write(f"{ramp_str}.hue_interpolation = {hue_interpolation}") + + #interpolation interpolation = enum_to_py_str(color_ramp.interpolation) - self._write((f"{ramp_str}.interpolation " - f"= {interpolation}\n")) - self._write("\n") + self._write(f"{ramp_str}.interpolation = {interpolation}") + self._write("") # key points - self._write(f"{inner}#initialize color ramp elements\n") + self._write(f"#initialize color ramp elements") self._write((f"{ramp_str}.elements.remove" - f"({ramp_str}.elements[0])\n")) + f"({ramp_str}.elements[0])")) for i, element in enumerate(color_ramp.elements): element_var = f"{node_var}_cre_{i}" if i == 0: - self._write(f"{inner}{element_var} = " - f"{ramp_str}.elements[{i}]\n") - self._write( - f"{inner}{element_var}.position = {element.position}\n") + self._write(f"{element_var} = {ramp_str}.elements[{i}]") + self._write(f"{element_var}.position = {element.position}") else: - self._write((f"{inner}{element_var} = " - f"{ramp_str}.elements" - f".new({element.position})\n")) + self._write(f"{element_var} = {ramp_str}.elements" + f".new({element.position})") - self._write((f"{inner}{element_var}.alpha = " - f"{element.alpha}\n")) + self._write(f"{element_var}.alpha = {element.alpha}") color_str = vec4_to_py_str(element.color) - self._write((f"{inner}{element_var}.color = {color_str}\n\n")) + self._write(f"{element_var}.color = {color_str}\n") - def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, - node_var: str, curve_mapping_name: str - ) -> None: + def _curve_mapping_settings(self, node: Node, + curve_mapping_name: str) -> None: """ Sets defaults for Float, Vector, and Color curves Parameters: - node (bpy.types.Node): curve node we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): variable name for the add-on's curve node + node (Node): curve node we're copying settings from curve_mapping_name (str): name of the curve mapping to be set """ @@ -737,73 +825,93 @@ def _curve_mapping_settings(self, node: bpy.types.Node, inner: str, raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " f"in node \"{node.bl_idname}\"")) + node_var = self._node_vars[node] + # mapping settings - self._write(f"{inner}#mapping settings\n") - mapping_var = f"{inner}{node_var}.{curve_mapping_name}" + self._write(f"#mapping settings") + mapping_var = f"{node_var}.{curve_mapping_name}" # extend extend = enum_to_py_str(mapping.extend) - self._write(f"{mapping_var}.extend = {extend}\n") + self._write(f"{mapping_var}.extend = {extend}") # tone tone = enum_to_py_str(mapping.tone) - self._write(f"{mapping_var}.tone = {tone}\n") + self._write(f"{mapping_var}.tone = {tone}") # black level b_lvl_str = vec3_to_py_str(mapping.black_level) - self._write((f"{mapping_var}.black_level = {b_lvl_str}\n")) + self._write(f"{mapping_var}.black_level = {b_lvl_str}") # white level w_lvl_str = vec3_to_py_str(mapping.white_level) - self._write((f"{mapping_var}.white_level = {w_lvl_str}\n")) + self._write(f"{mapping_var}.white_level = {w_lvl_str}") # minima and maxima min_x = mapping.clip_min_x - self._write(f"{mapping_var}.clip_min_x = {min_x}\n") + self._write(f"{mapping_var}.clip_min_x = {min_x}") min_y = mapping.clip_min_y - self._write(f"{mapping_var}.clip_min_y = {min_y}\n") + self._write(f"{mapping_var}.clip_min_y = {min_y}") max_x = mapping.clip_max_x - self._write(f"{mapping_var}.clip_max_x = {max_x}\n") + self._write(f"{mapping_var}.clip_max_x = {max_x}") max_y = mapping.clip_max_y - self._write(f"{mapping_var}.clip_max_y = {max_y}\n") + self._write(f"{mapping_var}.clip_max_y = {max_y}") # use_clip use_clip = mapping.use_clip - self._write(f"{mapping_var}.use_clip = {use_clip}\n") + self._write(f"{mapping_var}.use_clip = {use_clip}") # create curves for i, curve in enumerate(mapping.curves): # TODO: curve function - self._write(f"{inner}#curve {i}\n") + self._write(f"#curve {i}") curve_i = f"{node_var}_curve_{i}" - self._write((f"{inner}{curve_i} = " - f"{node_var}.{curve_mapping_name}.curves[{i}]\n")) + self._write(f"{curve_i} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]") # Remove default points when CurveMap is initialized with more than # two points (just CompositorNodeHueCorrect) if (node.bl_idname == 'CompositorNodeHueCorrect'): - self._write((f"{inner}for i in range" - f"(len({curve_i}.points.values()) - 1, 1, -1):\n")) + self._write((f"for i in range" + f"(len({curve_i}.points.values()) - 1, 1, -1):")) self._write( - f"{inner}\t{curve_i}.points.remove({curve_i}.points[i])\n") + f"\t{curve_i}.points.remove({curve_i}.points[i])") for j, point in enumerate(curve.points): # TODO: point function - point_j = f"{inner}{curve_i}_point_{j}" + point_j = f"{curve_i}_point_{j}" loc = point.location loc_str = f"{loc[0]}, {loc[1]}" if j < 2: - self._write(f"{point_j} = {curve_i}.points[{j}]\n") - self._write(f"{point_j}.location = ({loc_str})\n") + self._write(f"{point_j} = {curve_i}.points[{j}]") + self._write(f"{point_j}.location = ({loc_str})") else: - self._write( - (f"{point_j} = {curve_i}.points.new({loc_str})\n")) + self._write(f"{point_j} = {curve_i}.points.new({loc_str})") handle = enum_to_py_str(point.handle_type) - self._write(f"{point_j}.handle_type = {handle}\n") + self._write(f"{point_j}.handle_type = {handle}") # update curve - self._write(f"{inner}#update curve after changes\n") - self._write(f"{mapping_var}.update()\n") + self._write(f"#update curve after changes") + self._write(f"{mapping_var}.update()") + + def _node_tree_settings(self, node: Node, attr_name: str) -> None: + """ + Processes node tree of group node if one is present + + Parameters: + node (Node): the group node + attr_name (str): name of the node tree attribute + """ + node_tree = getattr(node, attr_name) + if node_tree is None: + return + if node_tree in self._node_tree_vars: + nt_var = self._node_tree_vars[node_tree] + node_var = self._node_vars[node] + self._write(f"{node_var}.{attr_name} = {nt_var}") + else: + self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " + f"wasn't properly initialized")) def _save_image(self, img: bpy.types.Image) -> None: """ @@ -827,16 +935,12 @@ def _save_image(self, img: bpy.types.Image) -> None: if not os.path.exists(img_path): img.save_render(img_path) - def _load_image(self, img: bpy.types.Image, - inner: str, - img_var: str - ) -> None: + def _load_image(self, img: bpy.types.Image, img_var: str) -> None: """ Loads an image from the add-on into a blend file and assigns it Parameters: img (bpy.types.Image): Blender image from the original node group - inner (str): indentation string img_var (str): variable name to be used for the image """ @@ -846,40 +950,37 @@ def _load_image(self, img: bpy.types.Image, img_str = img_to_py_str(img) # TODO: convert to special variables - self._write(f"{inner}#load image {img_str}\n") - self._write((f"{inner}base_dir = " - f"os.path.dirname(os.path.abspath(__file__))\n")) - self._write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " - f"\"{img_str}\")\n")) - self._write((f"{inner}{img_var} = bpy.data.images.load" - f"(image_path, check_existing = True)\n")) + self._write(f"#load image {img_str}") + self._write(f"base_dir = " + f"os.path.dirname(os.path.abspath(__file__))") + self._write(f"image_path = " + f"os.path.join(base_dir, \"{IMAGE_DIR_NAME}\", " + f"\"{img_str}\")") + self._write(f"{img_var} = bpy.data.images.load" + f"(image_path, check_existing = True)") # copy image settings - self._write(f"{inner}#set image settings\n") + self._write(f"#set image settings") # source source = enum_to_py_str(img.source) - self._write(f"{inner}{img_var}.source = {source}\n") + self._write(f"{img_var}.source = {source}") # color space settings color_space = enum_to_py_str(img.colorspace_settings.name) - self._write( - f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") + self._write(f"{img_var}.colorspace_settings.name = {color_space}") # alpha mode alpha_mode = enum_to_py_str(img.alpha_mode) - self._write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") + self._write(f"{img_var}.alpha_mode = {alpha_mode}") def _image_user_settings(self, img_user: bpy.types.ImageUser, - inner: str, img_user_var: str) -> None: """ Replicate the image user of an image node Parameters img_usr (bpy.types.ImageUser): image user to be copied - inner (str): indentation img_usr_var (str): variable name for the generated image user """ @@ -887,76 +988,71 @@ def _image_user_settings(self, img_user: bpy.types.ImageUser, "frame_start", "tile", "use_auto_refresh", "use_cyclic"] for img_usr_attr in img_usr_attrs: - self._write((f"{inner}{img_user_var}.{img_usr_attr} = " - f"{getattr(img_user, img_usr_attr)}\n")) + self._write(f"{img_user_var}.{img_usr_attr} = " + f"{getattr(img_user, img_usr_attr)}") - def _set_parents(self, node_tree: bpy.types.NodeTree, - inner: str) -> None: + def _set_parents(self, node_tree: NodeTree) -> None: """ Sets parents for all nodes, mostly used to put nodes in frames Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - inner (str): indentation string + node_tree (NodeTree): node tree we're obtaining nodes from """ parent_comment = False for node in node_tree.nodes: if node is not None and node.parent is not None: if not parent_comment: - self._write(f"{inner}#Set parents\n") + self._write(f"#Set parents") parent_comment = True node_var = self._node_vars[node] parent_var = self._node_vars[node.parent] - self._write(f"{inner}{node_var}.parent = {parent_var}\n") - self._write("\n") + self._write(f"{node_var}.parent = {parent_var}") + self._write("") - def _set_locations(self, node_tree: bpy.types.NodeTree, inner: str) -> None: + def _set_locations(self, node_tree: NodeTree) -> None: """ Set locations for all nodes Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - inner (str): indentation string + node_tree (NodeTree): node tree we're obtaining nodes from """ - self._write(f"{inner}#Set locations\n") + self._write(f"#Set locations") for node in node_tree.nodes: node_var = self._node_vars[node] - self._write((f"{inner}{node_var}.location " - f"= ({node.location.x}, {node.location.y})\n")) - self._write("\n") + self._write(f"{node_var}.location " + f"= ({node.location.x}, {node.location.y})") + self._write("") - def _set_dimensions(self, node_tree: bpy.types.NodeTree, inner: str, - ) -> None: + def _set_dimensions(self, node_tree: NodeTree) -> None: """ Set dimensions for all nodes Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - inner (str): indentation string + node_tree (NodeTree): node tree we're obtaining nodes from """ - self._write(f"{inner}#Set dimensions\n") + self._write(f"#Set dimensions") for node in node_tree.nodes: node_var = self._node_vars[node] - self._write((f"{inner}{node_var}.width, {node_var}.height " - f"= {node.width}, {node.height}\n")) - self._write("\n") + self._write(f"{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}") + self._write("") - def _init_links(self, node_tree: bpy.types.NodeTree, - inner: str, - node_tree_var: str) -> None: + def _init_links(self, node_tree: NodeTree) -> None: """ Create all the links between nodes Parameters: - node_tree (bpy.types.NodeTree): node tree we're copying - inner (str): indentation - node_tree_var (str): variable name we're using for the copied node tree + node_tree (NodeTree): node tree to copy, with variable """ - if node_tree.links: - self._write(f"{inner}#initialize {node_tree_var} links\n") - for link in node_tree.links: + nt_var = self._node_tree_vars[node_tree] + + links = node_tree.links + if links: + self._write(f"#initialize {nt_var} links") + + for link in links: in_node_var = self._node_vars[link.from_node] input_socket = link.from_socket @@ -980,66 +1076,60 @@ def _init_links(self, node_tree: bpy.types.NodeTree, output_idx = i break - self._write((f"{inner}#{in_node_var}.{input_socket.name} " - f"-> {out_node_var}.{output_socket.name}\n")) - self._write((f"{inner}{node_tree_var}.links.new({in_node_var}" - f".outputs[{input_idx}], " - f"{out_node_var}.inputs[{output_idx}])\n")) + self._write(f"#{in_node_var}.{input_socket.name} " + f"-> {out_node_var}.{output_socket.name}") + self._write(f"{nt_var}.links.new({in_node_var}" + f".outputs[{input_idx}], " + f"{out_node_var}.inputs[{output_idx}])") - def _hide_hidden_sockets(self, node: bpy.types.Node, inner: str, - node_var: str) -> None: + def _hide_hidden_sockets(self, node: Node) -> None: """ Hide hidden sockets Parameters: - node (bpy.types.Node): node object we're copying socket settings from - inner (str): indentation string - node_var (str): name of the variable we're using for this node + node (Node): node object we're copying socket settings from """ + node_var = self._node_vars[node] + for i, socket in enumerate(node.inputs): if socket.hide is True: - self._write(f"{inner}{node_var}.inputs[{i}].hide = True\n") + self._write(f"{node_var}.inputs[{i}].hide = True") for i, socket in enumerate(node.outputs): if socket.hide is True: - self._write(f"{inner}{node_var}.outputs[{i}].hide = True\n") - - def _set_socket_defaults(self, node: Node, node_var: str, inner: str): - self._set_input_defaults(node, inner, node_var) - self._set_output_defaults(node, inner, node_var) + self._write(f"{node_var}.outputs[{i}].hide = True") def _create_menu_func(self) -> None: """ Creates the menu function """ - - self._write("def menu_func(self, context):\n") - self._write(f"\tself.layout.operator({self._class_name}.bl_idname)\n") - self._write("\n") + self._write("def menu_func(self, context):", "") + self._write(f"self.layout.operator({self._class_name}.bl_idname)", "\t") + self._write("") def _create_register_func(self) -> None: """ Creates the register function """ - self._write("def register():\n") - self._write(f"\tbpy.utils.register_class({self._class_name})\n") - self._write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - self._write("\n") + self._write("def register():", "") + self._write(f"bpy.utils.register_class({self._class_name})", "\t") + self._write("bpy.types.VIEW3D_MT_object.append(menu_func)", "\t") + self._write("") def _create_unregister_func(self) -> None: """ Creates the unregister function """ - self._write("def unregister():\n") - self._write(f"\tbpy.utils.unregister_class({self._class_name})\n") - self._write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") - self._write("\n") + self._write("def unregister():", "") + self._write(f"bpy.utils.unregister_class({self._class_name})", "\t") + self._write("bpy.types.VIEW3D_MT_object.remove(menu_func)", "\t") + self._write("") def _create_main_func(self) -> None: """ Creates the main function """ - self._write("if __name__ == \"__main__\":\n") - self._write("\tregister()") + self._write("if __name__ == \"__main__\":", "") + self._write("register()", "\t") def _zip_addon(self) -> None: """ @@ -1049,12 +1139,11 @@ def _zip_addon(self) -> None: shutil.rmtree(self._zip_dir) # ABSTRACT - def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree, inner: str, - level: int) -> None: + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree) -> None: return # ABSTRACT - def _process_node_tree(self, node_tree: NodeTree, level: int) -> None: + def _process_node_tree(self, node_tree: NodeTree) -> None: return def _report_finished(self, object: str): @@ -1069,8 +1158,7 @@ def _report_finished(self, object: str): location = "clipboard" else: location = self._dir - self.report({'INFO'}, - f"NodeToPython: Saved {object} to {location}") + self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}") # ABSTRACT def execute(self): diff --git a/utils.py b/utils.py index 68e6798..ec5d04d 100644 --- a/utils.py +++ b/utils.py @@ -1,11 +1,11 @@ import bpy +from bpy_types import bpy_types import mathutils from enum import Enum, auto -import os +import keyword import re -import shutil -from typing import TextIO, Tuple +from typing import Tuple IMAGE_DIR_NAME = "imgs" @@ -34,6 +34,7 @@ class ST(Enum): # Special settings COLOR_RAMP = auto() CURVE_MAPPING = auto() + NODE_TREE = auto() # Asset Library MATERIAL = auto() # Handle with asset library @@ -63,13 +64,19 @@ def clean_string(string: str, lower: bool = True) -> str: string (str): The input string Returns: - clean_str: The input string with nasty characters converted to underscores + string (str): The input string ready to be used as a variable/file """ if lower: string = string.lower() - clean_str = re.sub(r"[^a-zA-Z0-9_]", '_', string) - return clean_str + string = re.sub(r"[^a-zA-Z0-9_]", '_', string) + + if keyword.iskeyword(string): + string = "_" + string + elif not (string[0].isalpha() or string[0] == '_'): + string = "_" + string + + return string def enum_to_py_str(enum: str) -> str: """ @@ -143,6 +150,24 @@ def vec4_to_py_str(vec4) -> str: """ return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})" +def array_to_py_str(array: bpy_types.bpy_prop_array) -> str: + """ + Converts a bpy_prop_array into a string + + Parameters: + array (bpy_prop_array): Blender Python array + + Returns: + (str): string version + """ + string = "(" + for i in range(0, array.__len__()): + if i > 0: + string += ", " + string += f"{array[i]}" + string += ")" + return string + def color_to_py_str(color: mathutils.Color) -> str: """ Converts a mathutils.Color into a string @@ -167,24 +192,4 @@ def img_to_py_str(img : bpy.types.Image) -> str: """ name = img.name.split('.', 1)[0] format = img.file_format.lower() - return f"{name}.{format}" - -#TODO: reconsider node tree definitions within node tree definitions -def make_indents(level: int) -> Tuple[str, str]: - """ - Returns strings with the correct number of indentations - given the level in the function. - - Node groups need processed recursively, - so there can sometimes be functions in functions. - - Parameters: - level (int): base number of indentations need - - Returns: - outer (str): a basic level of indentation for a node group. - inner (str): a level of indentation beyond outer - """ - outer = "\t"*level - inner = "\t"*(level + 1) - return outer, inner \ No newline at end of file + return f"{name}.{format}" \ No newline at end of file