blender/scripts/startup/bl_ui/space_view3d_sidebar.py
Christoph Lendenfeld b988d827b6 Anim: world space copy paste frame range
This adds functionality to store world space data in a temporary file
on disk and paste it on other objects.
(technically `AnimTransformable`s, which is objects or bones, but for
simplicity I will be talking about "objects" here)
Compared to the existing functionality this can also copy a
range of frames. Because the data is stored on disk, it is possible
to copy and paste between files.

**Matching Logic**
* When 1 object copied and 1 selected -> paste no matter the naming
* When 1 object copied and 2:n selected -> paste the same data to
all selected
* When 2:n objects copied and 1:n selected -> paste based on naming.
It is possible to paste between objects and bones as long
as the name matches.

**Depsgraph**
* Introduces a new API function `DEG_foreach_dependent_component`
* this function iterates the outgoing links starting at a given ID
* it is used to find the order in which to paste world space transforms
since we have to paste in order of dependency -> dependent
for it to work correctly.

**UI**
Adding a new panel because the new operators are not compatible
with the existing ones. I cannot remove the existing operators because
they provide functionality that I don't yet provide.
To make it clear that they don't work together, they get their own panel.

**Limitations**
* objects with constraints may not end up at the correct world space.
That is expected for limit constraints, but additive constraints would
need to be inverse solved which is a job for another day.
* when the rotation mode is animated, pasting world space animation
will override all keys in the baked range to be the value at the
start of the range.

**Performance**
Testing in a file with 10.000 frames of dense keyframe data.
Copying a single bone:
* Copying ~1.6 seconds
* Pasting ~1.6 seconds

Copying 3 bones of the same armature
* Copying ~1.6 seconds
* Pasting ~4.8 seconds

The extra evaluation required when pasting makes this scale with the
amount of bones we are pasting.

Part of #156579

Pull Request: https://projects.blender.org/blender/blender/pulls/159390
2026-09-17 16:02:26 +02:00

198 lines
7.8 KiB
Python

