-
Notifications
You must be signed in to change notification settings - Fork 60
/
action_to_scene_range.py
179 lines (129 loc) · 5.33 KB
/
action_to_scene_range.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
# SPDX-FileCopyrightText: 2024 Blender Foundation
#
# SPDX-License-Identifier: GPL-2.0-or-later
"""
Action to Scene Range
When an Action is assigned to an object, and that Action has 'Manual Frame
Range' checked, update the Scene frame range to match its length.
If the Scene has the preview range active, update the preview range instead.
"""
bl_info = {
"name": "Action to Scene Range",
"author": "Sybren A. Stüvel",
"version": (1, 5),
"blender": (4, 1, 0),
"location": "Action Editor",
"category": "Animation",
"support": 'COMMUNITY',
"doc_url": "",
"tracker_url": "",
}
from typing import Any
import bpy
class ACTION_OT_to_scene_range(bpy.types.Operator):
bl_idname = "action.to_scene_range"
bl_label = "Action to Scene Range"
bl_description = "Set the Scene (preview) range to the Action's frame range"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: bpy.types.Context) -> bool:
if not _is_dopesheet_editor(context):
return False
action = cls._get_action(context)
scene = context.scene
if not action:
cls.poll_message_set("I don't know which Action you want me to look at")
return False
if not scene:
cls.poll_message_set("I don't know which Scene you want me to update")
return False
if _get_range_action(action) == _get_range_scene(scene):
cls.poll_message_set("Scene already at Action range")
return False
return True
def execute(self, context: bpy.types.Context) -> set[str]:
action = self._get_action(context)
scene = context.scene
start, end = _action_to_scene_range(action, scene)
self.report({'INFO'}, f"Changed scene range to {start}-{end}")
return {'FINISHED'}
@staticmethod
def _get_action(context: bpy.types.Context) -> bpy.types.Action | None:
action = getattr(context.space_data, 'action', None)
if action:
return action
return getattr(context, 'active_action', None)
classes = (ACTION_OT_to_scene_range,)
_register, _unregister = bpy.utils.register_classes_factory(classes)
def _get_range_action(action: bpy.types.Action) -> tuple[int, int]:
action_range = tuple(action.frame_range)
frame_start = int(action_range[0])
frame_end = int(action_range[1])
return frame_start, frame_end
def _get_range_scene(scene: bpy.types.Scene) -> tuple[int, int]:
if scene.use_preview_range:
return scene.frame_preview_start, scene.frame_preview_end
return scene.frame_start, scene.frame_end
def _action_to_scene_range(action: bpy.types.Action, scene: bpy.types.Scene) -> tuple[float, float]:
frame_start, frame_end = _get_range_action(action)
if scene.use_preview_range:
scene.frame_preview_start = frame_start
scene.frame_preview_end = frame_end
else:
scene.frame_start = frame_start
scene.frame_end = frame_end
return (frame_start, frame_end)
def _on_action_change() -> None:
ob = bpy.context.object
if not ob or not ob.animation_data:
return
action = ob.animation_data.action
if not action:
return
if not action.use_frame_range:
# Only do automatic syncing on Actions that have 'Manual Frame Range' checked.
return
frame_start, frame_end = _action_to_scene_range(action, bpy.context.scene)
print(f"{ob.name} changed to action {action.name} with range {frame_start}-{frame_end}")
### Messagebus subscription to monitor changes & refresh panels.
_msgbus_owner = object()
def _register_message_bus() -> None:
bpy.msgbus.subscribe_rna(
key=(bpy.types.SpaceDopeSheetEditor, "action"),
owner=_msgbus_owner,
args=(),
notify=_on_action_change,
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 _draw_header_button(self, context: bpy.types.Context) -> None:
if not _is_action_editor(context):
return
self.layout.operator("action.to_scene_range", text="", icon='PREVIEW_RANGE')
def _draw_panel_button(self, context: bpy.types.Context) -> None:
range_name = {
False: 'Scene',
True: 'Preview',
}[context.scene.use_preview_range]
self.layout.operator("action.to_scene_range", text=f"Action → {range_name} Range", icon='PREVIEW_RANGE')
def _is_dopesheet_editor(context: bpy.types.Context) -> bool:
return context.space_data and context.space_data.type == 'DOPESHEET_EDITOR'
def _is_action_editor(context: bpy.types.Context) -> bool:
return _is_dopesheet_editor(context) and context.space_data.mode == 'ACTION'
def register():
_register()
_register_message_bus()
bpy.app.handlers.load_post.append(_on_blendfile_load_post)
bpy.types.DOPESHEET_HT_header.append(_draw_header_button)
bpy.types.DOPESHEET_PT_action.append(_draw_panel_button)
def unregister():
_unregister()
_unregister_message_bus()
bpy.app.handlers.load_post.remove(_on_blendfile_load_post)
bpy.types.DOPESHEET_HT_header.remove(_draw_header_button)
bpy.types.DOPESHEET_PT_action.remove(_draw_panel_button)