Export_3ds: Added distance cue chunk export
[blender-addons.git] / copy_global_transform.py
blob0f36d064940912135c3efd08fb9ab313205137d8
1 # SPDX-FileCopyrightText: 2021-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """
6 Copy Global Transform
8 Simple add-on for copying world-space transforms.
10 It's called "global" to avoid confusion with the Blender World data-block.
11 """
13 bl_info = {
14 "name": "Copy Global Transform",
15 "author": "Sybren A. Stüvel",
16 "version": (3, 0),
17 "blender": (4, 2, 0),
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",
25 import ast
26 import abc
27 import contextlib
28 from typing import Iterable, Optional, Union, Any, TypeAlias, Iterator
30 import bpy
31 from bpy.types import Context, Object, Operator, Panel, PoseBone, UILayout, FCurve, Camera, FModifierStepped
32 from mathutils import Matrix
35 _axis_enum_items = [
36 ("x", "X", "", 1),
37 ("y", "Y", "", 2),
38 ("z", "Z", "", 3),
42 class AutoKeying:
43 """Auto-keying support.
45 Based on Rigify code by Alexander Gavrilov.
46 """
48 # Use AutoKeying.keytype() or Authkeying.options() context to change those.
49 _keytype = 'KEYFRAME'
50 _force_autokey = False # Allow use without the user activating auto-keying.
51 _use_loc = True
52 _use_rot = True
53 _use_scale = True
55 @classmethod
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
60 try:
61 cls._keytype = the_keytype
62 yield
63 finally:
64 cls._keytype = default_keytype
66 @classmethod
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
75 try:
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
81 yield
82 finally:
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
89 @classmethod
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
95 options = set()
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')
103 return options
105 @classmethod
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):
112 return None
114 if ts.use_keyframe_insert_keyingset:
115 # No support for keying sets (yet).
116 return None
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')
125 return options
127 @staticmethod
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]
132 else:
133 return [all(bone.lock_rotation)] * 4
135 @classmethod
136 def keyframe_channels(
137 cls,
138 target: Union[Object, PoseBone],
139 options: set[str],
140 data_path: str,
141 group: str,
142 locks: Iterable[bool],
143 ) -> None:
144 if all(locks):
145 return
147 if not any(locks):
148 target.keyframe_insert(data_path, group=group, options=options, keytype=cls._keytype)
149 return
151 for index, lock in enumerate(locks):
152 if lock:
153 continue
154 target.keyframe_insert(data_path, index=index, group=group, options=options, keytype=cls._keytype)
156 @classmethod
157 def key_transformation(
158 cls,
159 target: Union[Object, PoseBone],
160 options: set[str],
161 ) -> None:
162 """Keyframe transformation properties, avoiding keying locked channels."""
164 is_bone = isinstance(target, PoseBone)
165 if is_bone:
166 group = target.name
167 else:
168 group = "Object Transforms"
170 def keyframe(data_path: str, locks: Iterable[bool]) -> None:
171 try:
172 cls.keyframe_channels(target, options, data_path, group, locks)
173 except RuntimeError:
174 # These are expected when "Insert Available" is turned on, and
175 # these curves are not available.
176 pass
178 if cls._use_loc and not (is_bone and target.bone.use_connect):
179 keyframe("location", target.lock_location)
181 if cls._use_rot:
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))
186 else:
187 keyframe("rotation_euler", target.lock_rotation)
189 if cls._use_scale:
190 keyframe("scale", target.lock_scale)
192 @classmethod
193 def autokey_transformation(cls, context: Context, target: Union[Object, PoseBone]) -> None:
194 """Auto-key transformation properties."""
196 options = cls.autokeying_options(context)
197 if options is None:
198 return
199 cls.key_transformation(target, options)
202 def get_matrix(context: Context) -> Matrix:
203 bone = context.active_pose_bone
204 if bone:
205 # Convert matrix to world space
206 arm = context.active_object
207 mat = arm.matrix_world @ bone.matrix
208 else:
209 mat = context.active_object.matrix_world
211 return mat
214 def set_matrix(context: Context, mat: Matrix) -> None:
215 bone = context.active_pose_bone
216 if 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)
221 else:
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
232 if 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
261 if action is None:
262 return []
264 keyframes = set()
265 for fcurve in action.fcurves:
266 if not fcurve.data_path.startswith(rna_path_prefix):
267 continue
269 for kp in fcurve.keyframe_points:
270 if not kp.select_control_point:
271 continue
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"
285 bl_description = (
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'}
291 @classmethod
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)
298 return {'FINISHED'}
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'}
318 @classmethod
319 def poll(cls, context: Context) -> bool:
320 rel_ob = _get_relative_ob(context)
321 if not rel_ob:
322 return False
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)
329 return {'FINISHED'}
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"
339 bl_description = (
340 "Pastes the matrix from the clipboard to the currently active pose bone or object. Uses world-space matrices"
342 bl_options = {'REGISTER', 'UNDO'}
344 _method_items = [
346 'CURRENT',
347 "Current Transform",
348 "Paste onto the current values only, only manipulating the animation data if auto-keying is enabled",
351 'EXISTING_KEYS',
352 "Selected Keys",
353 "Paste onto frames that have a selected key, potentially creating new keys on those frames",
356 'BAKE',
357 "Bake on Key Range",
358 "Paste onto all frames between the first and last selected key, creating new keyframes if necessary",
361 method: bpy.props.EnumProperty( # type: ignore
362 items=_method_items,
363 name="Paste Method",
364 description="Update the current transform, selected keyframes, or even create new keys",
366 bake_step: bpy.props.IntProperty( # type: ignore
367 name="Frame Step",
368 description="Only used for baking. Step=1 creates a key on every frame, step=2 bakes on 2s, etc",
369 min=1,
370 soft_min=1,
371 soft_max=5,
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",
377 default=False,
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",
384 default='x',
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",
390 default='z',
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)",
396 default=False,
399 @classmethod
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")
403 return False
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")
408 return False
409 return True
411 @staticmethod
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()
419 if len(lines) != 4:
420 return None
422 floats = tuple(tuple(float(item) for item in line.split()) for line in lines)
423 return Matrix(floats)
425 @staticmethod
426 def parse_repr_m4(value: str) -> Optional[Matrix]:
427 """Four lines of (a, b, c, d) floats."""
429 lines = value.strip().splitlines()
430 if len(lines) != 4:
431 return None
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])
442 else:
443 mat = self.parse_print_m4(clipboard)
445 if mat is None:
446 self.report({'ERROR'}, "Clipboard does not contain a valid matrix")
447 return {'CANCELLED'}
449 try:
450 mat = self._preprocess_matrix(context, mat)
451 except UnableToMirrorError:
452 self.report({'ERROR'}, "Unable to mirror, no mirror object/bone configured")
453 return {'CANCELLED'}
455 applicator = {
456 'CURRENT': self._paste_current,
457 'EXISTING_KEYS': self._paste_existing_keys,
458 'BAKE': self._paste_bake,
459 }[self.method]
460 return applicator(context, mat)
462 def _preprocess_matrix(self, context: Context, matrix: Matrix) -> Matrix:
463 matrix = self._relative_to_world(context, matrix)
465 if self.use_mirror:
466 matrix = self._mirror_matrix(context, matrix)
467 return matrix
469 def _relative_to_world(self, context: Context, matrix: Matrix) -> Matrix:
470 if not self.use_relative:
471 return matrix
473 rel_ob = _get_relative_ob(context)
474 if not rel_ob:
475 return matrix
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':
487 mirror_ob = ctx_ob
489 if not mirror_ob:
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:
519 case 'x':
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.
523 case 'y':
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.
527 case 'z':
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
539 @staticmethod
540 def _paste_current(context: Context, matrix: Matrix) -> set[str]:
541 set_matrix(context, matrix)
542 return {'FINISHED'}
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")
547 return {'CANCELLED'}
549 frame_numbers = _selected_keyframes(context)
550 if not frame_numbers:
551 self.report({'WARNING'}, "No selected frames found")
552 return {'CANCELLED'}
554 self._paste_on_frames(context, frame_numbers, matrix)
555 return {'FINISHED'}
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")
560 return {'CANCELLED'}
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)
569 return {'FINISHED'}
571 def _determine_bake_range(self, context: Context) -> tuple[float, float]:
572 frame_numbers = _selected_keyframes(context)
573 if frame_numbers:
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
586 try:
587 for frame in frame_numbers:
588 context.scene.frame_set(int(frame), subframe=frame % 1.0)
589 set_matrix(context, matrix)
590 finally:
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
605 @abc.abstractmethod
606 def matrix_world(self) -> Matrix:
607 pass
609 @abc.abstractmethod
610 def set_matrix_world(self, context: Context, matrix: Matrix) -> None:
611 pass
613 @abc.abstractmethod
614 def _my_fcurves(self) -> Iterable[bpy.types.FCurve]:
615 pass
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:
624 frame = kp.co.x
625 if kp.type == 'GENERATED' and frame in keyinfo:
626 # Don't bother overwriting other key types.
627 continue
628 keyinfo[frame] = kp.type
630 self._key_info_cache = keyinfo
631 return 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():
637 to_remove = [
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):
646 object: Object
648 def __init__(self, object: Object) -> None:
649 super().__init__()
650 self.object = object
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()
664 if not action:
665 return
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):
674 arm_object: Object
675 pose_bone: PoseBone
677 def __init__(self, pose_bone: PoseBone) -> None:
678 super().__init__()
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
684 return mat
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()
697 if not action:
698 return
700 rna_prefix = f"{self.pose_bone.path_from_id()}."
701 for fcurve in action.fcurves:
702 if fcurve.data_path.startswith(rna_prefix):
703 yield fcurve
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:
716 @classmethod
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()
727 @classmethod
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")
731 return False
732 if context.mode not in {'POSE', 'OBJECT'}:
733 cls.poll_message_set("Switch to Pose or Object mode")
734 return False
735 if not context.scene.camera:
736 cls.poll_message_set("The Scene needs a camera")
737 return False
738 return True
740 def execute(self, context: Context) -> set[str]:
741 match context.mode:
742 case 'OBJECT':
743 transformables = self._transformable_objects(context)
744 case 'POSE':
745 transformables = self._transformable_pbones(context)
746 case mode:
747 self.report({'ERROR'}, 'Unsupported mode: %r' % mode)
748 return {'CANCELLED'}
750 restore_frame = context.scene.frame_current
751 try:
752 self._execute(context, transformables)
753 finally:
754 context.scene.frame_set(restore_frame)
755 return {'FINISHED'}
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
771 name="Location",
772 description="Create Location keys when fixing to the scene camera",
773 default=True,
775 use_rot: bpy.props.BoolProperty( # type: ignore
776 name="Rotation",
777 description="Create Rotation keys when fixing to the scene camera",
778 default=True,
780 use_scale: bpy.props.BoolProperty( # type: ignore
781 name="Scale",
782 description="Create Scale keys when fixing to the scene camera",
783 default=True,
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
802 else:
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,
811 force_autokey=True,
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()
832 continue
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
849 else:
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)
857 class PanelMixin:
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:
867 layout = self.layout
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
886 if not has_autokey:
887 wants_autokey_col.label(text="These require auto-key:")
889 paste_col = wants_autokey_col.column(align=True)
890 paste_col.operator(
891 "object.paste_transform",
892 text="Paste to Selected Keys",
893 icon='PASTEDOWN',
894 ).method = 'EXISTING_KEYS'
895 paste_col.operator(
896 "object.paste_transform",
897 text="Paste and Bake",
898 icon='PASTEDOWN',
899 ).method = '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:
907 layout = self.layout
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:
930 layout = self.layout
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)
939 else:
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'
948 layout.prop_search(
949 scene,
950 "addon_copy_global_transform_mirror_bone",
951 armature_ob.data,
952 "edit_bones" if armature_ob.mode == 'EDIT' else "bones",
953 text="Bone",
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:
966 layout = self.layout
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:
1005 continue
1006 area.tag_redraw()
1009 classes = (
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,
1027 args=(),
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()
1043 def register():
1044 _register()
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(
1060 name="Mirror Bone",
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",
1072 default=True,
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",
1078 default=True,
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",
1084 default=True,
1085 options=set(), # Remove ANIMATABLE default option.
1089 def unregister():
1090 _unregister()
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