diff --git a/NodeToPython/__init__.py b/NodeToPython/__init__.py index 04b301c..08394f9 100644 --- a/NodeToPython/__init__.py +++ b/NodeToPython/__init__.py @@ -10,62 +10,43 @@ if "bpy" in locals(): import importlib + importlib.reload(export_operator) + importlib.reload(ntp_options) + importlib.reload(ui) importlib.reload(compositor) importlib.reload(geometry) importlib.reload(shader) - importlib.reload(options) else: + from . import export_operator + from . import ntp_options + from . import ui from . import compositor from . import geometry from . import shader - from . import options - import bpy - -class NodeToPythonMenu(bpy.types.Menu): - bl_idname = "NODE_MT_node_to_python" - bl_label = "Node To Python" - - @classmethod - def poll(cls, context): - return True - - def draw(self, context): - layout = self.layout.column_flow(columns=1) - layout.operator_context = 'INVOKE_DEFAULT' - - -classes = [ - NodeToPythonMenu, - #options - options.NTPOptions, - 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 - shader.operator.NTPShaderOperator, - shader.ui.NTPShaderMenu, - shader.ui.NTPShaderPanel, -] +modules = [export_operator, ntp_options] +for parent_module in [ui, compositor, geometry, shader]: + if hasattr(parent_module, "modules"): + modules += parent_module.modules + else: + raise Exception(f"Module {parent_module} does not have list of modules") def register(): - for cls in classes: - bpy.utils.register_class(cls) - scene = bpy.types.Scene - scene.ntp_options = bpy.props.PointerProperty(type=options.NTPOptions) + for module in modules: + if hasattr(module, "classes"): + for cls in getattr(module, "classes"): + bpy.utils.register_class(cls) + if hasattr(module, "register_props"): + getattr(module, "register_props")() def unregister(): - for cls in classes: - bpy.utils.unregister_class(cls) - del bpy.types.Scene.ntp_options + for module in modules: + if hasattr(module, "classes"): + for cls in getattr(module, "classes"): + bpy.utils.unregister_class(cls) + if hasattr(module, "unregister_props"): + getattr(module, "unregister_props")() if __name__ == "__main__": register() \ No newline at end of file diff --git a/NodeToPython/compositor/__init__.py b/NodeToPython/compositor/__init__.py index 1c6d5b3..cc9aa89 100644 --- a/NodeToPython/compositor/__init__.py +++ b/NodeToPython/compositor/__init__.py @@ -6,4 +6,9 @@ from . import operator from . import ui -import bpy \ No newline at end of file +import bpy + +modules = [ + operator +] +modules += ui.modules \ No newline at end of file diff --git a/NodeToPython/compositor/operator.py b/NodeToPython/compositor/operator.py index e71ef13..cf6723e 100644 --- a/NodeToPython/compositor/operator.py +++ b/NodeToPython/compositor/operator.py @@ -16,8 +16,8 @@ COMP_OP_RESERVED_NAMES = {SCENE, BASE_NAME, END_NAME, NODE} -class NTPCompositorOperator(NTP_Operator): - bl_idname = "node.ntp_compositor" +class NTP_OT_Compositor(NTP_Operator): + bl_idname = "ntp.compositor" bl_label = "Compositor to Python" bl_options = {'REGISTER', 'UNDO'} @@ -280,4 +280,8 @@ def execute(self, context): self._report_finished("compositor nodes") - return {'FINISHED'} \ No newline at end of file + return {'FINISHED'} + +classes: list[type] = [ + NTP_OT_Compositor +] \ No newline at end of file diff --git a/NodeToPython/compositor/ui.py b/NodeToPython/compositor/ui.py deleted file mode 100644 index ca19247..0000000 --- a/NodeToPython/compositor/ui.py +++ /dev/null @@ -1,83 +0,0 @@ -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" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @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/NodeToPython/compositor/ui/__init__.py b/NodeToPython/compositor/ui/__init__.py new file mode 100644 index 0000000..e8a9165 --- /dev/null +++ b/NodeToPython/compositor/ui/__init__.py @@ -0,0 +1,17 @@ +if "bpy" in locals(): + import importlib + importlib.reload(panel) + importlib.reload(scenes) + importlib.reload(compositor_node_groups) +else: + from . import panel + from . import scenes + from . import compositor_node_groups + +import bpy + +modules : list = [ + panel, + scenes, + compositor_node_groups +] \ No newline at end of file diff --git a/NodeToPython/compositor/ui/compositor_node_groups.py b/NodeToPython/compositor/ui/compositor_node_groups.py new file mode 100644 index 0000000..aaed0b8 --- /dev/null +++ b/NodeToPython/compositor/ui/compositor_node_groups.py @@ -0,0 +1,122 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_compositor_node_group_slots = bpy.props.CollectionProperty( + type=NTP_PG_CompositorNodeGroupSlot + ) + bpy.types.Scene.ntp_compositor_node_group_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_compositor_node_group_slots + del bpy.types.Scene.ntp_compositor_node_group_slots_index + +class NTP_PG_CompositorNodeGroupSlot(bpy.types.PropertyGroup): + """ + TODO: There's a bug where the filtering doesn't update when renaming a + slotted object. For now, we'll need to just remove and re-add the slot + to the UI list. + """ + name: bpy.props.StringProperty( + name="Node Tree Name", + default="" + ) + + def poll_node_tree(self, node_tree: bpy.types.NodeTree) -> bool: + scene = bpy.context.scene + + for slot in scene.ntp_compositor_node_group_slots: + if slot is not self and slot.node_tree == node_tree: + return False + return node_tree.bl_idname == 'CompositorNodeTree' + + def update_node_tree(self, context): + if self.node_tree: + self.name = self.node_tree.name + else: + self.name = "Compositor Node Group" + + node_tree: bpy.props.PointerProperty( + name="Node Tree", + type=bpy.types.NodeTree, + poll=poll_node_tree, + update=update_node_tree + ) + +class NTP_OT_AddCompositorNodeGroupSlot(bpy.types.Operator): + bl_idname = "ntp.add_compositor_node_group_slot" + bl_label = "Add Compositor Node Group Slot" + bl_description = "Add Compositor Node Group Slot" + + def execute(self, context): + slots = context.scene.ntp_compositor_node_group_slots + slots.add() + context.scene.ntp_compositor_node_group_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveCompositorNodeGroupSlot(bpy.types.Operator): + bl_idname = "ntp.remove_compositor_node_group_slot" + bl_label = "Remove Compositor Node Group Slot" + bl_description = "Remove Compositor Node Group Slot" + + def execute(self, context): + slots = context.scene.ntp_compositor_node_group_slots + idx = context.scene.ntp_compositor_node_group_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_compositor_node_group_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_CompositorNodeGroup(bpy.types.UIList): + bl_idname = "NTP_UL_compositor_node_group" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "node_tree", bpy.data, "node_groups", text="") + +class NTP_PT_CompositorNodeGroup(bpy.types.Panel): + bl_idname = "NTP_PT_compositor_node_group" + bl_label = "Node Groups" + bl_parent_id = panel.NTP_PT_Compositor.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = ("List of compositor node group objects to replicate.\n" + "These are typically subgroups within a larger scene tree") + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_CompositorNodeGroup.bl_idname, "", + context.scene, "ntp_compositor_node_group_slots", + context.scene, "ntp_compositor_node_group_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddCompositorNodeGroupSlot.bl_idname, + icon="ADD", text="") + col.operator(NTP_OT_RemoveCompositorNodeGroupSlot.bl_idname, + icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_CompositorNodeGroupSlot, + NTP_OT_AddCompositorNodeGroupSlot, + NTP_OT_RemoveCompositorNodeGroupSlot, + NTP_UL_CompositorNodeGroup, + NTP_PT_CompositorNodeGroup +] \ No newline at end of file diff --git a/NodeToPython/compositor/ui/panel.py b/NodeToPython/compositor/ui/panel.py new file mode 100644 index 0000000..a221ce0 --- /dev/null +++ b/NodeToPython/compositor/ui/panel.py @@ -0,0 +1,27 @@ +import bpy + +class NTP_PT_Compositor(bpy.types.Panel): + bl_label = "Compositor to Python" + bl_idname = "NTP_PT_compositor" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + +classes: list[type] = [ + NTP_PT_Compositor +] \ No newline at end of file diff --git a/NodeToPython/compositor/ui/scenes.py b/NodeToPython/compositor/ui/scenes.py new file mode 100644 index 0000000..c1b7f46 --- /dev/null +++ b/NodeToPython/compositor/ui/scenes.py @@ -0,0 +1,112 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_scene_slots = bpy.props.CollectionProperty( + type=NTP_PG_SceneSlot + ) + bpy.types.Scene.ntp_scene_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_scene_slots + del bpy.types.Scene.ntp_scene_slots_index + +class NTP_PG_SceneSlot(bpy.types.PropertyGroup): + name: bpy.props.StringProperty( + name="Scene Name", + default="" + ) + + def poll_scene(self, scene: bpy.types.Scene) -> bool: + for slot in bpy.context.scene.ntp_scene_slots: + if slot is not self and slot.scene == scene: + return False + return scene.use_nodes + + def update_scene(self, context): + if self.scene: + self.name = self.scene.name + else: + self.name = "Scene" + + scene: bpy.props.PointerProperty( + name="Scene", + type=bpy.types.Scene, + poll=poll_scene, + update=update_scene + ) + +class NTP_OT_AddSceneSlot(bpy.types.Operator): + bl_idname = "ntp.add_scene_slot" + bl_label = "Add Scene Slot" + bl_description = "Add Scene Slot" + + def execute(self, context): + slots = context.scene.ntp_scene_slots + slot = slots.add() + context.scene.ntp_scene_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveSceneSlot(bpy.types.Operator): + bl_idname = "ntp.remove_scene_slot" + bl_label = "Remove Scene Slot" + bl_description = "Remove Scene Slot" + + def execute(self, context): + slots = context.scene.ntp_scene_slots + idx = context.scene.ntp_scene_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_scene_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_Scene(bpy.types.UIList): + bl_idname = "NTP_UL_scene" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "scene", bpy.data, "scenes", text="") + +class NTP_PT_Scene(bpy.types.Panel): + bl_idname = "NTP_PT_scene" + bl_label = "Scenes" + bl_parent_id = panel.NTP_PT_Compositor.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "List of bpy.types.Scene objects to replicate" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_Scene.bl_idname, "", + context.scene, "ntp_scene_slots", + context.scene, "ntp_scene_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddSceneSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveSceneSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_SceneSlot, + NTP_OT_AddSceneSlot, + NTP_OT_RemoveSceneSlot, + NTP_UL_Scene, + NTP_PT_Scene +] \ No newline at end of file diff --git a/NodeToPython/export_operator.py b/NodeToPython/export_operator.py new file mode 100644 index 0000000..48a577a --- /dev/null +++ b/NodeToPython/export_operator.py @@ -0,0 +1,121 @@ +import bpy + +from enum import Enum, auto + +class NodeGroupType(Enum): + COMPOSITOR_NODE_GROUP = auto() + SCENE = auto() + GEOMETRY_NODE_GROUP = auto() + LIGHT = auto() + LINE_STYLE = auto() + MATERIAL = auto() + SHADER_NODE_GROUP = auto() + WORLD = auto() + +class NodeGroupGatherer: + def __init__(self): + self.node_groups : dict[NodeGroupType, list] = { + NodeGroupType.COMPOSITOR_NODE_GROUP : [], + NodeGroupType.SCENE : [], + NodeGroupType.GEOMETRY_NODE_GROUP : [], + NodeGroupType.LIGHT : [], + NodeGroupType.LINE_STYLE : [], + NodeGroupType.MATERIAL : [], + NodeGroupType.SHADER_NODE_GROUP : [], + NodeGroupType.WORLD : [], + } + + def gather_node_groups(self, context: bpy.types.Context): + for group_slot in context.scene.ntp_compositor_node_group_slots: + if group_slot.node_tree is not None: + self.node_groups[NodeGroupType.COMPOSITOR_NODE_GROUP].append( + group_slot.node_tree + ) + for scene_slot in context.scene.ntp_scene_slots: + if scene_slot.scene is not None: + self.node_groups[NodeGroupType.SCENE].append(scene_slot.scene) + + for group_slot in context.scene.ntp_geometry_node_group_slots: + if group_slot.node_tree is not None: + self.node_groups[NodeGroupType.GEOMETRY_NODE_GROUP].append( + group_slot.node_tree + ) + + for light_slot in context.scene.ntp_light_slots: + if light_slot.light is not None: + self.node_groups[NodeGroupType.LIGHT].append(light_slot.light) + + for line_style_slot in context.scene.ntp_line_style_slots: + if line_style_slot.line_style is not None: + self.node_groups[NodeGroupType.LINE_STYLE].append( + line_style_slot.line_style + ) + + for material_slot in context.scene.ntp_material_slots: + if material_slot.material is not None: + self.node_groups[NodeGroupType.MATERIAL].append( + material_slot.material + ) + + for group_slot in context.scene.ntp_shader_node_group_slots: + if group_slot.node_tree is not None: + self.node_groups[NodeGroupType.SHADER_NODE_GROUP].append( + group_slot.node_tree + ) + + for world_slot in context.scene.ntp_world_slots: + if world_slot.world is not None: + self.node_groups[NodeGroupType.WORLD].append(world_slot.world) + + def get_number_node_groups(self) -> int: + result = 0 + for lst in self.node_groups.values(): + result += len(lst) + return result + + def get_single_node_group(self): + # TODO: better name/ergonomics in general + if self.get_number_node_groups() != 1: + raise ValueError("Expected just one node group") + + for group_type, lst in self.node_groups.items(): + if len(lst) == 1: + return group_type, lst[0] + + raise AssertionError("Expected this to be unreachable") + +class NTP_OT_Export(bpy.types.Operator): + bl_idname = "ntp.export" + bl_label = "Export" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context: bpy.types.Context): + gatherer = NodeGroupGatherer() + gatherer.gather_node_groups(context) + + if gatherer.get_number_node_groups() != 1: + self.report({'ERROR'}, "Can only export one node group currently") + return {'CANCELLED'} + + group_type, obj = gatherer.get_single_node_group() + + if group_type == NodeGroupType.COMPOSITOR_NODE_GROUP: + bpy.ops.ntp.compositor(compositor_name=obj.name, is_scene=False) + elif group_type == NodeGroupType.SCENE: + bpy.ops.ntp.compositor(compositor_name=obj.name, is_scene=True) + elif group_type == NodeGroupType.GEOMETRY_NODE_GROUP: + bpy.ops.ntp.geometry_nodes(geo_nodes_group_name=obj.name) + elif group_type == NodeGroupType.LIGHT: + pass + elif group_type == NodeGroupType.MATERIAL: + bpy.ops.ntp.shader(material_name=obj.name) + elif group_type == NodeGroupType.SHADER_NODE_GROUP: + pass + elif group_type == NodeGroupType.WORLD: + pass + + return {'FINISHED'} + +classes = [ + NTP_OT_Export +] \ No newline at end of file diff --git a/NodeToPython/geometry/__init__.py b/NodeToPython/geometry/__init__.py index e40e80c..83ec3ca 100644 --- a/NodeToPython/geometry/__init__.py +++ b/NodeToPython/geometry/__init__.py @@ -8,4 +8,10 @@ from . import operator from . import ui -import bpy \ No newline at end of file +import bpy + +modules = [ + node_tree, + operator +] +modules += ui.modules \ No newline at end of file diff --git a/NodeToPython/geometry/node_tree.py b/NodeToPython/geometry/node_tree.py index 8129276..9dcd02c 100644 --- a/NodeToPython/geometry/node_tree.py +++ b/NodeToPython/geometry/node_tree.py @@ -1,21 +1,12 @@ import bpy from bpy.types import GeometryNodeTree, GeometryNode -if bpy.app.version >= (3, 6, 0): - from bpy.types import GeometryNodeSimulationInput - -if bpy.app.version >= (4, 0, 0): - from bpy.types import GeometryNodeRepeatInput - -if bpy.app.version >= (4, 3, 0): - from bpy.types import GeometryNodeForeachGeometryElementInput - from ..ntp_node_tree import NTP_NodeTree class NTP_GeoNodeTree(NTP_NodeTree): def __init__(self, node_tree: GeometryNodeTree, var: str): super().__init__(node_tree, var) - self.zone_inputs: dict[list[GeometryNode]] = {} + self.zone_inputs: dict[str, list[GeometryNode]] = {} if bpy.app.version >= (3, 6, 0): self.zone_inputs["GeometryNodeSimulationInput"] = [] if bpy.app.version >= (4, 0, 0): diff --git a/NodeToPython/geometry/operator.py b/NodeToPython/geometry/operator.py index 8fecd7e..f3bf146 100644 --- a/NodeToPython/geometry/operator.py +++ b/NodeToPython/geometry/operator.py @@ -16,9 +16,9 @@ OBJECT, MODIFIER} -class NTPGeoNodesOperator(NTP_Operator): - bl_idname = "node.ntp_geo_nodes" - bl_label = "Geo Nodes to Python" +class NTP_OT_GeometryNodes(NTP_Operator): + bl_idname = "ntp.geometry_nodes" + bl_label = "Geometry Nodes to Python" bl_options = {'REGISTER', 'UNDO'} geo_nodes_group_name: bpy.props.StringProperty(name="Node Group") @@ -94,12 +94,14 @@ def _set_geo_tree_properties(self, node_tree: GeometryNodeTree) -> None: if is_tool: self._write(f"{nt_var}.is_tool = True") - tool_flags = ["is_mode_object", - "is_mode_edit", - "is_mode_sculpt", - "is_type_curve", - "is_type_mesh", - "is_type_point_cloud"] + tool_flags = [ + "is_mode_object", + "is_mode_edit", + "is_mode_sculpt", + "is_type_curve", + "is_type_mesh", + "is_type_point_cloud" + ] for flag in tool_flags: if hasattr(node_tree, flag) is True: @@ -220,4 +222,8 @@ def execute(self, context): self._report_finished("geometry node group") - return {'FINISHED'} \ No newline at end of file + return {'FINISHED'} + +classes = [ + NTP_OT_GeometryNodes +] \ No newline at end of file diff --git a/NodeToPython/geometry/ui.py b/NodeToPython/geometry/ui.py deleted file mode 100644 index dff6353..0000000 --- a/NodeToPython/geometry/ui.py +++ /dev/null @@ -1,58 +0,0 @@ -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" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @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/NodeToPython/geometry/ui/__init__.py b/NodeToPython/geometry/ui/__init__.py new file mode 100644 index 0000000..5d29caa --- /dev/null +++ b/NodeToPython/geometry/ui/__init__.py @@ -0,0 +1,11 @@ +if "bpy" in locals(): + import importlib + importlib.reload(panel) + importlib.reload(geometry_node_groups) +else: + from . import panel + from . import geometry_node_groups + +import bpy + +modules = [panel, geometry_node_groups] \ No newline at end of file diff --git a/NodeToPython/geometry/ui/geometry_node_groups.py b/NodeToPython/geometry/ui/geometry_node_groups.py new file mode 100644 index 0000000..4804408 --- /dev/null +++ b/NodeToPython/geometry/ui/geometry_node_groups.py @@ -0,0 +1,119 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_geometry_node_group_slots = bpy.props.CollectionProperty( + type=NTP_PG_GeometryNodeGroupSlot + ) + bpy.types.Scene.ntp_geometry_node_group_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_geometry_node_group_slots + del bpy.types.Scene.ntp_geometry_node_group_slots_index + +class NTP_PG_GeometryNodeGroupSlot(bpy.types.PropertyGroup): + """ + TODO: There's a bug where the filtering doesn't update when renaming a + slotted object. For now, we'll need to just remove and re-add the slot + to the UI list. + """ + name: bpy.props.StringProperty( + name="Node Tree Name", + default="" + ) + + def poll_node_tree(self, node_tree: bpy.types.NodeTree) -> bool: + scene = bpy.context.scene + + for slot in scene.ntp_geometry_node_group_slots: + if slot is not self and slot.node_tree == node_tree: + return False + return node_tree.bl_idname == 'GeometryNodeTree' + + def update_node_tree(self, context): + if self.node_tree: + self.name = self.node_tree.name + else: + self.name = "Geometry Node Group" + + node_tree: bpy.props.PointerProperty( + name="Node Tree", + type=bpy.types.NodeTree, + poll=poll_node_tree, + update=update_node_tree + ) + +class NTP_OT_AddGeometryNodeGroupSlot(bpy.types.Operator): + bl_idname = "ntp.add_geometry_node_group_slot" + bl_label = "Add Geometry Node Group Slot" + bl_description = "Add Geometry Node Group Slot" + + def execute(self, context): + slots = context.scene.ntp_geometry_node_group_slots + slot = slots.add() + context.scene.ntp_geometry_node_group_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveGeometryNodeGroupSlot(bpy.types.Operator): + bl_idname = "ntp.remove_geometry_node_group_slot" + bl_label = "Remove Geometry Node Group Slot" + bl_description = "Remove Geometry Node Group Slot" + + def execute(self, context): + slots = context.scene.ntp_geometry_node_group_slots + idx = context.scene.ntp_geometry_node_group_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_geometry_node_group_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_GeometryNodeGroup(bpy.types.UIList): + bl_idname = "NTP_UL_geometry_node_group" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "node_tree", bpy.data, "node_groups", text="") + +class NTP_PT_GeometryNodeGroup(bpy.types.Panel): + bl_idname = "NTP_PT_geometry_node_group" + bl_label = "Node Groups" + bl_parent_id = panel.NTP_PT_GeometryNodes.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = ("List of geometry node group objects to replicate") + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_GeometryNodeGroup.bl_idname, "", + context.scene, "ntp_geometry_node_group_slots", + context.scene, "ntp_geometry_node_group_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddGeometryNodeGroupSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveGeometryNodeGroupSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_GeometryNodeGroupSlot, + NTP_OT_AddGeometryNodeGroupSlot, + NTP_OT_RemoveGeometryNodeGroupSlot, + NTP_UL_GeometryNodeGroup, + NTP_PT_GeometryNodeGroup +] \ No newline at end of file diff --git a/NodeToPython/geometry/ui/panel.py b/NodeToPython/geometry/ui/panel.py new file mode 100644 index 0000000..d63f01b --- /dev/null +++ b/NodeToPython/geometry/ui/panel.py @@ -0,0 +1,27 @@ +import bpy + +class NTP_PT_GeometryNodes(bpy.types.Panel): + bl_label = "Geometry Nodes to Python" + bl_idname = "NTP_PT_geometry_nodes" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + +classes: list[type] = [ + NTP_PT_GeometryNodes +] \ No newline at end of file diff --git a/NodeToPython/ntp_operator.py b/NodeToPython/ntp_operator.py index 9db9d44..4ca91b2 100644 --- a/NodeToPython/ntp_operator.py +++ b/NodeToPython/ntp_operator.py @@ -17,7 +17,7 @@ from .license_templates import license_templates from .ntp_node_tree import NTP_NodeTree -from .options import NTPOptions +from .ntp_options import NTP_PG_Options from .node_settings import NodeInfo, ST from .utils import * @@ -125,7 +125,7 @@ def _write(self, string: str, indent_level: int = None): indent_str = indent_level * self._indentation self._file.write(f"{indent_str}{string}\n") - def _setup_options(self, options: NTPOptions) -> bool: + def _setup_options(self, options: NTP_PG_Options) -> bool: if bpy.app.version >= MAX_BLENDER_VERSION: self.report({'WARNING'}, f"Blender version {bpy.app.version} is not supported yet!\n" @@ -137,8 +137,8 @@ def _setup_options(self, options: NTPOptions) -> bool: # General self._mode = options.mode - self._include_group_socket_values = options.include_group_socket_values - self._should_set_dimensions = options.set_dimensions + self._include_group_socket_values = options.set_group_defaults + self._should_set_dimensions = options.set_node_sizes if options.indentation_type == 'SPACES_2': self._indentation = " " diff --git a/NodeToPython/ntp_options.py b/NodeToPython/ntp_options.py new file mode 100644 index 0000000..b0488c7 --- /dev/null +++ b/NodeToPython/ntp_options.py @@ -0,0 +1,168 @@ +import bpy + +def register_props(): + bpy.types.Scene.ntp_options = bpy.props.PointerProperty( + type=NTP_PG_Options + ) + +def unregister_props(): + del bpy.types.Scene.ntp_options + +class NTP_PG_Options(bpy.types.PropertyGroup): + """ + Property group used during conversion of node group to python + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # General properties + mode: bpy.props.EnumProperty( + name = "Mode", + items = [ + ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), + ('ADDON', "Addon", "Create a full add-on") + ] + ) + set_group_defaults : bpy.props.BoolProperty( + name = "Set Group Defaults", + description = "Generate group socket default, min, and max values", + default = True + ) + set_node_sizes : bpy.props.BoolProperty( + name = "Set Node Sizes", + description = "Set dimensions of generated nodes", + default = True + ) + + indentation_type: bpy.props.EnumProperty( + name="Indentation Type", + description="Whitespace to use for each indentation block", + items = [ + ('SPACES_2', "2 Spaces", ""), + ('SPACES_4', "4 Spaces", ""), + ('SPACES_8', "8 Spaces", ""), + ('TABS', "Tabs", "") + ], + default = 'SPACES_4' + ) + + if bpy.app.version >= (3, 4, 0): + set_unavailable_defaults : bpy.props.BoolProperty( + name = "Set unavailable defaults", + description = "Set default values for unavailable sockets", + default = False + ) + + #Script properties + include_imports : bpy.props.BoolProperty( + name = "Include Imports", + description="Generate necessary import statements (i.e. bpy, mathutils, etc)", + default = True + ) + + # Addon properties + dir_path : bpy.props.StringProperty( + name = "Save Location", + subtype='DIR_PATH', + description="Save location if generating an add-on", + default = "//" + ) + name_override : bpy.props.StringProperty( + name = "Name Override", + description="Name used for the add-on's, default is node group name", + default = "" + ) + description : bpy.props.StringProperty( + name = "Description", + description="Description used for the add-on", + default="" + ) + author_name : bpy.props.StringProperty( + name = "Author Name", + description = "Name used for the author/maintainer of the add-on", + default = "Node To Python" + ) + version: bpy.props.IntVectorProperty( + name = "Version", + description="Version of the add-on", + default = (1, 0, 0) + ) + location: bpy.props.StringProperty( + name = "Location", + description="Location property of the addon", + default="Node" + ) + menu_id: bpy.props.StringProperty( + name = "Menu ID", + description = "Python ID of the menu you'd like to register the add-on " + "to. You can find these by enabling Python tooltips " + "(Preferences > Interface > Python tooltips) and " + "hovering over the desired menu", + default="NODE_MT_add" + ) + license: bpy.props.EnumProperty( + name="License", + items = [ + ('SPDX:GPL-3.0-or-later', "GNU General Public License v3.0 or later", ""), + ('OTHER', "Other", "User is responsible for including the license " + "and adding it to the manifest.\n" + "Please note that by using the Blender Python " + "API, your add-on must comply with the GNU GPL. " + "See https://www.blender.org/about/license/ for " + "more details") + ], + default = 'SPDX:GPL-3.0-or-later' + ) + should_create_license: bpy.props.BoolProperty( + name="Create License", + description="Should NodeToPython include a license file for your add-on", + default=True + ) + category: bpy.props.EnumProperty( + name = "Category", + items = [ + ('Custom', "Custom", "Use an unofficial category"), + ('3D View', "3D View", ""), + ('Add Curve', "Add Curve", ""), + ('Add Mesh', "Add Mesh", ""), + ('Animation', "Animation", ""), + ('Bake', "Bake", ""), + ('Compositing', "Compositing", ""), + ('Development', "Development", ""), + ('Game Engine', "Game Engine", ""), + ('Geometry Nodes', "Geometry Nodes", ""), + ("Grease Pencil", "Grease Pencil", ""), + ('Import-Export', "Import-Export", ""), + ('Lighting', "Lighting", ""), + ('Material', "Material", ""), + ('Mesh', "Mesh", ""), + ('Modeling', "Modeling", ""), + ('Node', "Node", ""), + ('Object', "Object", ""), + ('Paint', "Paint", ""), + ('Pipeline', "Pipeline", ""), + ('Physics', "Physics", ""), + ('Render', "Render", ""), + ('Rigging', "Rigging", ""), + ('Scene', "Scene", ""), + ('Sculpt', "Sculpt", ""), + ('Sequencer', "Sequencer", ""), + ('System', "System", ""), + ('Text Editor', "Text Editor", ""), + ('Tracking', "Tracking", ""), + ('UV', "UV", ""), + ('User Interface', "User Interface", ""), + ], + default = 'Node' + ) + custom_category: bpy.props.StringProperty( + name="Custom Category", + description="Set the custom category property for your add-on", + default = "" + ) + + +classes = [ + NTP_PG_Options +] \ No newline at end of file diff --git a/NodeToPython/options.py b/NodeToPython/options.py deleted file mode 100644 index da8c0f1..0000000 --- a/NodeToPython/options.py +++ /dev/null @@ -1,208 +0,0 @@ -import bpy - -class NTPOptions(bpy.types.PropertyGroup): - """ - Property group used during conversion of node group to python - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # General properties - mode: bpy.props.EnumProperty( - name = "Mode", - items = [ - ('SCRIPT', "Script", "Copy just the node group to the Blender clipboard"), - ('ADDON', "Addon", "Create a full add-on") - ] - ) - include_group_socket_values : bpy.props.BoolProperty( - name = "Include group socket values", - description = "Generate group socket default, min, and max values", - default = True - ) - set_dimensions : bpy.props.BoolProperty( - name = "Set dimensions", - description = "Set dimensions of generated nodes", - default = True - ) - - indentation_type: bpy.props.EnumProperty( - name="Indentation Type", - description="Whitespace to use for each indentation block", - items = [ - ('SPACES_2', "2 Spaces", ""), - ('SPACES_4', "4 Spaces", ""), - ('SPACES_8', "8 Spaces", ""), - ('TABS', "Tabs", "") - ], - default = 'SPACES_4' - ) - - if bpy.app.version >= (3, 4, 0): - set_unavailable_defaults : bpy.props.BoolProperty( - name = "Set unavailable defaults", - description = "Set default values for unavailable sockets", - default = False - ) - - #Script properties - include_imports : bpy.props.BoolProperty( - name = "Include imports", - description="Generate necessary import statements", - default = True - ) - - # Addon properties - dir_path : bpy.props.StringProperty( - name = "Save Location", - subtype='DIR_PATH', - description="Save location if generating an add-on", - default = "//" - ) - name_override : bpy.props.StringProperty( - name = "Name Override", - description="Name used for the add-on's, default is node group name", - default = "" - ) - description : bpy.props.StringProperty( - name = "Description", - description="Description used for the add-on", - default="" - ) - author_name : bpy.props.StringProperty( - name = "Author", - description = "Name used for the author/maintainer of the add-on", - default = "Node To Python" - ) - version: bpy.props.IntVectorProperty( - name = "Version", - description="Version of the add-on", - default = (1, 0, 0) - ) - location: bpy.props.StringProperty( - name = "Location", - description="Location of the addon", - default="Node" - ) - menu_id: bpy.props.StringProperty( - name = "Menu ID", - description = "Python ID of the menu you'd like to register the add-on " - "to. You can find this by enabling Python tooltips " - "(Preferences > Interface > Python tooltips) and " - "hovering over the desired menu", - default="NODE_MT_add" - ) - license: bpy.props.EnumProperty( - name="License", - items = [ - ('SPDX:GPL-3.0-or-later', "GNU General Public License v3.0 or later", ""), - ('OTHER', "Other", "User is responsible for including the license " - "and adding it to the manifest.\n" - "Please note that by using the Blender Python " - "API your add-on must comply with the GNU GPL. " - "See https://www.blender.org/about/license/ for " - "more details") - ], - default = 'SPDX:GPL-3.0-or-later' - ) - should_create_license: bpy.props.BoolProperty( - name="Create License", - description="Should NodeToPython include a license file", - default=True - ) - category: bpy.props.EnumProperty( - name = "Category", - items = [ - ('Custom', "Custom", "Use an unofficial category"), - ('3D View', "3D View", ""), - ('Add Curve', "Add Curve", ""), - ('Add Mesh', "Add Mesh", ""), - ('Animation', "Animation", ""), - ('Bake', "Bake", ""), - ('Compositing', "Compositing", ""), - ('Development', "Development", ""), - ('Game Engine', "Game Engine", ""), - ('Geometry Nodes', "Geometry Nodes", ""), - ("Grease Pencil", "Grease Pencil", ""), - ('Import-Export', "Import-Export", ""), - ('Lighting', "Lighting", ""), - ('Material', "Material", ""), - ('Mesh', "Mesh", ""), - ('Modeling', "Modeling", ""), - ('Node', "Node", ""), - ('Object', "Object", ""), - ('Paint', "Paint", ""), - ('Pipeline', "Pipeline", ""), - ('Physics', "Physics", ""), - ('Render', "Render", ""), - ('Rigging', "Rigging", ""), - ('Scene', "Scene", ""), - ('Sculpt', "Sculpt", ""), - ('Sequencer', "Sequencer", ""), - ('System', "System", ""), - ('Text Editor', "Text Editor", ""), - ('Tracking', "Tracking", ""), - ('UV', "UV", ""), - ('User Interface', "User Interface", ""), - ], - default = 'Node' - ) - custom_category: bpy.props.StringProperty( - name="Custom Category", - description="Custom category", - default = "" - ) - -class NTPOptionsPanel(bpy.types.Panel): - bl_label = "Options" - bl_idname = "NODE_PT_ntp_options" - bl_space_type = 'NODE_EDITOR' - bl_region_type = 'UI' - bl_context = '' - bl_category = "NodeToPython" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @classmethod - def poll(cls, context): - return True - def draw(self, context): - layout = self.layout - layout.operator_context = 'INVOKE_DEFAULT' - ntp_options = context.scene.ntp_options - - option_list = [ - "mode", - "include_group_socket_values", - "set_dimensions", - "indentation_type" - ] - if bpy.app.version >= (3, 4, 0): - option_list.append("set_unavailable_defaults") - - if ntp_options.mode == 'SCRIPT': - script_options = [ - "include_imports" - ] - option_list += script_options - elif ntp_options.mode == 'ADDON': - addon_options = [ - "dir_path", - "name_override", - "description", - "author_name", - "version", - "location", - "menu_id", - "license", - "should_create_license", - "category" - ] - option_list += addon_options - if ntp_options.category == 'CUSTOM': - option_list.append("custom_category") - - for option in option_list: - layout.prop(ntp_options, option) \ No newline at end of file diff --git a/NodeToPython/shader/__init__.py b/NodeToPython/shader/__init__.py index 1c6d5b3..cc9aa89 100644 --- a/NodeToPython/shader/__init__.py +++ b/NodeToPython/shader/__init__.py @@ -6,4 +6,9 @@ from . import operator from . import ui -import bpy \ No newline at end of file +import bpy + +modules = [ + operator +] +modules += ui.modules \ No newline at end of file diff --git a/NodeToPython/shader/operator.py b/NodeToPython/shader/operator.py index 2a6342e..831ca9e 100644 --- a/NodeToPython/shader/operator.py +++ b/NodeToPython/shader/operator.py @@ -13,9 +13,9 @@ NODE = "node" SHADER_OP_RESERVED_NAMES = {MAT_VAR, NODE} -class NTPShaderOperator(NTP_Operator): - bl_idname = "node.ntp_material" - bl_label = "Material to Python" +class NTP_OT_Shader(NTP_Operator): + bl_idname = "ntp.shader" + bl_label = "Shader to Python" bl_options = {'REGISTER', 'UNDO'} #TODO: add option for general shader node groups @@ -189,4 +189,8 @@ def execute(self, context): self._report_finished("material") - return {'FINISHED'} \ No newline at end of file + return {'FINISHED'} + +classes = [ + NTP_OT_Shader +] \ No newline at end of file diff --git a/NodeToPython/shader/ui.py b/NodeToPython/shader/ui.py deleted file mode 100644 index 06f84b1..0000000 --- a/NodeToPython/shader/ui.py +++ /dev/null @@ -1,52 +0,0 @@ -import bpy -from bpy.types import Panel -from bpy.types import Menu -from .operator import NTPShaderOperator - -class NTPShaderPanel(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" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - @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 NTPShaderMenu(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(NTPShaderOperator.bl_idname, - text=mat.name) - op.material_name = mat.name \ No newline at end of file diff --git a/NodeToPython/shader/ui/__init__.py b/NodeToPython/shader/ui/__init__.py new file mode 100644 index 0000000..cc8f63a --- /dev/null +++ b/NodeToPython/shader/ui/__init__.py @@ -0,0 +1,26 @@ +if "bpy" in locals(): + import importlib + importlib.reload(panel) + importlib.reload(materials) + importlib.reload(shader_node_groups) + importlib.reload(worlds) + importlib.reload(line_styles) + importlib.reload(lights) +else: + from . import panel + from . import materials + from . import shader_node_groups + from . import worlds + from . import line_styles + from . import lights + +import bpy + +modules : list = [ + panel, + materials, + shader_node_groups, + worlds, + line_styles, + lights +] \ No newline at end of file diff --git a/NodeToPython/shader/ui/lights.py b/NodeToPython/shader/ui/lights.py new file mode 100644 index 0000000..811b80a --- /dev/null +++ b/NodeToPython/shader/ui/lights.py @@ -0,0 +1,112 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_light_slots = bpy.props.CollectionProperty( + type=NTP_PG_LightSlot + ) + bpy.types.Scene.ntp_light_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_light_slots + del bpy.types.Scene.ntp_light_slots_index + +class NTP_PG_LightSlot(bpy.types.PropertyGroup): + name: bpy.props.StringProperty( + name="Light Name", + default="" + ) + + def poll_light(self, light: bpy.types.Light) -> bool: + for slot in bpy.context.scene.ntp_light_slots: + if slot is not self and slot.light == light: + return False + return light.use_nodes + + def update_light(self, context): + if self.light: + self.name = self.light.name + else: + self.name = "Light" + + light: bpy.props.PointerProperty( + name="Light", + type=bpy.types.Light, + poll=poll_light, + update=update_light + ) + +class NTP_OT_AddLightSlot(bpy.types.Operator): + bl_idname = "ntp.add_light_slot" + bl_label = "Add Light Slot" + bl_description = "Add Light Slot" + + def execute(self, context): + slots = context.scene.ntp_light_slots + slot = slots.add() + context.scene.ntp_light_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveLightSlot(bpy.types.Operator): + bl_idname = "ntp.remove_light_slot" + bl_label = "Remove Light Slot" + bl_description = "Remove Light Slot" + + def execute(self, context): + slots = context.scene.ntp_light_slots + idx = context.scene.ntp_light_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_light_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_Light(bpy.types.UIList): + bl_idname = "NTP_UL_light" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "light", bpy.data, "lights", text="") + +class NTP_PT_Light(bpy.types.Panel): + bl_idname = "NTP_PT_light" + bl_label = "Lights" + bl_parent_id = panel.NTP_PT_Shader.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "List of bpy.types.Light objects to replicate" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_Light.bl_idname, "", + context.scene, "ntp_light_slots", + context.scene, "ntp_light_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddLightSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveLightSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_LightSlot, + NTP_OT_AddLightSlot, + NTP_OT_RemoveLightSlot, + NTP_UL_Light, + NTP_PT_Light +] \ No newline at end of file diff --git a/NodeToPython/shader/ui/line_styles.py b/NodeToPython/shader/ui/line_styles.py new file mode 100644 index 0000000..2f724c2 --- /dev/null +++ b/NodeToPython/shader/ui/line_styles.py @@ -0,0 +1,112 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_line_style_slots = bpy.props.CollectionProperty( + type=NTP_PG_LineStyleSlot + ) + bpy.types.Scene.ntp_line_style_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_line_style_slots + del bpy.types.Scene.ntp_line_style_slots_index + +class NTP_PG_LineStyleSlot(bpy.types.PropertyGroup): + name: bpy.props.StringProperty( + name="Line Style Name", + default="" + ) + + def poll_line_style(self, line_style: bpy.types.FreestyleLineStyle) -> bool: + for slot in bpy.context.scene.ntp_line_style_slots: + if slot is not self and slot.line_style == line_style: + return False + return line_style.use_nodes + + def update_line_style(self, context): + if self.line_style: + self.name = self.line_style.name + else: + self.name = "Line Style" + + line_style: bpy.props.PointerProperty( + name="Line Style", + type=bpy.types.FreestyleLineStyle, + poll=poll_line_style, + update=update_line_style + ) + +class NTP_OT_AddLineStyleSlot(bpy.types.Operator): + bl_idname = "ntp.add_line_style_slot" + bl_label = "Add Line Style Slot" + bl_description = "Add Line Style Slot" + + def execute(self, context): + slots = context.scene.ntp_line_style_slots + slot = slots.add() + context.scene.ntp_line_style_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveLineStyleSlot(bpy.types.Operator): + bl_idname = "ntp.remove_line_style_slot" + bl_label = "Remove Line Style Slot" + bl_description = "Remove Line Style Slot" + + def execute(self, context): + slots = context.scene.ntp_line_style_slots + idx = context.scene.ntp_line_style_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_line_style_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_LineStyle(bpy.types.UIList): + bl_idname = "NTP_UL_line_style" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "line_style", bpy.data, "linestyles", text="") + +class NTP_PT_LineStyle(bpy.types.Panel): + bl_idname = "NTP_PT_line_style" + bl_label = "Line Styles" + bl_parent_id = panel.NTP_PT_Shader.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "List of bpy.types.FreestyleLineStyle objects to replicate" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_LineStyle.bl_idname, "", + context.scene, "ntp_line_style_slots", + context.scene, "ntp_line_style_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddLineStyleSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveLineStyleSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_LineStyleSlot, + NTP_OT_AddLineStyleSlot, + NTP_OT_RemoveLineStyleSlot, + NTP_UL_LineStyle, + NTP_PT_LineStyle +] \ No newline at end of file diff --git a/NodeToPython/shader/ui/materials.py b/NodeToPython/shader/ui/materials.py new file mode 100644 index 0000000..999738d --- /dev/null +++ b/NodeToPython/shader/ui/materials.py @@ -0,0 +1,112 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_material_slots = bpy.props.CollectionProperty( + type=NTP_PG_MaterialSlot + ) + bpy.types.Scene.ntp_material_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_material_slots + del bpy.types.Scene.ntp_material_slots_index + +class NTP_PG_MaterialSlot(bpy.types.PropertyGroup): + name: bpy.props.StringProperty( + name="Material Name", + default="" + ) + + def poll_material(self, material: bpy.types.Material) -> bool: + for slot in bpy.context.scene.ntp_material_slots: + if slot is not self and slot.material == material: + return False + return material.use_nodes + + def update_material(self, context): + if self.material: + self.name = self.material.name + else: + self.name = "Material" + + material: bpy.props.PointerProperty( + name="Material", + type=bpy.types.Material, + poll=poll_material, + update=update_material + ) + +class NTP_OT_AddMaterialSlot(bpy.types.Operator): + bl_idname = "ntp.add_material_slot" + bl_label = "Add Material Slot" + bl_description = "Add Material Slot" + + def execute(self, context): + slots = context.scene.ntp_material_slots + slot = slots.add() + context.scene.ntp_material_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveMaterialSlot(bpy.types.Operator): + bl_idname = "ntp.remove_material_slot" + bl_label = "Remove Material Slot" + bl_description = "Remove Material Slot" + + def execute(self, context): + slots = context.scene.ntp_material_slots + idx = context.scene.ntp_material_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_material_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_Material(bpy.types.UIList): + bl_idname = "NTP_UL_material" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "material", bpy.data, "materials", text="") + +class NTP_PT_Material(bpy.types.Panel): + bl_idname = "NTP_PT_material" + bl_label = "Materials" + bl_parent_id = panel.NTP_PT_Shader.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "List of bpy.types.Material objects to replicate" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_Material.bl_idname, "", + context.scene, "ntp_material_slots", + context.scene, "ntp_material_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddMaterialSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveMaterialSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_MaterialSlot, + NTP_OT_AddMaterialSlot, + NTP_OT_RemoveMaterialSlot, + NTP_UL_Material, + NTP_PT_Material +] \ No newline at end of file diff --git a/NodeToPython/shader/ui/panel.py b/NodeToPython/shader/ui/panel.py new file mode 100644 index 0000000..896907a --- /dev/null +++ b/NodeToPython/shader/ui/panel.py @@ -0,0 +1,27 @@ +import bpy + +class NTP_PT_Shader(bpy.types.Panel): + bl_label = "Shader to Python" + bl_idname = "NTP_PT_shader" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context): + layout = self.layout + +classes: list[type] = [ + NTP_PT_Shader +] \ No newline at end of file diff --git a/NodeToPython/shader/ui/shader_node_groups.py b/NodeToPython/shader/ui/shader_node_groups.py new file mode 100644 index 0000000..1f21351 --- /dev/null +++ b/NodeToPython/shader/ui/shader_node_groups.py @@ -0,0 +1,120 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_shader_node_group_slots = bpy.props.CollectionProperty( + type=NTP_PG_ShaderNodeGroupSlot + ) + bpy.types.Scene.ntp_shader_node_group_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_shader_node_group_slots + del bpy.types.Scene.ntp_shader_node_group_slots_index + +class NTP_PG_ShaderNodeGroupSlot(bpy.types.PropertyGroup): + """ + TODO: There's a bug where the filtering doesn't update when renaming a + slotted object. For now, we'll need to just remove and re-add the slot + to the UI list. + """ + name: bpy.props.StringProperty( + name="Node Tree Name", + default="" + ) + + def poll_node_tree(self, node_tree: bpy.types.NodeTree) -> bool: + scene = bpy.context.scene + + for slot in scene.ntp_shader_node_group_slots: + if slot is not self and slot.node_tree == node_tree: + return False + return node_tree.bl_idname == 'ShaderNodeTree' + + def update_node_tree(self, context): + if self.node_tree: + self.name = self.node_tree.name + else: + self.name = "Shader Node Group" + + node_tree: bpy.props.PointerProperty( + name="Node Tree", + type=bpy.types.NodeTree, + poll=poll_node_tree, + update=update_node_tree + ) + +class NTP_OT_AddShaderNodeGroupSlot(bpy.types.Operator): + bl_idname = "ntp.add_shader_node_group_slot" + bl_label = "Add Shader Node Group Slot" + bl_description = "Add Shader Node Group Slot" + + def execute(self, context): + slots = context.scene.ntp_shader_node_group_slots + slot = slots.add() + context.scene.ntp_shader_node_group_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveShaderNodeGroupSlot(bpy.types.Operator): + bl_idname = "ntp.remove_shader_node_group_slot" + bl_label = "Remove Shader Node Group Slot" + bl_description = "Remove Shader Node Group Slot" + + def execute(self, context): + slots = context.scene.ntp_shader_node_group_slots + idx = context.scene.ntp_shader_node_group_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_shader_node_group_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_ShaderNodeGroup(bpy.types.UIList): + bl_idname = "NTP_UL_shader_node_group" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "node_tree", bpy.data, "node_groups", text="") + +class NTP_PT_ShaderNodeGroup(bpy.types.Panel): + bl_idname = "NTP_PT_shader_node_group" + bl_label = "Node Groups" + bl_parent_id = panel.NTP_PT_Shader.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = ("List of shader node group objects to replicate.\n" + "These are typically subgroups within a larger material tree") + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_ShaderNodeGroup.bl_idname, "", + context.scene, "ntp_shader_node_group_slots", + context.scene, "ntp_shader_node_group_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddShaderNodeGroupSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveShaderNodeGroupSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_ShaderNodeGroupSlot, + NTP_OT_AddShaderNodeGroupSlot, + NTP_OT_RemoveShaderNodeGroupSlot, + NTP_UL_ShaderNodeGroup, + NTP_PT_ShaderNodeGroup +] \ No newline at end of file diff --git a/NodeToPython/shader/ui/worlds.py b/NodeToPython/shader/ui/worlds.py new file mode 100644 index 0000000..a9ee7a8 --- /dev/null +++ b/NodeToPython/shader/ui/worlds.py @@ -0,0 +1,112 @@ +import bpy + +from . import panel + +def register_props(): + bpy.types.Scene.ntp_world_slots = bpy.props.CollectionProperty( + type=NTP_PG_WorldSlot + ) + bpy.types.Scene.ntp_world_slots_index = bpy.props.IntProperty() + +def unregister_props(): + del bpy.types.Scene.ntp_world_slots + del bpy.types.Scene.ntp_world_slots_index + +class NTP_PG_WorldSlot(bpy.types.PropertyGroup): + name: bpy.props.StringProperty( + name="World Name", + default="" + ) + + def poll_world(self, world: bpy.types.World) -> bool: + for slot in bpy.context.scene.ntp_world_slots: + if slot is not self and slot.world == world: + return False + return world.use_nodes + + def update_world(self, context): + if self.world: + self.name = self.world.name + else: + self.name = "World" + + world: bpy.props.PointerProperty( + name="World", + type=bpy.types.World, + poll=poll_world, + update=update_world + ) + +class NTP_OT_AddWorldSlot(bpy.types.Operator): + bl_idname = "ntp.add_world_slot" + bl_label = "Add World Slot" + bl_description = "Add World Slot" + + def execute(self, context): + slots = context.scene.ntp_world_slots + slot = slots.add() + context.scene.ntp_world_slots_index = len(slots) - 1 + return {'FINISHED'} + +class NTP_OT_RemoveWorldSlot(bpy.types.Operator): + bl_idname = "ntp.remove_world_slot" + bl_label = "Remove World Slot" + bl_description = "Remove World Slot" + + def execute(self, context): + slots = context.scene.ntp_world_slots + idx = context.scene.ntp_world_slots_index + + if idx >= 0 and idx < len(slots): + slots.remove(idx) + context.scene.ntp_world_slots_index = min( + max(0, idx - 1), len(slots) - 1 + ) + return {'FINISHED'} + +class NTP_UL_World(bpy.types.UIList): + bl_idname = "NTP_UL_world" + + def draw_item(self, context, layout, data, item, icon, active_data, active): + if item: + layout.prop_search(item, "world", bpy.data, "worlds", text="") + +class NTP_PT_World(bpy.types.Panel): + bl_idname = "NTP_PT_world" + bl_label = "Worlds" + bl_parent_id = panel.NTP_PT_Shader.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "List of bpy.types.World objects to replicate" + bl_options = {'DEFAULT_CLOSED'} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + row = layout.row() + row.template_list( + NTP_UL_World.bl_idname, "", + context.scene, "ntp_world_slots", + context.scene, "ntp_world_slots_index", + rows=1 + ) + + col = row.column(align=True) + col.operator(NTP_OT_AddWorldSlot.bl_idname, icon="ADD", text="") + col.operator(NTP_OT_RemoveWorldSlot.bl_idname, icon="REMOVE", text="") + +classes: list[type] = [ + NTP_PG_WorldSlot, + NTP_OT_AddWorldSlot, + NTP_OT_RemoveWorldSlot, + NTP_UL_World, + NTP_PT_World +] \ No newline at end of file diff --git a/NodeToPython/ui/__init__.py b/NodeToPython/ui/__init__.py new file mode 100644 index 0000000..1a54699 --- /dev/null +++ b/NodeToPython/ui/__init__.py @@ -0,0 +1,20 @@ +if "bpy" in locals(): + import importlib + importlib.reload(main) + importlib.reload(settings) + importlib.reload(generation_settings) + importlib.reload(addon_settings) +else: + from . import main + from . import settings + from . import generation_settings + from . import addon_settings + +import bpy + +modules = [ + main, + settings, + generation_settings, + addon_settings +] \ No newline at end of file diff --git a/NodeToPython/ui/addon_settings.py b/NodeToPython/ui/addon_settings.py new file mode 100644 index 0000000..45224c8 --- /dev/null +++ b/NodeToPython/ui/addon_settings.py @@ -0,0 +1,46 @@ +import bpy + +from . import settings + +class NTP_PT_AddonSettings(bpy.types.Panel): + bl_idname = "NTP_PT_addon_settings" + bl_label = "Add-on Settings" + bl_parent_id = settings.NTP_PT_Settings.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return context.scene.ntp_options.mode == 'ADDON' + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_DEFAULT' + ntp_options = context.scene.ntp_options + + addon_options = [ + "dir_path", + "name_override", + "description", + "author_name", + "version", + "location", + "menu_id", + "license", + "should_create_license", + "category" + ] + if ntp_options.category == 'Custom': + addon_options.append("custom_category") + for option in addon_options: + layout.prop(ntp_options, option) + +classes: list[type] = [ + NTP_PT_AddonSettings +] \ No newline at end of file diff --git a/NodeToPython/ui/generation_settings.py b/NodeToPython/ui/generation_settings.py new file mode 100644 index 0000000..687a036 --- /dev/null +++ b/NodeToPython/ui/generation_settings.py @@ -0,0 +1,46 @@ +import bpy + +from . import settings + +class NTP_PT_GenerationSettings(bpy.types.Panel): + bl_idname = "NTP_PT_generation_settings" + bl_label = "Generation Settings" + bl_parent_id = settings.NTP_PT_Settings.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_DEFAULT' + ntp_options = context.scene.ntp_options + + generation_options = [ + "set_group_defaults", + "set_node_sizes", + "indentation_type" + ] + if bpy.app.version >= (3, 4, 0): + generation_options.append("set_unavailable_defaults") + + if ntp_options.mode == 'SCRIPT': + script_options = [ + "include_imports" + ] + generation_options += script_options + + for option in generation_options: + layout.prop(ntp_options, option) + +classes: list[type] = [ + NTP_PT_GenerationSettings +] \ No newline at end of file diff --git a/NodeToPython/ui/main.py b/NodeToPython/ui/main.py new file mode 100644 index 0000000..fb33206 --- /dev/null +++ b/NodeToPython/ui/main.py @@ -0,0 +1,64 @@ +import bpy + +from ..export_operator import NTP_OT_Export, NodeGroupGatherer + +import pathlib + +class NTP_PT_Main(bpy.types.Panel): + bl_idname = "NTP_PT_main" + bl_label = "Node To Python" + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw_header(self, context): + layout = self.layout + + def draw(self, context: bpy.types.Context): + layout = self.layout + col = layout.column(align=True) + row = col.row() + + ntp_options = context.scene.ntp_options + location = "" + export_icon = '' + if ntp_options.mode == 'SCRIPT': + location = "clipboard" + export_icon = 'COPYDOWN' + elif ntp_options.mode == 'ADDON': + location = pathlib.PurePath(ntp_options.dir_path).name + export_icon = 'CONSOLE' + + gatherer = NodeGroupGatherer() + gatherer.gather_node_groups(context) + num_node_groups = gatherer.get_number_node_groups() + + if num_node_groups == 1: + node_group = gatherer.get_single_node_group()[1].name + else: + node_group = f"{num_node_groups} node groups" + + export_text = f"Export {node_group} to {location}" + + if num_node_groups == 0: + export_text = f"0 node groups selected!" + export_icon = 'WARNING_LARGE' + + export_button = row.operator( + NTP_OT_Export.bl_idname, + text=export_text, + icon=export_icon + ) + row.enabled = num_node_groups > 0 + +classes: list[type] = [ + NTP_PT_Main +] \ No newline at end of file diff --git a/NodeToPython/ui/settings.py b/NodeToPython/ui/settings.py new file mode 100644 index 0000000..e92d1f5 --- /dev/null +++ b/NodeToPython/ui/settings.py @@ -0,0 +1,35 @@ +import bpy + +from . import main + +class NTP_PT_Settings(bpy.types.Panel): + bl_idname = "NTP_PT_settings" + bl_label = "Settings" + bl_parent_id = main.NTP_PT_Main.bl_idname + bl_space_type = 'NODE_EDITOR' + bl_region_type = 'UI' + bl_context = '' + bl_category = "NodeToPython" + bl_description = "" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + @classmethod + def poll(cls, context): + return True + + def draw(self, context): + layout = self.layout + layout.operator_context = 'INVOKE_DEFAULT' + ntp_options = context.scene.ntp_options + + option_list = [ + "mode" + ] + for option in option_list: + layout.prop(ntp_options, option) + +classes: list[type] = [ + NTP_PT_Settings +] \ No newline at end of file