forked from bartvdbraak/blender
Add-ons: make node wrangler a core add-on
Making node wrangler a core add-ons means that it will ship with Blender, but it still has to be enabled explicitly. It won't be available on the extensions platform anymore. We'll still continue to move parts of its functionality out of the add-on into default Blender. However, that takes a little bit longer, because we need to go over the design and code quality in more detail first. Once, the most important functionality (#121749) is merged, the remaining node wrangler features can be put on the extensions platform. Ref !122557
This commit is contained in:
parent
8501472c81
commit
5a43eb0cbc
5
scripts/addons_core/node_wrangler/README.md
Normal file
5
scripts/addons_core/node_wrangler/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Running Tests
|
||||
|
||||
```
|
||||
./utils/paths_test.py
|
||||
```
|
62
scripts/addons_core/node_wrangler/__init__.py
Normal file
62
scripts/addons_core/node_wrangler/__init__.py
Normal file
@ -0,0 +1,62 @@
|
||||
# SPDX-FileCopyrightText: 2013-2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
bl_info = {
|
||||
"name": "Node Wrangler",
|
||||
"author": "Bartek Skorupa, Greg Zaal, Sebastian Koenig, Christian Brinkmann, Florian Meyer",
|
||||
"version": (3, 54),
|
||||
"blender": (4, 2, 0),
|
||||
"location": "Node Editor Toolbar or Shift-W",
|
||||
"description": "Various tools to enhance and speed up node-based workflow",
|
||||
"warning": "",
|
||||
"doc_url": "{BLENDER_MANUAL_URL}/addons/node/node_wrangler.html",
|
||||
"category": "Node",
|
||||
}
|
||||
|
||||
import bpy
|
||||
from bpy.props import (
|
||||
BoolProperty,
|
||||
IntProperty,
|
||||
StringProperty,
|
||||
)
|
||||
|
||||
from . import operators
|
||||
from . import preferences
|
||||
from . import interface
|
||||
|
||||
|
||||
def register():
|
||||
# props
|
||||
bpy.types.Scene.NWBusyDrawing = StringProperty(
|
||||
name="Busy Drawing!",
|
||||
default="",
|
||||
description="An internal property used to store only the first mouse position")
|
||||
bpy.types.Scene.NWLazySource = StringProperty(
|
||||
name="Lazy Source!",
|
||||
default="x",
|
||||
description="An internal property used to store the first node in a Lazy Connect operation")
|
||||
bpy.types.Scene.NWLazyTarget = StringProperty(
|
||||
name="Lazy Target!",
|
||||
default="x",
|
||||
description="An internal property used to store the last node in a Lazy Connect operation")
|
||||
bpy.types.Scene.NWSourceSocket = IntProperty(
|
||||
name="Source Socket!",
|
||||
default=0,
|
||||
description="An internal property used to store the source socket in a Lazy Connect operation")
|
||||
|
||||
operators.register()
|
||||
interface.register()
|
||||
preferences.register()
|
||||
|
||||
|
||||
def unregister():
|
||||
preferences.unregister()
|
||||
interface.unregister()
|
||||
operators.unregister()
|
||||
|
||||
# props
|
||||
del bpy.types.Scene.NWBusyDrawing
|
||||
del bpy.types.Scene.NWLazySource
|
||||
del bpy.types.Scene.NWLazyTarget
|
||||
del bpy.types.Scene.NWSourceSocket
|
503
scripts/addons_core/node_wrangler/interface.py
Normal file
503
scripts/addons_core/node_wrangler/interface.py
Normal file
@ -0,0 +1,503 @@
|
||||
# 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 nodeitems_utils import node_categories_iter, NodeItemCustom
|
||||
|
||||
from . import operators
|
||||
|
||||
from .utils.constants import blend_types, geo_combine_operations, operations
|
||||
from .utils.nodes import get_nodes_links, NWBaseMenu
|
||||
|
||||
|
||||
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, text="Add Reroutes", icon='LAYER_USED')
|
||||
col.separator()
|
||||
|
||||
col = layout.column(align=True)
|
||||
col.menu(NWLinkActiveToSelectedMenu.bl_idname, text="Link Active To Selected", 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")
|
||||
col.separator()
|
||||
col.menu(NWCopyToSelectedMenu.bl_idname, text="Copy to Selected")
|
||||
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(operators.NWFrameSelected.bl_idname, 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')
|
||||
col.separator()
|
||||
|
||||
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
props.mode = type
|
||||
props.merge_type = 'MIX'
|
||||
|
||||
|
||||
class NWConnectionListOutputs(Menu, NWBaseMenu):
|
||||
bl_idname = "NODE_MT_nw_connection_list_out"
|
||||
bl_label = "From:"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
nodes, links = get_nodes_links(context)
|
||||
|
||||
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,
|
||||
icon="RADIOBUT_OFF").from_socket = index
|
||||
|
||||
|
||||
class NWConnectionListInputs(Menu, NWBaseMenu):
|
||||
bl_idname = "NODE_MT_nw_connection_list_in"
|
||||
bl_label = "To:"
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
nodes, links = get_nodes_links(context)
|
||||
|
||||
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, icon="FORWARD")
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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="Don't 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="Don't 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="Don't 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')
|
||||
|
||||
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).attr_name = attr
|
||||
else:
|
||||
l.label(text="No attributes on objects with this material")
|
||||
|
||||
|
||||
class NWSwitchNodeTypeMenu(Menu, NWBaseMenu):
|
||||
bl_idname = "NODE_MT_nw_switch_node_type_menu"
|
||||
bl_label = "Switch Type to..."
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.label(text="This operator is removed due to the changes of node menus.", icon='ERROR')
|
||||
layout.label(text="A native implementation of the function is expected in the future.")
|
||||
|
||||
#
|
||||
# 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(operators.NWAddMultipleImages.bl_idname, 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'
|
||||
and space.node_tree is not None
|
||||
and space.node_tree.library is None
|
||||
and space.tree_type == 'CompositorNodeTree'
|
||||
and context.scene.node_tree.nodes.active
|
||||
and context.scene.node_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,
|
||||
NWSwitchNodeTypeMenu,
|
||||
)
|
||||
|
||||
|
||||
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)
|
2481
scripts/addons_core/node_wrangler/operators.py
Normal file
2481
scripts/addons_core/node_wrangler/operators.py
Normal file
File diff suppressed because it is too large
Load Diff
397
scripts/addons_core/node_wrangler/preferences.py
Normal file
397
scripts/addons_core/node_wrangler/preferences.py
Normal file
@ -0,0 +1,397 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy.props import EnumProperty, BoolProperty, StringProperty
|
||||
from nodeitems_utils import node_categories_iter
|
||||
|
||||
from . import operators
|
||||
from . import interface
|
||||
|
||||
from .utils.constants import nice_hotkey_name
|
||||
|
||||
|
||||
# Principled prefs
|
||||
class NWPrincipledPreferences(bpy.types.PropertyGroup):
|
||||
base_color: StringProperty(
|
||||
name='Base Color',
|
||||
default='diffuse diff albedo base col color basecolor',
|
||||
description='Naming Components for Base Color maps')
|
||||
metallic: StringProperty(
|
||||
name='Metallic',
|
||||
default='metallic metalness metal mtl',
|
||||
description='Naming Components for metallness maps')
|
||||
specular: StringProperty(
|
||||
name='Specular',
|
||||
default='specularity specular spec spc',
|
||||
description='Naming Components for Specular maps')
|
||||
normal: StringProperty(
|
||||
name='Normal',
|
||||
default='normal nor nrm nrml norm',
|
||||
description='Naming Components for Normal maps')
|
||||
bump: StringProperty(
|
||||
name='Bump',
|
||||
default='bump bmp',
|
||||
description='Naming Components for bump maps')
|
||||
rough: StringProperty(
|
||||
name='Roughness',
|
||||
default='roughness rough rgh',
|
||||
description='Naming Components for roughness maps')
|
||||
gloss: StringProperty(
|
||||
name='Gloss',
|
||||
default='gloss glossy glossiness',
|
||||
description='Naming Components for glossy maps')
|
||||
displacement: StringProperty(
|
||||
name='Displacement',
|
||||
default='displacement displace disp dsp height heightmap',
|
||||
description='Naming Components for displacement maps')
|
||||
transmission: StringProperty(
|
||||
name='Transmission',
|
||||
default='transmission transparency',
|
||||
description='Naming Components for transmission maps')
|
||||
emission: StringProperty(
|
||||
name='Emission',
|
||||
default='emission emissive emit',
|
||||
description='Naming Components for emission maps')
|
||||
alpha: StringProperty(
|
||||
name='Alpha',
|
||||
default='alpha opacity',
|
||||
description='Naming Components for alpha maps')
|
||||
ambient_occlusion: StringProperty(
|
||||
name='Ambient Occlusion',
|
||||
default='ao ambient occlusion',
|
||||
description='Naming Components for AO maps')
|
||||
|
||||
|
||||
# Addon prefs
|
||||
class NWNodeWrangler(bpy.types.AddonPreferences):
|
||||
bl_idname = __package__
|
||||
|
||||
merge_hide: EnumProperty(
|
||||
name="Hide Mix nodes",
|
||||
items=(
|
||||
("ALWAYS", "Always", "Always collapse the new merge nodes"),
|
||||
("NON_SHADER", "Non-Shader", "Collapse in all cases except for shaders"),
|
||||
("NEVER", "Never", "Never collapse the new merge nodes")
|
||||
),
|
||||
default='NON_SHADER',
|
||||
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify whether to collapse them or show the full node with options expanded")
|
||||
merge_position: EnumProperty(
|
||||
name="Mix Node Position",
|
||||
items=(
|
||||
("CENTER", "Center", "Place the Mix node between the two nodes"),
|
||||
("BOTTOM", "Bottom", "Place the Mix node at the same height as the lowest node")
|
||||
),
|
||||
default='CENTER',
|
||||
description="When merging nodes with the Ctrl+Numpad0 hotkey (and similar) specify the position of the new nodes")
|
||||
|
||||
show_hotkey_list: BoolProperty(
|
||||
name="Show Hotkey List",
|
||||
default=False,
|
||||
description="Expand this box into a list of all the hotkeys for functions in this addon"
|
||||
)
|
||||
hotkey_list_filter: StringProperty(
|
||||
name=" Filter by Name",
|
||||
default="",
|
||||
description="Show only hotkeys that have this text in their name",
|
||||
options={'TEXTEDIT_UPDATE'}
|
||||
)
|
||||
show_principled_lists: BoolProperty(
|
||||
name="Show Principled naming tags",
|
||||
default=False,
|
||||
description="Expand this box into a list of all naming tags for principled texture setup"
|
||||
)
|
||||
principled_tags: bpy.props.PointerProperty(type=NWPrincipledPreferences)
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
col = layout.column()
|
||||
col.prop(self, "merge_position")
|
||||
col.prop(self, "merge_hide")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
col.prop(
|
||||
self,
|
||||
"show_principled_lists",
|
||||
text='Edit tags for auto texture detection in Principled BSDF setup',
|
||||
toggle=True)
|
||||
if self.show_principled_lists:
|
||||
tags = self.principled_tags
|
||||
|
||||
col.prop(tags, "base_color")
|
||||
col.prop(tags, "metallic")
|
||||
col.prop(tags, "specular")
|
||||
col.prop(tags, "rough")
|
||||
col.prop(tags, "gloss")
|
||||
col.prop(tags, "normal")
|
||||
col.prop(tags, "bump")
|
||||
col.prop(tags, "displacement")
|
||||
col.prop(tags, "transmission")
|
||||
col.prop(tags, "emission")
|
||||
col.prop(tags, "alpha")
|
||||
col.prop(tags, "ambient_occlusion")
|
||||
|
||||
box = layout.box()
|
||||
col = box.column(align=True)
|
||||
hotkey_button_name = "Show Hotkey List"
|
||||
if self.show_hotkey_list:
|
||||
hotkey_button_name = "Hide Hotkey List"
|
||||
col.prop(self, "show_hotkey_list", text=hotkey_button_name, toggle=True)
|
||||
if self.show_hotkey_list:
|
||||
col.prop(self, "hotkey_list_filter", icon="VIEWZOOM")
|
||||
col.separator()
|
||||
for hotkey in kmi_defs:
|
||||
if hotkey[7]:
|
||||
hotkey_name = hotkey[7]
|
||||
|
||||
if self.hotkey_list_filter.lower() in hotkey_name.lower():
|
||||
row = col.row(align=True)
|
||||
row.label(text=hotkey_name)
|
||||
keystr = nice_hotkey_name(hotkey[1])
|
||||
if hotkey[4]:
|
||||
keystr = "Shift " + keystr
|
||||
if hotkey[5]:
|
||||
keystr = "Alt " + keystr
|
||||
if hotkey[3]:
|
||||
keystr = "Ctrl " + keystr
|
||||
row.label(text=keystr)
|
||||
|
||||
|
||||
#
|
||||
# REGISTER/UNREGISTER CLASSES AND KEYMAP ITEMS
|
||||
#
|
||||
addon_keymaps = []
|
||||
# kmi_defs entry: (identifier, key, action, CTRL, SHIFT, ALT, props, nice name)
|
||||
# props entry: (property name, property value)
|
||||
kmi_defs = (
|
||||
# MERGE NODES
|
||||
# NWMergeNodes with Ctrl (AUTO).
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, False,
|
||||
(('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, False,
|
||||
(('mode', 'MIX'), ('merge_type', 'AUTO'),), "Merge Nodes (Automatic)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, False,
|
||||
(('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, False,
|
||||
(('mode', 'ADD'), ('merge_type', 'AUTO'),), "Merge Nodes (Add)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, False,
|
||||
(('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, False,
|
||||
(('mode', 'MULTIPLY'), ('merge_type', 'AUTO'),), "Merge Nodes (Multiply)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, False,
|
||||
(('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, False,
|
||||
(('mode', 'SUBTRACT'), ('merge_type', 'AUTO'),), "Merge Nodes (Subtract)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, False,
|
||||
(('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, False,
|
||||
(('mode', 'DIVIDE'), ('merge_type', 'AUTO'),), "Merge Nodes (Divide)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, False, False,
|
||||
(('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Less than)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, False, False,
|
||||
(('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Greater than)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_PERIOD', 'PRESS', True, False, False,
|
||||
(('mode', 'MIX'), ('merge_type', 'ZCOMBINE'),), "Merge Nodes (Z-Combine)"),
|
||||
# NWMergeNodes with Ctrl Alt (MIX or ALPHAOVER)
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_0', 'PRESS', True, False, True,
|
||||
(('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'ZERO', 'PRESS', True, False, True,
|
||||
(('mode', 'MIX'), ('merge_type', 'ALPHAOVER'),), "Merge Nodes (Alpha Over)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, False, True,
|
||||
(('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, False, True,
|
||||
(('mode', 'ADD'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Add)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, False, True,
|
||||
(('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, False, True,
|
||||
(('mode', 'MULTIPLY'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Multiply)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, False, True,
|
||||
(('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, False, True,
|
||||
(('mode', 'SUBTRACT'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Subtract)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, False, True,
|
||||
(('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, False, True,
|
||||
(('mode', 'DIVIDE'), ('merge_type', 'MIX'),), "Merge Nodes (Color, Divide)"),
|
||||
# NWMergeNodes with Ctrl Shift (MATH)
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', True, True, False,
|
||||
(('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'EQUAL', 'PRESS', True, True, False,
|
||||
(('mode', 'ADD'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Add)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', True, True, False,
|
||||
(('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'EIGHT', 'PRESS', True, True, False,
|
||||
(('mode', 'MULTIPLY'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Multiply)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', True, True, False,
|
||||
(('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'MINUS', 'PRESS', True, True, False,
|
||||
(('mode', 'SUBTRACT'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Subtract)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', True, True, False,
|
||||
(('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'SLASH', 'PRESS', True, True, False,
|
||||
(('mode', 'DIVIDE'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Divide)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'COMMA', 'PRESS', True, True, False,
|
||||
(('mode', 'LESS_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Less than)"),
|
||||
(operators.NWMergeNodes.bl_idname, 'PERIOD', 'PRESS', True, True, False,
|
||||
(('mode', 'GREATER_THAN'), ('merge_type', 'MATH'),), "Merge Nodes (Math, Greater than)"),
|
||||
# BATCH CHANGE NODES
|
||||
# NWBatchChangeNodes with Alt
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'NUMPAD_0', 'PRESS', False, False, True,
|
||||
(('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'ZERO', 'PRESS', False, False, True,
|
||||
(('blend_type', 'MIX'), ('operation', 'CURRENT'),), "Batch change blend type (Mix)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'NUMPAD_PLUS', 'PRESS', False, False, True,
|
||||
(('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'EQUAL', 'PRESS', False, False, True,
|
||||
(('blend_type', 'ADD'), ('operation', 'ADD'),), "Batch change blend type (Add)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'NUMPAD_ASTERIX', 'PRESS', False, False, True,
|
||||
(('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'EIGHT', 'PRESS', False, False, True,
|
||||
(('blend_type', 'MULTIPLY'), ('operation', 'MULTIPLY'),), "Batch change blend type (Multiply)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'NUMPAD_MINUS', 'PRESS', False, False, True,
|
||||
(('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'MINUS', 'PRESS', False, False, True,
|
||||
(('blend_type', 'SUBTRACT'), ('operation', 'SUBTRACT'),), "Batch change blend type (Subtract)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'NUMPAD_SLASH', 'PRESS', False, False, True,
|
||||
(('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'SLASH', 'PRESS', False, False, True,
|
||||
(('blend_type', 'DIVIDE'), ('operation', 'DIVIDE'),), "Batch change blend type (Divide)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'COMMA', 'PRESS', False, False, True,
|
||||
(('blend_type', 'CURRENT'), ('operation', 'LESS_THAN'),), "Batch change blend type (Current)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'PERIOD', 'PRESS', False, False, True,
|
||||
(('blend_type', 'CURRENT'), ('operation', 'GREATER_THAN'),), "Batch change blend type (Current)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'DOWN_ARROW', 'PRESS', False, False, True,
|
||||
(('blend_type', 'NEXT'), ('operation', 'NEXT'),), "Batch change blend type (Next)"),
|
||||
(operators.NWBatchChangeNodes.bl_idname, 'UP_ARROW', 'PRESS', False, False, True,
|
||||
(('blend_type', 'PREV'), ('operation', 'PREV'),), "Batch change blend type (Previous)"),
|
||||
# LINK ACTIVE TO SELECTED
|
||||
# Don't use names, don't replace links (K)
|
||||
(operators.NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, False, False,
|
||||
(('replace', False), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Don't replace links)"),
|
||||
# Don't use names, replace links (Shift K)
|
||||
(operators.NWLinkActiveToSelected.bl_idname, 'K', 'PRESS', False, True, False,
|
||||
(('replace', True), ('use_node_name', False), ('use_outputs_names', False),), "Link active to selected (Replace links)"),
|
||||
# Use node name, don't replace links (')
|
||||
(operators.NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, False, False,
|
||||
(('replace', False), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Don't replace links, node names)"),
|
||||
# Use node name, replace links (Shift ')
|
||||
(operators.NWLinkActiveToSelected.bl_idname, 'QUOTE', 'PRESS', False, True, False,
|
||||
(('replace', True), ('use_node_name', True), ('use_outputs_names', False),), "Link active to selected (Replace links, node names)"),
|
||||
# Don't use names, don't replace links (;)
|
||||
(operators.NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, False, False,
|
||||
(('replace', False), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Don't replace links, output names)"),
|
||||
# Don't use names, replace links (')
|
||||
(operators.NWLinkActiveToSelected.bl_idname, 'SEMI_COLON', 'PRESS', False, True, False,
|
||||
(('replace', True), ('use_node_name', False), ('use_outputs_names', True),), "Link active to selected (Replace links, output names)"),
|
||||
# CHANGE MIX FACTOR
|
||||
(operators.NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False,
|
||||
False, True, (('option', -0.1),), "Reduce Mix Factor by 0.1"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False,
|
||||
False, True, (('option', 0.1),), "Increase Mix Factor by 0.1"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS', False,
|
||||
True, True, (('option', -0.01),), "Reduce Mix Factor by 0.01"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS', False,
|
||||
True, True, (('option', 0.01),), "Increase Mix Factor by 0.01"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'LEFT_ARROW', 'PRESS',
|
||||
True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'RIGHT_ARROW', 'PRESS',
|
||||
True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'NUMPAD_0', 'PRESS',
|
||||
True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'ZERO', 'PRESS', True, True, True, (('option', 0.0),), "Set Mix Factor to 0.0"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'NUMPAD_1', 'PRESS', True, True, True, (('option', 1.0),), "Mix Factor to 1.0"),
|
||||
(operators.NWChangeMixFactor.bl_idname, 'ONE', 'PRESS', True, True, True, (('option', 1.0),), "Set Mix Factor to 1.0"),
|
||||
# CLEAR LABEL (Alt L)
|
||||
(operators.NWClearLabel.bl_idname, 'L', 'PRESS', False, False, True, (('option', False),), "Clear node labels"),
|
||||
# MODIFY LABEL (Alt Shift L)
|
||||
(operators.NWModifyLabels.bl_idname, 'L', 'PRESS', False, True, True, None, "Modify node labels"),
|
||||
# Copy Label from active to selected
|
||||
(operators.NWCopyLabel.bl_idname, 'V', 'PRESS', False, True, False,
|
||||
(('option', 'FROM_ACTIVE'),), "Copy label from active to selected"),
|
||||
# DETACH OUTPUTS (Alt Shift D)
|
||||
(operators.NWDetachOutputs.bl_idname, 'D', 'PRESS', False, True, True, None, "Detach outputs"),
|
||||
# LINK TO OUTPUT NODE (O)
|
||||
(operators.NWLinkToOutputNode.bl_idname, 'O', 'PRESS', False, False, False, None, "Link to output node"),
|
||||
# SELECT PARENT/CHILDREN
|
||||
# Select Children
|
||||
(operators.NWSelectParentChildren.bl_idname, 'RIGHT_BRACKET', 'PRESS',
|
||||
False, False, False, (('option', 'CHILD'),), "Select children"),
|
||||
# Select Parent
|
||||
(operators.NWSelectParentChildren.bl_idname, 'LEFT_BRACKET', 'PRESS',
|
||||
False, False, False, (('option', 'PARENT'),), "Select Parent"),
|
||||
# Add Texture Setup
|
||||
(operators.NWAddTextureSetup.bl_idname, 'T', 'PRESS', True, False, False, None, "Add texture setup"),
|
||||
# Add Principled BSDF Texture Setup
|
||||
(operators.NWAddPrincipledSetup.bl_idname, 'T', 'PRESS', True, True, False, None, "Add Principled texture setup"),
|
||||
# Reset backdrop
|
||||
(operators.NWResetBG.bl_idname, 'Z', 'PRESS', False, False, False, None, "Reset backdrop image zoom"),
|
||||
# Delete unused
|
||||
(operators.NWDeleteUnused.bl_idname, 'X', 'PRESS', False, False, True, None, "Delete unused nodes"),
|
||||
# Frame Selected
|
||||
(operators.NWFrameSelected.bl_idname, 'P', 'PRESS', False, True, False, None, "Frame selected nodes"),
|
||||
# Swap Links
|
||||
(operators.NWSwapLinks.bl_idname, 'S', 'PRESS', False, False, True, None, "Swap Links"),
|
||||
# Reload Images
|
||||
(operators.NWReloadImages.bl_idname, 'R', 'PRESS', False, False, True, None, "Reload images"),
|
||||
# Lazy Mix
|
||||
(operators.NWLazyMix.bl_idname, 'RIGHTMOUSE', 'PRESS', True, True, False, None, "Lazy Mix"),
|
||||
# Lazy Connect
|
||||
(operators.NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False, False, True, (('with_menu', False),), "Lazy Connect"),
|
||||
# Lazy Connect with Menu
|
||||
(operators.NWLazyConnect.bl_idname, 'RIGHTMOUSE', 'PRESS', False,
|
||||
True, True, (('with_menu', True),), "Lazy Connect with Socket Menu"),
|
||||
# Align Nodes
|
||||
(operators.NWAlignNodes.bl_idname, 'EQUAL', 'PRESS', False, True,
|
||||
False, None, "Align selected nodes neatly in a row/column"),
|
||||
# Reset Nodes (Back Space)
|
||||
(operators.NWResetNodes.bl_idname, 'BACK_SPACE', 'PRESS', False, False,
|
||||
False, None, "Revert node back to default state, but keep connections"),
|
||||
# MENUS
|
||||
('wm.call_menu', 'W', 'PRESS', False, True, False, (('name', interface.NodeWranglerMenu.bl_idname),), "Node Wrangler menu"),
|
||||
('wm.call_menu', 'SLASH', 'PRESS', False, False, False,
|
||||
(('name', interface.NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
|
||||
('wm.call_menu', 'NUMPAD_SLASH', 'PRESS', False, False, False,
|
||||
(('name', interface.NWAddReroutesMenu.bl_idname),), "Add Reroutes menu"),
|
||||
('wm.call_menu', 'BACK_SLASH', 'PRESS', False, False, False,
|
||||
(('name', interface.NWLinkActiveToSelectedMenu.bl_idname),), "Link active to selected (menu)"),
|
||||
('wm.call_menu', 'C', 'PRESS', False, True, False,
|
||||
(('name', interface.NWCopyToSelectedMenu.bl_idname),), "Copy to selected (menu)"),
|
||||
('wm.call_menu', 'S', 'PRESS', False, True, False,
|
||||
(('name', interface.NWSwitchNodeTypeMenu.bl_idname),), "Switch node type menu"),
|
||||
)
|
||||
|
||||
classes = (
|
||||
NWPrincipledPreferences, NWNodeWrangler
|
||||
)
|
||||
|
||||
|
||||
def register():
|
||||
from bpy.utils import register_class
|
||||
for cls in classes:
|
||||
register_class(cls)
|
||||
|
||||
# keymaps
|
||||
addon_keymaps.clear()
|
||||
kc = bpy.context.window_manager.keyconfigs.addon
|
||||
if kc:
|
||||
km = kc.keymaps.new(name='Node Editor', space_type="NODE_EDITOR")
|
||||
for (identifier, key, action, CTRL, SHIFT, ALT, props, nicename) in kmi_defs:
|
||||
kmi = km.keymap_items.new(identifier, key, action, ctrl=CTRL, shift=SHIFT, alt=ALT)
|
||||
if props:
|
||||
for prop, value in props:
|
||||
setattr(kmi.properties, prop, value)
|
||||
addon_keymaps.append((km, kmi))
|
||||
|
||||
|
||||
def unregister():
|
||||
|
||||
# keymaps
|
||||
for km, kmi in addon_keymaps:
|
||||
km.keymap_items.remove(kmi)
|
||||
addon_keymaps.clear()
|
||||
|
||||
from bpy.utils import unregister_class
|
||||
for cls in classes:
|
||||
unregister_class(cls)
|
232
scripts/addons_core/node_wrangler/utils/constants.py
Normal file
232
scripts/addons_core/node_wrangler/utils/constants.py
Normal file
@ -0,0 +1,232 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
|
||||
#################
|
||||
# rl_outputs:
|
||||
# list of outputs of Input Render Layer
|
||||
# with attributes determining if pass is used,
|
||||
# and MultiLayer EXR outputs names and corresponding render engines
|
||||
#
|
||||
# rl_outputs entry = (render_pass, rl_output_name, exr_output_name, in_eevee, in_cycles)
|
||||
RL_entry = namedtuple('RL_Entry', ['render_pass', 'output_name', 'exr_output_name', 'in_eevee', 'in_cycles'])
|
||||
rl_outputs = (
|
||||
RL_entry('use_pass_ambient_occlusion', 'AO', 'AO', True, True),
|
||||
RL_entry('use_pass_combined', 'Image', 'Combined', True, True),
|
||||
RL_entry('use_pass_diffuse_color', 'Diffuse Color', 'DiffCol', False, True),
|
||||
RL_entry('use_pass_diffuse_direct', 'Diffuse Direct', 'DiffDir', False, True),
|
||||
RL_entry('use_pass_diffuse_indirect', 'Diffuse Indirect', 'DiffInd', False, True),
|
||||
RL_entry('use_pass_emit', 'Emit', 'Emit', False, True),
|
||||
RL_entry('use_pass_environment', 'Environment', 'Env', False, False),
|
||||
RL_entry('use_pass_glossy_color', 'Glossy Color', 'GlossCol', False, True),
|
||||
RL_entry('use_pass_glossy_direct', 'Glossy Direct', 'GlossDir', False, True),
|
||||
RL_entry('use_pass_glossy_indirect', 'Glossy Indirect', 'GlossInd', False, True),
|
||||
RL_entry('use_pass_indirect', 'Indirect', 'Indirect', False, False),
|
||||
RL_entry('use_pass_material_index', 'IndexMA', 'IndexMA', False, True),
|
||||
RL_entry('use_pass_mist', 'Mist', 'Mist', True, True),
|
||||
RL_entry('use_pass_normal', 'Normal', 'Normal', True, True),
|
||||
RL_entry('use_pass_object_index', 'IndexOB', 'IndexOB', False, True),
|
||||
RL_entry('use_pass_shadow', 'Shadow', 'Shadow', False, True),
|
||||
RL_entry('use_pass_subsurface_color', 'Subsurface Color', 'SubsurfaceCol', True, True),
|
||||
RL_entry('use_pass_subsurface_direct', 'Subsurface Direct', 'SubsurfaceDir', True, True),
|
||||
RL_entry('use_pass_subsurface_indirect', 'Subsurface Indirect', 'SubsurfaceInd', False, True),
|
||||
RL_entry('use_pass_transmission_color', 'Transmission Color', 'TransCol', False, True),
|
||||
RL_entry('use_pass_transmission_direct', 'Transmission Direct', 'TransDir', False, True),
|
||||
RL_entry('use_pass_transmission_indirect', 'Transmission Indirect', 'TransInd', False, True),
|
||||
RL_entry('use_pass_uv', 'UV', 'UV', True, True),
|
||||
RL_entry('use_pass_vector', 'Speed', 'Vector', False, True),
|
||||
RL_entry('use_pass_z', 'Z', 'Depth', True, True),
|
||||
)
|
||||
|
||||
# list of blend types of "Mix" nodes in a form that can be used as 'items' for EnumProperty.
|
||||
# used list, not tuple for easy merging with other lists.
|
||||
blend_types = [
|
||||
('MIX', 'Mix', 'Mix Mode'),
|
||||
('ADD', 'Add', 'Add Mode'),
|
||||
('MULTIPLY', 'Multiply', 'Multiply Mode'),
|
||||
('SUBTRACT', 'Subtract', 'Subtract Mode'),
|
||||
('SCREEN', 'Screen', 'Screen Mode'),
|
||||
('DIVIDE', 'Divide', 'Divide Mode'),
|
||||
('DIFFERENCE', 'Difference', 'Difference Mode'),
|
||||
('DARKEN', 'Darken', 'Darken Mode'),
|
||||
('LIGHTEN', 'Lighten', 'Lighten Mode'),
|
||||
('OVERLAY', 'Overlay', 'Overlay Mode'),
|
||||
('DODGE', 'Dodge', 'Dodge Mode'),
|
||||
('BURN', 'Burn', 'Burn Mode'),
|
||||
('HUE', 'Hue', 'Hue Mode'),
|
||||
('SATURATION', 'Saturation', 'Saturation Mode'),
|
||||
('VALUE', 'Value', 'Value Mode'),
|
||||
('COLOR', 'Color', 'Color Mode'),
|
||||
('SOFT_LIGHT', 'Soft Light', 'Soft Light Mode'),
|
||||
('LINEAR_LIGHT', 'Linear Light', 'Linear Light Mode'),
|
||||
]
|
||||
|
||||
# list of operations of "Math" nodes in a form that can be used as 'items' for EnumProperty.
|
||||
# used list, not tuple for easy merging with other lists.
|
||||
operations = [
|
||||
('ADD', 'Add', 'Add Mode'),
|
||||
('SUBTRACT', 'Subtract', 'Subtract Mode'),
|
||||
('MULTIPLY', 'Multiply', 'Multiply Mode'),
|
||||
('DIVIDE', 'Divide', 'Divide Mode'),
|
||||
('MULTIPLY_ADD', 'Multiply Add', 'Multiply Add Mode'),
|
||||
('SINE', 'Sine', 'Sine Mode'),
|
||||
('COSINE', 'Cosine', 'Cosine Mode'),
|
||||
('TANGENT', 'Tangent', 'Tangent Mode'),
|
||||
('ARCSINE', 'Arcsine', 'Arcsine Mode'),
|
||||
('ARCCOSINE', 'Arccosine', 'Arccosine Mode'),
|
||||
('ARCTANGENT', 'Arctangent', 'Arctangent Mode'),
|
||||
('ARCTAN2', 'Arctan2', 'Arctan2 Mode'),
|
||||
('SINH', 'Hyperbolic Sine', 'Hyperbolic Sine Mode'),
|
||||
('COSH', 'Hyperbolic Cosine', 'Hyperbolic Cosine Mode'),
|
||||
('TANH', 'Hyperbolic Tangent', 'Hyperbolic Tangent Mode'),
|
||||
('POWER', 'Power', 'Power Mode'),
|
||||
('LOGARITHM', 'Logarithm', 'Logarithm Mode'),
|
||||
('SQRT', 'Square Root', 'Square Root Mode'),
|
||||
('INVERSE_SQRT', 'Inverse Square Root', 'Inverse Square Root Mode'),
|
||||
('EXPONENT', 'Exponent', 'Exponent Mode'),
|
||||
('MINIMUM', 'Minimum', 'Minimum Mode'),
|
||||
('MAXIMUM', 'Maximum', 'Maximum Mode'),
|
||||
('LESS_THAN', 'Less Than', 'Less Than Mode'),
|
||||
('GREATER_THAN', 'Greater Than', 'Greater Than Mode'),
|
||||
('SIGN', 'Sign', 'Sign Mode'),
|
||||
('COMPARE', 'Compare', 'Compare Mode'),
|
||||
('SMOOTH_MIN', 'Smooth Minimum', 'Smooth Minimum Mode'),
|
||||
('SMOOTH_MAX', 'Smooth Maximum', 'Smooth Maximum Mode'),
|
||||
('FRACT', 'Fraction', 'Fraction Mode'),
|
||||
('MODULO', 'Modulo', 'Modulo Mode'),
|
||||
('SNAP', 'Snap', 'Snap Mode'),
|
||||
('WRAP', 'Wrap', 'Wrap Mode'),
|
||||
('PINGPONG', 'Pingpong', 'Pingpong Mode'),
|
||||
('ABSOLUTE', 'Absolute', 'Absolute Mode'),
|
||||
('ROUND', 'Round', 'Round Mode'),
|
||||
('FLOOR', 'Floor', 'Floor Mode'),
|
||||
('CEIL', 'Ceil', 'Ceil Mode'),
|
||||
('TRUNCATE', 'Truncate', 'Truncate Mode'),
|
||||
('RADIANS', 'To Radians', 'To Radians Mode'),
|
||||
('DEGREES', 'To Degrees', 'To Degrees Mode'),
|
||||
]
|
||||
|
||||
# Operations used by the geometry boolean node and join geometry node
|
||||
geo_combine_operations = [
|
||||
('JOIN', 'Join Geometry', 'Join Geometry Mode'),
|
||||
('INTERSECT', 'Intersect', 'Intersect Mode'),
|
||||
('UNION', 'Union', 'Union Mode'),
|
||||
('DIFFERENCE', 'Difference', 'Difference Mode'),
|
||||
]
|
||||
|
||||
# in NWBatchChangeNodes additional types/operations. Can be used as 'items' for EnumProperty.
|
||||
# used list, not tuple for easy merging with other lists.
|
||||
navs = [
|
||||
('CURRENT', 'Current', 'Leave at current state'),
|
||||
('NEXT', 'Next', 'Next blend type/operation'),
|
||||
('PREV', 'Prev', 'Previous blend type/operation'),
|
||||
]
|
||||
|
||||
draw_color_sets = {
|
||||
"red_white": (
|
||||
(1.0, 1.0, 1.0, 0.7),
|
||||
(1.0, 0.0, 0.0, 0.7),
|
||||
(0.8, 0.2, 0.2, 1.0)
|
||||
),
|
||||
"green": (
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
(0.38, 0.77, 0.38, 1.0),
|
||||
(0.38, 0.77, 0.38, 1.0)
|
||||
),
|
||||
"yellow": (
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
(0.77, 0.77, 0.16, 1.0),
|
||||
(0.77, 0.77, 0.16, 1.0)
|
||||
),
|
||||
"purple": (
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
(0.38, 0.38, 0.77, 1.0),
|
||||
(0.38, 0.38, 0.77, 1.0)
|
||||
),
|
||||
"grey": (
|
||||
(0.0, 0.0, 0.0, 1.0),
|
||||
(0.63, 0.63, 0.63, 1.0),
|
||||
(0.63, 0.63, 0.63, 1.0)
|
||||
),
|
||||
"black": (
|
||||
(1.0, 1.0, 1.0, 0.7),
|
||||
(0.0, 0.0, 0.0, 0.7),
|
||||
(0.2, 0.2, 0.2, 1.0)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def get_texture_node_types():
|
||||
return [
|
||||
"ShaderNodeTexBrick",
|
||||
"ShaderNodeTexChecker",
|
||||
"ShaderNodeTexEnvironment",
|
||||
"ShaderNodeTexGradient",
|
||||
"ShaderNodeTexIES",
|
||||
"ShaderNodeTexImage",
|
||||
"ShaderNodeTexMagic",
|
||||
"ShaderNodeTexMusgrave",
|
||||
"ShaderNodeTexNoise",
|
||||
"ShaderNodeTexPointDensity",
|
||||
"ShaderNodeTexSky",
|
||||
"ShaderNodeTexVoronoi",
|
||||
"ShaderNodeTexWave",
|
||||
"ShaderNodeTexWhiteNoise"
|
||||
]
|
||||
|
||||
|
||||
def nice_hotkey_name(punc):
|
||||
# convert the ugly string name into the actual character
|
||||
nice_name = {
|
||||
'LEFTMOUSE': "LMB",
|
||||
'MIDDLEMOUSE': "MMB",
|
||||
'RIGHTMOUSE': "RMB",
|
||||
'WHEELUPMOUSE': "Wheel Up",
|
||||
'WHEELDOWNMOUSE': "Wheel Down",
|
||||
'WHEELINMOUSE': "Wheel In",
|
||||
'WHEELOUTMOUSE': "Wheel Out",
|
||||
'ZERO': "0",
|
||||
'ONE': "1",
|
||||
'TWO': "2",
|
||||
'THREE': "3",
|
||||
'FOUR': "4",
|
||||
'FIVE': "5",
|
||||
'SIX': "6",
|
||||
'SEVEN': "7",
|
||||
'EIGHT': "8",
|
||||
'NINE': "9",
|
||||
'OSKEY': "Super",
|
||||
'RET': "Enter",
|
||||
'LINE_FEED': "Enter",
|
||||
'SEMI_COLON': ";",
|
||||
'PERIOD': ".",
|
||||
'COMMA': ",",
|
||||
'QUOTE': '"',
|
||||
'MINUS': "-",
|
||||
'SLASH': "/",
|
||||
'BACK_SLASH': "\\",
|
||||
'EQUAL': "=",
|
||||
'NUMPAD_1': "Numpad 1",
|
||||
'NUMPAD_2': "Numpad 2",
|
||||
'NUMPAD_3': "Numpad 3",
|
||||
'NUMPAD_4': "Numpad 4",
|
||||
'NUMPAD_5': "Numpad 5",
|
||||
'NUMPAD_6': "Numpad 6",
|
||||
'NUMPAD_7': "Numpad 7",
|
||||
'NUMPAD_8': "Numpad 8",
|
||||
'NUMPAD_9': "Numpad 9",
|
||||
'NUMPAD_0': "Numpad 0",
|
||||
'NUMPAD_PERIOD': "Numpad .",
|
||||
'NUMPAD_SLASH': "Numpad /",
|
||||
'NUMPAD_ASTERIX': "Numpad *",
|
||||
'NUMPAD_MINUS': "Numpad -",
|
||||
'NUMPAD_ENTER': "Numpad Enter",
|
||||
'NUMPAD_PLUS': "Numpad +",
|
||||
}
|
||||
try:
|
||||
return nice_name[punc]
|
||||
except KeyError:
|
||||
return punc.replace("_", " ").title()
|
219
scripts/addons_core/node_wrangler/utils/draw.py
Normal file
219
scripts/addons_core/node_wrangler/utils/draw.py
Normal file
@ -0,0 +1,219 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
import gpu
|
||||
from gpu_extras.batch import batch_for_shader
|
||||
from math import cos, sin, pi
|
||||
|
||||
from .nodes import get_nodes_links, prefs_line_width, abs_node_location, dpi_fac
|
||||
|
||||
|
||||
def draw_line(x1, y1, x2, y2, size, colour=(1.0, 1.0, 1.0, 0.7)):
|
||||
shader = gpu.shader.from_builtin('POLYLINE_SMOOTH_COLOR')
|
||||
shader.uniform_float("viewportSize", gpu.state.viewport_get()[2:])
|
||||
shader.uniform_float("lineWidth", size * prefs_line_width())
|
||||
|
||||
vertices = ((x1, y1), (x2, y2))
|
||||
vertex_colors = ((colour[0] + (1.0 - colour[0]) / 4,
|
||||
colour[1] + (1.0 - colour[1]) / 4,
|
||||
colour[2] + (1.0 - colour[2]) / 4,
|
||||
colour[3] + (1.0 - colour[3]) / 4),
|
||||
colour)
|
||||
|
||||
batch = batch_for_shader(shader, 'LINE_STRIP', {"pos": vertices, "color": vertex_colors})
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_circle_2d_filled(mx, my, radius, colour=(1.0, 1.0, 1.0, 0.7)):
|
||||
radius = radius * prefs_line_width()
|
||||
sides = 12
|
||||
vertices = [(radius * cos(i * 2 * pi / sides) + mx,
|
||||
radius * sin(i * 2 * pi / sides) + my)
|
||||
for i in range(sides + 1)]
|
||||
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
shader.uniform_float("color", colour)
|
||||
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_rounded_node_border(node, radius=8, colour=(1.0, 1.0, 1.0, 0.7)):
|
||||
area_width = bpy.context.area.width
|
||||
sides = 16
|
||||
radius *= prefs_line_width()
|
||||
|
||||
nlocx, nlocy = abs_node_location(node)
|
||||
|
||||
nlocx = (nlocx + 1) * dpi_fac()
|
||||
nlocy = (nlocy + 1) * dpi_fac()
|
||||
ndimx = node.dimensions.x
|
||||
ndimy = node.dimensions.y
|
||||
|
||||
if node.hide:
|
||||
nlocx += -1
|
||||
nlocy += 5
|
||||
if node.type == 'REROUTE':
|
||||
# nlocx += 1
|
||||
nlocy -= 1
|
||||
ndimx = 0
|
||||
ndimy = 0
|
||||
radius += 6
|
||||
|
||||
shader = gpu.shader.from_builtin('UNIFORM_COLOR')
|
||||
shader.uniform_float("color", colour)
|
||||
|
||||
# Top left corner
|
||||
mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
|
||||
vertices = [(mx, my)]
|
||||
for i in range(sides + 1):
|
||||
if (4 <= i <= 8):
|
||||
if mx < area_width:
|
||||
cosine = radius * cos(i * 2 * pi / sides) + mx
|
||||
sine = radius * sin(i * 2 * pi / sides) + my
|
||||
vertices.append((cosine, sine))
|
||||
|
||||
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
|
||||
batch.draw(shader)
|
||||
|
||||
# Top right corner
|
||||
mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
|
||||
vertices = [(mx, my)]
|
||||
for i in range(sides + 1):
|
||||
if (0 <= i <= 4):
|
||||
if mx < area_width:
|
||||
cosine = radius * cos(i * 2 * pi / sides) + mx
|
||||
sine = radius * sin(i * 2 * pi / sides) + my
|
||||
vertices.append((cosine, sine))
|
||||
|
||||
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
|
||||
batch.draw(shader)
|
||||
|
||||
# Bottom left corner
|
||||
mx, my = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
|
||||
vertices = [(mx, my)]
|
||||
for i in range(sides + 1):
|
||||
if (8 <= i <= 12):
|
||||
if mx < area_width:
|
||||
cosine = radius * cos(i * 2 * pi / sides) + mx
|
||||
sine = radius * sin(i * 2 * pi / sides) + my
|
||||
vertices.append((cosine, sine))
|
||||
|
||||
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
|
||||
batch.draw(shader)
|
||||
|
||||
# Bottom right corner
|
||||
mx, my = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
|
||||
vertices = [(mx, my)]
|
||||
for i in range(sides + 1):
|
||||
if (12 <= i <= 16):
|
||||
if mx < area_width:
|
||||
cosine = radius * cos(i * 2 * pi / sides) + mx
|
||||
sine = radius * sin(i * 2 * pi / sides) + my
|
||||
vertices.append((cosine, sine))
|
||||
|
||||
batch = batch_for_shader(shader, 'TRI_FAN', {"pos": vertices})
|
||||
batch.draw(shader)
|
||||
|
||||
# prepare drawing all edges in one batch
|
||||
vertices = []
|
||||
indices = []
|
||||
id_last = 0
|
||||
|
||||
# Left edge
|
||||
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
|
||||
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
|
||||
if m1x < area_width and m2x < area_width:
|
||||
vertices.extend([(m2x - radius, m2y), (m2x, m2y),
|
||||
(m1x, m1y), (m1x - radius, m1y)])
|
||||
indices.extend([(id_last, id_last + 1, id_last + 3),
|
||||
(id_last + 3, id_last + 1, id_last + 2)])
|
||||
id_last += 4
|
||||
|
||||
# Top edge
|
||||
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy, clip=False)
|
||||
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
|
||||
m1x = min(m1x, area_width)
|
||||
m2x = min(m2x, area_width)
|
||||
vertices.extend([(m1x, m1y), (m2x, m1y),
|
||||
(m2x, m1y + radius), (m1x, m1y + radius)])
|
||||
indices.extend([(id_last, id_last + 1, id_last + 3),
|
||||
(id_last + 3, id_last + 1, id_last + 2)])
|
||||
id_last += 4
|
||||
|
||||
# Right edge
|
||||
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy, clip=False)
|
||||
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
|
||||
if m1x < area_width and m2x < area_width:
|
||||
vertices.extend([(m1x, m2y), (m1x + radius, m2y),
|
||||
(m1x + radius, m1y), (m1x, m1y)])
|
||||
indices.extend([(id_last, id_last + 1, id_last + 3),
|
||||
(id_last + 3, id_last + 1, id_last + 2)])
|
||||
id_last += 4
|
||||
|
||||
# Bottom edge
|
||||
m1x, m1y = bpy.context.region.view2d.view_to_region(nlocx, nlocy - ndimy, clip=False)
|
||||
m2x, m2y = bpy.context.region.view2d.view_to_region(nlocx + ndimx, nlocy - ndimy, clip=False)
|
||||
m1x = min(m1x, area_width)
|
||||
m2x = min(m2x, area_width)
|
||||
vertices.extend([(m1x, m2y), (m2x, m2y),
|
||||
(m2x, m1y - radius), (m1x, m1y - radius)])
|
||||
indices.extend([(id_last, id_last + 1, id_last + 3),
|
||||
(id_last + 3, id_last + 1, id_last + 2)])
|
||||
|
||||
# now draw all edges in one batch
|
||||
if len(vertices) != 0:
|
||||
batch = batch_for_shader(shader, 'TRIS', {"pos": vertices}, indices=indices)
|
||||
batch.draw(shader)
|
||||
|
||||
|
||||
def draw_callback_nodeoutline(self, context, mode):
|
||||
if self.mouse_path:
|
||||
gpu.state.blend_set('ALPHA')
|
||||
|
||||
nodes, _links = get_nodes_links(context)
|
||||
|
||||
if mode == "LINK":
|
||||
col_outer = (1.0, 0.2, 0.2, 0.4)
|
||||
col_inner = (0.0, 0.0, 0.0, 0.5)
|
||||
col_circle_inner = (0.3, 0.05, 0.05, 1.0)
|
||||
elif mode == "LINKMENU":
|
||||
col_outer = (0.4, 0.6, 1.0, 0.4)
|
||||
col_inner = (0.0, 0.0, 0.0, 0.5)
|
||||
col_circle_inner = (0.08, 0.15, .3, 1.0)
|
||||
elif mode == "MIX":
|
||||
col_outer = (0.2, 1.0, 0.2, 0.4)
|
||||
col_inner = (0.0, 0.0, 0.0, 0.5)
|
||||
col_circle_inner = (0.05, 0.3, 0.05, 1.0)
|
||||
|
||||
m1x = self.mouse_path[0][0]
|
||||
m1y = self.mouse_path[0][1]
|
||||
m2x = self.mouse_path[-1][0]
|
||||
m2y = self.mouse_path[-1][1]
|
||||
|
||||
n1 = nodes[context.scene.NWLazySource]
|
||||
n2 = nodes[context.scene.NWLazyTarget]
|
||||
|
||||
if n1 == n2:
|
||||
col_outer = (0.4, 0.4, 0.4, 0.4)
|
||||
col_inner = (0.0, 0.0, 0.0, 0.5)
|
||||
col_circle_inner = (0.2, 0.2, 0.2, 1.0)
|
||||
|
||||
draw_rounded_node_border(n1, radius=6, colour=col_outer) # outline
|
||||
draw_rounded_node_border(n1, radius=5, colour=col_inner) # inner
|
||||
draw_rounded_node_border(n2, radius=6, colour=col_outer) # outline
|
||||
draw_rounded_node_border(n2, radius=5, colour=col_inner) # inner
|
||||
|
||||
draw_line(m1x, m1y, m2x, m2y, 5, col_outer) # line outline
|
||||
draw_line(m1x, m1y, m2x, m2y, 2, col_inner) # line inner
|
||||
|
||||
# circle outline
|
||||
draw_circle_2d_filled(m1x, m1y, 7, col_outer)
|
||||
draw_circle_2d_filled(m2x, m2y, 7, col_outer)
|
||||
|
||||
# circle inner
|
||||
draw_circle_2d_filled(m1x, m1y, 5, col_circle_inner)
|
||||
draw_circle_2d_filled(m2x, m2y, 5, col_circle_inner)
|
||||
|
||||
gpu.state.blend_set('NONE')
|
288
scripts/addons_core/node_wrangler/utils/nodes.py
Normal file
288
scripts/addons_core/node_wrangler/utils/nodes.py
Normal file
@ -0,0 +1,288 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import bpy
|
||||
from bpy_extras.node_utils import connect_sockets
|
||||
from math import hypot, inf
|
||||
|
||||
|
||||
def force_update(context):
|
||||
context.space_data.node_tree.update_tag()
|
||||
|
||||
|
||||
def dpi_fac():
|
||||
prefs = bpy.context.preferences.system
|
||||
return prefs.dpi / 72
|
||||
|
||||
|
||||
def prefs_line_width():
|
||||
prefs = bpy.context.preferences.system
|
||||
return prefs.pixel_size
|
||||
|
||||
|
||||
def node_mid_pt(node, axis):
|
||||
if axis == 'x':
|
||||
d = node.location.x + (node.dimensions.x / 2)
|
||||
elif axis == 'y':
|
||||
d = node.location.y - (node.dimensions.y / 2)
|
||||
else:
|
||||
d = 0
|
||||
return d
|
||||
|
||||
|
||||
def autolink(node1, node2, links):
|
||||
available_inputs = [inp for inp in node2.inputs if inp.enabled]
|
||||
available_outputs = [outp for outp in node1.outputs if outp.enabled]
|
||||
for outp in available_outputs:
|
||||
for inp in available_inputs:
|
||||
if not inp.is_linked and inp.name == outp.name:
|
||||
connect_sockets(outp, inp)
|
||||
return True
|
||||
|
||||
for outp in available_outputs:
|
||||
for inp in available_inputs:
|
||||
if not inp.is_linked and inp.type == outp.type:
|
||||
connect_sockets(outp, inp)
|
||||
return True
|
||||
|
||||
# force some connection even if the type doesn't match
|
||||
if available_outputs:
|
||||
for inp in available_inputs:
|
||||
if not inp.is_linked:
|
||||
connect_sockets(available_outputs[0], inp)
|
||||
return True
|
||||
|
||||
# even if no sockets are open, force one of matching type
|
||||
for outp in available_outputs:
|
||||
for inp in available_inputs:
|
||||
if inp.type == outp.type:
|
||||
connect_sockets(outp, inp)
|
||||
return True
|
||||
|
||||
# do something!
|
||||
for outp in available_outputs:
|
||||
for inp in available_inputs:
|
||||
connect_sockets(outp, inp)
|
||||
return True
|
||||
|
||||
print("Could not make a link from " + node1.name + " to " + node2.name)
|
||||
return False
|
||||
|
||||
|
||||
def abs_node_location(node):
|
||||
abs_location = node.location
|
||||
if node.parent is None:
|
||||
return abs_location
|
||||
return abs_location + abs_node_location(node.parent)
|
||||
|
||||
|
||||
def node_at_pos(nodes, context, event):
|
||||
nodes_under_mouse = []
|
||||
target_node = None
|
||||
|
||||
store_mouse_cursor(context, event)
|
||||
x, y = context.space_data.cursor_location
|
||||
|
||||
# Make a list of each corner (and middle of border) for each node.
|
||||
# Will be sorted to find nearest point and thus nearest node
|
||||
node_points_with_dist = []
|
||||
for node in nodes:
|
||||
skipnode = False
|
||||
if node.type != 'FRAME': # no point trying to link to a frame node
|
||||
dimx = node.dimensions.x / dpi_fac()
|
||||
dimy = node.dimensions.y / dpi_fac()
|
||||
locx, locy = abs_node_location(node)
|
||||
|
||||
if not skipnode:
|
||||
node_points_with_dist.append([node, hypot(x - locx, y - locy)]) # Top Left
|
||||
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - locy)]) # Top Right
|
||||
node_points_with_dist.append([node, hypot(x - locx, y - (locy - dimy))]) # Bottom Left
|
||||
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - dimy))]) # Bottom Right
|
||||
|
||||
node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - locy)]) # Mid Top
|
||||
node_points_with_dist.append([node, hypot(x - (locx + (dimx / 2)), y - (locy - dimy))]) # Mid Bottom
|
||||
node_points_with_dist.append([node, hypot(x - locx, y - (locy - (dimy / 2)))]) # Mid Left
|
||||
node_points_with_dist.append([node, hypot(x - (locx + dimx), y - (locy - (dimy / 2)))]) # Mid Right
|
||||
|
||||
nearest_node = sorted(node_points_with_dist, key=lambda k: k[1])[0][0]
|
||||
|
||||
for node in nodes:
|
||||
if node.type != 'FRAME' and skipnode == False:
|
||||
locx, locy = abs_node_location(node)
|
||||
dimx = node.dimensions.x / dpi_fac()
|
||||
dimy = node.dimensions.y / dpi_fac()
|
||||
if (locx <= x <= locx + dimx) and \
|
||||
(locy - dimy <= y <= locy):
|
||||
nodes_under_mouse.append(node)
|
||||
|
||||
if len(nodes_under_mouse) == 1:
|
||||
if nodes_under_mouse[0] != nearest_node:
|
||||
target_node = nodes_under_mouse[0] # use the node under the mouse if there is one and only one
|
||||
else:
|
||||
target_node = nearest_node # else use the nearest node
|
||||
else:
|
||||
target_node = nearest_node
|
||||
return target_node
|
||||
|
||||
|
||||
def store_mouse_cursor(context, event):
|
||||
space = context.space_data
|
||||
v2d = context.region.view2d
|
||||
tree = space.edit_tree
|
||||
|
||||
# convert mouse position to the View2D for later node placement
|
||||
if context.region.type == 'WINDOW':
|
||||
space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
|
||||
else:
|
||||
space.cursor_location = tree.view_center
|
||||
|
||||
|
||||
def get_nodes_links(context):
|
||||
tree = context.space_data.edit_tree
|
||||
return tree.nodes, tree.links
|
||||
|
||||
|
||||
def get_internal_socket(socket):
|
||||
# get the internal socket from a socket inside or outside the group
|
||||
node = socket.node
|
||||
if node.type == 'GROUP_OUTPUT':
|
||||
iterator = node.id_data.interface.items_tree
|
||||
elif node.type == 'GROUP_INPUT':
|
||||
iterator = node.id_data.interface.items_tree
|
||||
elif hasattr(node, "node_tree"):
|
||||
iterator = node.node_tree.interface.items_tree
|
||||
else:
|
||||
return None
|
||||
|
||||
for s in iterator:
|
||||
if s.identifier == socket.identifier:
|
||||
return s
|
||||
return iterator[0]
|
||||
|
||||
|
||||
def get_group_output_node(tree, output_node_type='GROUP_OUTPUT'):
|
||||
for node in tree.nodes:
|
||||
if node.type == output_node_type and node.is_active_output:
|
||||
return node
|
||||
|
||||
|
||||
def get_output_location(tree):
|
||||
# get right-most location
|
||||
sorted_by_xloc = (sorted(tree.nodes, key=lambda x: x.location.x))
|
||||
max_xloc_node = sorted_by_xloc[-1]
|
||||
|
||||
# get average y location
|
||||
sum_yloc = 0
|
||||
for node in tree.nodes:
|
||||
sum_yloc += node.location.y
|
||||
|
||||
loc_x = max_xloc_node.location.x + max_xloc_node.dimensions.x + 80
|
||||
loc_y = sum_yloc / len(tree.nodes)
|
||||
return loc_x, loc_y
|
||||
|
||||
|
||||
def nw_check(cls, context):
|
||||
space = context.space_data
|
||||
if space.type != 'NODE_EDITOR':
|
||||
cls.poll_message_set("Current editor is not a node editor.")
|
||||
return False
|
||||
if space.node_tree is None:
|
||||
cls.poll_message_set("No node tree was found in the current node editor.")
|
||||
return False
|
||||
if space.node_tree.library is not None:
|
||||
cls.poll_message_set("Current node tree is linked from another .blend file.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_not_empty(cls, context):
|
||||
if not context.space_data.edit_tree.nodes:
|
||||
cls.poll_message_set("Current node tree does not contain any nodes.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_active(cls, context):
|
||||
if context.active_node is None or not context.active_node.select:
|
||||
cls.poll_message_set("No active node.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_selected(cls, context, min=1, max=inf):
|
||||
num_selected = len(context.selected_nodes)
|
||||
if num_selected < min:
|
||||
if min > 1:
|
||||
cls.poll_message_set(f"At least {min} nodes must be selected.")
|
||||
else:
|
||||
cls.poll_message_set(f"At least {min} node must be selected.")
|
||||
return False
|
||||
if num_selected > max:
|
||||
cls.poll_message_set(f"{num_selected} nodes are selected, but this operator can only work on {max}.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_space_type(cls, context, types):
|
||||
if context.space_data.tree_type not in types:
|
||||
tree_types_str = ", ".join(t.split('NodeTree')[0].lower() for t in sorted(types))
|
||||
cls.poll_message_set("Current node tree type not supported.\n"
|
||||
"Should be one of " + tree_types_str + ".")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_node_type(cls, context, type, invert=False):
|
||||
if invert and context.active_node.type == type:
|
||||
cls.poll_message_set(f"Active node should be not of type {type}.")
|
||||
return False
|
||||
elif not invert and context.active_node.type != type:
|
||||
cls.poll_message_set(f"Active node should be of type {type}.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_visible_outputs(cls, context):
|
||||
if not any(is_visible_socket(out) for out in context.active_node.outputs):
|
||||
cls.poll_message_set("Current node has no visible outputs.")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def nw_check_viewer_node(cls):
|
||||
for img in bpy.data.images:
|
||||
# False if not connected or connected but no image
|
||||
if (img.source == 'VIEWER'
|
||||
and len(img.render_slots) == 0
|
||||
and sum(img.size) > 0):
|
||||
return True
|
||||
cls.poll_message_set("Viewer image not found.")
|
||||
return False
|
||||
|
||||
|
||||
def get_first_enabled_output(node):
|
||||
for output in node.outputs:
|
||||
if output.enabled:
|
||||
return output
|
||||
else:
|
||||
return node.outputs[0]
|
||||
|
||||
|
||||
def is_visible_socket(socket):
|
||||
return not socket.hide and socket.enabled and socket.type != 'CUSTOM'
|
||||
|
||||
|
||||
class NWBase:
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
return nw_check(cls, context)
|
||||
|
||||
|
||||
class NWBaseMenu:
|
||||
@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)
|
169
scripts/addons_core/node_wrangler/utils/paths.py
Normal file
169
scripts/addons_core/node_wrangler/utils/paths.py
Normal file
@ -0,0 +1,169 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
from os import path
|
||||
import re
|
||||
|
||||
|
||||
def split_into_components(fname):
|
||||
"""
|
||||
Split filename into components
|
||||
'WallTexture_diff_2k.002.jpg' -> ['Wall', 'Texture', 'diff', 'k']
|
||||
"""
|
||||
# Remove extension
|
||||
fname = path.splitext(fname)[0]
|
||||
# Remove digits
|
||||
fname = "".join(i for i in fname if not i.isdigit())
|
||||
# Separate CamelCase by space
|
||||
fname = re.sub(r"([a-z])([A-Z])", r"\g<1> \g<2>", fname)
|
||||
# Replace common separators with SPACE
|
||||
separators = ["_", ".", "-", "__", "--", "#"]
|
||||
for sep in separators:
|
||||
fname = fname.replace(sep, " ")
|
||||
|
||||
components = fname.split(" ")
|
||||
components = [c.lower() for c in components]
|
||||
return components
|
||||
|
||||
|
||||
def remove_common_prefix(names_to_tag_lists):
|
||||
"""
|
||||
Accepts a mapping of file names to tag lists that should be used for socket
|
||||
matching.
|
||||
|
||||
This function modifies the provided mapping so that any common prefix
|
||||
between all the tag lists is removed.
|
||||
|
||||
Returns true if some prefix was removed, false otherwise.
|
||||
"""
|
||||
if not names_to_tag_lists:
|
||||
return False
|
||||
sample_tags = next(iter(names_to_tag_lists.values()))
|
||||
if not sample_tags:
|
||||
return False
|
||||
|
||||
common_prefix = sample_tags[0]
|
||||
for tag_list in names_to_tag_lists.values():
|
||||
if tag_list[0] != common_prefix:
|
||||
return False
|
||||
|
||||
for name, tag_list in names_to_tag_lists.items():
|
||||
names_to_tag_lists[name] = tag_list[1:]
|
||||
return True
|
||||
|
||||
|
||||
def remove_common_suffix(names_to_tag_lists):
|
||||
"""
|
||||
Accepts a mapping of file names to tag lists that should be used for socket
|
||||
matching.
|
||||
|
||||
This function modifies the provided mapping so that any common suffix
|
||||
between all the tag lists is removed.
|
||||
|
||||
Returns true if some suffix was removed, false otherwise.
|
||||
"""
|
||||
if not names_to_tag_lists:
|
||||
return False
|
||||
sample_tags = next(iter(names_to_tag_lists.values()))
|
||||
if not sample_tags:
|
||||
return False
|
||||
|
||||
common_suffix = sample_tags[-1]
|
||||
for tag_list in names_to_tag_lists.values():
|
||||
if tag_list[-1] != common_suffix:
|
||||
return False
|
||||
|
||||
for name, tag_list in names_to_tag_lists.items():
|
||||
names_to_tag_lists[name] = tag_list[:-1]
|
||||
return True
|
||||
|
||||
|
||||
def files_to_clean_file_names_for_sockets(files, sockets):
|
||||
"""
|
||||
Accepts a list of files and a list of sockets.
|
||||
|
||||
Returns a mapping from file names to tag lists that should be used for
|
||||
classification.
|
||||
|
||||
A file is something that we can do x.name on to figure out the file name.
|
||||
|
||||
A socket is a tuple containing:
|
||||
* name
|
||||
* list of tags
|
||||
* a None field where the selected file name will go later. Ignored by us.
|
||||
"""
|
||||
|
||||
names_to_tag_lists = {}
|
||||
for file in files:
|
||||
names_to_tag_lists[file.name] = split_into_components(file.name)
|
||||
|
||||
all_tags = set()
|
||||
for socket in sockets:
|
||||
socket_tags = socket[1]
|
||||
all_tags.update(socket_tags)
|
||||
|
||||
while len(names_to_tag_lists) > 1:
|
||||
something_changed = False
|
||||
|
||||
# Common prefixes / suffixes provide zero information about what file
|
||||
# should go to which socket, but they can confuse the mapping. So we get
|
||||
# rid of them here.
|
||||
something_changed |= remove_common_prefix(names_to_tag_lists)
|
||||
something_changed |= remove_common_suffix(names_to_tag_lists)
|
||||
|
||||
# Names matching zero tags provide no value, remove those
|
||||
names_to_remove = set()
|
||||
for name, tag_list in names_to_tag_lists.items():
|
||||
match_found = False
|
||||
for tag in tag_list:
|
||||
if tag in all_tags:
|
||||
match_found = True
|
||||
|
||||
if not match_found:
|
||||
names_to_remove.add(name)
|
||||
|
||||
for name_to_remove in names_to_remove:
|
||||
del names_to_tag_lists[name_to_remove]
|
||||
something_changed = True
|
||||
|
||||
if not something_changed:
|
||||
break
|
||||
|
||||
return names_to_tag_lists
|
||||
|
||||
|
||||
def match_files_to_socket_names(files, sockets):
|
||||
"""
|
||||
Given a list of files and a list of sockets, match file names to sockets.
|
||||
|
||||
A file is something that you can get a file name out of using x.name.
|
||||
|
||||
After this function returns, all possible sockets have had their file names
|
||||
filled in. Sockets without any matches will not get their file names
|
||||
changed.
|
||||
|
||||
Sockets list format. Note that all file names are initially expected to be
|
||||
None. Tags are strings, as are the socket names: [
|
||||
[
|
||||
socket_name, [tags], Optional[file_name]
|
||||
]
|
||||
]
|
||||
"""
|
||||
|
||||
names_to_tag_lists = files_to_clean_file_names_for_sockets(files, sockets)
|
||||
|
||||
for sname in sockets:
|
||||
for name, tag_list in names_to_tag_lists.items():
|
||||
if sname[0] == "Normal":
|
||||
# Blender wants GL normals, not DX (DirectX) ones:
|
||||
# https://www.reddit.com/r/blender/comments/rbuaua/texture_contains_normaldx_and_normalgl_files/
|
||||
if 'dx' in tag_list:
|
||||
continue
|
||||
if 'directx' in tag_list:
|
||||
continue
|
||||
|
||||
matches = set(sname[1]).intersection(set(tag_list))
|
||||
if matches:
|
||||
sname[2] = name
|
||||
break
|
290
scripts/addons_core/node_wrangler/utils/paths_test.py
Executable file
290
scripts/addons_core/node_wrangler/utils/paths_test.py
Executable file
@ -0,0 +1,290 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-FileCopyrightText: 2023 Blender Foundation
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
# pylint: disable=missing-function-docstring
|
||||
# pylint: disable=missing-class-docstring
|
||||
|
||||
import unittest
|
||||
from dataclasses import dataclass
|
||||
|
||||
# XXX Not really nice, but that hack is needed to allow execution of that test
|
||||
# from both automated CTest and by directly running the file manually.
|
||||
if __name__ == "__main__":
|
||||
from paths import match_files_to_socket_names
|
||||
else:
|
||||
from .paths import match_files_to_socket_names
|
||||
|
||||
|
||||
# From NWPrincipledPreferences 2023-01-06
|
||||
TAGS_DISPLACEMENT = "displacement displace disp dsp height heightmap".split(" ")
|
||||
TAGS_BASE_COLOR = "diffuse diff albedo base col color basecolor".split(" ")
|
||||
TAGS_METALLIC = "metallic metalness metal mtl".split(" ")
|
||||
TAGS_SPECULAR = "specularity specular spec spc".split(" ")
|
||||
TAGS_ROUGHNESS = "roughness rough rgh".split(" ")
|
||||
TAGS_GLOSS = "gloss glossy glossiness".split(" ")
|
||||
TAGS_NORMAL = "normal nor nrm nrml norm".split(" ")
|
||||
TAGS_BUMP = "bump bmp".split(" ")
|
||||
TAGS_TRANSMISSION = "transmission transparency".split(" ")
|
||||
TAGS_EMISSION = "emission emissive emit".split(" ")
|
||||
TAGS_ALPHA = "alpha opacity".split(" ")
|
||||
TAGS_AMBIENT_OCCLUSION = "ao ambient occlusion".split(" ")
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockFile:
|
||||
name: str
|
||||
|
||||
|
||||
def sockets_fixture():
|
||||
return [
|
||||
["Displacement", TAGS_DISPLACEMENT, None],
|
||||
["Base Color", TAGS_BASE_COLOR, None],
|
||||
["Metallic", TAGS_METALLIC, None],
|
||||
["Specular", TAGS_SPECULAR, None],
|
||||
["Roughness", TAGS_ROUGHNESS + TAGS_GLOSS, None],
|
||||
["Normal", TAGS_NORMAL + TAGS_BUMP, None],
|
||||
["Transmission Weight", TAGS_TRANSMISSION, None],
|
||||
["Emission Color", TAGS_EMISSION, None],
|
||||
["Alpha", TAGS_ALPHA, None],
|
||||
["Ambient Occlusion", TAGS_AMBIENT_OCCLUSION, None],
|
||||
]
|
||||
|
||||
|
||||
def assert_sockets(asserter, sockets, expected):
|
||||
checked_sockets = set()
|
||||
errors = []
|
||||
for socket_name, expected_path in expected.items():
|
||||
if isinstance(expected_path, str):
|
||||
expected_path = [expected_path]
|
||||
|
||||
socket_found = False
|
||||
for socket in sockets:
|
||||
if socket[0] != socket_name:
|
||||
continue
|
||||
socket_found = True
|
||||
|
||||
actual_path = socket[2]
|
||||
if actual_path not in expected_path:
|
||||
errors.append(
|
||||
f"{socket_name:12}: Got {actual_path} but expected {expected_path}"
|
||||
)
|
||||
checked_sockets.add(socket_name)
|
||||
break
|
||||
asserter.assertTrue(socket_found)
|
||||
asserter.assertCountEqual([], errors)
|
||||
|
||||
for socket in sockets:
|
||||
if socket[0] in checked_sockets:
|
||||
continue
|
||||
asserter.assertEqual(socket[2], None)
|
||||
|
||||
|
||||
class TestPutFileNamesInSockets(unittest.TestCase):
|
||||
def test_no_files_selected(self):
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names([], sockets)
|
||||
|
||||
assert_sockets(self, sockets, {})
|
||||
|
||||
def test_weird_filename(self):
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(
|
||||
[MockFile(""), MockFile(".jpg"), MockFile(" .png"), MockFile("...")],
|
||||
sockets,
|
||||
)
|
||||
|
||||
assert_sockets(self, sockets, {})
|
||||
|
||||
def test_poliigon(self):
|
||||
"""Texture from: https://www.poliigon.com/texture/metal-spotty-discoloration-001/3225"""
|
||||
|
||||
# NOTE: These files all have directory prefixes. That's on purpose. Files
|
||||
# without directory prefixes are tested in test_ambientcg_metal().
|
||||
files = [
|
||||
MockFile("d/MetalSpottyDiscoloration001_COL_2K_METALNESS.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_Cube.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_DISP16_2K_METALNESS.tif"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_DISP_2K_METALNESS.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_Flat.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_METALNESS_2K_METALNESS.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_NRM16_2K_METALNESS.tif"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_NRM_2K_METALNESS.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_ROUGHNESS_2K_METALNESS.jpg"),
|
||||
MockFile("d/MetalSpottyDiscoloration001_Sphere.jpg"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{
|
||||
"Base Color": "d/MetalSpottyDiscoloration001_COL_2K_METALNESS.jpg",
|
||||
"Displacement": [
|
||||
"d/MetalSpottyDiscoloration001_DISP16_2K_METALNESS.tif",
|
||||
"d/MetalSpottyDiscoloration001_DISP_2K_METALNESS.jpg",
|
||||
],
|
||||
"Metallic": "d/MetalSpottyDiscoloration001_METALNESS_2K_METALNESS.jpg",
|
||||
"Normal": [
|
||||
"d/MetalSpottyDiscoloration001_NRM16_2K_METALNESS.tif",
|
||||
"d/MetalSpottyDiscoloration001_NRM_2K_METALNESS.jpg",
|
||||
],
|
||||
"Roughness": "d/MetalSpottyDiscoloration001_ROUGHNESS_2K_METALNESS.jpg",
|
||||
},
|
||||
)
|
||||
|
||||
def test_ambientcg(self):
|
||||
"""Texture from: https://ambientcg.com/view?id=MetalPlates003"""
|
||||
|
||||
# NOTE: These files have no directory prefix. That's on purpose. Files
|
||||
# with directory prefixes are tested in test_poliigon_metal().
|
||||
files = [
|
||||
MockFile("MetalPlates001_1K-JPG.usda"),
|
||||
MockFile("MetalPlates001_1K-JPG.usdc"),
|
||||
MockFile("MetalPlates001_1K_Color.jpg"),
|
||||
MockFile("MetalPlates001_1K_Displacement.jpg"),
|
||||
MockFile("MetalPlates001_1K_Metalness.jpg"),
|
||||
MockFile("MetalPlates001_1K_NormalDX.jpg"),
|
||||
MockFile("MetalPlates001_1K_NormalGL.jpg"),
|
||||
MockFile("MetalPlates001_1K_Roughness.jpg"),
|
||||
MockFile("MetalPlates001_PREVIEW.jpg"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{
|
||||
"Base Color": "MetalPlates001_1K_Color.jpg",
|
||||
"Displacement": "MetalPlates001_1K_Displacement.jpg",
|
||||
"Metallic": "MetalPlates001_1K_Metalness.jpg",
|
||||
# Blender wants GL normals:
|
||||
# https://www.reddit.com/r/blender/comments/rbuaua/texture_contains_normaldx_and_normalgl_files/
|
||||
"Normal": "MetalPlates001_1K_NormalGL.jpg",
|
||||
"Roughness": "MetalPlates001_1K_Roughness.jpg",
|
||||
},
|
||||
)
|
||||
|
||||
def test_3dtextures_me(self):
|
||||
"""Texture from: https://3dtextures.me/2022/05/13/metal-006/"""
|
||||
|
||||
files = [
|
||||
MockFile("Material_2079.jpg"),
|
||||
MockFile("Metal_006_ambientOcclusion.jpg"),
|
||||
MockFile("Metal_006_basecolor.jpg"),
|
||||
MockFile("Metal_006_height.png"),
|
||||
MockFile("Metal_006_metallic.jpg"),
|
||||
MockFile("Metal_006_normal.jpg"),
|
||||
MockFile("Metal_006_roughness.jpg"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{
|
||||
"Ambient Occlusion": "Metal_006_ambientOcclusion.jpg",
|
||||
"Base Color": "Metal_006_basecolor.jpg",
|
||||
"Displacement": "Metal_006_height.png",
|
||||
"Metallic": "Metal_006_metallic.jpg",
|
||||
"Normal": "Metal_006_normal.jpg",
|
||||
"Roughness": "Metal_006_roughness.jpg",
|
||||
},
|
||||
)
|
||||
|
||||
def test_polyhaven(self):
|
||||
"""Texture from: https://polyhaven.com/a/rusty_metal_02"""
|
||||
|
||||
files = [
|
||||
MockFile("rusty_metal_02_ao_1k.jpg"),
|
||||
MockFile("rusty_metal_02_arm_1k.jpg"),
|
||||
MockFile("rusty_metal_02_diff_1k.jpg"),
|
||||
MockFile("rusty_metal_02_disp_1k.png"),
|
||||
MockFile("rusty_metal_02_nor_dx_1k.exr"),
|
||||
MockFile("rusty_metal_02_nor_gl_1k.exr"),
|
||||
MockFile("rusty_metal_02_rough_1k.exr"),
|
||||
MockFile("rusty_metal_02_spec_1k.png"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{
|
||||
"Ambient Occlusion": "rusty_metal_02_ao_1k.jpg",
|
||||
"Base Color": "rusty_metal_02_diff_1k.jpg",
|
||||
"Displacement": "rusty_metal_02_disp_1k.png",
|
||||
"Normal": "rusty_metal_02_nor_gl_1k.exr",
|
||||
"Roughness": "rusty_metal_02_rough_1k.exr",
|
||||
"Specular": "rusty_metal_02_spec_1k.png",
|
||||
},
|
||||
)
|
||||
|
||||
def test_texturecan(self):
|
||||
"""Texture from: https://www.texturecan.com/details/67/"""
|
||||
|
||||
files = [
|
||||
MockFile("metal_0010_ao_1k.jpg"),
|
||||
MockFile("metal_0010_color_1k.jpg"),
|
||||
MockFile("metal_0010_height_1k.png"),
|
||||
MockFile("metal_0010_metallic_1k.jpg"),
|
||||
MockFile("metal_0010_normal_directx_1k.png"),
|
||||
MockFile("metal_0010_normal_opengl_1k.png"),
|
||||
MockFile("metal_0010_roughness_1k.jpg"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{
|
||||
"Ambient Occlusion": "metal_0010_ao_1k.jpg",
|
||||
"Base Color": "metal_0010_color_1k.jpg",
|
||||
"Displacement": "metal_0010_height_1k.png",
|
||||
"Metallic": "metal_0010_metallic_1k.jpg",
|
||||
"Normal": "metal_0010_normal_opengl_1k.png",
|
||||
"Roughness": "metal_0010_roughness_1k.jpg",
|
||||
},
|
||||
)
|
||||
|
||||
def test_single_file_good(self):
|
||||
"""Regression test for https://projects.blender.org/blender/blender-addons/issues/104573"""
|
||||
|
||||
files = [
|
||||
MockFile("banana-color.webp"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{
|
||||
"Base Color": "banana-color.webp",
|
||||
},
|
||||
)
|
||||
|
||||
def test_single_file_bad(self):
|
||||
"""Regression test for https://projects.blender.org/blender/blender-addons/issues/104573"""
|
||||
|
||||
files = [
|
||||
MockFile("README-banana.txt"),
|
||||
]
|
||||
sockets = sockets_fixture()
|
||||
match_files_to_socket_names(files, sockets)
|
||||
|
||||
assert_sockets(
|
||||
self,
|
||||
sockets,
|
||||
{},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main(verbosity=2)
|
Loading…
Reference in New Issue
Block a user