1 # SPDX-FileCopyrightText: 2021-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
8 Simple add-on for copying world-space transforms.
10 It's called "global" to avoid confusion with the Blender World data-block.
14 "name": "Copy Global Transform",
15 "author": "Sybren A. Stüvel",
18 "location": "N-panel in the 3D Viewport",
19 "category": "Animation",
20 "support": 'OFFICIAL',
21 "doc_url": "{BLENDER_MANUAL_URL}/addons/animation/copy_global_transform.html",
22 "tracker_url": "https://projects.blender.org/blender/blender-addons/issues",
28 from typing
import Iterable
, Optional
, Union
, Any
, TypeAlias
, Iterator
31 from bpy
.types
import Context
, Object
, Operator
, Panel
, PoseBone
, UILayout
, FCurve
, Camera
, FModifierStepped
32 from mathutils
import Matrix
43 """Auto-keying support.
45 Based on Rigify code by Alexander Gavrilov.
48 # Use AutoKeying.keytype() or Authkeying.options() context to change those.
50 _force_autokey
= False # Allow use without the user activating auto-keying.
56 @contextlib.contextmanager
57 def keytype(cls
, the_keytype
: str) -> Iterator
[None]:
58 """Context manager to set the key type that's inserted."""
59 default_keytype
= cls
._keytype
61 cls
._keytype
= the_keytype
64 cls
._keytype
= default_keytype
67 @contextlib.contextmanager
68 def options(cls
, *, keytype
="", use_loc
=True, use_rot
=True, use_scale
=True, force_autokey
=False) -> Iterator
[None]:
69 """Context manager to set various options."""
70 default_keytype
= cls
._keytype
71 default_use_loc
= cls
._use
_loc
72 default_use_rot
= cls
._use
_rot
73 default_use_scale
= cls
._use
_scale
74 default_force_autokey
= cls
._force
_autokey
76 cls
._keytype
= keytype
77 cls
._use
_loc
= use_loc
78 cls
._use
_rot
= use_rot
79 cls
._use
_scale
= use_scale
80 cls
._force
_autokey
= force_autokey
83 cls
._keytype
= default_keytype
84 cls
._use
_loc
= default_use_loc
85 cls
._use
_rot
= default_use_rot
86 cls
._use
_scale
= default_use_scale
87 cls
._force
_autokey
= default_force_autokey
90 def keying_options(cls
, context
: Context
) -> set[str]:
91 """Retrieve the general keyframing options from user preferences."""
93 prefs
= context
.preferences
94 ts
= context
.scene
.tool_settings
97 if prefs
.edit
.use_visual_keying
:
98 options
.add('INSERTKEY_VISUAL')
99 if prefs
.edit
.use_keyframe_insert_needed
:
100 options
.add('INSERTKEY_NEEDED')
101 if ts
.use_keyframe_cycle_aware
:
102 options
.add('INSERTKEY_CYCLE_AWARE')
106 def autokeying_options(cls
, context
: Context
) -> Optional
[set[str]]:
107 """Retrieve the Auto Keyframe options, or None if disabled."""
109 ts
= context
.scene
.tool_settings
111 if not (cls
._force
_autokey
or ts
.use_keyframe_insert_auto
):
114 if ts
.use_keyframe_insert_keyingset
:
115 # No support for keying sets (yet).
118 prefs
= context
.preferences
119 options
= cls
.keying_options(context
)
121 if prefs
.edit
.use_keyframe_insert_available
:
122 options
.add('INSERTKEY_AVAILABLE')
123 if ts
.auto_keying_mode
== 'REPLACE_KEYS':
124 options
.add('INSERTKEY_REPLACE')
128 def get_4d_rotlock(bone
: PoseBone
) -> Iterable
[bool]:
129 "Retrieve the lock status for 4D rotation."
130 if bone
.lock_rotations_4d
:
131 return [bone
.lock_rotation_w
, *bone
.lock_rotation
]
133 return [all(bone
.lock_rotation
)] * 4
136 def keyframe_channels(
138 target
: Union
[Object
, PoseBone
],
142 locks
: Iterable
[bool],
148 target
.keyframe_insert(data_path
, group
=group
, options
=options
, keytype
=cls
._keytype
)
151 for index
, lock
in enumerate(locks
):
154 target
.keyframe_insert(data_path
, index
=index
, group
=group
, options
=options
, keytype
=cls
._keytype
)
157 def key_transformation(
159 target
: Union
[Object
, PoseBone
],
162 """Keyframe transformation properties, avoiding keying locked channels."""
164 is_bone
= isinstance(target
, PoseBone
)
168 group
= "Object Transforms"
170 def keyframe(data_path
: str, locks
: Iterable
[bool]) -> None:
172 cls
.keyframe_channels(target
, options
, data_path
, group
, locks
)
174 # These are expected when "Insert Available" is turned on, and
175 # these curves are not available.
178 if cls
._use
_loc
and not (is_bone
and target
.bone
.use_connect
):
179 keyframe("location", target
.lock_location
)
182 if target
.rotation_mode
== 'QUATERNION':
183 keyframe("rotation_quaternion", cls
.get_4d_rotlock(target
))
184 elif target
.rotation_mode
== 'AXIS_ANGLE':
185 keyframe("rotation_axis_angle", cls
.get_4d_rotlock(target
))
187 keyframe("rotation_euler", target
.lock_rotation
)
190 keyframe("scale", target
.lock_scale
)
193 def autokey_transformation(cls
, context
: Context
, target
: Union
[Object
, PoseBone
]) -> None:
194 """Auto-key transformation properties."""
196 options
= cls
.autokeying_options(context
)
199 cls
.key_transformation(target
, options
)
202 def get_matrix(context
: Context
) -> Matrix
:
203 bone
= context
.active_pose_bone
205 # Convert matrix to world space
206 arm
= context
.active_object
207 mat
= arm
.matrix_world
@ bone
.matrix
209 mat
= context
.active_object
.matrix_world
214 def set_matrix(context
: Context
, mat
: Matrix
) -> None:
215 bone
= context
.active_pose_bone
217 # Convert matrix to local space
218 arm_eval
= context
.active_object
.evaluated_get(context
.view_layer
.depsgraph
)
219 bone
.matrix
= arm_eval
.matrix_world
.inverted() @ mat
220 AutoKeying
.autokey_transformation(context
, bone
)
222 context
.active_object
.matrix_world
= mat
223 AutoKeying
.autokey_transformation(context
, context
.active_object
)
226 def _selected_keyframes(context
: Context
) -> list[float]:
227 """Return the list of frame numbers that have a selected key.
229 Only keys on the active bone/object are considered.
231 bone
= context
.active_pose_bone
233 return _selected_keyframes_for_bone(context
.active_object
, bone
)
234 return _selected_keyframes_for_object(context
.active_object
)
237 def _selected_keyframes_for_bone(object: Object
, bone
: PoseBone
) -> list[float]:
238 """Return the list of frame numbers that have a selected key.
240 Only keys on the given pose bone are considered.
242 name
= bpy
.utils
.escape_identifier(bone
.name
)
243 return _selected_keyframes_in_action(object, f
'pose.bones["{name}"].')
246 def _selected_keyframes_for_object(object: Object
) -> list[float]:
247 """Return the list of frame numbers that have a selected key.
249 Only keys on the given object are considered.
251 return _selected_keyframes_in_action(object, "")
254 def _selected_keyframes_in_action(object: Object
, rna_path_prefix
: str) -> list[float]:
255 """Return the list of frame numbers that have a selected key.
257 Only keys on the given object's Action on FCurves starting with rna_path_prefix are considered.
260 action
= object.animation_data
and object.animation_data
.action
265 for fcurve
in action
.fcurves
:
266 if not fcurve
.data_path
.startswith(rna_path_prefix
):
269 for kp
in fcurve
.keyframe_points
:
270 if not kp
.select_control_point
:
272 keyframes
.add(kp
.co
.x
)
273 return sorted(keyframes
)
276 def _copy_matrix_to_clipboard(window_manager
: bpy
.types
.WindowManager
, matrix
: Matrix
) -> None:
277 rows
= [f
" {tuple(row)!r}," for row
in matrix
]
278 as_string
= "\n".join(rows
)
279 window_manager
.clipboard
= f
"Matrix((\n{as_string}\n))"
282 class OBJECT_OT_copy_global_transform(Operator
):
283 bl_idname
= "object.copy_global_transform"
284 bl_label
= "Copy Global Transform"
286 "Copies the matrix of the currently active object or pose bone to the clipboard. Uses world-space matrices"
288 # This operator cannot be un-done because it manipulates data outside Blender.
289 bl_options
= {'REGISTER'}
292 def poll(cls
, context
: Context
) -> bool:
293 return bool(context
.active_pose_bone
) or bool(context
.active_object
)
295 def execute(self
, context
: Context
) -> set[str]:
296 mat
= get_matrix(context
)
297 _copy_matrix_to_clipboard(context
.window_manager
, mat
)
301 def _get_relative_ob(context
: Context
) -> Optional
[Object
]:
302 """Get the 'relative' object.
304 This is the object that's configured, or if that's empty, the active scene camera.
306 rel_ob
= context
.scene
.addon_copy_global_transform_relative_ob
307 return rel_ob
or context
.scene
.camera
310 class OBJECT_OT_copy_relative_transform(Operator
):
311 bl_idname
= "object.copy_relative_transform"
312 bl_label
= "Copy Relative Transform"
313 bl_description
= "Copies the matrix of the currently active object or pose bone to the clipboard. " \
314 "Uses matrices relative to a specific object or the active scene camera"
315 # This operator cannot be un-done because it manipulates data outside Blender.
316 bl_options
= {'REGISTER'}
319 def poll(cls
, context
: Context
) -> bool:
320 rel_ob
= _get_relative_ob(context
)
323 return bool(context
.active_pose_bone
) or bool(context
.active_object
)
325 def execute(self
, context
: Context
) -> set[str]:
326 rel_ob
= _get_relative_ob(context
)
327 mat
= rel_ob
.matrix_world
.inverted() @ get_matrix(context
)
328 _copy_matrix_to_clipboard(context
.window_manager
, mat
)
332 class UnableToMirrorError(Exception):
333 """Raised when mirroring is enabled but no mirror object/bone is set."""
336 class OBJECT_OT_paste_transform(Operator
):
337 bl_idname
= "object.paste_transform"
338 bl_label
= "Paste Global Transform"
340 "Pastes the matrix from the clipboard to the currently active pose bone or object. Uses world-space matrices"
342 bl_options
= {'REGISTER', 'UNDO'}
348 "Paste onto the current values only, only manipulating the animation data if auto-keying is enabled",
353 "Paste onto frames that have a selected key, potentially creating new keys on those frames",
358 "Paste onto all frames between the first and last selected key, creating new keyframes if necessary",
361 method
: bpy
.props
.EnumProperty( # type: ignore
364 description
="Update the current transform, selected keyframes, or even create new keys",
366 bake_step
: bpy
.props
.IntProperty( # type: ignore
368 description
="Only used for baking. Step=1 creates a key on every frame, step=2 bakes on 2s, etc",
374 use_mirror
: bpy
.props
.BoolProperty( # type: ignore
375 name
="Mirror Transform",
376 description
="When pasting, mirror the transform relative to a specific object or bone",
380 mirror_axis_loc
: bpy
.props
.EnumProperty( # type: ignore
381 items
=_axis_enum_items
,
382 name
="Location Axis",
383 description
="Coordinate axis used to mirror the location part of the transform",
386 mirror_axis_rot
: bpy
.props
.EnumProperty( # type: ignore
387 items
=_axis_enum_items
,
388 name
="Rotation Axis",
389 description
="Coordinate axis used to mirror the rotation part of the transform",
393 use_relative
: bpy
.props
.BoolProperty( # type: ignore
394 name
="Use Relative Paste",
395 description
="When pasting, assume the pasted matrix is relative to another object (set in the user interface)",
400 def poll(cls
, context
: Context
) -> bool:
401 if not context
.active_pose_bone
and not context
.active_object
:
402 cls
.poll_message_set("Select an object or pose bone")
405 clipboard
= context
.window_manager
.clipboard
.strip()
406 if not (clipboard
.startswith("Matrix(") or clipboard
.startswith("<Matrix 4x4")):
407 cls
.poll_message_set("Clipboard does not contain a valid matrix")
412 def parse_print_m4(value
: str) -> Optional
[Matrix
]:
413 """Parse output from Blender's print_m4() function.
415 Expects four lines of space-separated floats.
418 lines
= value
.strip().splitlines()
422 floats
= tuple(tuple(float(item
) for item
in line
.split()) for line
in lines
)
423 return Matrix(floats
)
426 def parse_repr_m4(value
: str) -> Optional
[Matrix
]:
427 """Four lines of (a, b, c, d) floats."""
429 lines
= value
.strip().splitlines()
433 floats
= tuple(tuple(float(item
.strip()) for item
in line
.strip()[1:-1].split(',')) for line
in lines
)
434 return Matrix(floats
)
436 def execute(self
, context
: Context
) -> set[str]:
437 clipboard
= context
.window_manager
.clipboard
.strip()
438 if clipboard
.startswith("Matrix"):
439 mat
= Matrix(ast
.literal_eval(clipboard
[6:]))
440 elif clipboard
.startswith("<Matrix 4x4"):
441 mat
= self
.parse_repr_m4(clipboard
[12:-1])
443 mat
= self
.parse_print_m4(clipboard
)
446 self
.report({'ERROR'}, "Clipboard does not contain a valid matrix")
450 mat
= self
._preprocess
_matrix
(context
, mat
)
451 except UnableToMirrorError
:
452 self
.report({'ERROR'}, "Unable to mirror, no mirror object/bone configured")
456 'CURRENT': self
._paste
_current
,
457 'EXISTING_KEYS': self
._paste
_existing
_keys
,
458 'BAKE': self
._paste
_bake
,
460 return applicator(context
, mat
)
462 def _preprocess_matrix(self
, context
: Context
, matrix
: Matrix
) -> Matrix
:
463 matrix
= self
._relative
_to
_world
(context
, matrix
)
466 matrix
= self
._mirror
_matrix
(context
, matrix
)
469 def _relative_to_world(self
, context
: Context
, matrix
: Matrix
) -> Matrix
:
470 if not self
.use_relative
:
473 rel_ob
= _get_relative_ob(context
)
477 rel_ob_eval
= rel_ob
.evaluated_get(context
.view_layer
.depsgraph
)
478 return rel_ob_eval
.matrix_world
@ matrix
480 def _mirror_matrix(self
, context
: Context
, matrix
: Matrix
) -> Matrix
:
481 mirror_ob
= context
.scene
.addon_copy_global_transform_mirror_ob
482 mirror_bone
= context
.scene
.addon_copy_global_transform_mirror_bone
484 # No mirror object means "current armature object".
485 ctx_ob
= context
.object
486 if not mirror_ob
and mirror_bone
and ctx_ob
and ctx_ob
.type == 'ARMATURE':
490 raise UnableToMirrorError()
492 if mirror_ob
.type == 'ARMATURE' and mirror_bone
:
493 return self
._mirror
_over
_bone
(matrix
, mirror_ob
, mirror_bone
)
494 return self
._mirror
_over
_ob
(matrix
, mirror_ob
)
496 def _mirror_over_ob(self
, matrix
: Matrix
, mirror_ob
: bpy
.types
.Object
) -> Matrix
:
497 mirror_matrix
= mirror_ob
.matrix_world
498 return self
._mirror
_over
_matrix
(matrix
, mirror_matrix
)
500 def _mirror_over_bone(self
, matrix
: Matrix
, mirror_ob
: bpy
.types
.Object
, mirror_bone_name
: str) -> Matrix
:
501 bone
= mirror_ob
.pose
.bones
[mirror_bone_name
]
502 mirror_matrix
= mirror_ob
.matrix_world
@ bone
.matrix
503 return self
._mirror
_over
_matrix
(matrix
, mirror_matrix
)
505 def _mirror_over_matrix(self
, matrix
: Matrix
, mirror_matrix
: Matrix
) -> Matrix
:
506 # Compute the matrix in the space of the mirror matrix:
507 mat_local
= mirror_matrix
.inverted() @ matrix
509 # Decompose the matrix, as we don't want to touch the scale. This
510 # operator should only mirror the translation and rotation components.
511 trans
, rot_q
, scale
= mat_local
.decompose()
513 # Mirror the translation component:
514 axis_index
= ord(self
.mirror_axis_loc
) - ord('x')
515 trans
[axis_index
] *= -1
517 # Flip the rotation, and use a rotation order that applies the to-be-flipped axes first.
518 match self
.mirror_axis_rot
:
520 rot_e
= rot_q
.to_euler('XYZ')
521 rot_e
.x
*= -1 # Flip the requested rotation axis.
522 rot_e
.y
*= -1 # Also flip the bone roll.
524 rot_e
= rot_q
.to_euler('YZX')
525 rot_e
.y
*= -1 # Flip the requested rotation axis.
526 rot_e
.z
*= -1 # Also flip another axis? Not sure how to handle this one.
528 rot_e
= rot_q
.to_euler('ZYX')
529 rot_e
.z
*= -1 # Flip the requested rotation axis.
530 rot_e
.y
*= -1 # Also flip the bone roll.
532 # Recompose the local matrix:
533 mat_local
= Matrix
.LocRotScale(trans
, rot_e
, scale
)
535 # Go back to world space:
536 mirrored_world
= mirror_matrix
@ mat_local
537 return mirrored_world
540 def _paste_current(context
: Context
, matrix
: Matrix
) -> set[str]:
541 set_matrix(context
, matrix
)
544 def _paste_existing_keys(self
, context
: Context
, matrix
: Matrix
) -> set[str]:
545 if not context
.scene
.tool_settings
.use_keyframe_insert_auto
:
546 self
.report({'ERROR'}, "This mode requires auto-keying to work properly")
549 frame_numbers
= _selected_keyframes(context
)
550 if not frame_numbers
:
551 self
.report({'WARNING'}, "No selected frames found")
554 self
._paste
_on
_frames
(context
, frame_numbers
, matrix
)
557 def _paste_bake(self
, context
: Context
, matrix
: Matrix
) -> set[str]:
558 if not context
.scene
.tool_settings
.use_keyframe_insert_auto
:
559 self
.report({'ERROR'}, "This mode requires auto-keying to work properly")
562 bake_step
= max(1, self
.bake_step
)
563 # Put the clamped bake step back into RNA for the redo panel.
564 self
.bake_step
= bake_step
566 frame_start
, frame_end
= self
._determine
_bake
_range
(context
)
567 frame_range
= range(round(frame_start
), round(frame_end
) + bake_step
, bake_step
)
568 self
._paste
_on
_frames
(context
, frame_range
, matrix
)
571 def _determine_bake_range(self
, context
: Context
) -> tuple[float, float]:
572 frame_numbers
= _selected_keyframes(context
)
574 # Note that these could be the same frame, if len(frame_numbers) == 1:
575 return frame_numbers
[0], frame_numbers
[-1]
577 if context
.scene
.use_preview_range
:
578 self
.report({'INFO'}, "No selected keys, pasting over preview range")
579 return context
.scene
.frame_preview_start
, context
.scene
.frame_preview_end
581 self
.report({'INFO'}, "No selected keys, pasting over scene range")
582 return context
.scene
.frame_start
, context
.scene
.frame_end
584 def _paste_on_frames(self
, context
: Context
, frame_numbers
: Iterable
[float], matrix
: Matrix
) -> None:
585 current_frame
= context
.scene
.frame_current_final
587 for frame
in frame_numbers
:
588 context
.scene
.frame_set(int(frame
), subframe
=frame
% 1.0)
589 set_matrix(context
, matrix
)
591 context
.scene
.frame_set(int(current_frame
), subframe
=current_frame
% 1.0)
594 # Mapping from frame number to the dominant key type.
595 # GENERATED is the only recessive key type, others are dominant.
596 KeyInfo
: TypeAlias
= dict[float, str]
599 class Transformable(metaclass
=abc
.ABCMeta
):
600 """Interface for a bone or an object."""
602 def __init__(self
) -> None:
603 self
._key
_info
_cache
: Optional
[KeyInfo
] = None
606 def matrix_world(self
) -> Matrix
:
610 def set_matrix_world(self
, context
: Context
, matrix
: Matrix
) -> None:
614 def _my_fcurves(self
) -> Iterable
[bpy
.types
.FCurve
]:
617 def key_info(self
) -> KeyInfo
:
618 if self
._key
_info
_cache
is not None:
619 return self
._key
_info
_cache
621 keyinfo
: KeyInfo
= {}
622 for fcurve
in self
._my
_fcurves
():
623 for kp
in fcurve
.keyframe_points
:
625 if kp
.type == 'GENERATED' and frame
in keyinfo
:
626 # Don't bother overwriting other key types.
628 keyinfo
[frame
] = kp
.type
630 self
._key
_info
_cache
= keyinfo
633 def remove_keys_of_type(self
, key_type
: str, *, frame_start
=float("-inf"), frame_end
=float("inf")) -> None:
634 self
._key
_info
_cache
= None
636 for fcurve
in self
._my
_fcurves
():
638 kp
for kp
in fcurve
.keyframe_points
if kp
.type == key_type
and (frame_start
<= kp
.co
.x
<= frame_end
)
640 for kp
in reversed(to_remove
):
641 fcurve
.keyframe_points
.remove(kp
, fast
=True)
642 fcurve
.keyframe_points
.handles_recalc()
645 class TransformableObject(Transformable
):
648 def __init__(self
, object: Object
) -> None:
652 def matrix_world(self
) -> Matrix
:
653 return self
.object.matrix_world
655 def set_matrix_world(self
, context
: Context
, matrix
: Matrix
) -> None:
656 self
.object.matrix_world
= matrix
657 AutoKeying
.autokey_transformation(context
, self
.object)
659 def __hash__(self
) -> int:
660 return hash(self
.object.as_pointer())
662 def _my_fcurves(self
) -> Iterable
[bpy
.types
.FCurve
]:
663 action
= self
._action
()
666 yield from action
.fcurves
668 def _action(self
) -> Optional
[bpy
.types
.Action
]:
669 adt
= self
.object.animation_data
670 return adt
and adt
.action
673 class TransformableBone(Transformable
):
677 def __init__(self
, pose_bone
: PoseBone
) -> None:
679 self
.arm_object
= pose_bone
.id_data
680 self
.pose_bone
= pose_bone
682 def matrix_world(self
) -> Matrix
:
683 mat
= self
.arm_object
.matrix_world
@ self
.pose_bone
.matrix
686 def set_matrix_world(self
, context
: Context
, matrix
: Matrix
) -> None:
687 # Convert matrix to armature-local space
688 arm_eval
= self
.arm_object
.evaluated_get(context
.view_layer
.depsgraph
)
689 self
.pose_bone
.matrix
= arm_eval
.matrix_world
.inverted() @ matrix
690 AutoKeying
.autokey_transformation(context
, self
.pose_bone
)
692 def __hash__(self
) -> int:
693 return hash(self
.pose_bone
.as_pointer())
695 def _my_fcurves(self
) -> Iterable
[bpy
.types
.FCurve
]:
696 action
= self
._action
()
700 rna_prefix
= f
"{self.pose_bone.path_from_id()}."
701 for fcurve
in action
.fcurves
:
702 if fcurve
.data_path
.startswith(rna_prefix
):
705 def _action(self
) -> Optional
[bpy
.types
.Action
]:
706 adt
= self
.arm_object
.animation_data
707 return adt
and adt
.action
710 class FixToCameraCommon
:
711 """Common functionality for the Fix To Scene Camera operator + its 'delete' button."""
713 keytype
= 'GENERATED'
715 # Operator method stubs to avoid PyLance/MyPy errors:
717 def poll_message_set(cls
, message
: str) -> None:
718 raise NotImplementedError()
720 def report(self
, level
: set[str], message
: str) -> None:
721 raise NotImplementedError()
723 # Implement in subclass:
724 def _execute(self
, context
: Context
, transformables
: list[Transformable
]) -> None:
725 raise NotImplementedError()
728 def poll(cls
, context
: Context
) -> bool:
729 if not context
.active_pose_bone
and not context
.active_object
:
730 cls
.poll_message_set("Select an object or pose bone")
732 if context
.mode
not in {'POSE', 'OBJECT'}:
733 cls
.poll_message_set("Switch to Pose or Object mode")
735 if not context
.scene
.camera
:
736 cls
.poll_message_set("The Scene needs a camera")
740 def execute(self
, context
: Context
) -> set[str]:
743 transformables
= self
._transformable
_objects
(context
)
745 transformables
= self
._transformable
_pbones
(context
)
747 self
.report({'ERROR'}, 'Unsupported mode: %r' % mode
)
750 restore_frame
= context
.scene
.frame_current
752 self
._execute
(context
, transformables
)
754 context
.scene
.frame_set(restore_frame
)
757 def _transformable_objects(self
, context
: Context
) -> list[Transformable
]:
758 return [TransformableObject(object=ob
) for ob
in context
.selected_editable_objects
]
760 def _transformable_pbones(self
, context
: Context
) -> list[Transformable
]:
761 return [TransformableBone(pose_bone
=bone
) for bone
in context
.selected_pose_bones
]
764 class OBJECT_OT_fix_to_camera(Operator
, FixToCameraCommon
):
765 bl_idname
= "object.fix_to_camera"
766 bl_label
= "Fix to Scene Camera"
767 bl_description
= "Generate new keys to fix the selected object/bone to the camera on unkeyed frames"
768 bl_options
= {'REGISTER', 'UNDO'}
770 use_loc
: bpy
.props
.BoolProperty( # type: ignore
772 description
="Create Location keys when fixing to the scene camera",
775 use_rot
: bpy
.props
.BoolProperty( # type: ignore
777 description
="Create Rotation keys when fixing to the scene camera",
780 use_scale
: bpy
.props
.BoolProperty( # type: ignore
782 description
="Create Scale keys when fixing to the scene camera",
786 def _get_matrices(self
, camera
: Camera
, transformables
: list[Transformable
]) -> dict[Transformable
, Matrix
]:
787 camera_mat_inv
= camera
.matrix_world
.inverted()
788 return {t
: camera_mat_inv
@ t
.matrix_world() for t
in transformables
}
790 def _execute(self
, context
: Context
, transformables
: list[Transformable
]) -> None:
791 depsgraph
= context
.view_layer
.depsgraph
792 scene
= context
.scene
794 scene
.frame_set(scene
.frame_start
)
795 camera_eval
= scene
.camera
.evaluated_get(depsgraph
)
796 last_camera_name
= scene
.camera
.name
797 matrices
= self
._get
_matrices
(camera_eval
, transformables
)
799 if scene
.use_preview_range
:
800 frame_start
= scene
.frame_preview_start
801 frame_end
= scene
.frame_preview_end
803 frame_start
= scene
.frame_start
804 frame_end
= scene
.frame_end
806 with AutoKeying
.options(
807 keytype
=self
.keytype
,
808 use_loc
=self
.use_loc
,
809 use_rot
=self
.use_rot
,
810 use_scale
=self
.use_scale
,
813 for frame
in range(frame_start
, frame_end
+ scene
.frame_step
, scene
.frame_step
):
814 scene
.frame_set(frame
)
816 camera_eval
= scene
.camera
.evaluated_get(depsgraph
)
817 cam_matrix_world
= camera_eval
.matrix_world
818 camera_mat_inv
= cam_matrix_world
.inverted()
820 if scene
.camera
.name
!= last_camera_name
:
821 # The scene camera changed, so the previous
822 # relative-to-camera matrices can no longer be used.
823 matrices
= self
._get
_matrices
(camera_eval
, transformables
)
824 last_camera_name
= scene
.camera
.name
826 for t
, camera_rel_matrix
in matrices
.items():
827 key_info
= t
.key_info()
828 key_type
= key_info
.get(frame
, "")
829 if key_type
not in {self
.keytype
, ""}:
830 # Manually set key, remember the current camera-relative matrix.
831 matrices
[t
] = camera_mat_inv
@ t
.matrix_world()
834 # No key, or a generated one. Overwrite it with a new transform.
835 t
.set_matrix_world(context
, cam_matrix_world
@ camera_rel_matrix
)
838 class OBJECT_OT_delete_fix_to_camera_keys(Operator
, FixToCameraCommon
):
839 bl_idname
= "object.delete_fix_to_camera_keys"
840 bl_label
= "Delete Generated Keys"
841 bl_description
= "Delete all keys that were generated by the 'Fix to Scene Camera' operator"
842 bl_options
= {'REGISTER', 'UNDO'}
844 def _execute(self
, context
: Context
, transformables
: list[Transformable
]) -> None:
845 scene
= context
.scene
846 if scene
.use_preview_range
:
847 frame_start
= scene
.frame_preview_start
848 frame_end
= scene
.frame_preview_end
850 frame_start
= scene
.frame_start
851 frame_end
= scene
.frame_end
853 for t
in transformables
:
854 t
.remove_keys_of_type(self
.keytype
, frame_start
=frame_start
, frame_end
=frame_end
)
858 bl_space_type
= 'VIEW_3D'
859 bl_region_type
= 'UI'
860 bl_category
= "Animation"
863 class VIEW3D_PT_copy_global_transform(PanelMixin
, Panel
):
864 bl_label
= "Global Transform"
866 def draw(self
, context
: Context
) -> None:
868 scene
= context
.scene
870 # No need to put "Global Transform" in the operator text, given that it's already in the panel title.
871 layout
.operator("object.copy_global_transform", text
="Copy", icon
='COPYDOWN')
873 paste_col
= layout
.column(align
=True)
875 paste_row
= paste_col
.row(align
=True)
876 paste_props
= paste_row
.operator("object.paste_transform", text
="Paste", icon
='PASTEDOWN')
877 paste_props
.method
= 'CURRENT'
878 paste_props
.use_mirror
= False
879 paste_props
= paste_row
.operator("object.paste_transform", text
="Mirrored", icon
='PASTEFLIPDOWN')
880 paste_props
.method
= 'CURRENT'
881 paste_props
.use_mirror
= True
883 wants_autokey_col
= paste_col
.column(align
=False)
884 has_autokey
= scene
.tool_settings
.use_keyframe_insert_auto
885 wants_autokey_col
.enabled
= has_autokey
887 wants_autokey_col
.label(text
="These require auto-key:")
889 paste_col
= wants_autokey_col
.column(align
=True)
891 "object.paste_transform",
892 text
="Paste to Selected Keys",
894 ).method
= 'EXISTING_KEYS'
896 "object.paste_transform",
897 text
="Paste and Bake",
902 class VIEW3D_PT_copy_global_transform_fix_to_camera(PanelMixin
, Panel
):
903 bl_label
= "Fix to Camera"
904 bl_parent_id
= "VIEW3D_PT_copy_global_transform"
906 def draw(self
, context
: Context
) -> None:
908 scene
= context
.scene
910 # Fix to Scene Camera:
911 layout
.use_property_split
= True
912 props_box
= layout
.column(heading
="Fix", align
=True)
913 props_box
.prop(scene
, "addon_copy_global_transform_fix_cam_use_loc", text
="Location")
914 props_box
.prop(scene
, "addon_copy_global_transform_fix_cam_use_rot", text
="Rotation")
915 props_box
.prop(scene
, "addon_copy_global_transform_fix_cam_use_scale", text
="Scale")
917 row
= layout
.row(align
=True)
918 props
= row
.operator("object.fix_to_camera")
919 props
.use_loc
= scene
.addon_copy_global_transform_fix_cam_use_loc
920 props
.use_rot
= scene
.addon_copy_global_transform_fix_cam_use_rot
921 props
.use_scale
= scene
.addon_copy_global_transform_fix_cam_use_scale
922 row
.operator("object.delete_fix_to_camera_keys", text
="", icon
='TRASH')
925 class VIEW3D_PT_copy_global_transform_mirror(PanelMixin
, Panel
):
926 bl_label
= "Mirror Options"
927 bl_parent_id
= "VIEW3D_PT_copy_global_transform"
929 def draw(self
, context
: Context
) -> None:
931 scene
= context
.scene
932 layout
.prop(scene
, 'addon_copy_global_transform_mirror_ob', text
="Object")
934 mirror_ob
= scene
.addon_copy_global_transform_mirror_ob
935 if mirror_ob
is None:
936 # No explicit mirror object means "the current armature", so then the bone name should be editable.
937 if context
.object and context
.object.type == 'ARMATURE':
938 self
._bone
_search
(layout
, scene
, context
.object)
940 self
._bone
_entry
(layout
, scene
)
941 elif mirror_ob
.type == 'ARMATURE':
942 self
._bone
_search
(layout
, scene
, mirror_ob
)
944 def _bone_search(self
, layout
: UILayout
, scene
: bpy
.types
.Scene
, armature_ob
: bpy
.types
.Object
) -> None:
945 """Search within the bones of the given armature."""
946 assert armature_ob
and armature_ob
.type == 'ARMATURE'
950 "addon_copy_global_transform_mirror_bone",
952 "edit_bones" if armature_ob
.mode
== 'EDIT' else "bones",
956 def _bone_entry(self
, layout
: UILayout
, scene
: bpy
.types
.Scene
) -> None:
957 """Allow manual entry of a bone name."""
958 layout
.prop(scene
, "addon_copy_global_transform_mirror_bone", text
="Bone")
961 class VIEW3D_PT_copy_global_transform_relative(PanelMixin
, Panel
):
962 bl_label
= "Relative"
963 bl_parent_id
= "VIEW3D_PT_copy_global_transform"
965 def draw(self
, context
: Context
) -> None:
967 scene
= context
.scene
969 # Copy/Paste relative to some object:
970 copy_paste_sub
= layout
.column(align
=False)
971 has_relative_ob
= bool(_get_relative_ob(context
))
972 copy_paste_sub
.label(text
="Work Relative to some Object")
973 copy_paste_sub
.prop(scene
, 'addon_copy_global_transform_relative_ob', text
="Object")
974 if not scene
.addon_copy_global_transform_relative_ob
:
975 copy_paste_sub
.label(text
="Using Active Scene Camera")
977 button_sub
= copy_paste_sub
.row(align
=True)
978 button_sub
.enabled
= has_relative_ob
979 button_sub
.operator("object.copy_relative_transform", text
="Copy", icon
='COPYDOWN')
981 paste_props
= button_sub
.operator("object.paste_transform", text
="Paste", icon
='PASTEDOWN')
982 paste_props
.method
= 'CURRENT'
983 paste_props
.use_mirror
= False
984 paste_props
.use_relative
= True
986 # It is unknown whether this combination of options is in any way
987 # sensible or usable, and of so, in which order the mirroring and
988 # relative'ing-to should happen. That's why, for now, it's disabled.
990 # paste_props = paste_row.operator("object.paste_transform", text="Mirrored", icon='PASTEFLIPDOWN')
991 # paste_props.method = 'CURRENT'
992 # paste_props.use_mirror = True
993 # paste_props.use_relative = True
996 # Messagebus subscription to monitor changes & refresh panels.
997 _msgbus_owner
= object()
1000 def _refresh_3d_panels():
1001 refresh_area_types
= {'VIEW_3D'}
1002 for win
in bpy
.context
.window_manager
.windows
:
1003 for area
in win
.screen
.areas
:
1004 if area
.type not in refresh_area_types
:
1010 OBJECT_OT_copy_global_transform
,
1011 OBJECT_OT_copy_relative_transform
,
1012 OBJECT_OT_paste_transform
,
1013 OBJECT_OT_fix_to_camera
,
1014 OBJECT_OT_delete_fix_to_camera_keys
,
1015 VIEW3D_PT_copy_global_transform
,
1016 VIEW3D_PT_copy_global_transform_mirror
,
1017 VIEW3D_PT_copy_global_transform_fix_to_camera
,
1018 VIEW3D_PT_copy_global_transform_relative
,
1020 _register
, _unregister
= bpy
.utils
.register_classes_factory(classes
)
1023 def _register_message_bus() -> None:
1024 bpy
.msgbus
.subscribe_rna(
1025 key
=(bpy
.types
.ToolSettings
, "use_keyframe_insert_auto"),
1026 owner
=_msgbus_owner
,
1028 notify
=_refresh_3d_panels
,
1029 options
={'PERSISTENT'},
1033 def _unregister_message_bus() -> None:
1034 bpy
.msgbus
.clear_by_owner(_msgbus_owner
)
1037 @bpy.app
.handlers
.persistent
# type: ignore
1038 def _on_blendfile_load_post(none
: Any
, other_none
: Any
) -> None:
1039 # The parameters are required, but both are None.
1040 _register_message_bus()
1045 bpy
.app
.handlers
.load_post
.append(_on_blendfile_load_post
)
1047 # The mirror object & bone name are stored on the scene, and not on the
1048 # operator. This makes it possible to set up the operator for use in a
1049 # certain scene, while keeping hotkey assignments working as usual.
1051 # The goal is to allow hotkeys for "copy", "paste", and "paste mirrored",
1052 # while keeping the other choices in a more global place.
1053 bpy
.types
.Scene
.addon_copy_global_transform_mirror_ob
= bpy
.props
.PointerProperty(
1054 type=bpy
.types
.Object
,
1055 name
="Mirror Object",
1056 description
="Object to mirror over. Leave empty and name a bone to always mirror "
1057 "over that bone of the active armature",
1059 bpy
.types
.Scene
.addon_copy_global_transform_mirror_bone
= bpy
.props
.StringProperty(
1061 description
="Bone to use for the mirroring",
1063 bpy
.types
.Scene
.addon_copy_global_transform_relative_ob
= bpy
.props
.PointerProperty(
1064 type=bpy
.types
.Object
,
1065 name
="Relative Object",
1066 description
="Object to which matrices are made relative",
1069 bpy
.types
.Scene
.addon_copy_global_transform_fix_cam_use_loc
= bpy
.props
.BoolProperty(
1070 name
="Fix Camera: Use Location",
1071 description
="Create Location keys when fixing to the scene camera",
1073 options
=set(), # Remove ANIMATABLE default option.
1075 bpy
.types
.Scene
.addon_copy_global_transform_fix_cam_use_rot
= bpy
.props
.BoolProperty(
1076 name
="Fix Camera: Use Rotation",
1077 description
="Create Rotation keys when fixing to the scene camera",
1079 options
=set(), # Remove ANIMATABLE default option.
1081 bpy
.types
.Scene
.addon_copy_global_transform_fix_cam_use_scale
= bpy
.props
.BoolProperty(
1082 name
="Fix Camera: Use Scale",
1083 description
="Create Scale keys when fixing to the scene camera",
1085 options
=set(), # Remove ANIMATABLE default option.
1091 _unregister_message_bus()
1092 bpy
.app
.handlers
.load_post
.remove(_on_blendfile_load_post
)
1094 del bpy
.types
.Scene
.addon_copy_global_transform_mirror_ob
1095 del bpy
.types
.Scene
.addon_copy_global_transform_mirror_bone
1096 del bpy
.types
.Scene
.addon_copy_global_transform_relative_ob
1098 del bpy
.types
.Scene
.addon_copy_global_transform_fix_cam_use_loc
1099 del bpy
.types
.Scene
.addon_copy_global_transform_fix_cam_use_rot
1100 del bpy
.types
.Scene
.addon_copy_global_transform_fix_cam_use_scale