Habib Gahbiche bd61e69be5 Compositor: make compositor node trees reusable
This is part of the short term roadmap goal of simplifying the
compositor workflow
(see https://projects.blender.org/blender/blender/issues/134214).
The problem is that many users don't know how to get started with
compositing in Blender, even when they have used Blender for other
areas, e.g. modeling.

Note: although the solution makes compositor node trees reusable
accross blend files, this is a nice side effect and not the main goal
of the PR.

This PR implements a "New" button that creates a new compositing node
tree, and manages trees as IDs. This has following advantages:
- Consistent with other node editors and other parts of Blender,
therefore making it easier to getting started with compositing if users
are familiar with shading or geometry nodes
- Give users the ability to reuse the compositing node tree by linking
or appending it.

Note: The parameter "Use Nodes" is still present in this PR, but will
be removed (in a backward compatible way) in a follow up PR.

Pull Request: https://projects.blender.org/blender/blender/pulls/135223
2025-06-10 17:46:55 +02:00

819 lines
29 KiB
Python

# SPDX-FileCopyrightText: 2011-2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
from __future__ import annotations
import bpy
from bpy.types import Operator
from bpy.props import (
IntProperty,
BoolProperty,
EnumProperty,
StringProperty,
)
from bpy.app.translations import (
pgettext_rpt as rpt_,
contexts as i18n_contexts,
)
class ANIM_OT_keying_set_export(Operator):
"""Export Keying Set to a Python script"""
bl_idname = "anim.keying_set_export"
bl_label = "Export Keying Set..."
filepath: StringProperty(
subtype='FILE_PATH',
)
filter_folder: BoolProperty(
name="Filter folders",
default=True,
options={'HIDDEN'},
)
filter_text: BoolProperty(
name="Filter text",
default=True,
options={'HIDDEN'},
)
filter_python: BoolProperty(
name="Filter Python",
default=True,
options={'HIDDEN'},
)
def execute(self, context):
from bpy.utils import escape_identifier
if not self.filepath:
raise Exception("Filepath not set")
f = open(self.filepath, "w", encoding="utf8")
if not f:
raise Exception("Could not open file")
scene = context.scene
ks = scene.keying_sets.active
f.write("# Keying Set: {:s}\n".format(ks.bl_idname))
f.write("import bpy\n\n")
f.write("scene = bpy.context.scene\n\n")
# Add KeyingSet and set general settings
f.write("# Keying Set Level declarations\n")
f.write("ks = scene.keying_sets.new(idname={!r}, name={!r})\n".format(ks.bl_idname, ks.bl_label))
f.write("ks.bl_description = {!r}\n".format(ks.bl_description))
# TODO: this isn't editable, it should be possible to set this flag for `scene.keying_sets.new`.
# if not ks.is_path_absolute:
# f.write("ks.is_path_absolute = False\n")
f.write("\n")
f.write("ks.use_insertkey_needed = {!r}\n".format(ks.use_insertkey_needed))
f.write("ks.use_insertkey_visual = {!r}\n".format(ks.use_insertkey_visual))
f.write("\n")
# --------------------------------------------------------
# generate and write set of lookups for id's used in paths
# cache for syncing ID-blocks to bpy paths + shorthand's
id_to_paths_cache = {}
for ksp in ks.paths:
if ksp.id is None:
continue
if ksp.id in id_to_paths_cache:
continue
# - `idtype_list` is used to get the list of ID-data-blocks from
# `bpy.data.*` since this info isn't available elsewhere.
# - `id.bl_rna.name` gives a name suitable for UI,
# with a capitalized first letter, but we need
# the plural form that's all lower case.
# - special handling is needed for "nested" ID-blocks
# (e.g. node-tree in Material).
if ksp.id.bl_rna.identifier.startswith("ShaderNodeTree"):
# Find material or light using this node tree...
id_bpy_path = "bpy.data.nodes[\"{:s}\"]"
found = False
for mat in bpy.data.materials:
if mat.node_tree == ksp.id:
id_bpy_path = "bpy.data.materials[\"{:s}\"].node_tree".format(escape_identifier(mat.name))
found = True
break
if not found:
for light in bpy.data.lights:
if light.node_tree == ksp.id:
id_bpy_path = "bpy.data.lights[\"{:s}\"].node_tree".format(escape_identifier(light.name))
found = True
break
if not found:
self.report(
{'WARN'},
rpt_("Could not find material or light using Shader Node Tree - {:s}").format(str(ksp.id)),
)
elif ksp.id.bl_rna.identifier.startswith("CompositorNodeTree"):
# Find compositor node-tree using this node tree.
for scene in bpy.data.scenes:
if scene.compositing_node_group == ksp.id:
id_bpy_path = "bpy.data.scenes[\"{:s}\"].compositing_node_group".format(
escape_identifier(scene.name))
break
else:
self.report(
{'WARN'},
rpt_("Could not find scene using Compositor Node Tree - {:s}").format(str(ksp.id)),
)
elif ksp.id.bl_rna.name == "Key":
# "keys" conflicts with a Python keyword, hence the simple solution won't work
id_bpy_path = "bpy.data.shape_keys[\"{:s}\"]".format(escape_identifier(ksp.id.name))
else:
idtype_list = ksp.id.bl_rna.name.lower() + "s"
id_bpy_path = "bpy.data.{:s}[\"{:s}\"]".format(idtype_list, escape_identifier(ksp.id.name))
# shorthand ID for the ID-block (as used in the script)
short_id = "id_{:d}".format(len(id_to_paths_cache))
# store this in the cache now
id_to_paths_cache[ksp.id] = [short_id, id_bpy_path]
f.write("# ID's that are commonly used\n")
for id_pair in id_to_paths_cache.values():
f.write("{:s} = {:s}\n".format(id_pair[0], id_pair[1]))
f.write("\n")
# write paths
f.write("# Path Definitions\n")
for ksp in ks.paths:
f.write("ksp = ks.paths.add(")
# id-block + data_path
if ksp.id:
# find the relevant shorthand from the cache
id_bpy_path = id_to_paths_cache[ksp.id][0]
else:
id_bpy_path = "None" # XXX...
f.write("{:s}, {!r}".format(id_bpy_path, ksp.data_path))
# array index settings (if applicable)
if ksp.use_entire_array:
f.write(", index=-1")
else:
f.write(", index={:d}".format(ksp.array_index))
# grouping settings (if applicable)
# NOTE: the current default is KEYINGSET, but if this changes,
# change this code too
if ksp.group_method == 'NAMED':
f.write(", group_method={!r}, group_name={!r}".format(ksp.group_method, ksp.group))
elif ksp.group_method != 'KEYINGSET':
f.write(", group_method={!r}".format(ksp.group_method))
# finish off
f.write(")\n")
f.write("\n")
f.close()
return {'FINISHED'}
def invoke(self, context, _event):
wm = context.window_manager
wm.fileselect_add(self)
return {'RUNNING_MODAL'}
class NLA_OT_bake(Operator):
"""Bake all selected objects location/scale/rotation animation to an action"""
bl_idname = "nla.bake"
bl_label = "Bake Action"
bl_options = {'REGISTER', 'UNDO'}
frame_start: IntProperty(
name="Start Frame",
description="Start frame for baking",
min=0, max=300000,
default=1,
)
frame_end: IntProperty(
name="End Frame",
description="End frame for baking",
min=1, max=300000,
default=250,
)
step: IntProperty(
name="Frame Step",
description="Number of frames to skip forward while baking each frame",
min=1, max=120,
default=1,
)
only_selected: BoolProperty(
name="Only Selected Bones",
description="Only key selected bones (Pose baking only)",
default=True,
)
visual_keying: BoolProperty(
name="Visual Keying",
description="Keyframe from the final transformations (with constraints applied)",
default=False,
)
clear_constraints: BoolProperty(
name="Clear Constraints",
description=(
"Remove all constraints from keyed object/bones. "
"To get a correct bake with this setting Visual Keying should be enabled"
),
default=False,
)
clear_parents: BoolProperty(
name="Clear Parents",
description="Bake animation onto the object then clear parents (objects only)",
default=False,
)
use_current_action: BoolProperty(
name="Overwrite Current Action",
description="Bake animation into current action, instead of creating a new one "
"(useful for baking only part of bones in an armature)",
default=False,
)
clean_curves: BoolProperty(
name="Clean Curves",
description="After baking curves, remove redundant keys",
default=False,
)
bake_types: EnumProperty(
name="Bake Data",
translation_context=i18n_contexts.id_action,
description="Which data's transformations to bake",
options={'ENUM_FLAG'},
items=(
('POSE', "Pose", "Bake bones transformations"),
('OBJECT', "Object", "Bake object transformations"),
),
default={'POSE'},
)
channel_types: EnumProperty(
name="Channels",
description="Which channels to bake",
options={'ENUM_FLAG'},
items=(
('LOCATION', "Location", "Bake location channels"),
('ROTATION', "Rotation", "Bake rotation channels"),
('SCALE', "Scale", "Bake scale channels"),
('BBONE', "B-Bone", "Bake B-Bone channels"),
('PROPS', "Custom Properties", "Bake custom properties"),
),
default={'LOCATION', 'ROTATION', 'SCALE', 'BBONE', 'PROPS'},
)
def execute(self, context):
from bpy_extras import anim_utils
bake_options = anim_utils.BakeOptions(
only_selected=self.only_selected,
do_pose='POSE' in self.bake_types,
do_object='OBJECT' in self.bake_types,
do_visual_keying=self.visual_keying,
do_constraint_clear=self.clear_constraints,
do_parents_clear=self.clear_parents,
do_clean=self.clean_curves,
do_location='LOCATION' in self.channel_types,
do_rotation='ROTATION' in self.channel_types,
do_scale='SCALE' in self.channel_types,
do_bbone='BBONE' in self.channel_types,
do_custom_props='PROPS' in self.channel_types,
)
if bake_options.do_pose and self.only_selected:
pose_bones = context.selected_pose_bones or []
armatures = {pose_bone.id_data for pose_bone in pose_bones}
objects = list(armatures)
else:
objects = context.selected_editable_objects
if bake_options.do_pose and not bake_options.do_object:
pose_object = getattr(context, "pose_object", None)
if pose_object and pose_object not in objects:
# The active object might not be selected, but it is the one in pose mode.
# It can be assumed this pose needs baking.
objects.append(pose_object)
objects = [obj for obj in objects if obj.pose is not None]
object_action_pairs = (
[(obj, getattr(obj.animation_data, "action", None)) for obj in objects]
if self.use_current_action else
[(obj, None) for obj in objects]
)
actions = anim_utils.bake_action_objects(
object_action_pairs,
frames=range(self.frame_start, self.frame_end + 1, self.step),
bake_options=bake_options,
)
if not any(actions):
self.report({'INFO'}, "Nothing to bake")
return {'CANCELLED'}
return {'FINISHED'}
def invoke(self, context, _event):
scene = context.scene
if scene.use_preview_range:
self.frame_start = scene.frame_preview_start
self.frame_end = scene.frame_preview_end
else:
self.frame_start = scene.frame_start
self.frame_end = scene.frame_end
self.bake_types = {'POSE'} if context.mode == 'POSE' else {'OBJECT'}
wm = context.window_manager
return wm.invoke_props_dialog(self)
class ClearUselessActions(Operator):
"""Mark actions with no F-Curves for deletion after save and reload of """ \
"""file preserving \"action libraries\""""
bl_idname = "anim.clear_useless_actions"
bl_label = "Clear Useless Actions"
bl_options = {'REGISTER', 'UNDO'}
only_unused: BoolProperty(
name="Only Unused",
description="Only unused (Fake User only) actions get considered",
default=True,
)
@classmethod
def poll(cls, _context):
return bool(bpy.data.actions)
def execute(self, _context):
removed = 0
for action in bpy.data.actions:
# if only user is "fake" user...
if (
(self.only_unused is False) or
(action.use_fake_user and action.users == 1)
):
# if it has F-Curves, then it's a "action library"
# (i.e. walk, wave, jump, etc.)
# and should be left alone as that's what fake users are for!
if not action.fcurves:
# mark action for deletion
action.user_clear()
removed += 1
self.report({'INFO'}, rpt_("Removed {:d} empty and/or fake-user only Actions").format(removed))
return {'FINISHED'}
class UpdateAnimatedTransformConstraint(Operator):
"""Update f-curves/drivers affecting Transform constraints (use it with files from 2.70 and earlier)"""
bl_idname = "anim.update_animated_transform_constraints"
bl_label = "Update Animated Transform Constraints"
bl_options = {'REGISTER', 'UNDO'}
use_convert_to_radians: BoolProperty(
name="Convert to Radians",
description="Convert f-curves/drivers affecting rotations to radians.\n"
"Warning: Use this only once",
default=True,
)
def execute(self, context):
import animsys_refactor
from math import radians
import io
from_paths = {"from_max_x", "from_max_y", "from_max_z", "from_min_x", "from_min_y", "from_min_z"}
to_paths = {"to_max_x", "to_max_y", "to_max_z", "to_min_x", "to_min_y", "to_min_z"}
paths = from_paths | to_paths
def update_cb(base, _class_name, old_path, fcurve, options):
# print(options)
def handle_deg2rad(fcurve):
if fcurve is not None:
if hasattr(fcurve, "keyframes"):
for k in fcurve.keyframes:
k.co.y = radians(k.co.y)
for mod in fcurve.modifiers:
if mod.type == 'GENERATOR':
if mod.mode == 'POLYNOMIAL':
mod.coefficients[:] = [radians(c) for c in mod.coefficients]
else: # if mod.type == 'POLYNOMIAL_FACTORISED':
mod.coefficients[:2] = [radians(c) for c in mod.coefficients[:2]]
elif mod.type == 'FNGENERATOR':
mod.amplitude = radians(mod.amplitude)
fcurve.update()
data = ...
try:
data = eval("base." + old_path)
except Exception:
pass
ret = (data, old_path)
if isinstance(base, bpy.types.TransformConstraint) and data is not ...:
new_path = None
map_info = base.map_from if old_path in from_paths else base.map_to
if map_info == 'ROTATION':
new_path = old_path + "_rot"
if options is not None and options["use_convert_to_radians"]:
handle_deg2rad(fcurve)
elif map_info == 'SCALE':
new_path = old_path + "_scale"
if new_path is not None:
data = ...
try:
data = eval("base." + new_path)
except Exception:
pass
ret = (data, new_path)
# print(ret)
return ret
options = {"use_convert_to_radians": self.use_convert_to_radians}
replace_ls = [("TransformConstraint", p, update_cb, options) for p in paths]
log = io.StringIO()
animsys_refactor.update_data_paths(replace_ls, log)
context.scene.frame_set(context.scene.frame_current)
log = log.getvalue()
if log:
print(log)
text = bpy.data.texts.new("UpdateAnimatedTransformConstraint Report")
text.from_string(log)
self.report({'INFO'}, rpt_("Complete report available on '{:s}' text datablock").format(text.name))
return {'FINISHED'}
class ARMATURE_OT_copy_bone_color_to_selected(Operator):
"""Copy the bone color of the active bone to all selected bones"""
bl_idname = "armature.copy_bone_color_to_selected"
bl_label = "Copy Colors to Selected"
bl_options = {'REGISTER', 'UNDO'}
_bone_type_enum = [
('EDIT', "Bone", "Copy Bone colors from the active bone to all selected bones"),
('POSE', "Pose Bone", "Copy Pose Bone colors from the active pose bone to all selected pose bones"),
]
bone_type: EnumProperty(
name="Type",
items=_bone_type_enum,
)
@classmethod
def poll(cls, context):
return context.mode in {'EDIT_ARMATURE', 'POSE'}
def execute(self, context):
match(self.bone_type, context.mode):
# Armature in edit mode:
case('POSE', 'EDIT_ARMATURE'):
self.report({'ERROR'}, "Go to pose mode to copy pose bone colors")
return {'OPERATOR_CANCELLED'}
case('EDIT', 'EDIT_ARMATURE'):
bone_source = context.active_bone
bones_dest = context.selected_bones
pose_bones_to_check = []
# Armature in pose mode:
case('POSE', 'POSE'):
bone_source = context.active_pose_bone
bones_dest = context.selected_pose_bones
pose_bones_to_check = []
case('EDIT', 'POSE'):
bone_source = context.active_bone
pose_bones_to_check = context.selected_pose_bones
bones_dest = [posebone.bone for posebone in pose_bones_to_check]
# Anything else:
case _:
self.report({'ERROR'}, "Cannot do anything in mode {!r}".format(context.mode))
return {'CANCELLED'}
if not bone_source:
self.report({'ERROR'}, "No active bone to copy from")
return {'CANCELLED'}
if not bones_dest:
self.report({'ERROR'}, "No selected bones to copy to")
return {'CANCELLED'}
num_pose_color_overrides = 0
for index, bone_dest in enumerate(bones_dest):
bone_dest.color.palette = bone_source.color.palette
for custom_field in ("normal", "select", "active"):
color = getattr(bone_source.color.custom, custom_field)
setattr(bone_dest.color.custom, custom_field, color)
if self.bone_type == 'EDIT' and pose_bones_to_check:
pose_bone = pose_bones_to_check[index]
if pose_bone.color.palette != 'DEFAULT':
# A pose color has been set, and we're now syncing edit bone
# colors. This means that the synced color will not be
# visible. Better to let the user know about this.
num_pose_color_overrides += 1
if num_pose_color_overrides:
self.report(
{'INFO'},
"Bone colors were synced; "
"for {:d} bones this will not be visible due to pose bone color overrides".format(
num_pose_color_overrides,
),
)
return {'FINISHED'}
def _armature_from_context(context):
pin_armature = getattr(context, "armature", None)
if pin_armature:
return pin_armature
ob = context.object
if ob and ob.type == 'ARMATURE':
return ob.data
return None
class ARMATURE_OT_collection_show_all(Operator):
"""Show all bone collections"""
bl_idname = "armature.collection_show_all"
bl_label = "Show All"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
return _armature_from_context(context) is not None
def execute(self, context):
arm = _armature_from_context(context)
for bcoll in arm.collections_all:
bcoll.is_visible = True
return {'FINISHED'}
class ARMATURE_OT_collection_unsolo_all(Operator):
"""Clear the 'solo' setting on all bone collections"""
bl_idname = "armature.collection_unsolo_all"
bl_label = "Un-solo All"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = _armature_from_context(context)
if not armature:
return False
if not armature.collections.is_solo_active:
cls.poll_message_set("None of the bone collections is marked 'solo'")
return False
return True
def execute(self, context):
arm = _armature_from_context(context)
for bcoll in arm.collections_all:
bcoll.is_solo = False
return {'FINISHED'}
class ARMATURE_OT_collection_remove_unused(Operator):
"""Remove all bone collections that have neither bones nor children. """ \
"""This is done recursively, so bone collections that only have unused children are also removed"""
bl_idname = "armature.collection_remove_unused"
bl_label = "Remove Unused Bone Collections"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
armature = _armature_from_context(context)
if not armature:
return False
return len(armature.collections) > 0
def execute(self, context):
if context.mode == 'EDIT_ARMATURE':
return self.execute_edit_mode(context)
armature = _armature_from_context(context)
# Build a set of bone collections that don't contain any bones, and
# whose children also don't contain any bones.
bcolls_to_remove = {
bcoll
for bcoll in armature.collections_all
if len(bcoll.bones_recursive) == 0}
if not bcolls_to_remove:
self.report({'INFO'}, "All bone collections are in use")
return {'CANCELLED'}
self.remove_bcolls(armature, bcolls_to_remove)
return {'FINISHED'}
def execute_edit_mode(self, context):
# BoneCollection.bones_recursive or .bones are not available in armature
# edit mode, because that has a completely separate list of edit bones.
# This is why edit mode needs separate handling.
armature = _armature_from_context(context)
bcolls_with_bones = {
bcoll
for ebone in armature.edit_bones
for bcoll in ebone.collections
}
bcolls_to_remove = []
for root in armature.collections:
self.visit(root, bcolls_with_bones, bcolls_to_remove)
if not bcolls_to_remove:
self.report({'INFO'}, "All bone collections are in use")
return {'CANCELLED'}
self.remove_bcolls(armature, bcolls_to_remove)
return {'FINISHED'}
def visit(self, bcoll, bcolls_with_bones, bcolls_to_remove):
has_bones = bcoll in bcolls_with_bones
for child in bcoll.children:
child_has_bones = self.visit(child, bcolls_with_bones, bcolls_to_remove)
has_bones = has_bones or child_has_bones
if not has_bones:
bcolls_to_remove.append(bcoll)
return has_bones
def remove_bcolls(self, armature, bcolls_to_remove):
# Count things before they get removed.
num_bcolls_before_removal = len(armature.collections_all)
num_bcolls_to_remove = len(bcolls_to_remove)
# Create a copy of bcolls_to_remove so that it doesn't change when we
# remove bone collections.
for bcoll in reversed(list(bcolls_to_remove)):
armature.collections.remove(bcoll)
self.report(
{'INFO'}, "Removed {:d} of {:d} bone collections".format(num_bcolls_to_remove, num_bcolls_before_removal),
)
class ANIM_OT_slot_new_for_id(Operator):
"""Create a new Action Slot for an ID.
Note that _which_ ID should get this slot must be set in the 'animated_id' context pointer, using:
>>> layout.context_pointer_set("animated_id", animated_id)
When the ID already has a slot assigned, the newly-created slot will be
named after it (ensuring uniqueness with a numerical suffix) and any
animation data of the assigned slot will be duplicated for the new slot.
"""
bl_idname = "anim.slot_new_for_id"
bl_label = "New Slot"
bl_description = "Create a new action slot for this data-block, to hold its animation"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
animated_id = getattr(context, "animated_id", None)
if not animated_id:
return False
if not animated_id.animation_data or not animated_id.animation_data.action:
cls.poll_message_set("An action slot can only be created when an action is assigned")
return False
if not animated_id.animation_data.action.is_action_layered:
cls.poll_message_set("Action slots are only supported by layered Actions. Upgrade this Action first")
return False
if not animated_id.animation_data.action.is_editable:
cls.poll_message_set("Creating a new Slot is not possible on a linked Action")
return False
return True
def execute(self, context):
animated_id = context.animated_id
adt = animated_id.animation_data
if adt.action_slot:
slot = adt.action_slot.duplicate()
else:
slot_name = adt.last_slot_identifier[2:] or animated_id.name
slot = adt.action.slots.new(animated_id.id_type, slot_name)
adt.action_slot = slot
return {'FINISHED'}
class ANIM_OT_slot_unassign_from_id(Operator):
"""Un-assign the assigned Action Slot from an ID.
Note that _which_ ID should get this slot unassigned must be set in the
"animated_id" context pointer, using:
>>> layout.context_pointer_set("animated_id", animated_id)
"""
bl_idname = "anim.slot_unassign_from_id"
bl_label = "Unassign Slot"
bl_description = "Un-assign the action slot, effectively making this data-block non-animated"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
animated_id = getattr(context, "animated_id", None)
if not animated_id:
return False
if not animated_id.animation_data or not animated_id.animation_data.action_slot:
cls.poll_message_set("This data-block has no Action slot assigned")
return False
return True
def execute(self, context):
animated_id = context.animated_id
animated_id.animation_data.action_slot = None
return {'FINISHED'}
class generic_slot_unassign_mixin:
context_property_name = ""
"""Which context attribute to use to get the to-be-manipulated data-block."""
@classmethod
def poll(cls, context):
slot_user = getattr(context, cls.context_property_name, None)
if not slot_user:
return False
if not slot_user.action_slot:
cls.poll_message_set("No Action slot is assigned, so there is nothing to un-assign")
return False
return True
def execute(self, context):
slot_user = getattr(context, self.context_property_name, None)
slot_user.action_slot = None
return {'FINISHED'}
class ANIM_OT_slot_unassign_from_nla_strip(generic_slot_unassign_mixin, Operator):
"""Un-assign the assigned Action Slot from an NLA strip.
Note that _which_ NLA strip should get this slot unassigned must be set in
the "nla_strip" context pointer, using:
>>> layout.context_pointer_set("nla_strip", nla_strip)
"""
bl_idname = "anim.slot_unassign_from_nla_strip"
bl_label = "Unassign Slot"
bl_description = "Un-assign the action slot from this NLA strip, effectively making it non-animated"
bl_options = {'REGISTER', 'UNDO'}
context_property_name = "nla_strip"
class ANIM_OT_slot_unassign_from_constraint(generic_slot_unassign_mixin, Operator):
"""Un-assign the assigned Action Slot from an Action constraint.
Note that _which_ constraint should get this slot unassigned must be set in
the "constraint" context pointer, using:
>>> layout.context_pointer_set("constraint", constraint)
"""
bl_idname = "anim.slot_unassign_from_constraint"
bl_label = "Unassign Slot"
bl_description = "Un-assign the action slot from this constraint"
bl_options = {'REGISTER', 'UNDO'}
context_property_name = "constraint"
classes = (
ANIM_OT_keying_set_export,
NLA_OT_bake,
ClearUselessActions,
UpdateAnimatedTransformConstraint,
ARMATURE_OT_copy_bone_color_to_selected,
ARMATURE_OT_collection_show_all,
ARMATURE_OT_collection_unsolo_all,
ARMATURE_OT_collection_remove_unused,
ANIM_OT_slot_new_for_id,
ANIM_OT_slot_unassign_from_id,
ANIM_OT_slot_unassign_from_nla_strip,
ANIM_OT_slot_unassign_from_constraint,
)