Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSybren A. Stüvel <sybren@blender.org>2021-12-16 13:10:11 +0300
committerSybren A. Stüvel <sybren@blender.org>2021-12-16 13:10:11 +0300
commitd737f2016dcc314ac562e0e7db4381a6281af19f (patch)
treebc4bbbb180af380f26a33565831cd84b0af1afed /copy_global_transform.py
parentece39d809ceb27b5a06da7c4b8f25ca77016b151 (diff)
New animation add-on: Copy Global Transform
This add-on allows animators to copy the global transform of the active object or pose bone onto the clipboard. It can then be pasted in three different ways: - To the current transform of the selected object/pose bone (could be a different one than was used for the copying). - To selected keyframes. - Baking to all frames between the first and last selected keyframe (defaulting to preview range or scene range). All three methods are compatible with auto-keying. The latter two methods *require* auto-keying to be enabled, to give the animator control over which keying set to use, etc. An earlier version of this add-on was used by the Blender Animation Studio during Sprite Fright. Since then the two paste-to-frame-range options were added, by request of the animators. Reviewed by: campbellbarton Differential Revision: https://developer.blender.org/D13570
Diffstat (limited to 'copy_global_transform.py')
-rw-r--r--copy_global_transform.py469
1 files changed, 469 insertions, 0 deletions
diff --git a/copy_global_transform.py b/copy_global_transform.py
new file mode 100644
index 00000000..2c3c5012
--- /dev/null
+++ b/copy_global_transform.py
@@ -0,0 +1,469 @@
+# ====================== BEGIN GPL LICENSE BLOCK ======================
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ======================= END GPL LICENSE BLOCK ========================
+
+"""
+Copy Global Transform
+
+Simple add-on for copying world-space transforms.
+
+It's called "global" to avoid confusion with the Blender World data-block.
+"""
+
+bl_info = {
+ "name": "Copy Global Transform",
+ "author": "Sybren A. Stüvel",
+ "version": (2, 0),
+ "blender": (3, 1, 0),
+ "location": "N-panel in the 3D Viewport",
+ "category": "Animation",
+ "support": 'OFFICIAL',
+}
+
+import ast
+from typing import Iterable, Optional, Union, Any
+
+import bpy
+from bpy.types import Context, Object, Operator, Panel, PoseBone
+from mathutils import Matrix
+
+
+class AutoKeying:
+ """Auto-keying support.
+
+ Based on Rigify code by Alexander Gavrilov.
+ """
+
+ @classmethod
+ def keying_options(cls, context: Context) -> set[str]:
+ """Retrieve the general keyframing options from user preferences."""
+
+ prefs = context.preferences
+ ts = context.scene.tool_settings
+ options = set()
+
+ if prefs.edit.use_visual_keying:
+ options.add('INSERTKEY_VISUAL')
+ if prefs.edit.use_keyframe_insert_needed:
+ options.add('INSERTKEY_NEEDED')
+ if prefs.edit.use_insertkey_xyz_to_rgb:
+ options.add('INSERTKEY_XYZ_TO_RGB')
+ if ts.use_keyframe_cycle_aware:
+ options.add('INSERTKEY_CYCLE_AWARE')
+ return options
+
+ @classmethod
+ def autokeying_options(cls, context: Context) -> Optional[set[str]]:
+ """Retrieve the Auto Keyframe options, or None if disabled."""
+
+ ts = context.scene.tool_settings
+
+ if not ts.use_keyframe_insert_auto:
+ return None
+
+ if ts.use_keyframe_insert_keyingset:
+ # No support for keying sets (yet).
+ return None
+
+ prefs = context.preferences
+ options = cls.keying_options(context)
+
+ if prefs.edit.use_keyframe_insert_available:
+ options.add('INSERTKEY_AVAILABLE')
+ if ts.auto_keying_mode == 'REPLACE_KEYS':
+ options.add('INSERTKEY_REPLACE')
+ return options
+
+ @staticmethod
+ def get_4d_rotlock(bone: PoseBone) -> Iterable[bool]:
+ "Retrieve the lock status for 4D rotation."
+ if bone.lock_rotations_4d:
+ return [bone.lock_rotation_w, *bone.lock_rotation]
+ else:
+ return [all(bone.lock_rotation)] * 4
+
+ @staticmethod
+ def keyframe_channels(
+ target: Union[Object, PoseBone],
+ options: set[str],
+ data_path: str,
+ group: str,
+ locks: Iterable[bool],
+ ) -> None:
+ if all(locks):
+ return
+
+ if not any(locks):
+ target.keyframe_insert(data_path, group=group, options=options)
+ return
+
+ for index, lock in enumerate(locks):
+ if lock:
+ continue
+ target.keyframe_insert(data_path, index=index, group=group, options=options)
+
+ @classmethod
+ def key_transformation(
+ cls,
+ target: Union[Object, PoseBone],
+ options: set[str],
+ ) -> None:
+ """Keyframe transformation properties, avoiding keying locked channels."""
+
+ is_bone = isinstance(target, PoseBone)
+ if is_bone:
+ group = target.name
+ else:
+ group = "Object Transforms"
+
+ def keyframe(data_path: str, locks: Iterable[bool]) -> None:
+ cls.keyframe_channels(target, options, data_path, group, locks)
+
+ if not (is_bone and target.bone.use_connect):
+ keyframe("location", target.lock_location)
+
+ if target.rotation_mode == 'QUATERNION':
+ keyframe("rotation_quaternion", cls.get_4d_rotlock(target))
+ elif target.rotation_mode == 'AXIS_ANGLE':
+ keyframe("rotation_axis_angle", cls.get_4d_rotlock(target))
+ else:
+ keyframe("rotation_euler", target.lock_rotation)
+
+ keyframe("scale", target.lock_scale)
+
+ @classmethod
+ def autokey_transformation(cls, context: Context, target: Union[Object, PoseBone]) -> None:
+ """Auto-key transformation properties."""
+
+ options = cls.autokeying_options(context)
+ if options is None:
+ return
+ cls.key_transformation(target, options)
+
+
+def get_matrix(context: Context) -> Matrix:
+ bone = context.active_pose_bone
+ if bone:
+ # Convert matrix to world space
+ arm = context.active_object
+ mat = arm.matrix_world @ bone.matrix
+ else:
+ mat = context.active_object.matrix_world
+
+ return mat
+
+
+def set_matrix(context: Context, mat: Matrix) -> None:
+ bone = context.active_pose_bone
+ if bone:
+ # Convert matrix to local space
+ arm_eval = context.active_object.evaluated_get(context.view_layer.depsgraph)
+ bone.matrix = arm_eval.matrix_world.inverted() @ mat
+ AutoKeying.autokey_transformation(context, bone)
+ else:
+ context.active_object.matrix_world = mat
+ AutoKeying.autokey_transformation(context, context.active_object)
+
+
+def _selected_keyframes(context: Context) -> list[float]:
+ """Return the list of frame numbers that have a selected key.
+
+ Only keys on the active bone/object are considered.
+ """
+ bone = context.active_pose_bone
+ if bone:
+ return _selected_keyframes_for_bone(context.active_object, bone)
+ return _selected_keyframes_for_object(context.active_object)
+
+
+def _selected_keyframes_for_bone(object: Object, bone: PoseBone) -> list[float]:
+ """Return the list of frame numbers that have a selected key.
+
+ Only keys on the given pose bone are considered.
+ """
+ name = bpy.utils.escape_identifier(bone.name)
+ return _selected_keyframes_in_action(object, f'pose.bones["{name}"].')
+
+
+def _selected_keyframes_for_object(object: Object) -> list[float]:
+ """Return the list of frame numbers that have a selected key.
+
+ Only keys on the given object are considered.
+ """
+ return _selected_keyframes_in_action(object, "")
+
+
+def _selected_keyframes_in_action(object: Object, rna_path_prefix: str) -> list[float]:
+ """Return the list of frame numbers that have a selected key.
+
+ Only keys on the given object's Action on FCurves starting with rna_path_prefix are considered.
+ """
+
+ action = object.animation_data and object.animation_data.action
+ if action is None:
+ return []
+
+ keyframes = set()
+ for fcurve in action.fcurves:
+ if not fcurve.data_path.startswith(rna_path_prefix):
+ continue
+
+ for kp in fcurve.keyframe_points:
+ if not kp.select_control_point:
+ continue
+ keyframes.add(kp.co.x)
+ return sorted(keyframes)
+
+
+class OBJECT_OT_copy_global_transform(Operator):
+ bl_idname = "object.copy_global_transform"
+ bl_label = "Copy Global Transform"
+ bl_description = (
+ "Copies the matrix of the currently active object or pose bone to the clipboard. Uses world-space matrices"
+ )
+ # This operator cannot be un-done because it manipulates data outside Blender.
+ bl_options = {'REGISTER'}
+
+ @classmethod
+ def poll(cls, context: Context) -> bool:
+ return bool(context.active_pose_bone) or bool(context.active_object)
+
+ def execute(self, context: Context) -> set[str]:
+ mat = get_matrix(context)
+ rows = [f" {tuple(row)!r}," for row in mat]
+ as_string = "\n".join(rows)
+ context.window_manager.clipboard = f"Matrix((\n{as_string}\n))"
+ return {'FINISHED'}
+
+
+class OBJECT_OT_paste_transform(Operator):
+ bl_idname = "object.paste_transform"
+ bl_label = "Paste Global Transform"
+ bl_description = (
+ "Pastes the matrix from the clipboard to the currently active pose bone or object. Uses world-space matrices"
+ )
+ bl_options = {'REGISTER', 'UNDO'}
+
+ _method_items = [
+ (
+ 'CURRENT',
+ "Current Transform",
+ "Paste onto the current values only, only manipulating the animation data if auto-keying is enabled",
+ ),
+ (
+ 'EXISTING_KEYS',
+ "Selected Keys",
+ "Paste onto frames that have a selected key, potentially creating new keys on those frames",
+ ),
+ (
+ 'BAKE',
+ "Bake on Key Range",
+ "Paste onto all frames between the first and last selected key, creating new keyframes if necessary",
+ ),
+ ]
+ method: bpy.props.EnumProperty( # type: ignore
+ items=_method_items,
+ name="Paste Method",
+ description="Update the current transform, selected keyframes, or even create new keys",
+ )
+ bake_step: bpy.props.IntProperty( # type: ignore
+ name="Frame Step",
+ description="Only used for baking. Step=1 creates a key on every frame, step=2 bakes on 2s, etc",
+ min=1,
+ soft_min=1,
+ soft_max=5,
+ )
+
+ @classmethod
+ def poll(cls, context: Context) -> bool:
+ if not context.active_pose_bone and not context.active_object:
+ cls.poll_message_set("Select an object or pose bone")
+ return False
+ if not context.window_manager.clipboard.startswith("Matrix("):
+ cls.poll_message_set("Clipboard does not contain a valid matrix")
+ return False
+ return True
+
+ @staticmethod
+ def parse_print_m4(value: str) -> Optional[Matrix]:
+ """Parse output from Blender's print_m4() function.
+
+ Expects four lines of space-separated floats.
+ """
+
+ lines = value.strip().splitlines()
+ if len(lines) != 4:
+ return None
+
+ floats = tuple(tuple(float(item) for item in line.split()) for line in lines)
+ return Matrix(floats)
+
+ def execute(self, context: Context) -> set[str]:
+ clipboard = context.window_manager.clipboard
+ if clipboard.startswith("Matrix"):
+ mat = Matrix(ast.literal_eval(clipboard[6:]))
+ else:
+ mat = self.parse_print_m4(clipboard)
+
+ if mat is None:
+ self.report({'ERROR'}, "Clipboard does not contain a valid matrix.")
+ return {'CANCELLED'}
+
+ applicator = {
+ 'CURRENT': self._paste_current,
+ 'EXISTING_KEYS': self._paste_existing_keys,
+ 'BAKE': self._paste_bake,
+ }[self.method]
+ return applicator(context, mat)
+
+ @staticmethod
+ def _paste_current(context: Context, matrix: Matrix) -> set[str]:
+ set_matrix(context, matrix)
+ return {'FINISHED'}
+
+ def _paste_existing_keys(self, context: Context, matrix: Matrix) -> set[str]:
+ if not context.scene.tool_settings.use_keyframe_insert_auto:
+ self.report({'ERROR'}, "This mode requires auto-keying to work properly")
+ return {'CANCELLED'}
+
+ frame_numbers = _selected_keyframes(context)
+ if not frame_numbers:
+ self.report({'WARNING'}, "No selected frames found")
+ return {'CANCELLED'}
+
+ self._paste_on_frames(context, frame_numbers, matrix)
+ return {'FINISHED'}
+
+ def _paste_bake(self, context: Context, matrix: Matrix) -> set[str]:
+ if not context.scene.tool_settings.use_keyframe_insert_auto:
+ self.report({'ERROR'}, "This mode requires auto-keying to work properly")
+ return {'CANCELLED'}
+
+ bake_step = max(1, self.bake_step)
+ # Put the clamped bake step back into RNA for the redo panel.
+ self.bake_step = bake_step
+
+ frame_start, frame_end = self._determine_bake_range(context)
+ frame_range = range(round(frame_start), round(frame_end) + bake_step, bake_step)
+ self._paste_on_frames(context, frame_range, matrix)
+ return {'FINISHED'}
+
+ def _determine_bake_range(self, context: Context) -> tuple[float, float]:
+ frame_numbers = _selected_keyframes(context)
+ if frame_numbers:
+ # Note that these could be the same frame, if len(frame_numbers) == 1:
+ return frame_numbers[0], frame_numbers[-1]
+
+ if context.scene.use_preview_range:
+ self.report({'INFO'}, "No selected keys, pasting over preview range")
+ return context.scene.frame_preview_start, context.scene.frame_preview_end
+
+ self.report({'INFO'}, "No selected keys, pasting over scene range")
+ return context.scene.frame_start, context.scene.frame_end
+
+ def _paste_on_frames(self, context: Context, frame_numbers: Iterable[float], matrix: Matrix) -> None:
+ current_frame = context.scene.frame_current_final
+ try:
+ for frame in frame_numbers:
+ context.scene.frame_set(int(frame), subframe=frame % 1.0)
+ set_matrix(context, matrix)
+ finally:
+ context.scene.frame_set(int(current_frame), subframe=current_frame % 1.0)
+
+
+class VIEW3D_PT_copy_global_transform(Panel):
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_category = "Animation"
+ bl_label = "Global Transform"
+
+ def draw(self, context: Context) -> None:
+ layout = self.layout
+
+ # 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_col.operator("object.paste_transform", text="Paste", icon='PASTEDOWN').method = 'CURRENT'
+ wants_autokey_col = paste_col.column(align=True)
+ has_autokey = context.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:")
+
+ wants_autokey_col.operator(
+ "object.paste_transform",
+ text="Paste to Selected Keys",
+ icon='PASTEDOWN',
+ ).method = 'EXISTING_KEYS'
+ wants_autokey_col.operator(
+ "object.paste_transform",
+ text="Paste and Bake",
+ icon='PASTEDOWN',
+ ).method = 'BAKE'
+
+
+### Messagebus subscription to monitor changes & refresh panels.
+_msgbus_owner = object()
+
+
+def _refresh_3d_panels():
+ refresh_area_types = {'VIEW_3D'}
+ for win in bpy.context.window_manager.windows:
+ for area in win.screen.areas:
+ if area.type not in refresh_area_types:
+ continue
+ area.tag_redraw()
+
+
+classes = (
+ OBJECT_OT_copy_global_transform,
+ OBJECT_OT_paste_transform,
+ VIEW3D_PT_copy_global_transform,
+)
+_register, _unregister = bpy.utils.register_classes_factory(classes)
+
+
+def _register_message_bus() -> None:
+ bpy.msgbus.subscribe_rna(
+ key=(bpy.types.ToolSettings, "use_keyframe_insert_auto"),
+ owner=_msgbus_owner,
+ args=(),
+ notify=_refresh_3d_panels,
+ options={'PERSISTENT'},
+ )
+
+
+def _unregister_message_bus() -> None:
+ bpy.msgbus.clear_by_owner(_msgbus_owner)
+
+
+@bpy.app.handlers.persistent # type: ignore
+def _on_blendfile_load_post(none: Any, other_none: Any) -> None:
+ # The parameters are required, but both are None.
+ _register_message_bus()
+
+
+def register():
+ _register()
+ bpy.app.handlers.load_post.append(_on_blendfile_load_post)
+
+
+def unregister():
+ _unregister()
+ _unregister_message_bus()
+ bpy.app.handlers.load_post.remove(_on_blendfile_load_post)