diff --git a/__init__.py b/__init__.py index 201080e..2378569 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ "name": "Node to Python", "description": "Convert Blender node groups to a Python add-on!", "author": "Brendan Parmer", - "version": (2, 2, 0), + "version": (3, 0, 0), "blender": (3, 0, 0), "location": "Node", "category": "Node", @@ -10,16 +10,19 @@ if "bpy" in locals(): import importlib - importlib.reload(materials) - importlib.reload(geo_nodes) + importlib.reload(compositor) + importlib.reload(geometry) + importlib.reload(material) importlib.reload(options) else: - from . import materials - from . import geo_nodes + from . import compositor + from . import geometry + from . import material from . import options import bpy + class NodeToPythonMenu(bpy.types.Menu): bl_idname = "NODE_MT_node_to_python" bl_label = "Node To Python" @@ -33,17 +36,23 @@ def draw(self, context): layout.operator_context = 'INVOKE_DEFAULT' - - classes = [NodeToPythonMenu, + #options options.NTPOptions, - geo_nodes.GeoNodesToPython, - geo_nodes.SelectGeoNodesMenu, - geo_nodes.GeoNodesToPythonPanel, - materials.MaterialToPython, - materials.SelectMaterialMenu, - materials.MaterialToPythonPanel, - options.NTPOptionsPanel + options.NTPOptionsPanel, + #compositor + compositor.operator.NTPCompositorOperator, + compositor.ui.NTPCompositorScenesMenu, + compositor.ui.NTPCompositorGroupsMenu, + compositor.ui.NTPCompositorPanel, + #geometry + geometry.operator.NTPGeoNodesOperator, + geometry.ui.NTPGeoNodesMenu, + geometry.ui.NTPGeoNodesPanel, + #material + material.operator.NTPMaterialOperator, + material.ui.NTPMaterialMenu, + material.ui.NTPMaterialPanel, ] def register(): diff --git a/compositor/__init__.py b/compositor/__init__.py new file mode 100644 index 0000000..ceb3a04 --- /dev/null +++ b/compositor/__init__.py @@ -0,0 +1,11 @@ +if "bpy" in locals(): + import importlib + importlib.reload(node_settings) + importlib.reload(operator) + importlib.reload(ui) +else: + from . import node_settings + from . import operator + from . import ui + +import bpy \ No newline at end of file diff --git a/compositor/node_settings.py b/compositor/node_settings.py new file mode 100644 index 0000000..8034b30 --- /dev/null +++ b/compositor/node_settings.py @@ -0,0 +1,610 @@ +from ..utils import ST, NTPNodeSetting + +compositor_node_settings : dict[str, list[NTPNodeSetting]] = { + # INPUT + 'CompositorNodeBokehImage' : [ + NTPNodeSetting("angle", ST.FLOAT), + NTPNodeSetting("catadioptric", ST.FLOAT), + NTPNodeSetting("flaps", ST.INT), + NTPNodeSetting("rounding", ST.FLOAT), + NTPNodeSetting("shift", ST.FLOAT) + ], + + 'CompositorNodeImage' : [ + NTPNodeSetting("frame_duration", ST.INT), + NTPNodeSetting("frame_offset", ST.INT), + NTPNodeSetting("frame_start", ST.INT), + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("layer", ST.ENUM), + NTPNodeSetting("use_auto_refresh", ST.BOOL), + NTPNodeSetting("use_cyclic", ST.BOOL), + NTPNodeSetting("use_straight_alpha_output", ST.BOOL), + NTPNodeSetting("view", ST.ENUM) + ], + + 'CompositorNodeMask' : [ + NTPNodeSetting("mask", ST.MASK), + NTPNodeSetting("motion_blur_samples", ST.INT), + NTPNodeSetting("motion_blur_shutter", ST.FLOAT), + NTPNodeSetting("size_source", ST.ENUM), + NTPNodeSetting("size_x", ST.INT), + NTPNodeSetting("size_y", ST.INT), + NTPNodeSetting("use_feather", ST.BOOL), + NTPNodeSetting("use_motion_blur", ST.BOOL) + ], + + 'CompositorNodeMovieClip' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP) + ], + + 'CompositorNodeTexture' : [ + NTPNodeSetting("node_output", ST.INT), #TODO: ?? + NTPNodeSetting("texture", ST.TEXTURE) + ], + + # Input > Constant + 'CompositorNodeRGB' : [], + 'CompositorNodeValue' : [], + + # Input > Scene + 'CompositorNodeRLayers' : [ + NTPNodeSetting("layer", ST.ENUM), + NTPNodeSetting("scene", ST.SCENE) + ], + + 'CompositorNodeSceneTime' : [], + + 'CompositorNodeTime' : [ + NTPNodeSetting("curve", ST.CURVE_MAPPING), + NTPNodeSetting("frame_end", ST.INT), + NTPNodeSetting("frame_start", ST.INT) + ], + + + # OUTPUT + 'CompositorNodeComposite' : [ + NTPNodeSetting("use_alpha", ST.BOOL) + ], + + 'CompositorNodeOutputFile' : [ + NTPNodeSetting("active_input_index", ST.INT), #TODO: probably not right at all + NTPNodeSetting("base_path", ST.STRING), + NTPNodeSetting("file_slots", ST.FILE_SLOTS), + NTPNodeSetting("format", ST.IMAGE_FORMAT_SETTINGS), + NTPNodeSetting("layer_slots", ST.LAYER_SLOTS) + ], + + 'CompositorNodeViewer' : [ + NTPNodeSetting("center_x", ST.FLOAT), + NTPNodeSetting("center_y", ST.FLOAT), + NTPNodeSetting("tile_order", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL) + ], + + 'CompositorNodeSplitViewer' : [ + NTPNodeSetting("axis", ST.ENUM), + NTPNodeSetting("factor", ST.INT) + ], + + + # COLOR + 'CompositorNodePremulKey' : [ + NTPNodeSetting("mapping", ST.ENUM) + ], + + 'CompositorNodeValToRGB' : [ + NTPNodeSetting("color_ramp", ST.COLOR_RAMP) + ], + + 'CompositorNodeConvertColorSpace' : [ + NTPNodeSetting("from_color_space", ST.ENUM, min_version=(3, 1, 0)), + NTPNodeSetting("to_color_space", ST.ENUM, min_version=(3, 1, 0)) + ], + + 'CompositorNodeSetAlpha' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'CompositorNodeInvert' : [ + NTPNodeSetting("invert_alpha", ST.BOOL), + NTPNodeSetting("invert_rgb", ST.BOOL) + ], + + 'CompositorNodeRGBToBW' : [], + + # Color > Adjust + 'CompositorNodeBrightContrast' : [ + NTPNodeSetting("use_premultiply", ST.BOOL) + ], + + 'CompositorNodeColorBalance' : [ + NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("gain", ST.COLOR), + NTPNodeSetting("gamma", ST.COLOR), + NTPNodeSetting("lift", ST.COLOR), + NTPNodeSetting("offset", ST.COLOR), + NTPNodeSetting("offset_basis", ST.FLOAT), + NTPNodeSetting("power", ST.COLOR), + NTPNodeSetting("slope", ST.COLOR) + ], + + 'CompositorNodeColorCorrection' : [ + NTPNodeSetting("red", ST.BOOL), + NTPNodeSetting("green", ST.BOOL), + NTPNodeSetting("blue", ST.BOOL), + #master + NTPNodeSetting("master_saturation", ST.FLOAT), + NTPNodeSetting("master_contrast", ST.FLOAT), + NTPNodeSetting("master_gamma", ST.FLOAT), + NTPNodeSetting("master_gain", ST.FLOAT), + NTPNodeSetting("master_lift", ST.FLOAT), + #highlights + NTPNodeSetting("highlights_saturation", ST.FLOAT), + NTPNodeSetting("highlights_contrast", ST.FLOAT), + NTPNodeSetting("highlights_gamma", ST.FLOAT), + NTPNodeSetting("highlights_gain", ST.FLOAT), + NTPNodeSetting("highlights_lift", ST.FLOAT), + #midtones + NTPNodeSetting("midtones_saturation", ST.FLOAT), + NTPNodeSetting("midtones_contrast", ST.FLOAT), + NTPNodeSetting("midtones_gamma", ST.FLOAT), + NTPNodeSetting("midtones_gain", ST.FLOAT), + NTPNodeSetting("midtones_lift", ST.FLOAT), + #shadows + NTPNodeSetting("shadows_saturation", ST.FLOAT), + NTPNodeSetting("shadows_contrast", ST.FLOAT), + NTPNodeSetting("shadows_gamma", ST.FLOAT), + NTPNodeSetting("shadows_gain", ST.FLOAT), + NTPNodeSetting("shadows_lift", ST.FLOAT), + #midtones location + NTPNodeSetting("midtones_start", ST.FLOAT), + NTPNodeSetting("midtones_end", ST.FLOAT) + ], + + 'CompositorNodeExposure' : [], + 'CompositorNodeGamma' : [], + + 'CompositorNodeHueCorrect' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'CompositorNodeHueSat' : [], + + 'CompositorNodeCurveRGB' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'CompositorNodeTonemap' : [ + NTPNodeSetting("adaptation", ST.FLOAT), + NTPNodeSetting("contrast", ST.FLOAT), + NTPNodeSetting("correction", ST.FLOAT), + NTPNodeSetting("gamma", ST.FLOAT), + NTPNodeSetting("intensity", ST.FLOAT), + NTPNodeSetting("key", ST.FLOAT), + NTPNodeSetting("offset", ST.FLOAT), + NTPNodeSetting("tonemap_type", ST.ENUM) + ], + + + # Color > Mix + 'CompositorNodeAlphaOver' : [ + NTPNodeSetting("premul", ST.FLOAT), + NTPNodeSetting("use_premultiply", ST.BOOL) + ], + + 'CompositorNodeCombineColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)), + NTPNodeSetting("ycc_mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'CompositorNodeSeparateColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)), + NTPNodeSetting("ycc_mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'CompositorNodeMixRGB' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'CompositorNodeZcombine' : [ + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_antialias_z", ST.BOOL) + ], + + + # FILTER + 'CompositorNodeAntiAliasing' : [ + NTPNodeSetting("contrast_limit", ST.FLOAT), + NTPNodeSetting("corner_rounding", ST.FLOAT), + NTPNodeSetting("threshold", ST.FLOAT) + ], + + 'CompositorNodeDenoise' : [ + NTPNodeSetting("prefilter", ST.ENUM), + NTPNodeSetting("use_hdr", ST.BOOL) + ], + + 'CompositorNodeDespeckle' : [ + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("threshold_neighbor", ST.FLOAT) + ], + + 'CompositorNodeDilateErode' : [ + NTPNodeSetting("distance", ST.INT), + NTPNodeSetting("edge", ST.FLOAT), + NTPNodeSetting("falloff", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM) + ], + + 'CompositorNodeInpaint' : [ + NTPNodeSetting("distance", ST.INT) + ], + + 'CompositorNodeFilter' : [ + NTPNodeSetting("filter_type", ST.ENUM) + ], + + 'CompositorNodeGlare' : [ + NTPNodeSetting("angle_offset", ST.FLOAT), + NTPNodeSetting("color_modulation", ST.FLOAT), + NTPNodeSetting("fade", ST.FLOAT), + NTPNodeSetting("glare_type", ST.ENUM), + NTPNodeSetting("iterations", ST.INT), + NTPNodeSetting("mix", ST.FLOAT), + NTPNodeSetting("quality", ST.ENUM), + NTPNodeSetting("size", ST.INT), + NTPNodeSetting("streaks", ST.INT), + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("use_rotate_45", ST.BOOL) + ], + + 'CompositorNodeKuwahara' : [ + NTPNodeSetting("eccentricity", ST.FLOAT, min_version = (4, 0, 0)), + NTPNodeSetting("sharpness", ST.FLOAT, min_version = (4, 0, 0)), + NTPNodeSetting("size", ST.INT, min_version = (4, 0, 0)), + NTPNodeSetting("uniformity", ST.INT, min_version = (4, 0, 0)), + NTPNodeSetting("variation", ST.ENUM, min_version = (4, 0, 0)) + ], + + 'CompositorNodePixelate' : [], + 'CompositorNodePosterize' : [], + + 'CompositorNodeSunBeams' : [ + NTPNodeSetting("ray_length", ST.FLOAT), + NTPNodeSetting("source", ST.VEC2) + ], + + # Filter > Blur + 'CompositorNodeBilateralblur' : [ + NTPNodeSetting("iterations", ST.INT), + NTPNodeSetting("sigma_color", ST.FLOAT), + NTPNodeSetting("sigma_space", ST.FLOAT) + ], + + 'CompositorNodeBlur' : [ + NTPNodeSetting("aspect_correction", ST.ENUM), + NTPNodeSetting("factor", ST.FLOAT), + NTPNodeSetting("factor_x", ST.FLOAT), + NTPNodeSetting("factor_y", ST.FLOAT), + NTPNodeSetting("filter_type", ST.ENUM), + NTPNodeSetting("size_x", ST.INT), + NTPNodeSetting("size_y", ST.INT), + NTPNodeSetting("use_bokeh", ST.BOOL), + NTPNodeSetting("use_extended_bounds", ST.BOOL), + NTPNodeSetting("use_gamma_correction", ST.BOOL), + NTPNodeSetting("use_relative", ST.BOOL), + NTPNodeSetting("use_variable_size", ST.BOOL) + ], + + 'CompositorNodeBokehBlur' : [ + NTPNodeSetting("blur_max", ST.FLOAT), + NTPNodeSetting("use_extended_bounds", ST.BOOL), + NTPNodeSetting("use_variable_size", ST.BOOL) + ], + + 'CompositorNodeDefocus' : [ + NTPNodeSetting("angle", ST.FLOAT), + NTPNodeSetting("blur_max", ST.FLOAT), + NTPNodeSetting("bokeh", ST.ENUM), + NTPNodeSetting("f_stop", ST.FLOAT), + NTPNodeSetting("scene", ST.SCENE), + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("use_gamma_correction", ST.BOOL), + NTPNodeSetting("use_preview", ST.BOOL), + NTPNodeSetting("use_zbuffer", ST.BOOL), + NTPNodeSetting("z_scale", ST.FLOAT) + ], + + 'CompositorNodeDBlur' : [ + NTPNodeSetting("angle", ST.FLOAT), + NTPNodeSetting("center_x", ST.FLOAT), + NTPNodeSetting("center_y", ST.FLOAT), + NTPNodeSetting("distance", ST.FLOAT), + NTPNodeSetting("iterations", ST.INT), + NTPNodeSetting("spin", ST.FLOAT), + NTPNodeSetting("use_wrap", ST.BOOL, max_version = (3, 4, 0)), + NTPNodeSetting("zoom", ST.FLOAT) + ], + + 'CompositorNodeVecBlur' : [ + NTPNodeSetting("factor", ST.FLOAT), + NTPNodeSetting("samples", ST.INT), + NTPNodeSetting("speed_max", ST.INT), + NTPNodeSetting("speed_min", ST.INT), + NTPNodeSetting("use_curved", ST.BOOL) + ], + + + # KEYING + 'CompositorNodeChannelMatte' : [ + NTPNodeSetting("color_space", ST.ENUM), + NTPNodeSetting("limit_channel", ST.ENUM), + NTPNodeSetting("limit_max", ST.FLOAT), + NTPNodeSetting("limit_method", ST.ENUM), + NTPNodeSetting("limit_min", ST.FLOAT), + NTPNodeSetting("matte_channel", ST.ENUM) + ], + + 'CompositorNodeChromaMatte' : [ + NTPNodeSetting("gain", ST.FLOAT), + NTPNodeSetting("lift", ST.FLOAT), + NTPNodeSetting("shadow_adjust", ST.FLOAT), + NTPNodeSetting("threshold", ST.FLOAT), + NTPNodeSetting("tolerance", ST.FLOAT) + ], + + 'CompositorNodeColorMatte' : [ + NTPNodeSetting("color_hue", ST.FLOAT), + NTPNodeSetting("color_saturation", ST.FLOAT), + NTPNodeSetting("color_value", ST.FLOAT) + ], + + 'CompositorNodeColorSpill' : [ + NTPNodeSetting("channel", ST.ENUM), + NTPNodeSetting("limit_channel", ST.ENUM), + NTPNodeSetting("limit_method", ST.ENUM), + NTPNodeSetting("ratio", ST.FLOAT), + NTPNodeSetting("unspill_blue", ST.FLOAT), + NTPNodeSetting("unspill_green", ST.FLOAT), + NTPNodeSetting("unspill_red", ST.FLOAT), + NTPNodeSetting("use_unspill", ST.BOOL) + ], + + 'CompositorNodeDiffMatte' : [ + NTPNodeSetting("falloff", ST.FLOAT), + NTPNodeSetting("tolerance", ST.FLOAT) + ], + + 'CompositorNodeDistanceMatte' : [ + NTPNodeSetting("channel", ST.ENUM), + NTPNodeSetting("falloff", ST.FLOAT), + NTPNodeSetting("tolerance", ST.FLOAT) + ], + + 'CompositorNodeKeying' : [ + NTPNodeSetting("blur_post", ST.INT), + NTPNodeSetting("blur_pre", ST.INT), + NTPNodeSetting("clip_black", ST.FLOAT), + NTPNodeSetting("clip_white", ST.FLOAT), + NTPNodeSetting("despill_balance", ST.FLOAT), + NTPNodeSetting("despill_factor", ST.FLOAT), + NTPNodeSetting("dilate_distance", ST.INT), + NTPNodeSetting("edge_kernel_radius", ST.INT), + NTPNodeSetting("edge_kernel_tolerance", ST.FLOAT), + NTPNodeSetting("feather_distance", ST.INT), + NTPNodeSetting("feather_falloff", ST.ENUM), + NTPNodeSetting("screen_balance", ST.FLOAT) + ], + + 'CompositorNodeKeyingScreen' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("tracking_object", ST.STRING) + ], + + 'CompositorNodeLumaMatte' : [ + NTPNodeSetting("limit_max", ST.FLOAT), + NTPNodeSetting("limit_min", ST.FLOAT) + ], + + + # MASK + 'CompositorNodeCryptomatteV2' : [ + NTPNodeSetting("add", ST.COLOR), + NTPNodeSetting("entries", ST.CRYPTOMATTE_ENTRIES), + NTPNodeSetting("frame_duration", ST.INT), + NTPNodeSetting("frame_offset", ST.INT), + NTPNodeSetting("frame_start", ST.INT), + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("layer", ST.ENUM), + NTPNodeSetting("layer_name", ST.ENUM), + NTPNodeSetting("matte_id", ST.STRING), + NTPNodeSetting("remove", ST.COLOR), + NTPNodeSetting("scene", ST.SCENE), + NTPNodeSetting("source", ST.ENUM), + NTPNodeSetting("use_auto_refresh", ST.BOOL), + NTPNodeSetting("use_cyclic", ST.BOOL), + NTPNodeSetting("view", ST.ENUM) + ], + + 'CompositorNodeCryptomatte' : [ + NTPNodeSetting("add", ST.COLOR), #TODO: may need special handling + NTPNodeSetting("matte_id", ST.STRING), + NTPNodeSetting("remove", ST.COLOR) + ], + + 'CompositorNodeBoxMask' : [ + NTPNodeSetting("height", ST.FLOAT), + NTPNodeSetting("mask_type", ST.ENUM), + NTPNodeSetting("rotation", ST.FLOAT), + NTPNodeSetting("width", ST.FLOAT), + NTPNodeSetting("x", ST.FLOAT), + NTPNodeSetting("y", ST.FLOAT) + ], + + 'CompositorNodeEllipseMask' : [ + NTPNodeSetting("height", ST.FLOAT), + NTPNodeSetting("mask_type", ST.ENUM), + NTPNodeSetting("rotation", ST.FLOAT), + NTPNodeSetting("width", ST.FLOAT), + NTPNodeSetting("x", ST.FLOAT), + NTPNodeSetting("y", ST.FLOAT) + ], + + 'CompositorNodeDoubleEdgeMask' : [ + NTPNodeSetting("edge_mode", ST.ENUM), + NTPNodeSetting("inner_mode", ST.ENUM) + ], + + 'CompositorNodeIDMask' : [ + NTPNodeSetting("index", ST.INT), + NTPNodeSetting("use_antialiasing", ST.BOOL) + ], + + + # TRACKING + 'CompositorNodePlaneTrackDeform' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("motion_blur_samples", ST.INT), + NTPNodeSetting("motion_blur_shutter", ST.FLOAT), + NTPNodeSetting("plane_track_name", ST.STRING), + NTPNodeSetting("tracking_object", ST.STRING), + NTPNodeSetting("use_motion_blur", ST.BOOL) + ], + + 'CompositorNodeStabilize' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("filter_type", ST.ENUM), + NTPNodeSetting("invert", ST.BOOL) + ], + + 'CompositorNodeTrackPos' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("frame_relative", ST.INT), + NTPNodeSetting("position", ST.ENUM), + NTPNodeSetting("track_name", ST.STRING), #TODO: probably not right + NTPNodeSetting("tracking_object", ST.STRING) + ], + + + # TRANSFORM + 'CompositorNodeRotate' : [ + NTPNodeSetting("filter_type", ST.ENUM) + ], + + 'CompositorNodeScale' : [ + NTPNodeSetting("frame_method", ST.ENUM), + NTPNodeSetting("offset_x", ST.FLOAT), + NTPNodeSetting("offset_y", ST.FLOAT), + NTPNodeSetting("space", ST.ENUM) + ], + + 'CompositorNodeTransform' : [ + NTPNodeSetting("filter_type", ST.ENUM) + ], + + 'CompositorNodeTranslate' : [ + NTPNodeSetting("use_relative", ST.BOOL), + NTPNodeSetting("wrap_axis", ST.ENUM) + ], + + 'CompositorNodeCornerPin' : [], + + 'CompositorNodeCrop' : [ + NTPNodeSetting("max_x", ST.INT), + NTPNodeSetting("max_y", ST.INT), + NTPNodeSetting("min_x", ST.INT), + NTPNodeSetting("min_y", ST.INT), + NTPNodeSetting("rel_max_x", ST.FLOAT), + NTPNodeSetting("rel_max_y", ST.FLOAT), + NTPNodeSetting("rel_min_x", ST.FLOAT), + NTPNodeSetting("rel_min_y", ST.FLOAT), + NTPNodeSetting("relative", ST.BOOL), + NTPNodeSetting("use_crop_size", ST.BOOL) + ], + + 'CompositorNodeDisplace' : [], + + 'CompositorNodeFlip' : [ + NTPNodeSetting("axis", ST.ENUM) + ], + + 'CompositorNodeMapUV' : [ + NTPNodeSetting("alpha", ST.INT) + ], + + 'CompositorNodeLensdist' : [ + NTPNodeSetting("use_fit", ST.BOOL), + NTPNodeSetting("use_jitter", ST.BOOL), + NTPNodeSetting("use_projector", ST.BOOL) + ], + + 'CompositorNodeMovieDistortion' : [ + NTPNodeSetting("clip", ST.MOVIE_CLIP), + NTPNodeSetting("distortion_type", ST.ENUM) + ], + + + # UTILITIES + 'CompositorNodeMapRange' : [ + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'CompositorNodeMapValue' : [ + NTPNodeSetting("max", ST.VEC1), + NTPNodeSetting("min", ST.VEC1), + NTPNodeSetting("offset", ST.VEC1), + NTPNodeSetting("size", ST.VEC1), + NTPNodeSetting("use_max", ST.BOOL), + NTPNodeSetting("use_min", ST.BOOL) + ], + + 'CompositorNodeMath' : [ + NTPNodeSetting("operation", ST.ENUM), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'CompositorNodeLevels' : [ + NTPNodeSetting("channel", ST.ENUM) + ], + + 'CompositorNodeNormalize' : [], + + + 'CompositorNodeSwitch' : [ + NTPNodeSetting("check", ST.BOOL) + ], + + 'CompositorNodeSwitchView' : [], + + + # VECTOR + 'CompositorNodeCombineXYZ' : [], + 'CompositorNodeSeparateXYZ' : [], + 'CompositorNodeNormal' : [], + + 'CompositorNodeCurveVec' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + + # MISC + 'CompositorNodeGroup' : [ + NTPNodeSetting("node_tree", ST.NODE_TREE) + ], + + 'NodeFrame' : [ + NTPNodeSetting("label_size", ST.INT), + NTPNodeSetting("shrink", ST.BOOL), + NTPNodeSetting("text", ST.TEXT) + ], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [ + NTPNodeSetting("is_active_output", ST.BOOL) + ], + + 'NodeReroute' : [] +} diff --git a/compositor/operator.py b/compositor/operator.py new file mode 100644 index 0000000..afe2d62 --- /dev/null +++ b/compositor/operator.py @@ -0,0 +1,229 @@ +import bpy + +from bpy.types import Node, CompositorNodeColorBalance, CompositorNodeTree + +from ..ntp_operator import NTP_Operator, INDEX +from ..ntp_node_tree import NTP_NodeTree +from ..utils import * +from io import StringIO +from .node_settings import compositor_node_settings + +SCENE = "scene" +BASE_NAME = "base_name" +END_NAME = "end_name" +NODE = "node" + +COMP_OP_RESERVED_NAMES = {SCENE, BASE_NAME, END_NAME, NODE} + +class NTPCompositorOperator(NTP_Operator): + bl_idname = "node.ntp_compositor" + bl_label = "Compositor to Python" + bl_options = {'REGISTER', 'UNDO'} + + mode : bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + compositor_name: bpy.props.StringProperty(name="Node Group") + is_scene : bpy.props.BoolProperty(name="Is Scene", description="Blender stores compositing node trees differently for scenes and in groups") + + def __init__(self): + super().__init__() + self._settings = compositor_node_settings + for name in COMP_OP_RESERVED_NAMES: + self._used_vars[name] = 0 + + + def _create_scene(self, indent: str): + #TODO: wrap in more general unique name util function + self._write(f"# Generate unique scene name", indent) + self._write(f"{BASE_NAME} = {str_to_py_str(self.compositor_name)}", + indent) + self._write(f"{END_NAME} = {BASE_NAME}", indent) + self._write(f"if bpy.data.scenes.get({END_NAME}) != None:", indent) + + indent2 = f"{indent}\t" + self._write(f"{INDEX} = 1", indent2) + self._write(f"{END_NAME} = {BASE_NAME} + f\".{{i:03d}}\"", + indent2) + self._write(f"while bpy.data.scenes.get({END_NAME}) != None:", + indent2) + + indent3 = f"{indent}\t\t" + self._write(f"{END_NAME} = {BASE_NAME} + f\".{{{INDEX}:03d}}\"", indent3) + self._write(f"{INDEX} += 1\n", indent3) + + self._write(f"{SCENE} = bpy.context.window.scene.copy()\n", indent) + self._write(f"{SCENE}.name = {END_NAME}", indent) + self._write(f"{SCENE}.use_fake_user = True", indent) + self._write(f"bpy.context.window.scene = {SCENE}", indent) + + def _initialize_compositor_node_tree(self, ntp_nt, nt_name): + #initialize node group + 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}.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"{ntp_nt.var} = bpy.data.node_groups.new(" + f"type = \'CompositorNodeTree\', " + 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 = [NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("gain", ST.COLOR), + NTPNodeSetting("gamma", ST.COLOR), + NTPNodeSetting("lift", ST.COLOR)] + else: + lst = [NTPNodeSetting("correction_method", ST.ENUM), + NTPNodeSetting("offset", ST.COLOR), + NTPNodeSetting("offset_basis", ST.FLOAT), + NTPNodeSetting("power", ST.COLOR), + NTPNodeSetting("slope", ST.COLOR)] + + self._settings['CompositorNodeColorBalance'] = lst + + 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': + 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: CompositorNodeTree): + """ + Generates a Python function to recreate a compositor node tree + + Parameters: + node_tree (CompositorNodeTree): node tree to be recreated + """ + 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 + + self._node_tree_vars[node_tree] = nt_var + + 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"#initialize {nt_var} nodes") + + for node in node_tree.nodes: + self._process_node(node, ntp_nt) + + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) + + self._init_links(node_tree) + + 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: + self._base_node_tree = bpy.data.scenes[self.compositor_name].node_tree + else: + self._base_node_tree = bpy.data.node_groups[self.compositor_name] + + 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 " + "selected?")) + return {'CANCELLED'} + + #set up names to use in generated addon + 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") + + self._create_header(self.compositor_name) + self._class_name = clean_string(self.compositor_name, lower=False) + self._init_operator(comp_var, self.compositor_name) + + self._write("def execute(self, context):", "\t") + else: + self._file = StringIO("") + + if self.is_scene: + if self.mode == 'ADDON': + self._create_scene("\t\t") + elif self.mode == 'SCRIPT': + self._create_scene("") + + 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("return {'FINISHED'}\n", self._outer) + + self._create_menu_func() + self._create_register_func() + self._create_unregister_func() + self._create_main_func() + else: + context.window_manager.clipboard = self._file.getvalue() + + self._file.close() + + if self.mode == 'ADDON': + self._zip_addon() + + self._report_finished("compositor nodes") + + return {'FINISHED'} \ No newline at end of file diff --git a/compositor/ui.py b/compositor/ui.py new file mode 100644 index 0000000..a1003ca --- /dev/null +++ b/compositor/ui.py @@ -0,0 +1,80 @@ +import bpy +from bpy.types import Panel +from bpy.types import Menu +from .operator import NTPCompositorOperator + +class NTPCompositorPanel(Panel): + bl_label = "Compositor to Python" + bl_idname = "NODE_PT_ntp_compositor" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + scenes_row = layout.row() + + # Disables menu when there are no compositing node groups + scenes = [scene for scene in bpy.data.scenes if scene.node_tree] + scenes_exist = len(scenes) > 0 + scenes_row.enabled = scenes_exist + + scenes_row.alignment = 'EXPAND' + scenes_row.operator_context = 'INVOKE_DEFAULT' + scenes_row.menu("NODE_MT_ntp_comp_scenes", + text="Scene Compositor Nodes") + + groups_row = layout.row() + groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'CompositorNodeTree'] + groups_exist = len(groups) > 0 + groups_row.enabled = groups_exist + + groups_row.alignment = 'EXPAND' + groups_row.operator_context = 'INVOKE_DEFAULT' + groups_row.menu("NODE_MT_ntp_comp_groups", + text="Group Compositor Nodes") + +class NTPCompositorScenesMenu(Menu): + bl_idname = "NODE_MT_ntp_comp_scenes" + bl_label = "Select " + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for scene in bpy.data.scenes: + if scene.node_tree: + op = layout.operator(NTPCompositorOperator.bl_idname, + text=scene.name) + op.compositor_name = scene.name + op.is_scene = True + +class NTPCompositorGroupsMenu(Menu): + bl_idname = "NODE_MT_ntp_comp_groups" + bl_label = "Select " + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for node_group in bpy.data.node_groups: + if node_group.bl_idname == 'CompositorNodeTree': + op = layout.operator(NTPCompositorOperator.bl_idname, + text=node_group.name) + op.compositor_name = node_group.name + op.is_scene = False \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index e82bbb8..bc461c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,7 +2,7 @@ ![Node To Python Logo](./img/ntp.jpg "Node To Python Logo") -[![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) +[![GitHub release (latest by date)](https://img.shields.io/github/v/release/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/releases) [![GitHub](https://img.shields.io/github/license/BrendanParmer/NodeToPython)](https://github.com/BrendanParmer/NodeToPython/blob/main/LICENSE) ![](https://visitor-badge.laobi.icu/badge?page_id=BrendanParmer.NodeToPython) ![](https://img.shields.io/github/downloads/BrendanParmer/NodeToPython/total.svg) ## About A Blender add-on to create scripts and add-ons! This add-on will take your Geometry Nodes or Materials and convert them into legible Python code. @@ -15,7 +15,7 @@ Blender's node-based editors are powerful, yet accessible tools, and I wanted to * interfacing with other parts of the software or properties of an object ## Supported Versions -NodeToPython v2.2 is compatible with Blender 3.0 - 3.6 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update. +NodeToPython v3.0 is supported for Blender 3.0 - 4.0 on Windows, macOS, and Linux. I generally try to update the add-on to handle new nodes around the beta release of each update. ## Installation 1. Download the `NodeToPython.zip` file from the [latest release](https://github.com/BrendanParmer/NodeToPython/releases) @@ -35,33 +35,29 @@ Select the node group you want code for, and you'll be prompted with a **Script* * Doesn't include `import bpy` line * To keep NodeToPython cross-platform and independent of third-party libraries, to get it into your system clipboard you need to paste into the Blender text editor and recopy it currently * **Add-on** mode generates a zip file for you in the save directory specified in the NodeToPython menu. From here, you can install it like a regular add-on. The generated add-on comes complete with operator registration and creating a modifier/material for the node tree to be used in. - -## Future -### v3.x -* Expansion to Compositing nodes -* New Blender 4.0 nodes and changes - -### Later -* Better asset handling -* Auto-set handle movies and image sequences -* Automatically format code to be PEP8 compliant -* Automatically detect the minimum version of Blender needed to run the add-on - -## Potential Issues -* As of version 2.2, the add-on will not set default values for - * Scripts - * IES files - * Filepaths - * UV maps -* This add-on doesn't currently set default values in Geometry Nodes modifiers, just the node groups themselves -* Currently when setting default values for the following, the add-on must be run in the same blend file as the node group was created in to set the default, otherwise it will just set it to `None`: + * The current default operator install location is in the Object menu + +## Future Plans +* Investigate drivers and keyframes +* A better default operator install location for generated add-ons +* A development repository with useful scripts and tests +* Better handling of more setting types, including + * Image sequences + * Movie clips * Materials * Objects - * Collections * Textures - -* In a future version, I plan on having the add-on adding all of the above to the Asset Library for reference -* You may run into naming conflicts if your add-on shares a name with another Blender add-on or operator (see [#56](https://github.com/BrendanParmer/NodeToPython/issues/56)) + * Text objects + * Scenes + * Particle systems + * Fonts + * Masks + * Cryptomatte entries + * Image format settings + * File slots + * Layer slots +* Automatic detection of the minimum/maximum version of Blender compatible with a generated add-on +* Autoformatting of generated code ## Bug Reports and Suggestions diff --git a/geo_nodes.py b/geo_nodes.py deleted file mode 100644 index c70f219..0000000 --- a/geo_nodes.py +++ /dev/null @@ -1,437 +0,0 @@ -import bpy -import os - -from .utils import * -from io import StringIO - -geo_node_settings = { - # Attribute nodes - "GeometryNodeAttributeStatistic" : ["data_type", "domain"], - "GeometryNodeAttributeDomainSize" : ["component"], - - "GeometryNodeBlurAttribute" : ["data_type"], - "GeometryNodeCaptureAttribute" : ["data_type", "domain"], - "GeometryNodeStoreNamedAttribute" : ["data_type", "domain"], - "GeometryNodeAttributeTransfer" : ["data_type", "mapping"], - - # Input Nodes - # Input > Constant - "FunctionNodeInputBool" : ["boolean"], - "FunctionNodeInputColor" : ["color"], - "FunctionNodeInputInt" : ["integer"], - "GeometryNodeInputMaterial" : ["material"], - "FunctionNodeInputString" : ["string"], - "FunctionNodeInputVector" : ["vector"], - - # Input > Scene - "GeometryNodeCollectionInfo" : ["transform_space"], - "GeometryNodeObjectInfo" : ["transform_space"], - - # Output Nodes - "GeometryNodeViewer" : ["domain"], - - # Geometry Nodes - # Geometry > Read - "GeometryNodeInputNamedAttribute" : ["data_type"], - - # Geometry > Sample - "GeometryNodeProximity" : ["target_element"], - "GeometryNodeRaycast" : ["data_type", "mapping"], - "GeometryNodeSampleIndex" : ["data_type", "domain", "clamp"], - "GeometryNodeSampleNearest" : ["domain"], - - # Geometry > Operations - "GeometryNodeDeleteGeometry" : ["domain", "mode"], - "GeometryNodeDuplicateElements" : ["domain"], - "GeometryNodeMergeByDistance" : ["mode"], - "GeometryNodeSeparateGeometry" : ["domain"], - - - # Curve - # Curve > Read - "GeometryNodeCurveHandleTypeSelection" : ["mode", "handle_type"], - - # Curve > Sample - "GeometryNodeSampleCurve" : ["data_type", "mode", "use_all_curves"], - - # Curve > Write - "GeometryNodeSetCurveNormal" : ["mode"], - "GeometryNodeSetCurveHandlePositions" : ["mode"], - "GeometryNodeCurveSetHandles" : ["mode", "handle_type"], - "GeometryNodeCurveSplineType" : ["spline_type"], - - # Curve > Operations - "GeometryNodeCurveToPoints" : ["mode"], - "GeometryNodeFillCurve" : ["mode"], - "GeometryNodeFilletCurve" : ["mode"], - "GeometryNodeResampleCurve" : ["mode"], - "GeometryNodeTrimCurve" : ["mode"], - - # Curve > Primitives - "GeometryNodeCurveArc" : ["mode"], - "GeometryNodeCurvePrimitiveBezierSegment" : ["mode"], - "GeometryNodeCurvePrimitiveCircle" : ["mode"], - "GeometryNodeCurvePrimitiveLine" : ["mode"], - "GeometryNodeCurvePrimitiveQuadrilateral" : ["mode"], - - - # Mesh Nodes - # Mesh > Sample - "GeometryNodeSampleNearestSurface" : ["data_type"], - "GeometryNodeSampleUVSurface" : ["data_type"], - - # Mesh > Operations - "GeometryNodeExtrudeMesh" : ["mode"], - "GeometryNodeMeshBoolean" : ["operation"], - "GeometryNodeMeshToPoints" : ["mode"], - "GeometryNodeMeshToVolume" : ["resolution_mode"], - "GeometryNodeScaleElements" : ["domain", "scale_mode"], - "GeometryNodeSubdivisionSurface" : ["uv_smooth", "boundary_smooth"], - "GeometryNodeTriangulate" : ["quad_method", "ngon_method"], - - # Mesh > Primitives - "GeometryNodeMeshCone" : ["fill_type"], - "GeometryNodeMeshCylinder" : ["fill_type"], - "GeometryNodeMeshCircle" : ["fill_type"], - "GeometryNodeMeshLine" : ["mode"], - - # Mesh > UV - "GeometryNodeUVUnwrap" : ["method"], - - - # Point Nodes - "GeometryNodeDistributePointsInVolume" : ["mode"], - "GeometryNodeDistributePointsOnFaces" : ["distribute_method"], - "GeometryNodePointsToVolume" : ["resolution_mode"], - - # Volume Nodes - "GeometryNodeVolumeToMesh" : ["resolution_mode"], - - - # Texture Nodes - "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", - "squash_frequency"], - "ShaderNodeTexGradient" : ["gradient_type"], - "GeometryNodeImageTexture" : ["interpolation", "extension"], - "ShaderNodeTexMagic" : ["turbulence_depth"], - "ShaderNodeTexNoise" : ["noise_dimensions"], - "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], - "ShaderNodeTexWave" : ["wave_type", "bands_direction", "wave_profile"], - "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], - - - # Utilities - # Utilities > Color - "FunctionNodeCombineColor" : ["mode"], - "ShaderNodeMixRGB" : ["blend_type", "use_clamp"], #legacy - "FunctionNodeSeparateColor" : ["mode"], - - # Utilities > Text - "GeometryNodeStringToCurves" : ["overflow", "align_x", "align_y", - "pivot_mode"], - - # Utilities > Vector - "ShaderNodeVectorMath" : ["operation"], - "ShaderNodeVectorRotate" : ["rotation_type", "invert"], - - # Utilities > Field - "GeometryNodeAccumulateField" : ["data_type", "domain"], - "GeometryNodeFieldAtIndex" : ["data_type", "domain"], - "GeometryNodeFieldOnDomain" : ["data_type", "domain" ], - - # Utilities > Math - "FunctionNodeBooleanMath" : ["operation"], - "ShaderNodeClamp" : ["clamp_type"], - "FunctionNodeCompare" : ["data_type", "operation", "mode"], - "FunctionNodeFloatToInt" : ["rounding_mode"], - "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], - "ShaderNodeMath" : ["operation", "use_clamp"], - - # Utilities > Rotate - "FunctionNodeAlignEulerToVector" : ["axis", "pivot_axis"], - "FunctionNodeRotateEuler" : ["type", "space"], - - # Utilities > General - "ShaderNodeMix" : ["data_type", "blend_type", "clamp_result", - "clamp_factor", "factor_mode"], - "FunctionNodeRandomValue" : ["data_type"], - "GeometryNodeSwitch" : ["input_type"] -} - -curve_nodes = {'ShaderNodeFloatCurve', - 'ShaderNodeVectorCurve', - 'ShaderNodeRGBCurve'} - -image_nodes = {'GeometryNodeInputImage'} - -class GeoNodesToPython(bpy.types.Operator): - bl_idname = "node.geo_nodes_to_python" - bl_label = "Geo Nodes to Python" - bl_options = {'REGISTER', 'UNDO'} - - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - - geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") - - def execute(self, context): - #find node group to replicate - nt = bpy.data.node_groups[self.geo_nodes_group_name] - - #set up names to use in generated addon - nt_var = clean_string(nt.name) - - if self.mode == 'ADDON': - #find base directory to save new addon - dir = bpy.path.abspath(context.scene.ntp_options.dir_path) - if not dir or dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blend file before using " - "NodeToPython!")) - return {'CANCELLED'} - - #save in addons/ subdirectory - zip_dir = os.path.join(dir, nt_var) - addon_dir = os.path.join(zip_dir, nt_var) - - if not os.path.exists(addon_dir): - os.makedirs(addon_dir) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, nt.name) - class_name = clean_string(nt.name.replace(" ", "").replace('.', ""), - lower = False) - init_operator(file, class_name, nt_var, nt.name) - file.write("\tdef execute(self, context):\n") - else: - file = StringIO("") - - #set to keep track of already created node trees - node_trees = set() - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - #dictionary to keep track of variables->usage count pairs - used_vars = {} - - def process_geo_nodes_group(node_tree, level, node_vars, used_vars): - nt_var = create_var(node_tree.name, used_vars) - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'GeometryNodeTree\', " - f"name = {str_to_py_str(node_tree.name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - sim_inputs = [] - - for node in node_tree.nodes: - if node.bl_idname == 'GeometryNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in node_trees: - process_geo_nodes_group(node_nt, level + 1, node_vars, - used_vars) - node_trees.add(node_nt) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - group_io_settings(node, file, inner, "input", nt_var, - node_tree) - inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - group_io_settings(node, file, inner, "output", nt_var, - node_tree) - outputs_set = True - - #create node - node_var = create_node(node, file, inner, nt_var, - node_vars, used_vars) - set_settings_defaults(node, geo_node_settings, file, inner, - node_var) - hide_sockets(node, file, inner, node_var) - - if node.bl_idname == 'GeometryNodeGroup': - if node.node_tree is not None: - file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[{str_to_py_str(node.node_tree.name)}]\n")) - - elif node.bl_idname == 'ShaderNodeValToRGB': - color_ramp_settings(node, file, inner, node_var) - - elif node.bl_idname in curve_nodes: - curve_node_settings(node, file, inner, node_var) - - elif node.bl_idname in image_nodes and self.mode == 'ADDON': - img = node.image - if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}: - save_image(img, addon_dir) - load_image(img, file, inner, f"{node_var}.image") - - elif node.bl_idname == 'GeometryNodeSimulationInput': - sim_inputs.append(node) - - elif node.bl_idname == 'GeometryNodeSimulationOutput': - file.write(f"{inner}#remove generated sim state items\n") - file.write(f"{inner}for item in {node_var}.state_items:\n") - file.write(f"{inner}\t{node_var}.state_items.remove(item)\n") - - for i, si in enumerate(node.state_items): - socket_type = enum_to_py_str(si.socket_type) - name = str_to_py_str(si.name) - file.write(f"{inner}#create SSI {name}\n") - file.write((f"{inner}{node_var}.state_items.new" - f"({socket_type}, {name})\n")) - si_var = f"{node_var}.state_items[{i}]" - attr_domain = enum_to_py_str(si.attribute_domain) - file.write((f"{inner}{si_var}.attribute_domain = " - f"{attr_domain}\n")) - - if node.bl_idname != 'GeometryNodeSimulationInput': - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - #create simulation zones - for sim_input in sim_inputs: - sim_input_var = node_vars[sim_input] - sim_output_var = node_vars[sim_input.paired_output] - file.write((f"{inner}{sim_input_var}.pair_with_output" - f"({sim_output_var})\n")) - - #must set defaults after paired with output - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(sim_input, file, inner, sim_input_var) - - #set look of nodes - set_parents(node_tree, file, inner, node_vars) - set_locations(node_tree, file, inner, node_vars) - set_dimensions(node_tree, file, inner, node_vars) - - #create connections - init_links(node_tree, file, inner, nt_var, node_vars) - - file.write(f"{inner}return {nt_var}\n") - - #create node group - file.write((f"\n{outer}{nt_var} = " - f"{nt_var}_node_group()\n\n")) - return used_vars - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - process_geo_nodes_group(nt, level, node_vars, used_vars) - - def apply_modifier(): - #get object - file.write(f"\t\tname = bpy.context.object.name\n") - file.write(f"\t\tobj = bpy.data.objects[name]\n") - - #set modifier to the one we just created - mod_name = str_to_py_str(nt.name) - file.write((f"\t\tmod = obj.modifiers.new(name = {mod_name}, " - f"type = 'NODES')\n")) - file.write(f"\t\tmod.node_group = {nt_var}\n") - if self.mode == 'ADDON': - apply_modifier() - - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) - else: - context.window_manager.clipboard = file.getvalue() - file.close() - - if self.mode == 'ADDON': - zip_addon(zip_dir) - - #alert user that NTP is finished - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = dir - self.report({'INFO'}, - f"NodeToPython: Saved geometry nodes group to {location}") - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - - def draw(self, context): - self.layout.prop(self, "mode") - -class SelectGeoNodesMenu(bpy.types.Menu): - bl_idname = "NODE_MT_ntp_geo_nodes_selection" - bl_label = "Select Geo Nodes" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - - geo_node_groups = [node for node in bpy.data.node_groups - if node.type == 'GEOMETRY'] - - for geo_ng in geo_node_groups: - op = layout.operator(GeoNodesToPython.bl_idname, text=geo_ng.name) - op.geo_nodes_group_name = geo_ng.name - -class GeoNodesToPythonPanel(bpy.types.Panel): - bl_label = "Geometry Nodes to Python" - bl_idname = "NODE_PT_geo_nodes_to_python" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - @classmethod - def poll(cls, context): - return True - - def draw_header(self, context): - layout = self.layout - - def draw(self, context): - layout = self.layout - col = layout.column() - row = col.row() - - # Disables menu when len of geometry nodes is 0 - geo_node_groups = [node - for node in bpy.data.node_groups - if node.type == 'GEOMETRY'] - geo_node_groups_exist = len(geo_node_groups) > 0 - row.enabled = geo_node_groups_exist - - row.alignment = 'EXPAND' - row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_ntp_geo_nodes_selection", text="Geometry Nodes") \ No newline at end of file diff --git a/geometry/__init__.py b/geometry/__init__.py new file mode 100644 index 0000000..e2707b0 --- /dev/null +++ b/geometry/__init__.py @@ -0,0 +1,13 @@ +if "bpy" in locals(): + import importlib + importlib.reload(node_settings) + importlib.reload(node_tree) + importlib.reload(operator) + importlib.reload(ui) +else: + from . import node_settings + from . import node_tree + from . import operator + from . import ui + +import bpy \ No newline at end of file diff --git a/geometry/node_settings.py b/geometry/node_settings.py new file mode 100644 index 0000000..ffa30c6 --- /dev/null +++ b/geometry/node_settings.py @@ -0,0 +1,675 @@ +from ..utils import ST, NTPNodeSetting + +geo_node_settings : dict[str, list[NTPNodeSetting]] = { + # ATTRIBUTE + 'GeometryNodeAttributeStatistic' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM) + ], + + 'GeometryNodeAttributeDomainSize' : [ + NTPNodeSetting("component", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'GeometryNodeBlurAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 5, 0)) + ], + + 'GeometryNodeCaptureAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM) + ], + + 'GeometryNodeRemoveAttribute' : [], + + 'GeometryNodeStoreNamedAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 2, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 2, 0)) + ], + + 'GeometryNodeAttributeTransfer' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM), + NTPNodeSetting("mapping", ST.ENUM) + ], + + # INPUT + # Input > Constant + 'FunctionNodeInputBool' : [ + NTPNodeSetting("boolean", ST.BOOL) + ], + + 'FunctionNodeInputColor' : [ + NTPNodeSetting("color", ST.VEC4) + ], + + 'GeometryNodeInputImage' : [ + NTPNodeSetting("image", ST.IMAGE, min_version = (3, 5, 0)) + ], + + 'FunctionNodeInputInt' : [ + NTPNodeSetting("integer", ST.INT) + ], + + 'GeometryNodeInputMaterial' : [ + NTPNodeSetting("material", ST.MATERIAL) + ], + + 'FunctionNodeInputString' : [ + NTPNodeSetting("string", ST.STRING) + ], + + 'ShaderNodeValue' : [], + + 'FunctionNodeInputVector' : [ + NTPNodeSetting("vector", ST.VEC3) + ], + + #Input > Group + 'NodeGroupInput' : [], + + # Input > Scene + 'GeometryNodeTool3DCursor' : [], + + 'GeometryNodeCollectionInfo' : [ + NTPNodeSetting("transform_space", ST.ENUM) + ], + + 'GeometryNodeImageInfo' : [], + 'GeometryNodeIsViewport' : [], + + 'GeometryNodeObjectInfo' : [ + NTPNodeSetting("transform_space", ST.ENUM) + ], + + 'GeometryNodeSelfObject' : [], + 'GeometryNodeInputSceneTime' : [], + + + # OUTPUT + 'GeometryNodeViewer' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0)) + ], + + + # GEOMETRY + 'GeometryNodeJoinGeometry' : [], + 'GeometryNodeGeometryToInstance' : [], + + # Geometry > Read + 'GeometryNodeInputID' : [], + 'GeometryNodeInputIndex' : [], + + 'GeometryNodeInputNamedAttribute' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 2, 0)) + ], + + 'GeometryNodeInputNormal' : [], + 'GeometryNodeInputPosition' : [], + 'GeometryNodeInputRadius' : [], + 'GeometryNodeToolSelection' : [], + + # Geometry > Sample + 'GeometryNodeProximity' : [ + NTPNodeSetting("target_element", ST.ENUM) + ], + + 'GeometryNodeIndexOfNearest' : [], + + 'GeometryNodeRaycast' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("mapping", ST.ENUM) + ], + + 'GeometryNodeSampleIndex' : [ + NTPNodeSetting("clamp", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'GeometryNodeSampleNearest' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 4, 0)) + ], + + # Geometry > Write + 'GeometryNodeSetID' : [], + 'GeometryNodeSetPosition' : [], + 'GeometryNodeToolSetSelection' : [], + + # Geometry > Operations + 'GeometryNodeBoundBox' : [], + 'GeometryNodeConvexHull' : [], + + 'GeometryNodeDeleteGeometry' : [ + NTPNodeSetting("domain", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeDuplicateElements' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 2, 0)) + ], + + 'GeometryNodeMergeByDistance' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'GeometryNodeTransform' : [], + 'GeometryNodeSeparateComponents' : [], + + 'GeometryNodeSeparateGeometry' : [ + NTPNodeSetting("domain", ST.ENUM) + ], + + + # CURVE + # Curve > Read + 'GeometryNodeInputCurveHandlePositions' : [], + 'GeometryNodeCurveLength' : [], + 'GeometryNodeInputTangent' : [], + 'GeometryNodeInputCurveTilt' : [], + 'GeometryNodeCurveEndpointSelection' : [], + + 'GeometryNodeCurveHandleTypeSelection' : [ + NTPNodeSetting("handle_type", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM_SET) + ], + + 'GeometryNodeInputSplineCyclic' : [], + 'GeometryNodeSplineLength' : [], + 'GeometryNodeSplineParameter' : [], + 'GeometryNodeInputSplineResolution' : [], + + # Curve > Sample + 'GeometryNodeSampleCurve' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("mode", ST.ENUM), + NTPNodeSetting("use_all_curves", ST.BOOL, min_version = (3, 4, 0)) + ], + + # Curve > Write + 'GeometryNodeSetCurveNormal' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'GeometryNodeSetCurveRadius' : [], + 'GeometryNodeSetCurveTilt' : [], + + 'GeometryNodeSetCurveHandlePositions' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeCurveSetHandles' : [ + NTPNodeSetting("handle_type", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM_SET) + ], + + 'GeometryNodeSetSplineCyclic' : [], + 'GeometryNodeSetSplineResolution' : [], + + 'GeometryNodeCurveSplineType' : [ + NTPNodeSetting("spline_type", ST.ENUM) + ], + + # Curve > Operations + 'GeometryNodeCurveToMesh' : [], + + 'GeometryNodeCurveToPoints' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeDeformCurvesOnSurface' : [], + + 'GeometryNodeFillCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeFilletCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeInterpolateCurves' : [], + + 'GeometryNodeResampleCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeReverseCurve' : [], + 'GeometryNodeSubdivideCurve' : [], + + 'GeometryNodeTrimCurve' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + # Curve > Primitives + 'GeometryNodeCurveArc' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'GeometryNodeCurvePrimitiveBezierSegment' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeCurvePrimitiveCircle' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeCurvePrimitiveLine' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeCurveSpiral' : [], + 'GeometryNodeCurveQuadraticBezier' : [], + + 'GeometryNodeCurvePrimitiveQuadrilateral' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeCurveStar' : [], + + # Curve > Topology + 'GeometryNodeOffsetPointInCurve' : [], + 'GeometryNodeCurveOfPoint' : [], + 'GeometryNodePointsOfCurve' : [], + + + # INSTANCES + 'GeometryNodeInstanceOnPoints' : [], + 'GeometryNodeInstancesToPoints' : [], + + 'GeometryNodeRealizeInstances' : [ + NTPNodeSetting("legacy_behavior", ST.BOOL, min_version = (3, 1, 0), + max_version = (3, 6, 0)) + ], + + 'GeometryNodeRotateInstances' : [], + 'GeometryNodeScaleInstances' : [], + 'GeometryNodeTranslateInstances' : [], + 'GeometryNodeInputInstanceRotation' : [], + 'GeometryNodeInputInstanceScale' : [], + + + # MESH + # Mesh > Read + 'GeometryNodeInputMeshEdgeAngle' : [], + 'GeometryNodeInputMeshEdgeNeighbors' : [], + 'GeometryNodeInputMeshEdgeVertices' : [], + 'GeometryNodeEdgesToFaceGroups' : [], + 'GeometryNodeInputMeshFaceArea' : [], + 'GeometryNodeInputMeshFaceNeighbors' : [], + 'GeometryNodeToolFaceSet' : [], + 'GeometryNodeMeshFaceSetBoundaries' : [], + 'GeometryNodeInputMeshFaceIsPlanar' : [], + 'GeometryNodeInputShadeSmooth' : [], + 'GeometryNodeInputEdgeSmooth' : [], + 'GeometryNodeInputMeshIsland' : [], + 'GeometryNodeInputShortestEdgePaths' : [], + 'GeometryNodeInputMeshVertexNeighbors' : [], + + # Mesh > Sample + 'GeometryNodeSampleNearestSurface' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'GeometryNodeSampleUVSurface' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)) + ], + + # Mesh > Write + 'GeometryNodeToolSetFaceSet' : [], + + 'GeometryNodeSetShadeSmooth' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (4, 0, 0)) + ], + + # Mesh > Operations + 'GeometryNodeDualMesh' : [], + 'GeometryNodeEdgePathsToCurves' : [], + 'GeometryNodeEdgePathsToSelection' : [], + + 'GeometryNodeExtrudeMesh' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'GeometryNodeFlipFaces' : [], + + 'GeometryNodeMeshBoolean' : [ + NTPNodeSetting("operation", ST.ENUM) + ], + + 'GeometryNodeMeshToCurve' : [], + + 'GeometryNodeMeshToPoints' : [ + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeMeshToVolume' : [ + NTPNodeSetting("resolution_mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'GeometryNodeScaleElements' : [ + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("scale_mode", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'GeometryNodeSplitEdges' : [], + 'GeometryNodeSubdivideMesh' : [], + + 'GeometryNodeSubdivisionSurface' : [ + NTPNodeSetting("boundary_smooth", ST.ENUM), + NTPNodeSetting("uv_smooth", ST.ENUM) + ], + + 'GeometryNodeTriangulate' : [ + NTPNodeSetting("ngon_method", ST.ENUM), + NTPNodeSetting("quad_method", ST.ENUM) + ], + + # Mesh > Primitives + 'GeometryNodeMeshCone' : [ + NTPNodeSetting("fill_type", ST.ENUM) + ], + + 'GeometryNodeMeshCube' : [], + + 'GeometryNodeMeshCylinder' : [ + NTPNodeSetting("fill_type", ST.ENUM) + ], + + 'GeometryNodeMeshGrid' : [], + 'GeometryNodeMeshIcoSphere' : [], + + 'GeometryNodeMeshCircle' : [ + NTPNodeSetting("fill_type", ST.ENUM) + ], + + 'GeometryNodeMeshLine' : [ + NTPNodeSetting("count_mode", ST.ENUM), + NTPNodeSetting("mode", ST.ENUM) + ], + + 'GeometryNodeMeshUVSphere' : [], + + # Mesh > Topology + 'GeometryNodeCornersOfFace' : [], + 'GeometryNodeCornersOfVertex' : [], + 'GeometryNodeEdgesOfCorner' : [], + 'GeometryNodeEdgesOfVertex' : [], + 'GeometryNodeFaceOfCorner' : [], + 'GeometryNodeOffsetCornerInFace' : [], + 'GeometryNodeVertexOfCorner' : [], + + # Mesh > UV + 'GeometryNodeUVPackIslands' : [], + + 'GeometryNodeUVUnwrap': [ + NTPNodeSetting("method", ST.ENUM, min_version = (3, 3, 0)) + ], + + + # POINT + 'GeometryNodeDistributePointsInVolume' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'GeometryNodeDistributePointsOnFaces' : [ + NTPNodeSetting("distribute_method", ST.ENUM), + NTPNodeSetting("use_legacy_normal", ST.BOOL, min_version = (3, 5, 0)) + ], + + 'GeometryNodePoints' : [], + 'GeometryNodePointsToCurves' : [], + 'GeometryNodePointsToVertices' : [], + + 'GeometryNodePointsToVolume' : [ + NTPNodeSetting("resolution_mode", ST.ENUM) + ], + + 'GeometryNodeSetPointRadius' : [], + + + # VOLUME + 'GeometryNodeVolumeCube' : [], + 'GeometryNodeVolumeToMesh' : [ + NTPNodeSetting("resolution_mode", ST.ENUM) + ], + + + # SIMULATION + 'GeometryNodeSimulationInput' : [], + 'GeometryNodeSimulationOutput' : [], + + + # MATERIAL + 'GeometryNodeReplaceMaterial' : [], + 'GeometryNodeInputMaterialIndex' : [], + 'GeometryNodeMaterialSelection' : [], + 'GeometryNodeSetMaterial' : [], + 'GeometryNodeSetMaterialIndex' : [], + + + # TEXTURE + 'ShaderNodeTexBrick' : [ + NTPNodeSetting("offset", ST.FLOAT), + NTPNodeSetting("offset_frequency", ST.INT), + NTPNodeSetting("squash", ST.FLOAT), + NTPNodeSetting("squash_frequency", ST.INT) + ], + + 'ShaderNodeTexChecker' : [], + + 'ShaderNodeTexGradient' : [ + NTPNodeSetting("gradient_type", ST.ENUM) + ], + + 'GeometryNodeImageTexture' : [ + NTPNodeSetting("extension", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("interpolation", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'ShaderNodeTexMagic' : [ + NTPNodeSetting("turbulence_depth", ST.INT) + ], + + 'ShaderNodeTexMusgrave' : [ + NTPNodeSetting("musgrave_dimensions", ST.ENUM), + NTPNodeSetting("musgrave_type", ST.ENUM) + ], + + 'ShaderNodeTexNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM) + ], + + 'ShaderNodeTexVoronoi' : [ + NTPNodeSetting("distance", ST.ENUM), + NTPNodeSetting("feature", ST.ENUM), + NTPNodeSetting("voronoi_dimensions", ST.ENUM) + ], + + 'ShaderNodeTexWave' : [ + NTPNodeSetting("bands_direction", ST.ENUM), + NTPNodeSetting("rings_direction", ST.ENUM), + NTPNodeSetting("wave_profile", ST.ENUM), + NTPNodeSetting("wave_type", ST.ENUM) + ], + + 'ShaderNodeTexWhiteNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM) + ], + + + # UTILITIES + 'ShaderNodeMix' : [ + NTPNodeSetting("blend_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_factor", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_result", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("factor_mode", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'FunctionNodeRandomValue' : [ + NTPNodeSetting("data_type", ST.ENUM) + ], + + 'GeometryNodeRepeatInput' : [], + + 'GeometryNodeRepeatOutput' : [ + NTPNodeSetting("inspection_index", ST.INT, min_version = (4, 0, 0)) + ], + + 'GeometryNodeSwitch' : [ + NTPNodeSetting("input_type", ST.ENUM) + ], + + # Utilities > Color + 'ShaderNodeValToRGB' : [ + NTPNodeSetting("color_ramp", ST.COLOR_RAMP) + ], + + 'ShaderNodeRGBCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'FunctionNodeCombineColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'ShaderNodeMixRGB' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], #legacy + + 'FunctionNodeSeparateColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + # Utilities > Text + 'GeometryNodeStringJoin' : [], + 'FunctionNodeReplaceString' : [], + 'FunctionNodeSliceString' : [], + 'FunctionNodeStringLength' : [], + + 'GeometryNodeStringToCurves' : [ + NTPNodeSetting("align_x", ST.ENUM), + NTPNodeSetting("align_y", ST.ENUM), + NTPNodeSetting("font", ST.FONT), + NTPNodeSetting("overflow", ST.ENUM), + NTPNodeSetting("pivot_mode", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'FunctionNodeValueToString' : [], + 'FunctionNodeInputSpecialCharacters' : [], + + # Utilities > Vector + 'ShaderNodeVectorCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'ShaderNodeVectorMath' : [ + NTPNodeSetting("operation", ST.ENUM) + ], + + 'ShaderNodeVectorRotate' : [ + NTPNodeSetting("invert", ST.BOOL), + NTPNodeSetting("rotation_type", ST.ENUM) + ], + + 'ShaderNodeCombineXYZ' : [], + 'ShaderNodeSeparateXYZ' : [], + + # Utilities > Field + 'GeometryNodeAccumulateField' : [ + NTPNodeSetting("data_type", ST.ENUM), + NTPNodeSetting("domain", ST.ENUM) + ], + + 'GeometryNodeFieldAtIndex' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'GeometryNodeFieldOnDomain' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 3, 0)), + NTPNodeSetting("domain", ST.ENUM, min_version = (3, 3, 0)) + ], + + # Utilities > Math + 'FunctionNodeBooleanMath' : [ + NTPNodeSetting("operation", ST.ENUM) + ], + + 'ShaderNodeClamp' : [ + NTPNodeSetting("clamp_type", ST.ENUM) + ], + + 'FunctionNodeCompare' : [ + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("operation", ST.ENUM, min_version = (3, 1, 0)) + ], + + 'FunctionNodeCompareFloats' : [ + NTPNodeSetting("operation", ST.ENUM, max_version = (3, 0, 0)) + ], + + 'ShaderNodeFloatCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'FunctionNodeFloatToInt' : [ + NTPNodeSetting("rounding_mode", ST.ENUM) + ], + + 'ShaderNodeMapRange' : [ + NTPNodeSetting("clamp", ST.BOOL), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("interpolation_type", ST.ENUM) + ], + + 'ShaderNodeMath' : [ + NTPNodeSetting("operation", ST.ENUM), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + # Utilities > Rotation + 'FunctionNodeAlignEulerToVector' : [ + NTPNodeSetting("axis", ST.ENUM), + NTPNodeSetting("pivot_axis", ST.ENUM) + ], + + 'FunctionNodeAxisAngleToRotation' : [], + 'FunctionNodeEulerToRotation' : [], + 'FunctionNodeInvertRotation' : [], + + 'FunctionNodeRotateEuler' : [ + NTPNodeSetting("space", ST.ENUM), + NTPNodeSetting("type", ST.ENUM) + ], + + 'FunctionNodeRotateVector' : [], + 'FunctionNodeRotationToAxisAngle' : [], + 'FunctionNodeRotationToEuler' : [], + 'FunctionNodeRotationToQuaternion' : [], + 'FunctionNodeQuaternionToRotation' : [], + + # MISC + 'GeometryNodeGroup' : [ + NTPNodeSetting("node_tree", ST.NODE_TREE) + ], + + 'NodeFrame' : [ + NTPNodeSetting("label_size", ST.INT), + NTPNodeSetting("shrink", ST.BOOL), + NTPNodeSetting("text", ST.TEXT) + ], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [ + NTPNodeSetting("is_active_output", ST.BOOL) + ], + + 'NodeReroute' : [] + +} \ No newline at end of file diff --git a/geometry/node_tree.py b/geometry/node_tree.py new file mode 100644 index 0000000..73b3565 --- /dev/null +++ b/geometry/node_tree.py @@ -0,0 +1,18 @@ +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) + 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 new file mode 100644 index 0000000..d88d6cc --- /dev/null +++ b/geometry/operator.py @@ -0,0 +1,258 @@ +import bpy +from bpy.types import GeometryNode, GeometryNodeTree +from bpy.types import Node + +from io import StringIO + +from ..ntp_operator import NTP_Operator +from ..utils import * +from .node_tree import NTP_GeoNodeTree +from .node_settings import geo_node_settings + +ITEM = "item" +OBJECT_NAME = "name" +OBJECT = "obj" +MODIFIER = "mod" +GEO_OP_RESERVED_NAMES = {ITEM, + OBJECT_NAME, + OBJECT, + MODIFIER} + +class NTPGeoNodesOperator(NTP_Operator): + bl_idname = "node.ntp_geo_nodes" + bl_label = "Geo Nodes to Python" + bl_options = {'REGISTER', 'UNDO'} + + mode: bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") + + def __init__(self): + super().__init__() + self._settings = geo_node_settings + for name in GEO_OP_RESERVED_NAMES: + self._used_vars[name] = 0 + + 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({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 + + 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 + + if node.bl_idname == 'GeometryNodeSimulationInput': + ntp_nt.sim_inputs.append(node) + + 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) -> None: + """ + Generates a Python function to recreate a node tree + + Parameters: + node_tree (GeometryNodeTree): geometry node tree to be recreated + """ + + nt_var = self._create_var(node_tree.name) + self._node_tree_vars[node_tree] = nt_var + + #initialize node group + self._write(f"#initialize {nt_var} node group", self._outer) + self._write(f"def {nt_var}_node_group():", self._outer) + self._write(f"{nt_var} = bpy.data.node_groups.new(" + f"type = \'GeometryNodeTree\', " + f"name = {str_to_py_str(node_tree.name)})\n") + + if bpy.app.version >= (4, 0, 0): + self._set_geo_tree_properties(node_tree) + + #initialize nodes + self._write(f"#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) + + 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) + + #set look of nodes + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) + + #create connections + self._init_links(node_tree) + + self._write(f"return {nt_var}\n") + + #create node group + 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"{OBJECT_NAME} = bpy.context.object.name", self._outer) + self._write(f"{OBJECT} = bpy.data.objects[{OBJECT_NAME}]", self._outer) + + #set modifier to the one we just created + mod_name = str_to_py_str(nt.name) + self._write(f"{MODIFIER} = obj.modifiers.new(name = {mod_name}, " + f"type = 'NODES')", self._outer) + self._write(f"{MODIFIER}.node_group = {nt_var}", self._outer) + + + def execute(self, context): + #find node group to replicate + nt = bpy.data.node_groups[self.geo_nodes_group_name] + + #set up names to use in generated addon + nt_var = clean_string(nt.name) + + if self.mode == 'ADDON': + self._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") + + self._create_header(nt.name) + self._class_name = clean_string(nt.name, lower = False) + self._init_operator(nt_var, nt.name) + self._write("def execute(self, context):", "\t") + else: + self._file = StringIO("") + + 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("return {'FINISHED'}\n", self._outer) + self._create_menu_func() + self._create_register_func() + self._create_unregister_func() + self._create_main_func() + else: + context.window_manager.clipboard = self._file.getvalue() + self._file.close() + + if self.mode == 'ADDON': + self._zip_addon() + + self._report_finished("geometry node group") + + return {'FINISHED'} \ No newline at end of file diff --git a/geometry/ui.py b/geometry/ui.py new file mode 100644 index 0000000..b3a8283 --- /dev/null +++ b/geometry/ui.py @@ -0,0 +1,55 @@ +import bpy +from bpy.types import Panel +from bpy.types import Menu + +from .operator import NTPGeoNodesOperator + +class NTPGeoNodesPanel(Panel): + bl_label = "Geometry Nodes to Python" + bl_idname = "NODE_PT_geo_nodes" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + col = layout.column() + row = col.row() + + # Disables menu when len of geometry nodes is 0 + geo_node_groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] + geo_node_groups_exist = len(geo_node_groups) > 0 + row.enabled = geo_node_groups_exist + + row.alignment = 'EXPAND' + row.operator_context = 'INVOKE_DEFAULT' + row.menu("NODE_MT_ntp_geo_nodes", text="Geometry Nodes") + +class NTPGeoNodesMenu(Menu): + bl_idname = "NODE_MT_ntp_geo_nodes" + bl_label = "Select Geo Nodes" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + + geo_node_groups = [node_tree for node_tree in bpy.data.node_groups + if node_tree.bl_idname == 'GeometryNodeTree'] + + for node_tree in geo_node_groups: + op = layout.operator(NTPGeoNodesOperator.bl_idname, + text=node_tree.name) + op.geo_nodes_group_name = node_tree.name \ No newline at end of file diff --git a/material/__init__.py b/material/__init__.py new file mode 100644 index 0000000..ceb3a04 --- /dev/null +++ b/material/__init__.py @@ -0,0 +1,11 @@ +if "bpy" in locals(): + import importlib + importlib.reload(node_settings) + importlib.reload(operator) + importlib.reload(ui) +else: + from . import node_settings + from . import operator + from . import ui + +import bpy \ No newline at end of file diff --git a/material/node_settings.py b/material/node_settings.py new file mode 100644 index 0000000..3729d84 --- /dev/null +++ b/material/node_settings.py @@ -0,0 +1,396 @@ +from ..utils import ST, NTPNodeSetting + +shader_node_settings : dict[str, list[NTPNodeSetting]] = { + # INPUT + 'ShaderNodeAmbientOcclusion' : [ + NTPNodeSetting("inside", ST.BOOL), + NTPNodeSetting("only_local", ST.BOOL), + NTPNodeSetting("samples", ST.INT) + ], + + 'ShaderNodeAttribute' : [ + NTPNodeSetting("attribute_name", ST.STRING), #TODO: separate attribute type? + NTPNodeSetting("attribute_type", ST.ENUM) + ], + + 'ShaderNodeBevel' : [ + NTPNodeSetting("samples", ST.INT) + ], + + 'ShaderNodeCameraData' : [], + + 'ShaderNodeVertexColor' : [ + NTPNodeSetting("layer_name", ST.STRING) #TODO: separate color attribute type? + ], + + 'ShaderNodeHairInfo' : [], + 'ShaderNodeFresnel' : [], + 'ShaderNodeNewGeometry' : [], + 'ShaderNodeLayerWeight' : [], + 'ShaderNodeLightPath' : [], + 'ShaderNodeObjectInfo' : [], + 'ShaderNodeParticleInfo' : [], + 'ShaderNodePointInfo' : [], + 'ShaderNodeRGB' : [], + + 'ShaderNodeTangent' : [ + NTPNodeSetting("axis", ST.ENUM), + NTPNodeSetting("direction_type", ST.ENUM), + NTPNodeSetting("uv_map", ST.STRING) #TODO: special UV Map type? + ], + + 'ShaderNodeTexCoord' : [ + NTPNodeSetting("from_instancer", ST.BOOL), + NTPNodeSetting("object", ST.OBJECT) + ], + + 'ShaderNodeUVAlongStroke' : [ + NTPNodeSetting("use_tips", ST.BOOL) + ], + + 'ShaderNodeUVMap' : [ + NTPNodeSetting("from_instancer", ST.BOOL), + NTPNodeSetting("uv_map", ST.STRING) + ], #TODO: see ShaderNodeTangent + + 'ShaderNodeValue' : [], + 'ShaderNodeVolumeInfo' : [], + + 'ShaderNodeWireframe' : [ + NTPNodeSetting("use_pixel_size", ST.BOOL) + ], + + + # OUTPUT + 'ShaderNodeOutputAOV' : [ + NTPNodeSetting("name", ST.STRING) + ], + + 'ShaderNodeOutputLight' : [ + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM) + ], + + 'ShaderNodeOutputLineStyle' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'ShaderNodeOutputMaterial' : [ + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM) + ], + + 'ShaderNodeOutputWorld' : [ + NTPNodeSetting("is_active_output", ST.BOOL), + NTPNodeSetting("target", ST.ENUM) + ], + + + # SHADER + 'ShaderNodeAddShader' : [], + + 'ShaderNodeBsdfAnisotropic' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], + + 'ShaderNodeBackground' : [], + 'ShaderNodeBsdfDiffuse' : [], + 'ShaderNodeEmission' : [], + + 'ShaderNodeBsdfGlass' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], + + 'ShaderNodeBsdfGlossy' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], + + 'ShaderNodeBsdfHair' : [ + NTPNodeSetting("component", ST.ENUM) + ], + + 'ShaderNodeHoldout' : [], + 'ShaderNodeMixShader' : [], + + 'ShaderNodeBsdfPrincipled' : [ + NTPNodeSetting("distribution", ST.ENUM), + NTPNodeSetting("subsurface_method", ST.ENUM) + ], + + 'ShaderNodeBsdfHairPrincipled' : [ + NTPNodeSetting("model", ST.ENUM), + NTPNodeSetting("parametrization", ST.ENUM) + ], + + 'ShaderNodeVolumePrincipled' : [], + + 'ShaderNodeBsdfRefraction' : [ + NTPNodeSetting("distribution", ST.ENUM) + ], + + 'ShaderNodeBsdfSheen' : [ + NTPNodeSetting("distribution", ST.ENUM, min_version = (4, 0, 0)) + ], + + 'ShaderNodeEeveeSpecular' : [], + + 'ShaderNodeSubsurfaceScattering' : [ + NTPNodeSetting("falloff", ST.ENUM) + ], + + 'ShaderNodeBsdfToon' : [ + NTPNodeSetting("component", ST.ENUM) + ], + + 'ShaderNodeBsdfTranslucent' : [], + 'ShaderNodeBsdfTransparent' : [], + 'ShaderNodeBsdfVelvet' : [], + 'ShaderNodeVolumeAbsorption' : [], + 'ShaderNodeVolumeScatter' : [], + + + # TEXTURE + 'ShaderNodeTexBrick' : [ + NTPNodeSetting("offset", ST.FLOAT), + NTPNodeSetting("offset_frequency", ST.INT), + NTPNodeSetting("squash", ST.FLOAT), + NTPNodeSetting("squash_frequency", ST.INT) + ], + + 'ShaderNodeTexChecker' : [], + + 'ShaderNodeTexEnvironment' : [ + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("image_user", ST.IMAGE_USER), + NTPNodeSetting("interpolation", ST.ENUM), + NTPNodeSetting("projection", ST.ENUM) + ], + + 'ShaderNodeTexGradient' : [ + NTPNodeSetting("gradient_type", ST.ENUM) + ], + + 'ShaderNodeTexIES' : [ + NTPNodeSetting("filepath", ST.STRING), #TODO + NTPNodeSetting("ies", ST.TEXT), + NTPNodeSetting("mode", ST.ENUM) + ], + + 'ShaderNodeTexImage' : [ + NTPNodeSetting("extension", ST.ENUM), + NTPNodeSetting("image", ST.IMAGE), + NTPNodeSetting("image_user", ST.IMAGE_USER), + NTPNodeSetting("interpolation", ST.ENUM), + NTPNodeSetting("projection", ST.ENUM), + NTPNodeSetting("projection_blend", ST.FLOAT) + ], + + 'ShaderNodeTexMagic' : [ + NTPNodeSetting("turbulence_depth", ST.INT) + ], + + 'ShaderNodeTexMusgrave' : [ + NTPNodeSetting("musgrave_dimensions", ST.ENUM), + NTPNodeSetting("musgrave_type", ST.ENUM) + ], + + 'ShaderNodeTexNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM), + NTPNodeSetting("normalize", ST.BOOL, min_version = (4, 0, 0)) + ], + + 'ShaderNodeTexPointDensity' : [ + NTPNodeSetting("interpolation", ST.ENUM), + NTPNodeSetting("object", ST.OBJECT), + NTPNodeSetting("particle_color_source", ST.ENUM), + NTPNodeSetting("particle_system", ST.PARTICLE_SYSTEM), + NTPNodeSetting("point_source", ST.ENUM), + NTPNodeSetting("radius", ST.FLOAT), + NTPNodeSetting("resolution", ST.INT), + NTPNodeSetting("space", ST.ENUM), + NTPNodeSetting("vertex_attribute_name", ST.STRING), #TODO + NTPNodeSetting("vertex_color_source", ST.ENUM) + ], + + 'ShaderNodeTexSky' : [ + NTPNodeSetting("air_density", ST.FLOAT), + NTPNodeSetting("altitude", ST.FLOAT), + NTPNodeSetting("dust_density", ST.FLOAT), + NTPNodeSetting("ground_albedo", ST.FLOAT), + NTPNodeSetting("ozone_density", ST.FLOAT), + NTPNodeSetting("sky_type", ST.ENUM), + NTPNodeSetting("sun_direction", ST.VEC3), + NTPNodeSetting("sun_disc", ST.BOOL), + NTPNodeSetting("sun_elevation", ST.FLOAT), + NTPNodeSetting("sun_intensity", ST.FLOAT), + NTPNodeSetting("sun_rotation", ST.FLOAT), + NTPNodeSetting("sun_size", ST.FLOAT), + NTPNodeSetting("turbidity", ST.FLOAT) + ], + + 'ShaderNodeTexVoronoi' : [ + NTPNodeSetting("distance", ST.ENUM), + NTPNodeSetting("feature", ST.ENUM), + NTPNodeSetting("normalize", ST.BOOL, min_version = (4, 0, 0)), + NTPNodeSetting("voronoi_dimensions", ST.ENUM) + ], + + 'ShaderNodeTexWave' : [ + NTPNodeSetting("bands_direction", ST.ENUM), + NTPNodeSetting("rings_direction", ST.ENUM), + NTPNodeSetting("wave_profile", ST.ENUM), + NTPNodeSetting("wave_type", ST.ENUM) + ], + + 'ShaderNodeTexWhiteNoise' : [ + NTPNodeSetting("noise_dimensions", ST.ENUM) + ], + + + # COLOR + 'ShaderNodeBrightContrast' : [], + 'ShaderNodeGamma' : [], + 'ShaderNodeHueSaturation' : [], + 'ShaderNodeInvert' : [], + 'ShaderNodeLightFalloff' : [], + + 'ShaderNodeMix' : [ + NTPNodeSetting("blend_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_factor", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("clamp_result", ST.BOOL, min_version = (3, 4, 0)), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 4, 0)), + NTPNodeSetting("factor_mode", ST.ENUM, min_version = (3, 4, 0)) + ], + + 'ShaderNodeMixRGB' : [ + NTPNodeSetting("blend_type", ST.ENUM), + NTPNodeSetting("use_alpha", ST.BOOL), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'ShaderNodeRGBCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + + # VECTOR + 'ShaderNodeBump' : [ + NTPNodeSetting("invert", ST.BOOL) + ], + + 'ShaderNodeDisplacement' : [ + NTPNodeSetting("space", ST.ENUM) + ], + + 'ShaderNodeMapping' : [ + NTPNodeSetting("vector_type", ST.ENUM) + ], + + 'ShaderNodeNormal' : [], + + 'ShaderNodeNormalMap' : [ + NTPNodeSetting("space", ST.ENUM), + NTPNodeSetting("uv_map", ST.STRING) #TODO + ], + + 'ShaderNodeVectorCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'ShaderNodeVectorDisplacement' : [ + NTPNodeSetting("space", ST.ENUM) + ], + + 'ShaderNodeVectorRotate' : [ + NTPNodeSetting("invert", ST.BOOL), + NTPNodeSetting("rotation_type", ST.ENUM) + ], + + 'ShaderNodeVectorTransform' : [ + NTPNodeSetting("convert_from", ST.ENUM), + NTPNodeSetting("convert_to", ST.ENUM), + NTPNodeSetting("vector_type", ST.ENUM) + ], + + + # CONVERTER + 'ShaderNodeBlackbody' : [], + + 'ShaderNodeClamp' : [ + NTPNodeSetting("clamp_type", ST.ENUM) + ], + + 'ShaderNodeValToRGB' : [ + NTPNodeSetting("color_ramp", ST.COLOR_RAMP) + ], + + 'ShaderNodeCombineColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'ShaderNodeCombineXYZ' : [], + + 'ShaderNodeFloatCurve' : [ + NTPNodeSetting("mapping", ST.CURVE_MAPPING) + ], + + 'ShaderNodeMapRange' : [ + NTPNodeSetting("clamp", ST.BOOL), + NTPNodeSetting("data_type", ST.ENUM, min_version = (3, 1, 0)), + NTPNodeSetting("interpolation_type", ST.ENUM) + ], + + 'ShaderNodeMath' : [ + NTPNodeSetting("operation", ST.ENUM), + NTPNodeSetting("use_clamp", ST.BOOL) + ], + + 'ShaderNodeRGBToBW' : [], + + 'ShaderNodeSeparateColor' : [ + NTPNodeSetting("mode", ST.ENUM, min_version = (3, 3, 0)) + ], + + 'ShaderNodeSeparateXYZ' : [], + 'ShaderNodeShaderToRGB' : [], + + 'ShaderNodeVectorMath' : [ + NTPNodeSetting("operation", ST.ENUM) + ], + + 'ShaderNodeWavelength' : [], + + + # SCRIPT + 'ShaderNodeScript' : [ + NTPNodeSetting("bytecode", ST.STRING), #TODO: test all that + NTPNodeSetting("bytecode_hash", ST.STRING), + NTPNodeSetting("filepath", ST.STRING), + NTPNodeSetting("mode", ST.ENUM), + NTPNodeSetting("script", ST.TEXT), + NTPNodeSetting("use_auto_update", ST.BOOL) + ], + + # MISC + 'ShaderNodeGroup' : [ + NTPNodeSetting('node_tree', ST.NODE_TREE) + ], + + 'NodeFrame' : [ + NTPNodeSetting("label_size", ST.INT), + NTPNodeSetting("shrink", ST.BOOL), + NTPNodeSetting("text", ST.TEXT) + ], + + 'NodeGroupInput' : [], + + 'NodeGroupOutput' : [ + NTPNodeSetting("is_active_output", ST.BOOL) + ], + + 'NodeReroute' : [] +} diff --git a/material/operator.py b/material/operator.py new file mode 100644 index 0000000..0cc8461 --- /dev/null +++ b/material/operator.py @@ -0,0 +1,178 @@ +import bpy +from bpy.types import Node +from bpy.types import ShaderNodeTree + +from io import StringIO + +from ..utils import * +from ..ntp_operator import NTP_Operator +from ..ntp_node_tree import NTP_NodeTree +from .node_settings import shader_node_settings + +MAT_VAR = "mat" +NODE = "node" +SHADER_OP_RESERVED_NAMES = {MAT_VAR, NODE} + +class NTPMaterialOperator(NTP_Operator): + bl_idname = "node.ntp_material" + bl_label = "Material to Python" + bl_options = {'REGISTER', 'UNDO'} + + #TODO: add option for general shader node groups + material_name: bpy.props.StringProperty(name="Node Group") + + def __init__(self): + super().__init__() + self._settings = shader_node_settings + for name in SHADER_OP_RESERVED_NAMES: + self._used_vars[name] = 0 + + def _create_material(self, indent: str): + 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"{ntp_node_tree.var} = bpy.data.node_groups.new(" + f"type = \'ShaderNodeTree\', " + 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 + + Parameters: + node_tree (NodeTree): node tree to be recreated + level (int): number of tabs to use for each line, used with + node groups within node groups and script/add-on differences + """ + + 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 + + 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"#initialize {nt_var} nodes") + + for node in node_tree.nodes: + self._process_node(node, ntp_nt) + + self._set_parents(node_tree) + self._set_locations(node_tree) + self._set_dimensions(node_tree) + + self._init_links(node_tree) + + 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 + 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'} + + #set up names to use in generated addon + 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") + + self._create_header(self.material_name) + self._class_name = clean_string(self.material_name, lower=False) + self._init_operator(mat_var, self.material_name) + + self._write("def execute(self, context):", "\t") + else: + self._file = StringIO("") + + if self.mode == 'ADDON': + self._create_material("\t\t") + elif self.mode == 'SCRIPT': + self._create_material("") + + node_trees_to_process = self._topological_sort(self._base_node_tree) + + for node_tree in node_trees_to_process: + self._process_node_tree(node_tree) + + if self.mode == 'ADDON': + self._write("return {'FINISHED'}", self._outer) + self._create_menu_func() + self._create_register_func() + self._create_unregister_func() + self._create_main_func() + else: + context.window_manager.clipboard = self._file.getvalue() + + self._file.close() + + if self.mode == 'ADDON': + self._zip_addon(self._zip_dir) + + self._report_finished("material") + + return {'FINISHED'} \ No newline at end of file diff --git a/material/ui.py b/material/ui.py new file mode 100644 index 0000000..1bf6181 --- /dev/null +++ b/material/ui.py @@ -0,0 +1,49 @@ +import bpy +from bpy.types import Panel +from bpy.types import Menu +from .operator import NTPMaterialOperator + +class NTPMaterialPanel(Panel): + bl_label = "Material to Python" + bl_idname = "NODE_PT_mat_to_python" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + row = layout.row() + + # Disables menu when there are no materials + materials = [mat for mat in bpy.data.materials if mat.node_tree] + materials_exist = len(materials) > 0 + row.enabled = materials_exist + + row.alignment = 'EXPAND' + row.operator_context = 'INVOKE_DEFAULT' + row.menu("NODE_MT_ntp_material", text="Materials") + +class NTPMaterialMenu(Menu): + bl_idname = "NODE_MT_ntp_material" + bl_label = "Select Material" + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout.column_flow(columns=1) + layout.operator_context = 'INVOKE_DEFAULT' + for mat in bpy.data.materials: + if mat.node_tree: + op = layout.operator(NTPMaterialOperator.bl_idname, + text=mat.name) + op.material_name = mat.name \ No newline at end of file diff --git a/materials.py b/materials.py deleted file mode 100644 index 466bee5..0000000 --- a/materials.py +++ /dev/null @@ -1,315 +0,0 @@ -import bpy -import os - -from .utils import * -from io import StringIO - -node_settings = { - #input - "ShaderNodeAmbientOcclusion" : ["samples", "inside", "only_local"], - "ShaderNodeAttribute" : ["attribute_type", "attribute_name"], - "ShaderNodeBevel" : ["samples"], - "ShaderNodeVertexColor" : ["layer_name"], - "ShaderNodeTangent" : ["direction_type", "axis"], - "ShaderNodeTexCoord" : ["object", "from_instancer"], - "ShaderNodeUVMap" : ["from_instancer", "uv_map"], - "ShaderNodeWireframe" : ["use_pixel_size"], - - #output - "ShaderNodeOutputAOV" : ["name"], - "ShaderNodeOutputMaterial" : ["target"], - - #shader - "ShaderNodeBsdfGlass" : ["distribution"], - "ShaderNodeBsdfGlossy" : ["distribution"], - "ShaderNodeBsdfPrincipled" : ["distribution", "subsurface_method"], - "ShaderNodeBsdfRefraction" : ["distribution"], - "ShaderNodeSubsurfaceScattering" : ["falloff"], - - #texture - "ShaderNodeTexBrick" : ["offset", "offset_frequency", "squash", "squash_frequency"], - "ShaderNodeTexEnvironment" : ["interpolation", "projection", "image_user.frame_duration", "image_user.frame_start", "image_user.frame_offset", "image_user.use_cyclic", "image_user.use_auto_refresh"], - "ShaderNodeTexGradient" : ["gradient_type"], - "ShaderNodeTexIES" : ["mode"], - "ShaderNodeTexImage" : ["interpolation", "projection", "projection_blend", - "extension"], - "ShaderNodeTexMagic" : ["turbulence_depth"], - "ShaderNodeTexMusgrave" : ["musgrave_dimensions", "musgrave_type"], - "ShaderNodeTexNoise" : ["noise_dimensions"], - "ShaderNodeTexPointDensity" : ["point_source", "object", "space", "radius", - "interpolation", "resolution", - "vertex_color_source"], - "ShaderNodeTexSky" : ["sky_type", "sun_direction", "turbidity", - "ground_albedo", "sun_disc", "sun_size", - "sun_intensity", "sun_elevation", - "sun_rotation", "altitude", "air_density", - "dust_density", "ozone_density"], - "ShaderNodeTexVoronoi" : ["voronoi_dimensions", "feature", "distance"], - "ShaderNodeTexWave" : ["wave_type", "rings_direction", "wave_profile"], - "ShaderNodeTexWhiteNoise" : ["noise_dimensions"], - - #color - "ShaderNodeMix" : ["data_type", "clamp_factor", "factor_mode", "blend_type", - "clamp_result"], - - #vector - "ShaderNodeBump" : ["invert"], - "ShaderNodeDisplacement" : ["space"], - "ShaderNodeMapping" : ["vector_type"], - "ShaderNodeNormalMap" : ["space", "uv_map"], - "ShaderNodeVectorDisplacement" : ["space"], - "ShaderNodeVectorRotate" : ["rotation_type", "invert"], - "ShaderNodeVectorTransform" : ["vector_type", "convert_from", "convert_to"], - - #converter - "ShaderNodeClamp" : ["clamp_type"], - "ShaderNodeCombineColor" : ["mode"], - "ShaderNodeMapRange" : ["data_type", "interpolation_type", "clamp"], - "ShaderNodeMath" : ["operation", "use_clamp"], - "ShaderNodeSeparateColor" : ["mode"], - "ShaderNodeVectorMath" : ["operation"] -} - -curve_nodes = {'ShaderNodeFloatCurve', - 'ShaderNodeVectorCurve', - 'ShaderNodeRGBCurve'} - -image_nodes = {'ShaderNodeTexEnvironment', - 'ShaderNodeTexImage'} - -class MaterialToPython(bpy.types.Operator): - bl_idname = "node.material_to_python" - bl_label = "Material to Python" - bl_options = {'REGISTER', 'UNDO'} - - mode : bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full addon") - ] - ) - material_name: bpy.props.StringProperty(name="Node Group") - - def execute(self, context): - #find node group to replicate - nt = bpy.data.materials[self.material_name].node_tree - if nt is None: - self.report({'ERROR'},("NodeToPython: This doesn't seem to be a " - "valid material. Is Use Nodes selected?")) - return {'CANCELLED'} - - #set up names to use in generated addon - mat_var = clean_string(self.material_name) - - if self.mode == 'ADDON': - dir = bpy.path.abspath(context.scene.ntp_options.dir_path) - if not dir or dir == "": - self.report({'ERROR'}, - ("NodeToPython: Save your blender file before using " - "NodeToPython!")) - return {'CANCELLED'} - - zip_dir = os.path.join(dir, mat_var) - addon_dir = os.path.join(zip_dir, mat_var) - if not os.path.exists(addon_dir): - os.makedirs(addon_dir) - file = open(f"{addon_dir}/__init__.py", "w") - - create_header(file, self.material_name) - class_name = clean_string(self.material_name, lower=False) - init_operator(file, class_name, mat_var, self.material_name) - - file.write("\tdef execute(self, context):\n") - else: - file = StringIO("") - - def create_material(indent: str): - file.write((f"{indent}mat = bpy.data.materials.new(" - f"name = {str_to_py_str(self.material_name)})\n")) - file.write(f"{indent}mat.use_nodes = True\n") - - if self.mode == 'ADDON': - create_material("\t\t") - elif self.mode == 'SCRIPT': - create_material("") - - #set to keep track of already created node trees - node_trees = set() - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - #keeps track of all used variables - used_vars = {} - - def is_outermost_node_group(level: int) -> bool: - if self.mode == 'ADDON' and level == 2: - return True - elif self.mode == 'SCRIPT' and level == 0: - return True - return False - - def process_mat_node_group(node_tree, level, node_vars, used_vars): - if is_outermost_node_group(level): - nt_var = create_var(self.material_name, used_vars) - nt_name = self.material_name - else: - nt_var = create_var(node_tree.name, used_vars) - nt_name = node_tree.name - - outer, inner = make_indents(level) - - #initialize node group - file.write(f"{outer}#initialize {nt_var} node group\n") - file.write(f"{outer}def {nt_var}_node_group():\n") - - if is_outermost_node_group(level): #outermost node group - file.write(f"{inner}{nt_var} = mat.node_tree\n") - file.write(f"{inner}#start with a clean node tree\n") - file.write(f"{inner}for node in {nt_var}.nodes:\n") - file.write(f"{inner}\t{nt_var}.nodes.remove(node)\n") - else: - file.write((f"{inner}{nt_var}" - f"= bpy.data.node_groups.new(" - f"type = \'ShaderNodeTree\', " - f"name = {str_to_py_str(nt_name)})\n")) - file.write("\n") - - inputs_set = False - outputs_set = False - - #initialize nodes - file.write(f"{inner}#initialize {nt_var} nodes\n") - - #dictionary to keep track of node->variable name pairs - node_vars = {} - - for node in node_tree.nodes: - if node.bl_idname == 'ShaderNodeGroup': - node_nt = node.node_tree - if node_nt is not None and node_nt not in node_trees: - process_mat_node_group(node_nt, level + 1, node_vars, - used_vars) - node_trees.add(node_nt) - - node_var = create_node(node, file, inner, nt_var, node_vars, - used_vars) - - set_settings_defaults(node, node_settings, file, inner, node_var) - hide_sockets(node, file, inner, node_var) - - if node.bl_idname == 'ShaderNodeGroup': - if node.node_tree is not None: - file.write((f"{inner}{node_var}.node_tree = " - f"bpy.data.node_groups" - f"[\"{node.node_tree.name}\"]\n")) - elif node.bl_idname == 'NodeGroupInput' and not inputs_set: - group_io_settings(node, file, inner, "input", nt_var, node_tree) - inputs_set = True - - elif node.bl_idname == 'NodeGroupOutput' and not outputs_set: - group_io_settings(node, file, inner, "output", nt_var, node_tree) - outputs_set = True - - elif node.bl_idname in image_nodes and self.mode == 'ADDON': - img = node.image - if img is not None and img.source in {'FILE', 'GENERATED', 'TILED'}: - save_image(img, addon_dir) - load_image(img, file, inner, f"{node_var}.image") - image_user_settings(node, file, inner, node_var) - - elif node.bl_idname == 'ShaderNodeValToRGB': - color_ramp_settings(node, file, inner, node_var) - - elif node.bl_idname in curve_nodes: - curve_node_settings(node, file, inner, node_var) - - if self.mode == 'ADDON': - set_input_defaults(node, file, inner, node_var, addon_dir) - else: - set_input_defaults(node, file, inner, node_var) - set_output_defaults(node, file, inner, node_var) - - set_parents(node_tree, file, inner, node_vars) - set_locations(node_tree, file, inner, node_vars) - set_dimensions(node_tree, file, inner, node_vars) - - init_links(node_tree, file, inner, nt_var, node_vars) - - file.write(f"\n{outer}{nt_var}_node_group()\n\n") - - if self.mode == 'ADDON': - level = 2 - else: - level = 0 - process_mat_node_group(nt, level, node_vars, used_vars) - - if self.mode == 'ADDON': - file.write("\t\treturn {'FINISHED'}\n\n") - - create_menu_func(file, class_name) - create_register_func(file, class_name) - create_unregister_func(file, class_name) - create_main_func(file) - else: - context.window_manager.clipboard = file.getvalue() - - file.close() - - if self.mode == 'ADDON': - zip_addon(zip_dir) - if self.mode == 'SCRIPT': - location = "clipboard" - else: - location = dir - self.report({'INFO'}, f"NodeToPython: Saved material to {location}") - return {'FINISHED'} - - def invoke(self, context, event): - return context.window_manager.invoke_props_dialog(self) - def draw(self, context): - self.layout.prop(self, "mode") - -class SelectMaterialMenu(bpy.types.Menu): - bl_idname = "NODE_MT_npt_mat_selection" - bl_label = "Select Material" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - for mat in bpy.data.materials: - op = layout.operator(MaterialToPython.bl_idname, text=mat.name) - op.material_name = mat.name - -class MaterialToPythonPanel(bpy.types.Panel): - bl_label = "Material to Python" - bl_idname = "NODE_PT_mat_to_python" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - @classmethod - def poll(cls, context): - return True - - def draw_header(self, context): - layout = self.layout - - def draw(self, context): - layout = self.layout - row = layout.row() - - # Disables menu when there are no materials - materials = bpy.data.materials - materials_exist = len(materials) > 0 - row.enabled = materials_exist - - row.alignment = 'EXPAND' - row.operator_context = 'INVOKE_DEFAULT' - row.menu("NODE_MT_npt_mat_selection", text="Materials") \ No newline at end of file diff --git a/ntp_node_tree.py b/ntp_node_tree.py new file mode 100644 index 0000000..a551d01 --- /dev/null +++ b/ntp_node_tree.py @@ -0,0 +1,16 @@ +from bpy.types import NodeTree +import bpy + +class NTP_NodeTree: + def __init__(self, node_tree: NodeTree, var: str): + # Blender node tree object being copied + self.node_tree: NodeTree = node_tree + + # The variable named for the regenerated node tree + self.var: str = var + + 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 new file mode 100644 index 0000000..732985a --- /dev/null +++ b/ntp_operator.py @@ -0,0 +1,1214 @@ +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 * + +INDEX = "i" +IMAGE_DIR_NAME = "imgs" +IMAGE_PATH = "image_path" +BASE_DIR = "base_dir" + +RESERVED_NAMES = { + INDEX, + IMAGE_DIR_NAME, + IMAGE_PATH, + BASE_DIR + } + +#node input sockets that are messy to set default values for +DONT_SET_DEFAULTS = {'NodeSocketGeometry', + 'NodeSocketShader', + 'NodeSocketVirtual'} + +class NTP_Operator(Operator): + """ + "Abstract" base class for all NTP operators. Blender types and abstraction + don't seem to mix well, but this should only be inherited from + """ + + bl_idname = "" + bl_label = "" + + mode: bpy.props.EnumProperty( + name="Mode", + items=[ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full addon") + ] + ) + + # node tree input sockets that have default properties + if bpy.app.version < (4, 0, 0): + default_sockets_v3 = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} + else: + nondefault_sockets_v4 = { + bpy.types.NodeTreeInterfaceSocketCollection, + bpy.types.NodeTreeInterfaceSocketGeometry, + bpy.types.NodeTreeInterfaceSocketImage, + bpy.types.NodeTreeInterfaceSocketMaterial, + bpy.types.NodeTreeInterfaceSocketObject, + bpy.types.NodeTreeInterfaceSocketShader, + bpy.types.NodeTreeInterfaceSocketTexture + } + + def __init__(self): + super().__init__() + + # File (TextIO) or string (StringIO) the add-on/script is generated into + self._file: TextIO = None + + # Path to the current directory + self._dir: str = None + + # Path to the directory of the zip file + self._zip_dir: str = None + + # Path to the directory for the generated addon + self._addon_dir: str = None + + # Class named for the generated operator + self._class_name: str = None + + # 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] = {} + + # Dictionary to keep track of variables->usage count pairs + self._used_vars: dict[str, int] = {} + + # Dictionary used for setting node properties + self._settings: dict[str, list[(str, ST)]] = {} + + for name in RESERVED_NAMES: + self._used_vars[name] = 0 + + 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: + """ + Finds/creates directories to save add-on to + """ + # find base directory to save new addon + self._dir = bpy.path.abspath(context.scene.ntp_options.dir_path) + if not self._dir or self._dir == "": + self.report({'ERROR'}, + ("NodeToPython: Save your blend file before using " + "NodeToPython!")) # TODO: Still valid?? + return {'CANCELLED'} # TODO + + self._zip_dir = os.path.join(self._dir, nt_var) + self._addon_dir = os.path.join(self._zip_dir, nt_var) + + if not os.path.exists(self._addon_dir): + os.makedirs(self._addon_dir) + + def _create_header(self, name: str) -> None: + """ + Sets up the bl_info and imports the Blender API + + Parameters: + file (TextIO): the file for the generated add-on + name (str): name of the add-on + """ + + self._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: + """ + Initializes the add-on's operator + + Parameters: + file (TextIO): the file for the generated add-on + name (str): name for the class + idname (str): name for the operator + label (str): appearence inside Blender + """ + self._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]: + """ + 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 + """ + 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: + """ + Creates a unique variable name for a node tree + + Parameters: + name (str): basic string we'd like to create the variable name out of + + Returns: + clean_name (str): variable name for the node tree + """ + if name == "": + name = "unnamed" + clean_name = clean_string(name) + var = clean_name + if var in self._used_vars: + self._used_vars[var] += 1 + return f"{clean_name}_{self._used_vars[var]}" + else: + self._used_vars[var] = 0 + return clean_name + + def _create_node(self, node: Node, node_tree_var: str) -> str: + """ + Initializes a new node with location, dimension, and label info + + Parameters: + node (Node): node to be copied + node_tree_var (str): variable name for the node tree + Returns: + node_var (str): variable name for the node + """ + + self._write(f"#node {node.name}") + + node_var = self._create_var(node.name) + self._node_vars[node] = node_var + + 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"{node_var}.label = {str_to_py_str(node.label)}") + + # name + self._write(f"{node_var}.name = {str_to_py_str(node.name)}") + + # color + if node.use_custom_color: + 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"{node_var}.mute = True") + + return node_var + + def _set_settings_defaults(self, node: Node) -> None: + """ + Sets the defaults for any settings a node may have + + Parameters: + node (Node): the node object we're copying settings from + node_var (str): name of the variable we're using for the node in our add-on + """ + if node.bl_idname not in self._settings: + 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 setting in self._settings[node.bl_idname]: + + attr_name = setting.name + st = setting.st + + if not hasattr(node, attr_name): + if (bpy.app.version >= setting.min_version and + bpy.app.version <= setting.max_version): + self.report({'WARNING'}, + f"NodeToPython: Couldn't find attribute " + f"\"{attr_name}\" for node {node.name} of type " + f"{node.bl_idname}") + continue + attr = getattr(node, attr_name, None) + if attr is None: + continue + + setting_str = f"{node_var}.{attr_name}" + if st == ST.ENUM: + if attr != '': + self._write(f"{setting_str} = {enum_to_py_str(attr)}") + elif st == ST.ENUM_SET: + self._write(f"{setting_str} = {attr}") + elif st == ST.STRING: + self._write(f"{setting_str} = {str_to_py_str(attr)}") + elif st == ST.BOOL or st == ST.INT or st == ST.FLOAT: + self._write(f"{setting_str} = {attr}") + elif st == ST.VEC1: + self._write(f"{setting_str} = {vec1_to_py_str(attr)}") + elif st == ST.VEC2: + self._write(f"{setting_str} = {vec2_to_py_str(attr)}") + elif st == ST.VEC3: + self._write(f"{setting_str} = {vec3_to_py_str(attr)}") + elif st == ST.VEC4: + self._write(f"{setting_str} = {vec4_to_py_str(attr)}") + elif st == ST.COLOR: + self._write(f"{setting_str} = {color_to_py_str(attr)}") + elif st == ST.MATERIAL: + name = str_to_py_str(attr.name) + self._write((f"if {name} in bpy.data.materials:")) + self._write((f"\t{setting_str} = bpy.data.materials[{name}]")) + elif st == ST.OBJECT: + name = str_to_py_str(attr.name) + self._write((f"if {name} in bpy.data.objects:")) + self._write((f"\t{setting_str} = bpy.data.objects[{name}]")) + elif st == ST.COLOR_RAMP: + self._color_ramp_settings(node, attr_name) + elif st == ST.CURVE_MAPPING: + self._curve_mapping_settings(node, attr_name) + elif st == ST.NODE_TREE: + self._node_tree_settings(node, attr_name) + elif st == ST.IMAGE: + if self._addon_dir is not None and attr is not None: + if attr.source in {'FILE', 'GENERATED', 'TILED'}: + self._save_image(attr) + self._load_image(attr, f"{node_var}.{attr_name}") + elif st == ST.IMAGE_USER: + self._image_user_settings(attr, f"{node_var}.{attr_name}") + + if bpy.app.version < (4, 0, 0): + def _set_group_socket_defaults(self, socket_interface: NodeSocketInterface, + 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 + socket_var (str): variable name for the socket + """ + if socket_interface.type not in self.default_sockets_v3: + return + + if socket_interface.type == 'RGBA': + dv = vec4_to_py_str(socket_interface.default_value) + elif socket_interface.type == 'VECTOR': + dv = vec3_to_py_str(socket_interface.default_value) + else: + dv = socket_interface.default_value + self._write(f"{socket_var}.default_value = {dv}") + + # min value + if hasattr(socket_interface, "min_value"): + min_val = socket_interface.min_value + 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"{socket_var}.max_value = {max_val}") + + 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 (Node) : group input/output node + io (str): whether we're generating the input or output settings + ntp_node_tree (NTP_NodeTree): node tree that we're generating + input and output settings for + """ + node_tree_var = ntp_node_tree.var + node_tree = ntp_node_tree.node_tree + + if io == "input": + io_sockets = node.outputs + io_socket_interfaces = node_tree.inputs + else: + io_sockets = node.inputs + io_socket_interfaces = node_tree.outputs + + self._write(f"#{node_tree_var} {io}s") + for i, inout in enumerate(io_sockets): + if inout.bl_idname == 'NodeSocketVirtual': + continue + self._write(f"#{io} {inout.name}") + idname = enum_to_py_str(inout.bl_idname) + name = str_to_py_str(inout.name) + self._write(f"{node_tree_var}.{io}s.new({idname}, {name})") + socket_interface = io_socket_interfaces[i] + socket_var = f"{node_tree_var}.{io}s[{i}]" + + self._set_group_socket_defaults(socket_interface, 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"{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"{socket_var}.attribute_domain = {ad}") + + # tooltip + if socket_interface.description != "": + description = str_to_py_str(socket_interface.description) + self._write(f"{socket_var}.description = {description}") + + # hide_value + if socket_interface.hide_value is True: + 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"{socket_var}.hide_in_modifier = True") + + self._write("") + self._write("") + + elif bpy.app.version >= (4, 0, 0): + def _set_tree_socket_defaults(self, socket_interface: NodeTreeInterfaceSocket, + socket_var: str) -> None: + """ + 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 + socket_var (str): variable name for the socket + """ + if type(socket_interface) in self.nondefault_sockets_v4: + return + + dv = socket_interface.default_value + + if type(socket_interface) == bpy.types.NodeTreeInterfaceSocketColor: + dv = vec4_to_py_str(dv) + elif type(dv) in {mathutils.Vector, mathutils.Euler}: + dv = vec3_to_py_str(dv) + elif type(dv) == bpy_types.bpy_prop_array: + dv = array_to_py_str(dv) + elif type(dv) == str: + dv = str_to_py_str(dv) + 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"{socket_var}.min_value = {min_val}") + # max value + if hasattr(socket_interface, "min_value"): + max_val = socket_interface.max_value + self._write(f"{socket_var}.max_value = {max_val}") + + def _create_socket(self, socket: NodeTreeInterfaceSocket, + parent: NodeTreeInterfacePanel, + panel_dict: dict[NodeTreeInterfacePanel, str], + ntp_nt: NTP_NodeTree) -> None: + """ + Initialize a new tree socket + + Helper function to _process_items() + + Parameters: + socket (NodeTreeInterfaceSocket): the socket to recreate + parent (NodeTreeInterfacePanel): parent panel of the socket + (possibly None) + panel_dict (dict[NodeTreeInterfacePanel, str]: panel -> variable + ntp_nt (NTP_NodeTree): owner of the socket + """ + + self._write(f"#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: + 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") + + # tooltip + if socket.description != "": + description = str_to_py_str(socket.description) + self._write(f"{socket_var}.description = {description}") + + self._write("") + + 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 + + Helper function to _process_items() + + 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 + """ + + 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 panel.description != "": + description = str_to_py_str(panel.description) + self._write(f"{panel_var}.description = {description}") + + panel_dict[panel] = panel_var + + if len(panel.interface_items) > 0: + self._process_items(panel, panel_dict, items_processed, ntp_nt) + + self._write("") + + 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 + + Helper function to _tree_interface_settings() + + 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 + + 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) + + 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 (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 + socket_var = f"{node_var}.inputs[{i}]" + + # colors + if input.bl_idname == 'NodeSocketColor': + default_val = vec4_to_py_str(input.default_value) + + # vector types + elif "Vector" in input.bl_idname: + default_val = vec3_to_py_str(input.default_value) + + # 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) + + # images + elif input.bl_idname == 'NodeSocketImage': + img = input.default_value + if img is not None and self._addon_dir != None: # write in a better way + self._save_image(img) + self._load_image(img, f"{socket_var}.default_value") + default_val = None + + # materials + elif input.bl_idname == 'NodeSocketMaterial': + self._in_file_inputs(input, socket_var, "materials") + default_val = None + + # collections + elif input.bl_idname == 'NodeSocketCollection': + self._in_file_inputs(input, socket_var, "collections") + default_val = None + + # objects + elif input.bl_idname == 'NodeSocketObject': + self._in_file_inputs(input, socket_var, "objects") + default_val = None + + # textures + elif input.bl_idname == 'NodeSocketTexture': + 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"#{input.identifier}") + self._write(f"{socket_var}.default_value = {default_val}") + self._write("") + + def _set_output_defaults(self, node: Node) -> None: + """ + Some output sockets need default values set. It's rather annoying + + Parameters: + node (Node): node for the output we're setting + """ + # TODO: probably should define elsewhere + output_default_nodes = {'ShaderNodeValue', + 'ShaderNodeRGB', + 'ShaderNodeNormal', + 'CompositorNodeValue', + 'CompositorNodeRGB', + 'CompositorNodeNormal'} + + 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"{node_var}.outputs[0].default_value = {dv}") + + 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 + socket_var (str): variable name we're using for the socket + type (str): from what section of bpy.data to pull the default value from + """ + + if input.default_value is None: + return + name = str_to_py_str(input.default_value.name) + self._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 (Node): node object we're copying settings from + color_ramp_name (str): name of the color ramp to be copied + """ + + color_ramp: bpy.types.ColorRamp = getattr(node, color_ramp_name) + if not color_ramp: + raise ValueError(f"No color ramp named \"{color_ramp_name}\" found") + + node_var = self._node_vars[node] + + # settings + 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}") + + #hue interpolation + hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) + self._write(f"{ramp_str}.hue_interpolation = {hue_interpolation}") + + #interpolation + interpolation = enum_to_py_str(color_ramp.interpolation) + self._write(f"{ramp_str}.interpolation = {interpolation}") + self._write("") + + # key points + self._write(f"#initialize color ramp elements") + self._write((f"{ramp_str}.elements.remove" + f"({ramp_str}.elements[0])")) + for i, element in enumerate(color_ramp.elements): + element_var = self._create_var(f"{node_var}_cre_{i}") + if i == 0: + self._write(f"{element_var} = {ramp_str}.elements[{i}]") + self._write(f"{element_var}.position = {element.position}") + else: + self._write(f"{element_var} = {ramp_str}.elements" + f".new({element.position})") + + self._write(f"{element_var}.alpha = {element.alpha}") + color_str = vec4_to_py_str(element.color) + self._write(f"{element_var}.color = {color_str}\n") + + def _curve_mapping_settings(self, node: Node, + curve_mapping_name: str) -> None: + """ + Sets defaults for Float, Vector, and Color curves + + Parameters: + node (Node): curve node we're copying settings from + curve_mapping_name (str): name of the curve mapping to be set + """ + + mapping = getattr(node, curve_mapping_name) + if not mapping: + raise ValueError((f"Curve mapping \"{curve_mapping_name}\" not found " + f"in node \"{node.bl_idname}\"")) + + node_var = self._node_vars[node] + + # mapping settings + 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}") + # tone + tone = enum_to_py_str(mapping.tone) + 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}") + # white level + w_lvl_str = vec3_to_py_str(mapping.white_level) + 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}") + min_y = mapping.clip_min_y + 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}") + max_y = mapping.clip_max_y + 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}") + + # create curves + for i, curve in enumerate(mapping.curves): + self._create_curve_map(node, i, curve, curve_mapping_name) + + # update curve + self._write(f"#update curve after changes") + self._write(f"{mapping_var}.update()") + + def _create_curve_map(self, node: Node, i: int, curve: bpy.types.CurveMap, + curve_mapping_name: str) -> None: + """ + Helper function to create the ith curve of a node's curve mapping + + Parameters: + node (Node): the node with a curve mapping + i (int): index of the CurveMap within the mapping + curve (bpy.types.CurveMap): the curve map to recreate + curve_mapping_name (str): attribute name of the recreated curve mapping + """ + node_var = self._node_vars[node] + + self._write(f"#curve {i}") + curve_i_var = self._create_var(f"{node_var}_curve_{i}") + self._write(f"{curve_i_var} = " + f"{node_var}.{curve_mapping_name}.curves[{i}]") + + # Remove default points when CurveMap is initialized with more than + # two points (just CompositorNodeHueCorrect) + if (node.bl_idname == 'CompositorNodeHueCorrect'): + self._write(f"for {INDEX} in range" + f"(len({curve_i_var}.points.values()) - 1, 1, -1):") + self._write(f"\t{curve_i_var}.points.remove(" + f"{curve_i_var}.points[{INDEX}])") + + for j, point in enumerate(curve.points): + self._create_curve_map_point(j, point, curve_i_var) + + def _create_curve_map_point(self, j: int, point: bpy.types.CurveMapPoint, + curve_i_var: str) -> None: + """ + Helper function to recreate a curve map point + + Parameters: + j (int): index of the point within the curve map + point (CurveMapPoint): point to recreate + curve_i_var (str): variable name of the point's curve map + """ + point_j_var = self._create_var(f"{curve_i_var}_point_{j}") + + loc = point.location + loc_str = f"{loc[0]}, {loc[1]}" + if j < 2: + self._write(f"{point_j_var} = {curve_i_var}.points[{j}]") + self._write(f"{point_j_var}.location = ({loc_str})") + else: + self._write(f"{point_j_var} = {curve_i_var}.points.new({loc_str})") + + handle = enum_to_py_str(point.handle_type) + self._write(f"{point_j_var}.handle_type = {handle}") + + def _node_tree_settings(self, node: Node, attr_name: str) -> None: + """ + Processes node tree of group node if one is present + + Parameters: + node (Node): the group node + attr_name (str): name of the node tree attribute + """ + node_tree = getattr(node, attr_name) + if node_tree is None: + return + if node_tree in self._node_tree_vars: + nt_var = self._node_tree_vars[node_tree] + node_var = self._node_vars[node] + self._write(f"{node_var}.{attr_name} = {nt_var}") + else: + self.report({'WARNING'}, (f"NodeToPython: Node tree dependency graph " + f"wasn't properly initialized")) + + def _save_image(self, img: bpy.types.Image) -> None: + """ + Saves an image to an image directory of the add-on + + Parameters: + img (bpy.types.Image): image to be saved + """ + + if img is None: + return + + # create image dir if one doesn't exist + img_dir = os.path.join(self._addon_dir, IMAGE_DIR_NAME) + if not os.path.exists(img_dir): + os.mkdir(img_dir) + + # save the image + img_str = img_to_py_str(img) + img_path = f"{img_dir}/{img_str}" + if not os.path.exists(img_path): + img.save_render(img_path) + + def _load_image(self, img: bpy.types.Image, 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 + img_var (str): variable name to be used for the image + """ + + if img is None: + return + + img_str = img_to_py_str(img) + + # TODO: convert to special variables + self._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"#set image settings") + + # source + source = enum_to_py_str(img.source) + self._write(f"{img_var}.source = {source}") + + # color space settings + color_space = enum_to_py_str(img.colorspace_settings.name) + self._write(f"{img_var}.colorspace_settings.name = {color_space}") + + # alpha mode + alpha_mode = enum_to_py_str(img.alpha_mode) + self._write(f"{img_var}.alpha_mode = {alpha_mode}") + + def _image_user_settings(self, img_user: bpy.types.ImageUser, + img_user_var: str) -> None: + """ + Replicate the image user of an image node + + Parameters + img_usr (bpy.types.ImageUser): image user to be copied + img_usr_var (str): variable name for the generated image user + """ + + img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", + "frame_start", "tile", "use_auto_refresh", "use_cyclic"] + + for img_usr_attr in img_usr_attrs: + self._write(f"{img_user_var}.{img_usr_attr} = " + f"{getattr(img_user, img_usr_attr)}") + + def _set_parents(self, node_tree: NodeTree) -> None: + """ + Sets parents for all nodes, mostly used to put nodes in frames + + Parameters: + 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"#Set parents") + parent_comment = True + node_var = self._node_vars[node] + parent_var = self._node_vars[node.parent] + self._write(f"{node_var}.parent = {parent_var}") + self._write("") + + def _set_locations(self, node_tree: NodeTree) -> None: + """ + Set locations for all nodes + + Parameters: + node_tree (NodeTree): node tree we're obtaining nodes from + """ + + self._write(f"#Set locations") + for node in node_tree.nodes: + node_var = self._node_vars[node] + self._write(f"{node_var}.location " + f"= ({node.location.x}, {node.location.y})") + self._write("") + + def _set_dimensions(self, node_tree: NodeTree) -> None: + """ + Set dimensions for all nodes + + Parameters: + node_tree (NodeTree): node tree we're obtaining nodes from + """ + self._write(f"#Set dimensions") + for node in node_tree.nodes: + node_var = self._node_vars[node] + self._write(f"{node_var}.width, {node_var}.height " + f"= {node.width}, {node.height}") + self._write("") + + def _init_links(self, node_tree: NodeTree) -> None: + """ + Create all the links between nodes + + Parameters: + node_tree (NodeTree): node tree to copy, with variable + """ + + 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 + + """ + Blender's socket dictionary doesn't guarantee + unique keys, which has caused much wailing and + gnashing of teeth. This is a quick fix that + doesn't run quick + """ + # TODO: try using index() method + for i, item in enumerate(link.from_node.outputs.items()): + if item[1] == input_socket: + input_idx = i + break + + out_node_var = self._node_vars[link.to_node] + output_socket = link.to_socket + + for i, item in enumerate(link.to_node.inputs.items()): + if item[1] == output_socket: + output_idx = i + break + + self._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: Node) -> None: + """ + Hide hidden sockets + + Parameters: + 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"{node_var}.inputs[{i}].hide = True") + for i, socket in enumerate(node.outputs): + if socket.hide is True: + 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):", "") + 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():", "") + 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():", "") + 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__\":", "") + self._write("register()", "\t") + + def _zip_addon(self) -> None: + """ + Zips up the addon and removes the directory + """ + shutil.make_archive(self._zip_dir, "zip", self._zip_dir) + shutil.rmtree(self._zip_dir) + + # ABSTRACT + def _process_node(self, node: Node, ntp_node_tree: NTP_NodeTree) -> None: + return + + # ABSTRACT + def _process_node_tree(self, node_tree: NodeTree) -> None: + return + + def _report_finished(self, object: str): + """ + Alert user that NTP is finished + + Parameters: + object (str): the copied node tree or encapsulating structure + (geometry node modifier, material, scene, etc.) + """ + if self.mode == 'SCRIPT': + location = "clipboard" + else: + location = self._dir + self.report({'INFO'}, f"NodeToPython: Saved {object} to {location}") + + # ABSTRACT + def execute(self): + return {'FINISHED'} + + def invoke(self, context, event): + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + self.layout.prop(self, "mode") diff --git a/utils.py b/utils.py index d66632b..08d68fb 100644 --- a/utils.py +++ b/utils.py @@ -1,20 +1,60 @@ import bpy +from bpy_types import bpy_types import mathutils -import os +from enum import Enum, auto +import keyword import re -import shutil -from typing import TextIO, Tuple - -image_dir_name = "imgs" - -#node input sockets that are messy to set default values for -dont_set_defaults = {'NodeSocketGeometry', - 'NodeSocketShader', - 'NodeSocketVirtual'} +from typing import NamedTuple + +class ST(Enum): + """ + Settings Types + """ + # Primitives + ENUM = auto() + ENUM_SET = auto() + STRING = auto() + BOOL = auto() + INT = auto() + FLOAT = auto() + VEC1 = auto() + VEC2 = auto() + VEC3 = auto() + VEC4 = auto() + COLOR = auto() + + # Special settings + COLOR_RAMP = auto() + CURVE_MAPPING = auto() + NODE_TREE = auto() + + # Asset Library + MATERIAL = auto() # Handle with asset library + OBJECT = auto() # Handle with asset library + + # Image + IMAGE = auto() #needs refactor + IMAGE_USER = auto() #needs refactor + MOVIE_CLIP = auto() #unimplmented + + TEXTURE = auto() #unimplemented + TEXT = auto() #unimplemented + SCENE = auto() #unimplemented + PARTICLE_SYSTEM = auto() #unimplemented + FONT = auto() #unimplemented + MASK = auto() #unimplemented + CRYPTOMATTE_ENTRIES = auto() #unimplemented + IMAGE_FORMAT_SETTINGS = auto() #unimplemented + FILE_SLOTS = auto() #unimplemented + LAYER_SLOTS = auto() #unimplemented + +class NTPNodeSetting(NamedTuple): + name: str + st: ST + min_version: tuple = (3, 0, 0) + max_version: tuple = (4, 1, 0) -#node tree input sockets that have default properties -default_sockets = {'VALUE', 'INT', 'BOOLEAN', 'VECTOR', 'RGBA'} def clean_string(string: str, lower: bool = True) -> str: """ @@ -24,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: """ @@ -56,740 +102,94 @@ def str_to_py_str(string: str) -> str: """ return f"\"{string}\"" -def vec3_to_py_str(vec) -> str: +def vec1_to_py_str(vec1) -> str: """ - Converts a 3D vector to a string usable by the add-on + Converts a 1D vector to a string usable by the add-on Parameters: - vec (mathutils.Vector): a 3d vector + vec1: a 1d vector Returns: - (str): string version + (str): string representation of the vector """ - return f"({vec[0]}, {vec[1]}, {vec[2]})" + return f"[{vec1[0]}]" -def vec4_to_py_str(vec) -> str: +def vec2_to_py_str(vec2) -> str: """ - Converts a 4D vector to a string usable by the add-on + Converts a 2D vector to a string usable by the add-on Parameters: - vec (mathutils.Vector): a 4d vector - - Returns: - (str): string version - """ - return f"({vec[0]}, {vec[1]}, {vec[2]}, {vec[3]})" + vec2: a 2D vector -def img_to_py_str(img) -> str: - """ - Converts a Blender image into its string - - Paramters: - img (bpy.types.Image): a Blender image - Returns: - (str): string version - """ - name = img.name.split('.', 1)[0] - format = img.file_format.lower() - return f"{name}.{format}" - -def create_header(file: TextIO, name: str): - """ - Sets up the bl_info and imports the Blender API - - Parameters: - file (TextIO): the file for the generated add-on - name (str): name of the add-on - """ - - file.write("bl_info = {\n") - file.write(f"\t\"name\" : \"{name}\",\n") - file.write("\t\"author\" : \"Node To Python\",\n") - file.write("\t\"version\" : (1, 0, 0),\n") - file.write(f"\t\"blender\" : {bpy.app.version},\n") - file.write("\t\"location\" : \"Object\",\n") - file.write("\t\"category\" : \"Node\"\n") - file.write("}\n") - file.write("\n") - file.write("import bpy\n") - file.write("import os\n") - file.write("\n") - -def init_operator(file: TextIO, name: str, idname: str, label: str): + (str): string representation of the vector """ - Initializes the add-on's operator + return f"({vec2[0]}, {vec2[1]})" - Parameters: - file (TextIO): the file for the generated add-on - name (str): name for the class - idname (str): name for the operator - label (str): appearence inside Blender - """ - file.write(f"class {name}(bpy.types.Operator):\n") - file.write(f"\tbl_idname = \"object.{idname}\"\n") - file.write(f"\tbl_label = \"{label}\"\n") - file.write("\tbl_options = {\'REGISTER\', \'UNDO\'}\n") - file.write("\n") - -def create_var(name: str, used_vars: dict) -> str: - """ - Creates a unique variable name for a node tree - - Parameters: - name (str): basic string we'd like to create the variable name out of - used_vars (dict): dictionary containing variable names and usage counts - - Returns: - clean_name (str): variable name for the node tree +def vec3_to_py_str(vec3) -> str: """ - if name == "": - name = "unnamed" - clean_name = clean_string(name) - var = clean_name - if var in used_vars: - used_vars[var] += 1 - return f"{clean_name}_{used_vars[var]}" - else: - used_vars[var] = 0 - return clean_name - -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. + Converts a 3D vector to a string usable by the add-on Parameters: - level (int): base number of indentations need + vec3: a 3d vector Returns: - outer (str): a basic level of indentation for a node group. - inner (str): a level of indentation beyond outer + (str): string representation of the vector """ - outer = "\t"*level - inner = "\t"*(level + 1) - return outer, inner + return f"({vec3[0]}, {vec3[1]}, {vec3[2]})" -def create_node(node, file: TextIO, inner: str, node_tree_var: str, - node_vars: dict, used_vars: set) -> str: +def vec4_to_py_str(vec4) -> str: """ - Initializes a new node with location, dimension, and label info + Converts a 4D vector to a string usable by the add-on Parameters: - node (bpy.types.Node): node to be copied - file (TextIO): file containing the generated add-on - inner (str): indentation level for this logic - node_tree_var (str): variable name for the node tree - node_vars (dict): dictionary containing (bpy.types.Node, str) - pairs, with a Node and its corresponding variable name - used_vars (set): set of used variable names + vec4: a 4d vector Returns: - node_var (str): variable name for the node - """ - - file.write(f"{inner}#node {node.name}\n") - - node_var = create_var(node.name, used_vars) - node_vars[node] = node_var - - file.write((f"{inner}{node_var} " - f"= {node_tree_var}.nodes.new(\"{node.bl_idname}\")\n")) - #label - if node.label: - file.write(f"{inner}{node_var}.label = \"{node.label}\"\n") - - #color - if node.use_custom_color: - file.write(f"{inner}{node_var}.use_custom_color = True\n") - file.write(f"{inner}{node_var}.color = {vec3_to_py_str(node.color)}\n") - - #mute - if node.mute: - file.write(f"{inner}{node_var}.mute = True\n") - - return node_var - -def set_settings_defaults(node, settings: dict, file: TextIO, inner: str, - node_var: str): - """ - Sets the defaults for any settings a node may have - - Parameters: - node (bpy.types.Node): the node object we're copying settings from - settings (dict): a predefined dictionary of all settings every node has - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): name of the variable we're using for the node in our add-on - """ - if node.bl_idname in settings: - for setting in settings[node.bl_idname]: - attr = getattr(node, setting, None) - if attr: - if type(attr) == str: - attr = enum_to_py_str(attr) - if type(attr) == mathutils.Vector: - attr = vec3_to_py_str(attr) - if type(attr) == bpy.types.bpy_prop_array: - attr = vec4_to_py_str(list(attr)) - if type(attr) == bpy.types.Material: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.materials:\n")) - file.write((f"{inner}\t{node_var}.{setting} = " - f"bpy.data.materials[{name}]\n")) - continue - if type(attr) == bpy.types.Object: - name = str_to_py_str(attr.name) - file.write((f"{inner}if {name} in bpy.data.objects:\n")) - file.write((f"{inner}\t{node_var}.{setting} = " - f"bpy.data.objects[{name}]\n")) - continue - file.write((f"{inner}{node_var}.{setting} " - f"= {attr}\n")) - -def hide_sockets(node, file: TextIO, inner: str, node_var: str): - """ - Hide hidden sockets - - Parameters: - node (bpy.types.Node): node object we're copying socket settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation string - node_var (str): name of the variable we're using for this node - """ - for i, socket in enumerate(node.inputs): - if socket.hide is True: - file.write(f"{inner}{node_var}.inputs[{i}].hide = True\n") - for i, socket in enumerate(node.outputs): - if socket.hide is True: - file.write(f"{inner}{node_var}.outputs[{i}].hide = True\n") - -def group_io_settings(node, file: TextIO, inner: str, io: str, node_tree_var: str, node_tree): - if io == "input": - ios = node.outputs - ntio = node_tree.inputs - else: - ios = node.inputs - ntio = node_tree.outputs - file.write(f"{inner}#{node_tree_var} {io}s\n") - for i, inout in enumerate(ios): - if inout.bl_idname == 'NodeSocketVirtual': - continue - file.write(f"{inner}#{io} {inout.name}\n") - idname = enum_to_py_str(inout.bl_idname) - name = str_to_py_str(inout.name) - file.write(f"{inner}{node_tree_var}.{io}s.new({idname}, {name})\n") - socket = ntio[i] - socket_var = f"{node_tree_var}.{io}s[{i}]" - - if inout.type in default_sockets: - #default value - if inout.type == 'RGBA': - dv = vec4_to_py_str(socket.default_value) - elif inout.type == 'VECTOR': - dv = vec3_to_py_str(socket.default_value) - else: - dv = socket.default_value - file.write(f"{inner}{socket_var}.default_value = {dv}\n") - - #min value - if hasattr(socket, "min_value"): - file.write(f"{inner}{socket_var}.min_value = {socket.min_value}\n") - #max value - if hasattr(socket, "min_value"): - file.write((f"{inner}{socket_var}.max_value = {socket.max_value}\n")) - - #default attribute name - if hasattr(socket, "default_attribute_name"): - if socket.default_attribute_name != "": - dan = str_to_py_str(socket.default_attribute_name) - file.write((f"{inner}{socket_var}" - f".default_attribute_name = {dan}\n")) - - #attribute domain - if hasattr(socket, "attribute_domain"): - ad = enum_to_py_str(socket.attribute_domain) - file.write(f"{inner}{socket_var}.attribute_domain = {ad}\n") - - #tooltip - if socket.description != "": - description = str_to_py_str(socket.description) - file.write((f"{inner}{socket_var}.description = {description}\n")) - - #hide_value - if socket.hide_value is True: - file.write(f"{inner}{socket_var}.hide_value = True\n") - - #hide in modifier - if hasattr(socket, "hide_in_modifier"): - if socket.hide_in_modifier is True: - file.write(f"{inner}{socket_var}.hide_in_modifier = True\n") - - file.write("\n") - file.write("\n") - -def color_ramp_settings(node, file: TextIO, inner: str, node_var: str): - """ - Replicate a color ramp node - - Parameters - node (bpy.types.Node): node object we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): name of the variable we're using for the color ramp - """ - - color_ramp = node.color_ramp - #settings - color_mode = enum_to_py_str(color_ramp.color_mode) - file.write(f"{inner}{node_var}.color_ramp.color_mode = {color_mode}\n") - - hue_interpolation = enum_to_py_str(color_ramp.hue_interpolation) - file.write((f"{inner}{node_var}.color_ramp.hue_interpolation = " - f"{hue_interpolation}\n")) - interpolation = enum_to_py_str(color_ramp.interpolation) - file.write((f"{inner}{node_var}.color_ramp.interpolation " - f"= {interpolation}\n")) - file.write("\n") - - #key points - file.write((f"{inner}{node_var}.color_ramp.elements.remove" - f"({node_var}.color_ramp.elements[0])\n")) - for i, element in enumerate(color_ramp.elements): - element_var = f"{node_var}_cre_{i}" - if i == 0: - file.write(f"{inner}{element_var} = " - f"{node_var}.color_ramp.elements[{i}]\n") - file.write(f"{inner}{element_var}.position = {element.position}\n") - else: - file.write((f"{inner}{element_var} = " - f"{node_var}.color_ramp.elements" - f".new({element.position})\n")) - file.write((f"{inner}{element_var}.alpha = " - f"{element.alpha}\n")) - color_str = vec4_to_py_str(element.color) - file.write((f"{inner}{element_var}.color = {color_str}\n\n")) - -def curve_node_settings(node, file: TextIO, inner: str, node_var: str): - """ - 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 - """ - - #mapping settings - file.write(f"{inner}#mapping settings\n") - mapping_var = f"{inner}{node_var}.mapping" - - #extend - extend = enum_to_py_str(node.mapping.extend) - file.write(f"{mapping_var}.extend = {extend}\n") - #tone - tone = enum_to_py_str(node.mapping.tone) - file.write(f"{mapping_var}.tone = {tone}\n") - - #black level - b_lvl_str = vec3_to_py_str(node.mapping.black_level) - file.write((f"{mapping_var}.black_level = {b_lvl_str}\n")) - #white level - w_lvl_str = vec3_to_py_str(node.mapping.white_level) - file.write((f"{mapping_var}.white_level = {w_lvl_str}\n")) - - #minima and maxima - min_x = node.mapping.clip_min_x - file.write(f"{mapping_var}.clip_min_x = {min_x}\n") - min_y = node.mapping.clip_min_y - file.write(f"{mapping_var}.clip_min_y = {min_y}\n") - max_x = node.mapping.clip_max_x - file.write(f"{mapping_var}.clip_max_x = {max_x}\n") - max_y = node.mapping.clip_max_y - file.write(f"{mapping_var}.clip_max_y = {max_y}\n") - - #use_clip - use_clip = node.mapping.use_clip - file.write(f"{mapping_var}.use_clip = {use_clip}\n") - - #create curves - for i, curve in enumerate(node.mapping.curves): - file.write(f"{inner}#curve {i}\n") - curve_i = f"{node_var}_curve_{i}" - file.write((f"{inner}{curve_i} = {node_var}.mapping.curves[{i}]\n")) - for j, point in enumerate(curve.points): - point_j = f"{inner}{curve_i}_point_{j}" - - loc = point.location - loc_str = f"{loc[0]}, {loc[1]}" - if j < 2: - file.write(f"{point_j} = {curve_i}.points[{j}]\n") - file.write(f"{point_j}.location = ({loc_str})\n") - else: - file.write((f"{point_j} = {curve_i}.points.new({loc_str})\n")) - - handle = enum_to_py_str(point.handle_type) - file.write(f"{point_j}.handle_type = {handle}\n") - - #update curve - file.write(f"{inner}#update curve after changes\n") - file.write(f"{mapping_var}.update()\n") - -def set_input_defaults(node, file: TextIO, inner: str, node_var: str, - addon_dir: str = ""): - """ - Sets defaults for input sockets - - Parameters: - node (bpy.types.Node): node we're setting inputs for - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): variable name we're using for the copied node - addon_dir (str): directory of the add-on, for if we need to save other - objects for the add-on - """ - if node.bl_idname == 'NodeReroute': - return - - for i, input in enumerate(node.inputs): - if input.bl_idname not in dont_set_defaults and not input.is_linked: - socket_var = f"{node_var}.inputs[{i}]" - - #colors - if input.bl_idname == 'NodeSocketColor': - default_val = vec4_to_py_str(input.default_value) - - #vector types - elif "Vector" in input.bl_idname: - default_val = vec3_to_py_str(input.default_value) - - #strings - elif input.bl_idname == 'NodeSocketString': - default_val = str_to_py_str(input.default_value) - - #images - elif input.bl_idname == 'NodeSocketImage': - img = input.default_value - if img is not None and addon_dir != "": #write in a better way - save_image(img, addon_dir) - load_image(img, file, inner, f"{socket_var}.default_value") - default_val = None - - #materials - elif input.bl_idname == 'NodeSocketMaterial': - in_file_inputs(input, file, inner, socket_var, "materials") - default_val = None - - #collections - elif input.bl_idname == 'NodeSocketCollection': - in_file_inputs(input, file, inner, socket_var, "collections") - default_val = None - - #objects - elif input.bl_idname == 'NodeSocketObject': - in_file_inputs(input, file, inner, socket_var, "objects") - default_val = None - - #textures - elif input.bl_idname == 'NodeSocketTexture': - in_file_inputs(input, file, inner, socket_var, "textures") - default_val = None - - else: - default_val = input.default_value - if default_val is not None: - file.write(f"{inner}#{input.identifier}\n") - file.write((f"{inner}{socket_var}.default_value" - f" = {default_val}\n")) - file.write("\n") - -def in_file_inputs(input, file: TextIO, inner: str, socket_var: str, type: str): - """ - Sets inputs for a node input if one already exists in the blend file - - Parameters: - input (bpy.types.NodeSocket): input socket we're setting the value for - file (TextIO): file we're writing the add-on into - inner (str): indentation string - socket_var (str): variable name we're using for the socket - type (str): from what section of bpy.data to pull the default value from - """ - - if input.default_value is not None: - name = str_to_py_str(input.default_value.name) - file.write(f"{inner}if {name} in bpy.data.{type}:\n") - file.write((f"{inner}\t{socket_var}.default_value = " - f"bpy.data.{type}[{name}]\n")) - -def set_output_defaults(node, file: TextIO, inner: str, node_var: str): - """ - Some output sockets need default values set. It's rather annoying - - Parameters: - node (bpy.types.Node): node for the output we're setting - file (TextIO): file we're generating the add-on into - inner (str): indentation string - node_var (str): variable name for the node we're setting output defaults for - """ - output_default_nodes = {'ShaderNodeValue', - 'ShaderNodeRGB', - 'ShaderNodeNormal'} - - if node.bl_idname in output_default_nodes: - dv = node.outputs[0].default_value - if node.bl_idname == 'ShaderNodeRGB': - dv = vec4_to_py_str(list(dv)) - if node.bl_idname == 'ShaderNodeNormal': - dv = vec3_to_py_str(dv) - file.write((f"{inner}{node_var}.outputs[0].default_value = {dv}\n")) - -def set_parents(node_tree, file: TextIO, inner: str, node_vars: dict): - """ - Sets parents for all nodes, mostly used to put nodes in frames - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - file (TextIO): file for the generated add-on - inner (str): indentation string - node_vars (dict): dictionary for (node, variable) name pairs - """ - parent_comment = False - for node in node_tree.nodes: - if node is not None and node.parent is not None: - if not parent_comment: - file.write(f"{inner}#Set parents\n") - parent_comment = True - node_var = node_vars[node] - parent_var = node_vars[node.parent] - file.write(f"{inner}{node_var}.parent = {parent_var}\n") - file.write("\n") - -def set_locations(node_tree, file: TextIO, inner: str, node_vars: dict): - """ - Set locations for all nodes - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - file (TextIO): file for the generated add-on - inner (str): indentation string - node_vars (dict): dictionary for (node, variable) name pairs - """ - - file.write(f"{inner}#Set locations\n") - for node in node_tree.nodes: - node_var = node_vars[node] - file.write((f"{inner}{node_var}.location " - f"= ({node.location.x}, {node.location.y})\n")) - file.write("\n") - -def set_dimensions(node_tree, file: TextIO, inner: str, node_vars: dict): - """ - Set dimensions for all nodes - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're obtaining nodes from - file (TextIO): file for the generated add-on - inner (str): indentation string - node_vars (dict): dictionary for (node, variable) name pairs - """ - - file.write(f"{inner}#Set dimensions\n") - for node in node_tree.nodes: - node_var = node_vars[node] - file.write((f"{inner}{node_var}.width, {node_var}.height " - f"= {node.width}, {node.height}\n")) - file.write("\n") - -def init_links(node_tree, file: TextIO, inner: str, node_tree_var: str, - node_vars: dict): - """ - Create all the links between nodes - - Parameters: - node_tree (bpy.types.NodeTree): node tree we're copying - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_tree_var (str): variable name we're using for the copied node tree - node_vars (dict): dictionary containing node to variable name pairs - """ - - if node_tree.links: - file.write(f"{inner}#initialize {node_tree_var} links\n") - for link in node_tree.links: - in_node_var = node_vars[link.from_node] - input_socket = link.from_socket - - """ - Blender's socket dictionary doesn't guarantee - unique keys, which has caused much wailing and - gnashing of teeth. This is a quick fix that - doesn't run quick - """ - for i, item in enumerate(link.from_node.outputs.items()): - if item[1] == input_socket: - input_idx = i - break - - out_node_var = node_vars[link.to_node] - output_socket = link.to_socket - - for i, item in enumerate(link.to_node.inputs.items()): - if item[1] == output_socket: - output_idx = i - break - - file.write((f"{inner}#{in_node_var}.{input_socket.name} " - f"-> {out_node_var}.{output_socket.name}\n")) - file.write((f"{inner}{node_tree_var}.links.new({in_node_var}" - f".outputs[{input_idx}], " - f"{out_node_var}.inputs[{output_idx}])\n")) - -def create_menu_func(file: TextIO, name: str): - """ - Creates the menu function - - Parameters: - file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class - """ - - file.write("def menu_func(self, context):\n") - file.write(f"\tself.layout.operator({name}.bl_idname)\n") - file.write("\n") - -def create_register_func(file: TextIO, name: str): - """ - Creates the register function - - Parameters: - file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class - """ - file.write("def register():\n") - file.write(f"\tbpy.utils.register_class({name})\n") - file.write("\tbpy.types.VIEW3D_MT_object.append(menu_func)\n") - file.write("\n") - -def create_unregister_func(file: TextIO, name: str): - """ - Creates the unregister function - - Parameters: - file (TextIO): file we're generating the add-on into - name (str): name of the generated operator class + (str): string version """ - file.write("def unregister():\n") - file.write(f"\tbpy.utils.unregister_class({name})\n") - file.write("\tbpy.types.VIEW3D_MT_object.remove(menu_func)\n") - file.write("\n") + return f"({vec4[0]}, {vec4[1]}, {vec4[2]}, {vec4[3]})" -def create_main_func(file: TextIO): +def array_to_py_str(array: bpy_types.bpy_prop_array) -> str: """ - Creates the main function + Converts a bpy_prop_array into a string Parameters: - file (TextIO): file we're generating the add-on into - """ - file.write("if __name__ == \"__main__\":\n") - file.write("\tregister()") + array (bpy_prop_array): Blender Python array -def save_image(img, addon_dir: str): + Returns: + (str): string version """ - Saves an image to an image directory of the add-on + string = "(" + for i in range(0, array.__len__()): + if i > 0: + string += ", " + string += f"{array[i]}" + string += ")" + return string - Parameters: - img (bpy.types.Image): image to be saved - addon_dir (str): directory of the addon +def color_to_py_str(color: mathutils.Color) -> str: """ - - if img is None: - return - - #create image dir if one doesn't exist - img_dir = os.path.join(addon_dir, image_dir_name) - if not os.path.exists(img_dir): - os.mkdir(img_dir) - - #save the image - img_str = img_to_py_str(img) - img_path = f"{img_dir}/{img_str}" - if not os.path.exists(img_path): - img.save_render(img_path) - -def load_image(img, file: TextIO, inner: str, img_var: str): - """ - Loads an image from the add-on into a blend file and assigns it + Converts a mathutils.Color into a string Parameters: - img (bpy.types.Image): Blender image from the original node group - file (TextIO): file for the generated add-on - inner (str): indentation string - img_var (str): variable name to be used for the image - """ + color (mathutils.Color): a Blender color - if img is None: - return - - img_str = img_to_py_str(img) - - file.write(f"{inner}#load image {img_str}\n") - file.write((f"{inner}base_dir = " - f"os.path.dirname(os.path.abspath(__file__))\n")) - file.write((f"{inner}image_path = " - f"os.path.join(base_dir, \"{image_dir_name}\", " - f"\"{img_str}\")\n")) - file.write((f"{inner}{img_var} = " - f"bpy.data.images.load(image_path, check_existing = True)\n")) - - #copy image settings - file.write(f"{inner}#set image settings\n") - - #source - source = enum_to_py_str(img.source) - file.write(f"{inner}{img_var}.source = {source}\n") - - #color space settings - color_space = enum_to_py_str(img.colorspace_settings.name) - file.write(f"{inner}{img_var}.colorspace_settings.name = {color_space}\n") - - #alpha mode - alpha_mode = enum_to_py_str(img.alpha_mode) - file.write(f"{inner}{img_var}.alpha_mode = {alpha_mode}\n") - -def image_user_settings(node, file: TextIO, inner: str, node_var: str): + Returns: + (str): string version """ - Replicate the image user of an image node + return f"mathutils.Color(({color.r}, {color.g}, {color.b}))" - Parameters - node (bpy.types.Node): node object we're copying settings from - file (TextIO): file we're generating the add-on into - inner (str): indentation - node_var (str): name of the variable we're using for the color ramp +def img_to_py_str(img : bpy.types.Image) -> str: """ + Converts a Blender image into its string - if not hasattr(node, "image_user"): - raise ValueError("Node must have attribute \"image_user\"") - - img_usr = node.image_user - img_usr_var = f"{node_var}.image_user" - - img_usr_attrs = ["frame_current", "frame_duration", "frame_offset", - "frame_start", "tile", "use_auto_refresh", "use_cyclic"] - - for img_usr_attr in img_usr_attrs: - file.write((f"{inner}{img_usr_var}.{img_usr_attr} = " - f"{getattr(img_usr, img_usr_attr)}\n")) + Paramters: + img (bpy.types.Image): a Blender image -def zip_addon(zip_dir: str): - """ - Zips up the addon and removes the directory - - Parameters: - zip_dir (str): path to the top-level addon directory + Returns: + (str): string version """ - shutil.make_archive(zip_dir, "zip", zip_dir) - shutil.rmtree(zip_dir) \ No newline at end of file + name = img.name.split('.', 1)[0] + format = img.file_format.lower() + return f"{name}.{format}" \ No newline at end of file