Files

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

527 lines
18 KiB
Python
Raw Permalink Normal View History

# SPDX-FileCopyrightText: 2023 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import Panel, Menu
from bpy.props import StringProperty
from bpy.app.translations import contexts as i18n_contexts
from . import operators
from .utils.constants import blend_types, geo_combine_operations, operations
from .utils.nodes import get_nodes_links, NWBaseMenu
def socket_to_icon(socket):
socket_type = socket.type
if socket_type == "CUSTOM":
return "RADIOBUT_OFF"
if socket_type == "VALUE":
socket_type = "FLOAT"
return "NODE_SOCKET_" + socket_type
def drawlayout(context, layout, mode='non-panel'):
tree_type = context.space_data.tree_type
col = layout.column(align=True)
col.menu(NWMergeNodesMenu.bl_idname)
col.separator()
if tree_type == 'ShaderNodeTree':
col = layout.column(align=True)
col.operator(operators.NWAddTextureSetup.bl_idname, text="Add Texture Setup", icon='NODE_SEL')
col.operator(operators.NWAddPrincipledSetup.bl_idname, text="Add Principled Setup", icon='NODE_SEL')
col.separator()
col = layout.column(align=True)
col.operator(operators.NWDetachOutputs.bl_idname, icon='UNLINKED')
col.operator(operators.NWSwapLinks.bl_idname)
col.menu(NWAddReroutesMenu.bl_idname, icon='LAYER_USED')
col.separator()
col = layout.column(align=True)
col.menu(NWLinkActiveToSelectedMenu.bl_idname, icon='LINKED')
if tree_type != 'GeometryNodeTree':
col.operator(operators.NWLinkToOutputNode.bl_idname, icon='DRIVER')
col.separator()
col = layout.column(align=True)
if mode == 'panel':
row = col.row(align=True)
row.operator(operators.NWClearLabel.bl_idname).option = True
row.operator(operators.NWModifyLabels.bl_idname)
else:
col.operator(operators.NWClearLabel.bl_idname).option = True
col.operator(operators.NWModifyLabels.bl_idname)
col.menu(NWBatchChangeNodesMenu.bl_idname, text="Batch Change", text_ctxt=i18n_contexts.operator_default)
col.separator()
col.menu(NWCopyToSelectedMenu.bl_idname)
col.separator()
col = layout.column(align=True)
if tree_type == 'CompositorNodeTree':
col.operator(operators.NWResetBG.bl_idname, icon='ZOOM_PREVIOUS')
if tree_type != 'GeometryNodeTree':
col.operator(operators.NWReloadImages.bl_idname, icon='FILE_REFRESH')
col.separator()
col = layout.column(align=True)
col.operator('node.join', icon='STICKY_UVS_LOC')
col.separator()
col = layout.column(align=True)
col.operator(operators.NWAlignNodes.bl_idname, icon='CENTER_ONLY')
col.separator()
col = layout.column(align=True)
col.operator(operators.NWDeleteUnused.bl_idname, icon='CANCEL')
class NodeWranglerPanel(Panel, NWBaseMenu):
bl_idname = "NODE_PT_nw_node_wrangler"
bl_space_type = 'NODE_EDITOR'
bl_label = "Node Wrangler"
bl_region_type = "UI"
bl_category = "Node Wrangler"
prepend: StringProperty(
name='prepend',
)
append: StringProperty()
remove: StringProperty()
def draw(self, context):
self.layout.label(text="(Quick access: Shift+W)")
drawlayout(context, self.layout, mode='panel')
#
# M E N U S
#
class NodeWranglerMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_node_wrangler_menu"
bl_label = "Node Wrangler"
def draw(self, context):
self.layout.operator_context = 'INVOKE_DEFAULT'
drawlayout(context, self.layout)
class NWMergeNodesMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_merge_nodes_menu"
bl_label = "Merge Selected Nodes"
def draw(self, context):
type = context.space_data.tree_type
layout = self.layout
if type == 'ShaderNodeTree':
layout.menu(NWMergeShadersMenu.bl_idname, text="Use Shaders")
if type == 'GeometryNodeTree':
layout.menu(NWMergeGeometryMenu.bl_idname, text="Use Geometry Nodes")
layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
else:
layout.menu(NWMergeMixMenu.bl_idname, text="Use Mix Nodes")
layout.menu(NWMergeMathMenu.bl_idname, text="Use Math Nodes")
props = layout.operator(operators.NWMergeNodes.bl_idname, text="Use Z-Combine Nodes")
props.mode = 'MIX'
props.merge_type = 'ZCOMBINE'
props = layout.operator(operators.NWMergeNodes.bl_idname, text="Use Alpha Over Nodes")
props.mode = 'MIX'
props.merge_type = 'ALPHAOVER'
class NWMergeGeometryMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_merge_geometry_menu"
bl_label = "Merge Selected Nodes using Geometry Nodes"
def draw(self, context):
layout = self.layout
# The boolean node + Join Geometry node
for type, name, description in geo_combine_operations:
props = layout.operator(operators.NWMergeNodes.bl_idname, text=name, text_ctxt=i18n_contexts.id_nodetree)
props.mode = type
props.merge_type = 'GEOMETRY'
class NWMergeShadersMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_merge_shaders_menu"
bl_label = "Merge Selected Nodes using Shaders"
def draw(self, context):
layout = self.layout
for type in ('MIX', 'ADD'):
name = f'{type.capitalize()} Shader'
props = layout.operator(operators.NWMergeNodes.bl_idname, text=name, text_ctxt=i18n_contexts.default)
props.mode = type
props.merge_type = 'SHADER'
class NWMergeMixMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_merge_mix_menu"
bl_label = "Merge Selected Nodes using Mix"
def draw(self, context):
layout = self.layout
for type, name, description in blend_types:
props = layout.operator(operators.NWMergeNodes.bl_idname, text=name, text_ctxt=i18n_contexts.id_nodetree)
props.mode = type
props.merge_type = 'MIX'
class NWConnectionListOutputs(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_connection_list_out"
bl_label = ""
def draw(self, context):
layout = self.layout
nodes, links = get_nodes_links(context)
layout.label(text="From Socket", icon='RADIOBUT_OFF')
layout.separator()
n1 = nodes[context.scene.NWLazySource]
for index, output in enumerate(n1.outputs):
# Only show sockets that are exposed.
if output.enabled:
layout.operator(
operators.NWCallInputsMenu.bl_idname,
text=output.name,
text_ctxt=i18n_contexts.default,
icon=socket_to_icon(output),
).from_socket = index
class NWConnectionListInputs(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_connection_list_in"
bl_label = ""
def draw(self, context):
layout = self.layout
nodes, links = get_nodes_links(context)
layout.label(text="To Socket", icon='FORWARD')
layout.separator()
n2 = nodes[context.scene.NWLazyTarget]
for index, input in enumerate(n2.inputs):
# Only show sockets that are exposed.
# This prevents, for example, the scale value socket
# of the vector math node being added to the list when
# the mode is not 'SCALE'.
if input.enabled:
op = layout.operator(
operators.NWMakeLink.bl_idname, text=input.name,
text_ctxt=i18n_contexts.default,
icon=socket_to_icon(input),
)
op.from_socket = context.scene.NWSourceSocket
op.to_socket = index
class NWMergeMathMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_merge_math_menu"
bl_label = "Merge Selected Nodes using Math"
def draw(self, context):
layout = self.layout
for type, name, description in operations:
props = layout.operator(operators.NWMergeNodes.bl_idname, text=name, text_ctxt=i18n_contexts.id_nodetree)
props.mode = type
props.merge_type = 'MATH'
class NWBatchChangeNodesMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_batch_change_nodes_menu"
bl_label = "Batch Change Selected Nodes"
def draw(self, context):
layout = self.layout
layout.menu(NWBatchChangeBlendTypeMenu.bl_idname)
layout.menu(NWBatchChangeOperationMenu.bl_idname)
class NWBatchChangeBlendTypeMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_batch_change_blend_type_menu"
bl_label = "Batch Change Blend Type"
def draw(self, context):
layout = self.layout
for type, name, description in blend_types:
props = layout.operator(
operators.NWBatchChangeNodes.bl_idname,
text=name,
text_ctxt=i18n_contexts.id_nodetree,
)
props.blend_type = type
props.operation = 'CURRENT'
class NWBatchChangeOperationMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_batch_change_operation_menu"
bl_label = "Batch Change Math Operation"
def draw(self, context):
layout = self.layout
for type, name, description in operations:
props = layout.operator(operators.NWBatchChangeNodes.bl_idname, text=name, text_ctxt=i18n_contexts.id_nodetree)
props.blend_type = 'CURRENT'
props.operation = type
class NWCopyToSelectedMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_copy_node_properties_menu"
bl_label = "Copy to Selected"
def draw(self, context):
layout = self.layout
layout.operator(operators.NWCopySettings.bl_idname, text="Settings from Active")
layout.menu(NWCopyLabelMenu.bl_idname)
class NWCopyLabelMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_copy_label_menu"
bl_label = "Copy Label"
def draw(self, context):
layout = self.layout
layout.operator(operators.NWCopyLabel.bl_idname, text="From Active Node's Label").option = 'FROM_ACTIVE'
layout.operator(operators.NWCopyLabel.bl_idname, text="From Linked Node's Label").option = 'FROM_NODE'
layout.operator(operators.NWCopyLabel.bl_idname, text="From Linked Output's Name").option = 'FROM_SOCKET'
class NWAddReroutesMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_add_reroutes_menu"
bl_label = "Add Reroutes"
bl_description = "Add Reroute Nodes to Selected Nodes' Outputs"
def draw(self, context):
layout = self.layout
layout.operator(operators.NWAddReroutes.bl_idname, text="To All Outputs").option = 'ALL'
layout.operator(operators.NWAddReroutes.bl_idname, text="To Loose Outputs").option = 'LOOSE'
layout.operator(operators.NWAddReroutes.bl_idname, text="To Linked Outputs").option = 'LINKED'
class NWLinkActiveToSelectedMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_link_active_to_selected_menu"
bl_label = "Link Active to Selected"
def draw(self, context):
layout = self.layout
layout.menu(NWLinkStandardMenu.bl_idname)
layout.menu(NWLinkUseNodeNameMenu.bl_idname)
layout.menu(NWLinkUseOutputsNamesMenu.bl_idname)
class NWLinkStandardMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_link_standard_menu"
bl_label = "To All Selected"
def draw(self, context):
layout = self.layout
props = layout.operator(operators.NWLinkActiveToSelected.bl_idname, text="Do Not Replace Links")
props.replace = False
props.use_node_name = False
props.use_outputs_names = False
props = layout.operator(operators.NWLinkActiveToSelected.bl_idname, text="Replace Links")
props.replace = True
props.use_node_name = False
props.use_outputs_names = False
class NWLinkUseNodeNameMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_link_use_node_name_menu"
bl_label = "Use Node Name/Label"
def draw(self, context):
layout = self.layout
props = layout.operator(operators.NWLinkActiveToSelected.bl_idname, text="Do Not Replace Links")
props.replace = False
props.use_node_name = True
props.use_outputs_names = False
props = layout.operator(operators.NWLinkActiveToSelected.bl_idname, text="Replace Links")
props.replace = True
props.use_node_name = True
props.use_outputs_names = False
class NWLinkUseOutputsNamesMenu(Menu, NWBaseMenu):
bl_idname = "NODE_MT_nw_link_use_outputs_names_menu"
bl_label = "Use Outputs Names"
def draw(self, context):
layout = self.layout
props = layout.operator(operators.NWLinkActiveToSelected.bl_idname, text="Do Not Replace Links")
props.replace = False
props.use_node_name = False
props.use_outputs_names = True
props = layout.operator(operators.NWLinkActiveToSelected.bl_idname, text="Replace Links")
props.replace = True
props.use_node_name = False
props.use_outputs_names = True
class NWAttributeMenu(bpy.types.Menu):
bl_idname = "NODE_MT_nw_node_attribute_menu"
bl_label = "Attributes"
@classmethod
def poll(cls, context):
space = context.space_data
return (space.type == 'NODE_EDITOR'
and space.node_tree is not None
and space.node_tree.library is None
and space.tree_type == 'ShaderNodeTree'
and space.shader_type == 'OBJECT')
def draw(self, context):
l = self.layout
nodes, links = get_nodes_links(context)
mat = context.object.active_material
objs = []
for obj in bpy.data.objects:
for slot in obj.material_slots:
if slot.material == mat:
objs.append(obj)
attrs = []
for obj in objs:
if obj.data.attributes:
for attr in obj.data.attributes:
if not attr.is_internal:
attrs.append(attr.name)
attrs = list(set(attrs)) # get a unique list
if attrs:
for attr in attrs:
l.operator(
operators.NWAddAttrNode.bl_idname,
text=attr,
translate=False,
).attr_name = attr
else:
l.label(text="No attributes on objects with this material")
#
# APPENDAGES TO EXISTING UI
#
def select_parent_children_buttons(self, context):
layout = self.layout
layout.operator(operators.NWSelectParentChildren.bl_idname,
text="Select frame's members (children)").option = 'CHILD'
layout.operator(operators.NWSelectParentChildren.bl_idname, text="Select parent frame").option = 'PARENT'
def attr_nodes_menu_func(self, context):
col = self.layout.column(align=True)
col.menu("NODE_MT_nw_node_attribute_menu")
col.separator()
def multipleimages_menu_func(self, context):
col = self.layout.column(align=True)
col.operator("node.add_image", text="Multiple Images")
col.operator(operators.NWAddSequence.bl_idname, text="Image Sequence")
col.separator()
def bgreset_menu_func(self, context):
self.layout.operator(operators.NWResetBG.bl_idname)
def save_viewer_menu_func(self, context):
space = context.space_data
if (space.type == 'NODE_EDITOR'
Node Wrangler: Fixes for Save Viewer operator ## Fix: Error in Node Wrangler UI when in Sequencer mode Node Wrangler tried to find the current scene compositing node tree, even when there was none. This could happen if working on a sequencer compositing modifier. This commit hides the Save This Image button when not in Scene compositing mode. ## Fix: Error in Node Wrangler's Save Image op when file path is empty If the file path is empty in Save Image operator, it returns `None` and errors out. This commit makes it return `{'CANCELLED'}` early in that case. ## Add webp to list of file formats for Save Viewer ## Fix: Error when Viewer Node image renamed and saved The "Viewer Node" image that contains the current compositing node tree's viewer data has a hardcoded name by default. However it can be renamed by the user, which will make the Save Viewer operator fail. This commit extracts a utility function to get the current viewer image based on its properties, and simply saves that. In addition, this avoids having to change the area type to an image editor to get the current viewer node, and restoring the node editor afterwards. This action did not restore the current node tree if the editor was inside a node group. ## Rename "Save This Image" to "Save Viewer Image" When calling the operator through Menu Search, it is not clear what "Save This Image" refers to. This commit renames the operator to "Save Viewer Image", but keeps the old name for the button label. Pull Request: https://projects.blender.org/blender/blender/pulls/147085
2025-10-02 16:14:32 +02:00
and space.tree_type == 'CompositorNodeTree'
and space.node_tree_sub_type == 'SCENE'
and space.node_tree is not None
and space.node_tree.library is None
Node Wrangler: Fixes for Save Viewer operator ## Fix: Error in Node Wrangler UI when in Sequencer mode Node Wrangler tried to find the current scene compositing node tree, even when there was none. This could happen if working on a sequencer compositing modifier. This commit hides the Save This Image button when not in Scene compositing mode. ## Fix: Error in Node Wrangler's Save Image op when file path is empty If the file path is empty in Save Image operator, it returns `None` and errors out. This commit makes it return `{'CANCELLED'}` early in that case. ## Add webp to list of file formats for Save Viewer ## Fix: Error when Viewer Node image renamed and saved The "Viewer Node" image that contains the current compositing node tree's viewer data has a hardcoded name by default. However it can be renamed by the user, which will make the Save Viewer operator fail. This commit extracts a utility function to get the current viewer image based on its properties, and simply saves that. In addition, this avoids having to change the area type to an image editor to get the current viewer node, and restoring the node editor afterwards. This action did not restore the current node tree if the editor was inside a node group. ## Rename "Save This Image" to "Save Viewer Image" When calling the operator through Menu Search, it is not clear what "Save This Image" refers to. This commit renames the operator to "Save Viewer Image", but keeps the old name for the button label. Pull Request: https://projects.blender.org/blender/blender/pulls/147085
2025-10-02 16:14:32 +02:00
and space.edit_tree.nodes.active
and space.edit_tree.nodes.active.type == "VIEWER"):
self.layout.operator(operators.NWSaveViewer.bl_idname, icon='FILE_IMAGE')
def reset_nodes_button(self, context):
node_active = context.active_node
node_selected = context.selected_nodes
# Check if active node is in the selection, ignore some node types
if (len(node_selected) != 1
or node_active is None
or not node_active.select
or node_active.type in {"REROUTE", "GROUP"}):
return
row = self.layout.row()
if node_active.type == "FRAME":
row.operator(operators.NWResetNodes.bl_idname, text="Reset Nodes in Frame", icon="FILE_REFRESH")
else:
row.operator(operators.NWResetNodes.bl_idname, text="Reset Node", icon="FILE_REFRESH")
self.layout.separator()
classes = (
NodeWranglerPanel,
NodeWranglerMenu,
NWMergeNodesMenu,
NWMergeGeometryMenu,
NWMergeShadersMenu,
NWMergeMixMenu,
NWConnectionListOutputs,
NWConnectionListInputs,
NWMergeMathMenu,
NWBatchChangeNodesMenu,
NWBatchChangeBlendTypeMenu,
NWBatchChangeOperationMenu,
NWCopyToSelectedMenu,
NWCopyLabelMenu,
NWAddReroutesMenu,
NWLinkActiveToSelectedMenu,
NWLinkStandardMenu,
NWLinkUseNodeNameMenu,
NWLinkUseOutputsNamesMenu,
NWAttributeMenu,
)
def register():
from bpy.utils import register_class
for cls in classes:
register_class(cls)
# menu items
bpy.types.NODE_MT_select.append(select_parent_children_buttons)
bpy.types.NODE_MT_category_shader_input.prepend(attr_nodes_menu_func)
bpy.types.NODE_PT_backdrop.append(bgreset_menu_func)
bpy.types.NODE_PT_active_node_generic.append(save_viewer_menu_func)
bpy.types.NODE_MT_category_shader_texture.prepend(multipleimages_menu_func)
bpy.types.NODE_MT_category_compositor_input.prepend(multipleimages_menu_func)
bpy.types.NODE_PT_active_node_generic.prepend(reset_nodes_button)
bpy.types.NODE_MT_node.prepend(reset_nodes_button)
def unregister():
# menu items
bpy.types.NODE_MT_select.remove(select_parent_children_buttons)
bpy.types.NODE_MT_category_shader_input.remove(attr_nodes_menu_func)
bpy.types.NODE_PT_backdrop.remove(bgreset_menu_func)
bpy.types.NODE_PT_active_node_generic.remove(save_viewer_menu_func)
bpy.types.NODE_MT_category_shader_texture.remove(multipleimages_menu_func)
bpy.types.NODE_MT_category_compositor_input.remove(multipleimages_menu_func)
bpy.types.NODE_PT_active_node_generic.remove(reset_nodes_button)
bpy.types.NODE_MT_node.remove(reset_nodes_button)
from bpy.utils import unregister_class
for cls in classes:
unregister_class(cls)