Previously, one had to search for the name of an input node (Geometry, Light Path, etc.) instead of for the actual desired values. This patch makes it possible to search for the output names of various input nodes directly. All other outputs of the input node are hidden automatically. This was partially support for the Scene Time before. Supported nodes: * Compositor: Scene Time * Geometry Nodes: Camera Info, Mouse Position, Scene Time, Viewport Transform * Shader Nodes: Camera Data, Curves Info, Geometry, Volume Info, Light Path, Object Info, Particle Info Right now, the output names are hardcoded in the menu. We don't have a great way to access those without an actual node instance currently. For that we'll need to make the node declarations available in Python, which is a good project but out of scope for this this feature. It also does not seem too bad to have more explicit control over what's shown in the search. Pull Request: https://projects.blender.org/blender/blender/pulls/139477
744 lines
24 KiB
Python
744 lines
24 KiB
Python
# SPDX-FileCopyrightText: 2012-2023 Blender Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
from __future__ import annotations
|
|
|
|
import bpy
|
|
from bpy.types import (
|
|
FileHandler,
|
|
Operator,
|
|
PropertyGroup,
|
|
)
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
CollectionProperty,
|
|
EnumProperty,
|
|
FloatVectorProperty,
|
|
StringProperty,
|
|
IntProperty,
|
|
)
|
|
from mathutils import (
|
|
Vector,
|
|
)
|
|
|
|
from bpy.app.translations import (
|
|
pgettext_tip as tip_,
|
|
pgettext_rpt as rpt_,
|
|
)
|
|
|
|
|
|
class NodeSetting(PropertyGroup):
|
|
value: StringProperty(
|
|
name="Value",
|
|
description="Python expression to be evaluated "
|
|
"as the initial node setting",
|
|
default="",
|
|
)
|
|
|
|
|
|
# Base class for node "Add" operators.
|
|
class NodeAddOperator:
|
|
|
|
use_transform: BoolProperty(
|
|
name="Use Transform",
|
|
description="Start transform operator after inserting the node",
|
|
default=False,
|
|
)
|
|
settings: CollectionProperty(
|
|
name="Settings",
|
|
description="Settings to be applied on the newly created node",
|
|
type=NodeSetting,
|
|
options={'SKIP_SAVE'},
|
|
)
|
|
|
|
@staticmethod
|
|
def store_mouse_cursor(context, event):
|
|
space = context.space_data
|
|
tree = space.edit_tree
|
|
|
|
# convert mouse position to the View2D for later node placement
|
|
if context.region.type == 'WINDOW':
|
|
# convert mouse position to the View2D for later node placement
|
|
space.cursor_location_from_region(event.mouse_region_x, event.mouse_region_y)
|
|
else:
|
|
space.cursor_location = tree.view_center
|
|
|
|
# Deselect all nodes in the tree.
|
|
@staticmethod
|
|
def deselect_nodes(context):
|
|
space = context.space_data
|
|
tree = space.edit_tree
|
|
for n in tree.nodes:
|
|
n.select = False
|
|
|
|
def create_node(self, context, node_type):
|
|
space = context.space_data
|
|
tree = space.edit_tree
|
|
|
|
try:
|
|
node = tree.nodes.new(type=node_type)
|
|
except RuntimeError as ex:
|
|
self.report({'ERROR'}, str(ex))
|
|
return None
|
|
|
|
for setting in self.settings:
|
|
# XXX catch exceptions here?
|
|
value = eval(setting.value)
|
|
node_data = node
|
|
node_attr_name = setting.name
|
|
|
|
# Support path to nested data.
|
|
if '.' in node_attr_name:
|
|
node_data_path, node_attr_name = node_attr_name.rsplit(".", 1)
|
|
node_data = node.path_resolve(node_data_path)
|
|
|
|
try:
|
|
setattr(node_data, node_attr_name, value)
|
|
except AttributeError as ex:
|
|
self.report(
|
|
{'ERROR_INVALID_INPUT'},
|
|
rpt_("Node has no attribute {:s}").format(setting.name))
|
|
print(str(ex))
|
|
# Continue despite invalid attribute
|
|
|
|
node.select = True
|
|
tree.nodes.active = node
|
|
node.location = space.cursor_location
|
|
return node
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
space = context.space_data
|
|
# needs active node editor and a tree to add nodes to
|
|
return (space and (space.type == 'NODE_EDITOR') and
|
|
space.edit_tree and space.edit_tree.is_editable)
|
|
|
|
# Default invoke stores the mouse position to place the node correctly
|
|
# and optionally invokes the transform operator
|
|
def invoke(self, context, event):
|
|
self.store_mouse_cursor(context, event)
|
|
result = self.execute(context)
|
|
|
|
if self.use_transform and ('FINISHED' in result):
|
|
# removes the node again if transform is canceled
|
|
bpy.ops.node.translate_attach_remove_on_cancel('INVOKE_DEFAULT')
|
|
|
|
return result
|
|
|
|
|
|
# Simple basic operator for adding a node.
|
|
class NODE_OT_add_node(NodeAddOperator, Operator):
|
|
"""Add a node to the active tree"""
|
|
bl_idname = "node.add_node"
|
|
bl_label = "Add Node"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
type: StringProperty(
|
|
name="Node Type",
|
|
description="Node type",
|
|
)
|
|
|
|
visible_output: StringProperty(
|
|
name="Output Name",
|
|
description="If provided, all outputs that are named differently will be hidden",
|
|
options={'SKIP_SAVE'},
|
|
)
|
|
|
|
# Default execute simply adds a node.
|
|
def execute(self, context):
|
|
if self.properties.is_property_set("type"):
|
|
self.deselect_nodes(context)
|
|
if node := self.create_node(context, self.type):
|
|
if self.visible_output:
|
|
for socket in node.outputs:
|
|
if socket.name != self.visible_output:
|
|
socket.hide = True
|
|
return {'FINISHED'}
|
|
else:
|
|
return {'CANCELLED'}
|
|
|
|
@classmethod
|
|
def description(cls, _context, properties):
|
|
from nodeitems_builtins import node_tree_group_type
|
|
|
|
nodetype = properties["type"]
|
|
if nodetype in node_tree_group_type.values():
|
|
for setting in properties.settings:
|
|
if setting.name == "node_tree":
|
|
node_group = eval(setting.value)
|
|
if node_group.description:
|
|
return node_group.description
|
|
bl_rna = bpy.types.Node.bl_rna_get_subclass(nodetype)
|
|
if bl_rna is not None:
|
|
return tip_(bl_rna.description)
|
|
else:
|
|
return ""
|
|
|
|
|
|
class NODE_OT_add_empty_group(NodeAddOperator, bpy.types.Operator):
|
|
bl_idname = "node.add_empty_group"
|
|
bl_label = "Add Empty Group"
|
|
bl_description = "Add a group node with an empty group"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
from nodeitems_builtins import node_tree_group_type
|
|
tree = context.space_data.edit_tree
|
|
group = self.create_empty_group(tree.bl_idname)
|
|
self.deselect_nodes(context)
|
|
node = self.create_node(context, node_tree_group_type[tree.bl_idname])
|
|
node.node_tree = group
|
|
return {"FINISHED"}
|
|
|
|
@staticmethod
|
|
def create_empty_group(idname):
|
|
group = bpy.data.node_groups.new(name="NodeGroup", type=idname)
|
|
input_node = group.nodes.new('NodeGroupInput')
|
|
input_node.select = False
|
|
input_node.location.x = -200 - input_node.width
|
|
|
|
output_node = group.nodes.new('NodeGroupOutput')
|
|
output_node.is_active_output = True
|
|
output_node.select = False
|
|
output_node.location.x = 200
|
|
return group
|
|
|
|
|
|
class NodeAddZoneOperator(NodeAddOperator):
|
|
offset: FloatVectorProperty(
|
|
name="Offset",
|
|
description="Offset of nodes from the cursor when added",
|
|
size=2,
|
|
default=(150, 0),
|
|
)
|
|
|
|
add_default_geometry_link = True
|
|
|
|
def execute(self, context):
|
|
space = context.space_data
|
|
tree = space.edit_tree
|
|
|
|
self.deselect_nodes(context)
|
|
input_node = self.create_node(context, self.input_node_type)
|
|
output_node = self.create_node(context, self.output_node_type)
|
|
if input_node is None or output_node is None:
|
|
return {'CANCELLED'}
|
|
|
|
# Simulation input must be paired with the output.
|
|
input_node.pair_with_output(output_node)
|
|
|
|
input_node.location -= Vector(self.offset)
|
|
output_node.location += Vector(self.offset)
|
|
|
|
if self.add_default_geometry_link:
|
|
# Connect geometry sockets by default if available.
|
|
# Get the sockets by their types, because the name is not guaranteed due to i18n.
|
|
from_socket = next(s for s in input_node.outputs if s.type == 'GEOMETRY')
|
|
to_socket = next(s for s in output_node.inputs if s.type == 'GEOMETRY')
|
|
tree.links.new(to_socket, from_socket)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_add_simulation_zone(NodeAddZoneOperator, Operator):
|
|
"""Add simulation zone input and output nodes to the active tree"""
|
|
bl_idname = "node.add_simulation_zone"
|
|
bl_label = "Add Simulation Zone"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
input_node_type = "GeometryNodeSimulationInput"
|
|
output_node_type = "GeometryNodeSimulationOutput"
|
|
|
|
|
|
class NODE_OT_add_repeat_zone(NodeAddZoneOperator, Operator):
|
|
"""Add a repeat zone that allows executing nodes a dynamic number of times"""
|
|
bl_idname = "node.add_repeat_zone"
|
|
bl_label = "Add Repeat Zone"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
input_node_type = "GeometryNodeRepeatInput"
|
|
output_node_type = "GeometryNodeRepeatOutput"
|
|
|
|
|
|
class NODE_OT_add_foreach_geometry_element_zone(NodeAddZoneOperator, Operator):
|
|
"""Add a For Each Geometry Element zone that allows executing nodes e.g. for each vertex separately"""
|
|
bl_idname = "node.add_foreach_geometry_element_zone"
|
|
bl_label = "Add For Each Geometry Element Zone"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
input_node_type = "GeometryNodeForeachGeometryElementInput"
|
|
output_node_type = "GeometryNodeForeachGeometryElementOutput"
|
|
add_default_geometry_link = False
|
|
|
|
|
|
class NODE_OT_add_closure_zone(NodeAddZoneOperator, Operator):
|
|
"""Add a Closure zone"""
|
|
bl_idname = "node.add_closure_zone"
|
|
bl_label = "Add Closure Zone"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
input_node_type = "GeometryNodeClosureInput"
|
|
output_node_type = "GeometryNodeClosureOutput"
|
|
add_default_geometry_link = False
|
|
|
|
|
|
class NODE_OT_collapse_hide_unused_toggle(Operator):
|
|
"""Toggle collapsed nodes and hide unused sockets"""
|
|
bl_idname = "node.collapse_hide_unused_toggle"
|
|
bl_label = "Collapse and Hide Unused Sockets"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
space = context.space_data
|
|
# needs active node editor and a tree
|
|
return (space and (space.type == 'NODE_EDITOR') and
|
|
(space.edit_tree and space.edit_tree.is_editable))
|
|
|
|
def execute(self, context):
|
|
space = context.space_data
|
|
tree = space.edit_tree
|
|
|
|
for node in tree.nodes:
|
|
if node.select:
|
|
hide = (not node.hide)
|
|
|
|
node.hide = hide
|
|
# Note: connected sockets are ignored internally
|
|
for socket in node.inputs:
|
|
socket.hide = hide
|
|
for socket in node.outputs:
|
|
socket.hide = hide
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_tree_path_parent(Operator):
|
|
"""Go to parent node tree"""
|
|
bl_idname = "node.tree_path_parent"
|
|
bl_label = "Parent Node Tree"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
space = context.space_data
|
|
# needs active node editor and a tree
|
|
return (space and (space.type == 'NODE_EDITOR') and len(space.path) > 1)
|
|
|
|
def execute(self, context):
|
|
space = context.space_data
|
|
|
|
space.path.pop()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NodeInterfaceOperator():
|
|
@classmethod
|
|
def poll(cls, context):
|
|
space = context.space_data
|
|
if not space or space.type != 'NODE_EDITOR' or not space.edit_tree:
|
|
return False
|
|
if space.edit_tree.is_embedded_data:
|
|
return False
|
|
return True
|
|
|
|
|
|
class NODE_OT_interface_item_new(NodeInterfaceOperator, Operator):
|
|
"""Add a new item to the interface"""
|
|
bl_idname = "node.interface_item_new"
|
|
bl_label = "New Item"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def get_items(_self, context):
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
|
|
items = [
|
|
('INPUT', "Input", ""),
|
|
('OUTPUT', "Output", ""),
|
|
('PANEL', "Panel", ""),
|
|
]
|
|
|
|
active_item = interface.active
|
|
# Panels have the extra option to add a toggle.
|
|
if active_item and active_item.item_type == 'PANEL':
|
|
items.append(('PANEL_TOGGLE', "Panel Toggle", ""))
|
|
|
|
return items
|
|
|
|
item_type: EnumProperty(
|
|
name="Item Type",
|
|
description="Type of the item to create",
|
|
items=get_items,
|
|
default=0,
|
|
)
|
|
|
|
# Returns a valid socket type for the given tree or None.
|
|
@staticmethod
|
|
def find_valid_socket_type(tree):
|
|
socket_type = 'NodeSocketFloat'
|
|
# Socket type validation function is only available for custom
|
|
# node trees. Assume that 'NodeSocketFloat' is valid for
|
|
# built-in node tree types.
|
|
if not hasattr(tree, "valid_socket_type") or tree.valid_socket_type(socket_type):
|
|
return socket_type
|
|
# Custom nodes may not support float sockets, search all
|
|
# registered socket subclasses.
|
|
types_to_check = [bpy.types.NodeSocket]
|
|
while types_to_check:
|
|
t = types_to_check.pop()
|
|
idname = getattr(t, "bl_idname", "")
|
|
if tree.valid_socket_type(idname):
|
|
return idname
|
|
# Test all subclasses
|
|
types_to_check.extend(t.__subclasses__())
|
|
|
|
def execute(self, context):
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
|
|
# Remember active item and position to determine target position.
|
|
active_item = interface.active
|
|
active_pos = active_item.position if active_item else -1
|
|
|
|
if self.item_type == 'INPUT':
|
|
item = interface.new_socket("Socket", socket_type=self.find_valid_socket_type(tree), in_out='INPUT')
|
|
elif self.item_type == 'OUTPUT':
|
|
item = interface.new_socket("Socket", socket_type=self.find_valid_socket_type(tree), in_out='OUTPUT')
|
|
elif self.item_type == 'PANEL':
|
|
item = interface.new_panel("Panel")
|
|
elif self.item_type == 'PANEL_TOGGLE':
|
|
active_panel = active_item
|
|
if len(active_panel.interface_items) > 0:
|
|
first_item = active_panel.interface_items[0]
|
|
if type(first_item) is bpy.types.NodeTreeInterfaceSocketBool and first_item.is_panel_toggle:
|
|
self.report({'INFO'}, "Panel already has a toggle")
|
|
return {'CANCELLED'}
|
|
item = interface.new_socket(active_panel.name, socket_type='NodeSocketBool', in_out='INPUT')
|
|
item.is_panel_toggle = True
|
|
interface.move_to_parent(item, active_panel, 0)
|
|
# Return in this case because we don't want to move the item.
|
|
return {'FINISHED'}
|
|
else:
|
|
return {'CANCELLED'}
|
|
|
|
if active_item:
|
|
# Insert into active panel if possible, otherwise insert after active item.
|
|
if active_item.item_type == 'PANEL' and item.item_type != 'PANEL':
|
|
interface.move_to_parent(item, active_item, len(active_item.interface_items))
|
|
else:
|
|
interface.move_to_parent(item, active_item.parent, active_pos + 1)
|
|
interface.active = item
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_interface_item_duplicate(NodeInterfaceOperator, Operator):
|
|
"""Add a copy of the active item to the interface"""
|
|
bl_idname = "node.interface_item_duplicate"
|
|
bl_label = "Duplicate Item"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not super().poll(context):
|
|
return False
|
|
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
return interface.active is not None
|
|
|
|
def execute(self, context):
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
item = interface.active
|
|
|
|
if item:
|
|
item_copy = interface.copy(item)
|
|
interface.active = item_copy
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_interface_item_remove(NodeInterfaceOperator, Operator):
|
|
"""Remove active item from the interface"""
|
|
bl_idname = "node.interface_item_remove"
|
|
bl_label = "Remove Item"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
def execute(self, context):
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
item = interface.active
|
|
|
|
if item:
|
|
if item.item_type == 'PANEL':
|
|
child = item.interface_items
|
|
if child and child[0].is_panel_toggle:
|
|
panel_toggle = item.interface_items[0]
|
|
interface.remove(panel_toggle)
|
|
interface.remove(item)
|
|
interface.active_index = min(interface.active_index, len(interface.items_tree) - 1)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_interface_item_make_panel_toggle(NodeInterfaceOperator, Operator):
|
|
"""Make the active boolean socket a toggle for its parent panel"""
|
|
bl_idname = "node.interface_item_make_panel_toggle"
|
|
bl_label = "Make Panel Toggle"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not super().poll(context):
|
|
return False
|
|
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
active_item = interface.active
|
|
if not active_item:
|
|
return False
|
|
|
|
if type(active_item) is not bpy.types.NodeTreeInterfaceSocketBool or active_item.in_out != 'INPUT':
|
|
cls.poll_message_set("Only boolean input sockets are supported")
|
|
return False
|
|
|
|
parent_panel = active_item.parent
|
|
if parent_panel.parent is None:
|
|
cls.poll_message_set("Socket must be in a panel")
|
|
return False
|
|
if len(parent_panel.interface_items) > 0:
|
|
first_item = parent_panel.interface_items[0]
|
|
if first_item.is_panel_toggle:
|
|
cls.poll_message_set("Panel already has a toggle")
|
|
return False
|
|
return True
|
|
|
|
def execute(self, context):
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
active_item = interface.active
|
|
|
|
parent_panel = active_item.parent
|
|
if not parent_panel:
|
|
return {'CANCELLED'}
|
|
|
|
if type(active_item) is not bpy.types.NodeTreeInterfaceSocketBool:
|
|
return {'CANCELLED'}
|
|
|
|
active_item.is_panel_toggle = True
|
|
# Use the same name as the panel in the UI for clarity.
|
|
active_item.name = parent_panel.name
|
|
|
|
# Move the socket to the first position.
|
|
interface.move_to_parent(active_item, parent_panel, 0)
|
|
# Make the panel active.
|
|
interface.active = parent_panel
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_interface_item_unlink_panel_toggle(NodeInterfaceOperator, Operator):
|
|
"""Make the panel toggle a stand-alone socket"""
|
|
bl_idname = "node.interface_item_unlink_panel_toggle"
|
|
bl_label = "Unlink Panel Toggle"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
if not super().poll(context):
|
|
return False
|
|
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
active_item = interface.active
|
|
if not active_item or active_item.item_type != 'PANEL':
|
|
return False
|
|
if len(active_item.interface_items) == 0:
|
|
return False
|
|
|
|
first_item = active_item.interface_items[0]
|
|
return first_item.is_panel_toggle
|
|
|
|
def execute(self, context):
|
|
snode = context.space_data
|
|
tree = snode.edit_tree
|
|
interface = tree.interface
|
|
active_item = interface.active
|
|
|
|
if not active_item or active_item.item_type != 'PANEL':
|
|
return {'CANCELLED'}
|
|
|
|
if len(active_item.interface_items) == 0:
|
|
return {'CANCELLED'}
|
|
|
|
first_item = active_item.interface_items[0]
|
|
if type(first_item) is not bpy.types.NodeTreeInterfaceSocketBool or not first_item.is_panel_toggle:
|
|
return {'CANCELLED'}
|
|
|
|
first_item.is_panel_toggle = False
|
|
first_item.name = active_item.name
|
|
|
|
# Make the socket active.
|
|
interface.active = first_item
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_viewer_shortcut_set(Operator):
|
|
"""Create a compositor viewer shortcut for the selected node by pressing ctrl+1,2,..9"""
|
|
bl_idname = "node.viewer_shortcut_set"
|
|
bl_label = "Fast Preview"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
viewer_index: IntProperty(
|
|
name="Viewer Index",
|
|
description="Index corresponding to the shortcut, e.g. number key 1 corresponds to index 1 etc..")
|
|
|
|
def get_connected_viewer(self, node):
|
|
for out in node.outputs:
|
|
for link in out.links:
|
|
nv = link.to_node
|
|
if nv.type == 'VIEWER':
|
|
return nv
|
|
return None
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
del cls
|
|
space = context.space_data
|
|
return (
|
|
(space is not None) and
|
|
space.type == 'NODE_EDITOR' and
|
|
space.node_tree is not None and
|
|
space.tree_type in {'CompositorNodeTree', 'GeometryNodeTree'}
|
|
)
|
|
|
|
def execute(self, context):
|
|
selected_nodes = context.selected_nodes
|
|
|
|
if len(selected_nodes) == 0:
|
|
self.report({'ERROR'}, "Select a node to assign a shortcut")
|
|
return {'CANCELLED'}
|
|
|
|
fav_node = selected_nodes[0]
|
|
|
|
# Only viewer nodes can be set to favorites. However, the user can
|
|
# create a new favorite viewer by selecting any node and pressing ctrl+1.
|
|
if fav_node.type == 'VIEWER':
|
|
viewer_node = fav_node
|
|
else:
|
|
viewer_node = self.get_connected_viewer(fav_node)
|
|
if not viewer_node:
|
|
# Calling `link_viewer()` if a viewer node is connected
|
|
# will connect the next available socket to the viewer node.
|
|
# This behavior is not desired as we want to create a shortcut to the existing connected viewer node.
|
|
# Therefore `link_viewer()` is called only when no viewer node is connected.
|
|
bpy.ops.node.link_viewer()
|
|
viewer_node = self.get_connected_viewer(fav_node)
|
|
|
|
if not viewer_node:
|
|
self.report(
|
|
{'ERROR'},
|
|
"Unable to set shortcut, selected node is not a viewer node or does not support viewing",
|
|
)
|
|
return {'CANCELLED'}
|
|
|
|
with bpy.context.temp_override(node=viewer_node):
|
|
bpy.ops.node.activate_viewer()
|
|
|
|
viewer_node.ui_shortcut = self.viewer_index
|
|
self.report({'INFO'}, "Assigned shortcut {:d} to {:s}".format(self.viewer_index, viewer_node.name))
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_OT_viewer_shortcut_get(Operator):
|
|
"""Activate a specific compositor viewer node using 1,2,..,9 keys"""
|
|
bl_idname = "node.viewer_shortcut_get"
|
|
bl_label = "Fast Preview"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
viewer_index: IntProperty(
|
|
name="Viewer Index",
|
|
description="Index corresponding to the shortcut, e.g. number key 1 corresponds to index 1 etc..")
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
del cls
|
|
space = context.space_data
|
|
return (
|
|
(space is not None) and
|
|
space.type == 'NODE_EDITOR' and
|
|
space.node_tree is not None and
|
|
space.tree_type in {'CompositorNodeTree', 'GeometryNodeTree'}
|
|
)
|
|
|
|
def execute(self, context):
|
|
nodes = context.space_data.edit_tree.nodes
|
|
|
|
# Get viewer node with existing shortcut.
|
|
viewer_node = None
|
|
for n in nodes:
|
|
if n.type == 'VIEWER' and n.ui_shortcut == self.viewer_index:
|
|
viewer_node = n
|
|
|
|
if not viewer_node:
|
|
self.report({'INFO'}, "Shortcut {:d} is not assigned to a Viewer node yet".format(self.viewer_index))
|
|
return {'CANCELLED'}
|
|
|
|
with bpy.context.temp_override(node=viewer_node):
|
|
bpy.ops.node.activate_viewer()
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class NODE_FH_image_node(FileHandler):
|
|
bl_idname = "NODE_FH_image_node"
|
|
bl_label = "Image node"
|
|
bl_import_operator = "node.add_image"
|
|
bl_file_extensions = ";".join((*bpy.path.extensions_image, *bpy.path.extensions_movie))
|
|
|
|
@classmethod
|
|
def poll_drop(cls, context):
|
|
return (
|
|
(context.area is not None) and
|
|
(context.area.type == 'NODE_EDITOR') and
|
|
(context.region is not None) and
|
|
(context.region.type == 'WINDOW')
|
|
)
|
|
|
|
|
|
classes = (
|
|
NodeSetting,
|
|
|
|
NODE_FH_image_node,
|
|
|
|
NODE_OT_add_empty_group,
|
|
NODE_OT_add_node,
|
|
NODE_OT_add_simulation_zone,
|
|
NODE_OT_add_repeat_zone,
|
|
NODE_OT_add_foreach_geometry_element_zone,
|
|
NODE_OT_add_closure_zone,
|
|
NODE_OT_collapse_hide_unused_toggle,
|
|
NODE_OT_interface_item_new,
|
|
NODE_OT_interface_item_duplicate,
|
|
NODE_OT_interface_item_remove,
|
|
NODE_OT_interface_item_make_panel_toggle,
|
|
NODE_OT_interface_item_unlink_panel_toggle,
|
|
NODE_OT_tree_path_parent,
|
|
NODE_OT_viewer_shortcut_get,
|
|
NODE_OT_viewer_shortcut_set,
|
|
)
|