# SPDX-FileCopyrightText: 2025 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
import bpy
from bpy.types import Context, Panel, UILayout
from bpy.app.translations import contexts as i18n_contexts
class GlobalTransformPanelMixin:
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Animation"
class VIEW3D_PT_copy_world_space_animation(GlobalTransformPanelMixin, Panel):
bl_label = "World Space Animation"
def draw(self, context: Context) -> None:
layout = self.layout
layout.operator("anim.world_space_copy", icon='COPYDOWN', text="Copy Playback Range").range_mode = 'PLAYBACK'
row = layout.row(align=True)
row.operator("anim.world_space_paste", icon='PASTEDOWN', text="Paste")
row.operator("anim.world_space_paste", icon='PASTEDOWN', text="At Playhead").offset = 'START'
class VIEW3D_PT_copy_global_transform(GlobalTransformPanelMixin, Panel):
bl_label = "World Space Transform"
def draw(self, context: Context) -> None:
layout = self.layout
scene = context.scene
# No need to put "Global Transform" in the operator text, given that it's already in the panel title.
layout.operator("object.copy_global_transform", text="Copy", icon='COPYDOWN')
paste_col = layout.column(align=True)
paste_row = paste_col.row(align=True)
paste_props = paste_row.operator("object.paste_transform", text="Paste", icon='PASTEDOWN')
paste_props.method = 'CURRENT'
paste_props.use_mirror = False
paste_props = paste_row.operator("object.paste_transform", text="Mirrored", icon='PASTEFLIPDOWN')
paste_props.method = 'CURRENT'
paste_props.use_mirror = True
wants_autokey_col = paste_col.column(align=False)
has_autokey = scene.tool_settings.use_keyframe_insert_auto
wants_autokey_col.enabled = has_autokey
if not has_autokey:
wants_autokey_col.label(text="These require auto-key:")
paste_col = wants_autokey_col.column(align=True)
paste_col.operator(
"object.paste_transform",
text="Paste to Selected Keys",
icon='PASTEDOWN',
).method = 'EXISTING_KEYS'
paste_col.operator(
"object.paste_transform",
text="Paste and Bake",
icon='PASTEDOWN',
).method = 'BAKE'
class VIEW3D_PT_copy_global_transform_fix_to_camera(GlobalTransformPanelMixin, Panel):
bl_label = "Fix to Camera"
bl_parent_id = "VIEW3D_PT_copy_global_transform"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
from bpy_extras.anim_utils import AutoKeying
layout = self.layout
scene = context.scene
# Fix to Scene Camera:
layout.use_property_split = True
props_box = layout.column(heading="Fix", heading_ctxt=i18n_contexts.id_camera, align=True)
props_box.prop(scene.tool_settings, "anim_fix_to_cam_use_loc", text="Location")
props_box.prop(scene.tool_settings, "anim_fix_to_cam_use_rot", text="Rotation")
props_box.prop(scene.tool_settings, "anim_fix_to_cam_use_scale", text="Scale")
keyingset = AutoKeying.active_keyingset(context)
if keyingset:
# Show an explicit message here, even though the keying set affects
# the other operators as well. Fix to Camera is treated as a special
# case because it also has options for selecting what to key. The
# logical AND of the settings is used, so a property is only keyed
# when the keying set AND the above check-boxes say it's OK.
props_box.label_multiline(
text="A keying set is active, which may reduce the effect of the options above.",
icon="STATUS_WARNING",
max_lines=3)
row = layout.row(align=True)
props = row.operator("object.fix_to_camera")
props.use_location = scene.tool_settings.anim_fix_to_cam_use_loc
props.use_rotation = scene.tool_settings.anim_fix_to_cam_use_rot
props.use_scale = scene.tool_settings.anim_fix_to_cam_use_scale
row.operator("object.delete_fix_to_camera_keys", text="", icon='TRASH')
class VIEW3D_PT_copy_global_transform_mirror(GlobalTransformPanelMixin, Panel):
bl_label = "Mirror"
bl_parent_id = "VIEW3D_PT_copy_global_transform"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
col = layout.column(align=True)
col.prop(scene.tool_settings, "anim_mirror_object", text="Object")
mirror_ob = scene.tool_settings.anim_mirror_object
if mirror_ob is None:
# No explicit mirror object means "the current armature", so then the bone name should be editable.
if context.object and context.object.type == 'ARMATURE':
self._bone_search(col, scene, context.object)
else:
self._bone_entry(layout, scene)
elif mirror_ob.type == 'ARMATURE':
self._bone_search(col, scene, mirror_ob)
def _bone_search(self, layout: UILayout, scene: bpy.types.Scene, armature_ob: bpy.types.Object) -> None:
"""Search within the bones of the given armature."""
assert armature_ob and armature_ob.type == 'ARMATURE'
layout.prop_search(
scene.tool_settings,
"anim_mirror_bone",
armature_ob.data,
"edit_bones" if armature_ob.mode == 'EDIT' else "bones",
text="Bone",
)
def _bone_entry(self, layout: UILayout, scene: bpy.types.Scene) -> None:
"""Allow manual entry of a bone name."""
layout.prop(scene.tool_settings, "anim_mirror_bone", text="Bone")
class VIEW3D_PT_copy_global_transform_relative(GlobalTransformPanelMixin, Panel):
bl_label = "Relative"
bl_parent_id = "VIEW3D_PT_copy_global_transform"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context: Context) -> None:
from bl_operators.copy_global_transform import get_relative_ob
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
# Copy/Paste relative to some object:
copy_paste_sub = layout.column(align=False)
has_relative_ob = bool(get_relative_ob(context))
copy_paste_sub.prop(scene.tool_settings, 'anim_relative_object', text="Object")
if not scene.tool_settings.anim_relative_object:
copy_paste_sub.label(text="Using Active Scene Camera")
button_sub = copy_paste_sub.row(align=True)
button_sub.enabled = has_relative_ob
button_sub.operator("object.copy_relative_transform", text="Copy", icon='COPYDOWN')
paste_props = button_sub.operator("object.paste_transform", text="Paste", icon='PASTEDOWN')
paste_props.method = 'CURRENT'
paste_props.use_mirror = False
paste_props.use_relative = True
# It is unknown whether this combination of options is in any way
# sensible or usable, and of so, in which order the mirroring and
# relative'ing-to should happen. That's why, for now, it's disabled.
#
# paste_props = paste_row.operator("object.paste_transform", text="Mirrored", icon='PASTEFLIPDOWN')
# paste_props.method = 'CURRENT'
# paste_props.use_mirror = True
# paste_props.use_relative = True
classes = (
VIEW3D_PT_copy_world_space_animation,
VIEW3D_PT_copy_global_transform,
VIEW3D_PT_copy_global_transform_mirror,
VIEW3D_PT_copy_global_transform_relative,
VIEW3D_PT_copy_global_transform_fix_to_camera,
)
if __name__ == "__main__": # only for live edit.
from bpy.utils import register_class
for cls in classes:
register_class(cls)