This implements bundles and closures which are described in more detail in this blog post: https://code.blender.org/2024/11/geometry-nodes-workshop-october-2024/ tl;dr: * Bundles are containers that allow storing multiple socket values in a single value. Each value in the bundle is identified by a name. Bundles can be nested. * Closures are functions that are created with the Closure Zone and can be evaluated with the Evaluate Closure node. To use the patch, the `Bundle and Closure Nodes` experimental feature has to be enabled. This is necessary, because these features are not fully done yet and still need iterations to improve the workflow before they can be officially released. These iterations are easier to do in `main` than in a separate branch though. That's because this patch is quite large and somewhat prone to merge conflicts. Also other work we want to do, depends on this. This adds the following new nodes: * Combine Bundle: can pack multiple values into one. * Separate Bundle: extracts values from a bundle. * Closure Zone: outputs a closure zone for use in the `Evaluate Closure` node. * Evaluate Closure: evaluates the passed in closure. Things that will be added soon after this lands: * Fields in bundles and closures. The way this is done changes with #134811, so I rather implement this once both are in `main`. * UI features for keeping sockets in sync (right now there are warnings only). One bigger issue is the limited support for lazyness. For example, all inputs of a Combine Bundle node will be evaluated, even if they are not all needed. The same is true for all captured values of a closure. This is a deeper limitation that needs to be resolved at some point. This will likely be done after an initial version of this patch is done. Pull Request: https://projects.blender.org/blender/blender/pulls/128340
697 lines
23 KiB
Python
697 lines
23 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",
|
|
)
|
|
|
|
# Default execute simply adds a node.
|
|
def execute(self, context):
|
|
if self.properties.is_property_set("type"):
|
|
self.deselect_nodes(context)
|
|
self.create_node(context, self.type)
|
|
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 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:
|
|
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:
|
|
cls.poll_message_set("Only boolean 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_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,
|
|
)
|