1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
20 Pose Library - operators.
23 from pathlib
import Path
24 from typing
import Optional
, Set
26 _need_reload
= "functions" in locals()
27 from . import asset_browser
, functions
, pose_creation
, pose_usage
32 asset_browser
= importlib
.reload(asset_browser
)
33 functions
= importlib
.reload(functions
)
34 pose_creation
= importlib
.reload(pose_creation
)
35 pose_usage
= importlib
.reload(pose_usage
)
39 from bpy
.props
import BoolProperty
, StringProperty
40 from bpy
.types
import (
50 class PoseAssetCreator
:
52 def poll(cls
, context
: Context
) -> bool:
54 # There must be an object.
56 # It must be in pose mode with selected bones.
57 and context
.object.mode
== "POSE"
58 and context
.object.pose
59 and context
.selected_pose_bones_from_active_object
63 class LocalPoseAssetUser
:
65 def poll(cls
, context
: Context
) -> bool:
67 isinstance(getattr(context
, "id", None), Action
)
69 and context
.object.mode
== "POSE" # This condition may not be desired.
73 class POSELIB_OT_create_pose_asset(PoseAssetCreator
, Operator
):
74 bl_idname
= "poselib.create_pose_asset"
75 bl_label
= "Create Pose Asset"
77 "Create a new Action that contains the pose of the selected bones, and mark it as Asset"
79 bl_options
= {"REGISTER", "UNDO"}
81 pose_name
: StringProperty(name
="Pose Name") # type: ignore
82 activate_new_action
: BoolProperty(name
="Activate New Action", default
=True) # type: ignore
84 def execute(self
, context
: Context
) -> Set
[str]:
85 pose_name
= self
.pose_name
or context
.object.name
86 asset
= pose_creation
.create_pose_asset_from_context(context
, pose_name
)
88 self
.report({"WARNING"}, "No keyframes were found for this pose")
91 if self
.activate_new_action
:
92 self
._set
_active
_action
(context
, asset
)
93 self
._activate
_asset
_in
_browser
(context
, asset
)
96 def _set_active_action(self
, context
: Context
, asset
: Action
) -> None:
97 self
._prevent
_action
_loss
(context
.object)
99 anim_data
= context
.object.animation_data_create()
100 context
.window_manager
.poselib_previous_action
= anim_data
.action
101 anim_data
.action
= asset
103 def _activate_asset_in_browser(self
, context
: Context
, asset
: Action
) -> None:
104 """Activate the new asset in the appropriate Asset Browser.
106 This makes it possible to immediately check & edit the created pose asset.
109 asset_browse_area
: Optional
[bpy
.types
.Area
] = asset_browser
.area_for_category(
110 context
.screen
, "ANIMATION"
112 if not asset_browse_area
:
115 # After creating an asset, the window manager has to process the
116 # notifiers before editors should be manipulated.
117 pose_creation
.assign_tags_from_asset_browser(asset
, asset_browse_area
)
119 # Pass deferred=True, because we just created a new asset that isn't
120 # known to the Asset Browser space yet. That requires the processing of
121 # notifiers, which will only happen after this code has finished
123 asset_browser
.activate_asset(asset
, asset_browse_area
, deferred
=True)
125 def _prevent_action_loss(self
, object: Object
) -> None:
126 """Mark the action with Fake User if necessary.
128 This is to prevent action loss when we reduce its reference counter by one.
131 if not object.animation_data
:
134 action
= object.animation_data
.action
138 if action
.use_fake_user
or action
.users
> 1:
139 # Removing one user won't GC it.
142 action
.use_fake_user
= True
143 self
.report({'WARNING'}, "Action %s marked Fake User to prevent loss" % action
.name
)
146 class POSELIB_OT_restore_previous_action(Operator
):
147 bl_idname
= "poselib.restore_previous_action"
148 bl_label
= "Restore Previous Action"
149 bl_description
= "Switch back to the previous Action, after creating a pose asset"
150 bl_options
= {"REGISTER", "UNDO"}
153 def poll(cls
, context
: Context
) -> bool:
155 context
.window_manager
.poselib_previous_action
157 and context
.object.animation_data
158 and context
.object.animation_data
.action
159 and context
.object.animation_data
.action
.asset_data
is not None
162 def execute(self
, context
: Context
) -> Set
[str]:
163 # This is the Action that was just created with "Create Pose Asset".
164 # It has to be re-applied after switching to the previous action,
165 # to ensure the character keeps the same pose.
166 self
.pose_action
= context
.object.animation_data
.action
168 prev_action
= context
.window_manager
.poselib_previous_action
169 context
.object.animation_data
.action
= prev_action
170 context
.window_manager
.poselib_previous_action
= None
172 # Wait a bit for the action assignment to be handled, before applying the pose.
173 wm
= context
.window_manager
174 self
._timer
= wm
.event_timer_add(0.001, window
=context
.window
)
175 wm
.modal_handler_add(self
)
177 return {'RUNNING_MODAL'}
179 def modal(self
, context
, event
):
180 if event
.type != 'TIMER':
181 return {'RUNNING_MODAL'}
183 wm
= context
.window_manager
184 wm
.event_timer_remove(self
._timer
)
186 context
.object.pose
.apply_pose_from_action(self
.pose_action
)
190 class ASSET_OT_assign_action(LocalPoseAssetUser
, Operator
):
191 bl_idname
= "asset.assign_action"
192 bl_label
= "Assign Action"
193 bl_description
= "Set this pose Action as active Action on the active Object"
194 bl_options
= {"REGISTER", "UNDO"}
196 def execute(self
, context
: Context
) -> Set
[str]:
197 context
.object.animation_data_create().action
= context
.id
201 class POSELIB_OT_copy_as_asset(PoseAssetCreator
, Operator
):
202 bl_idname
= "poselib.copy_as_asset"
203 bl_label
= "Copy Pose As Asset"
204 bl_description
= "Create a new pose asset on the clipboard, to be pasted into an Asset Browser"
205 bl_options
= {"REGISTER"}
207 CLIPBOARD_ASSET_MARKER
= "ASSET-BLEND="
209 def execute(self
, context
: Context
) -> Set
[str]:
210 asset
= pose_creation
.create_pose_asset_from_context(
211 context
, new_asset_name
=context
.object.name
214 self
.report({"WARNING"}, "No animation data found to create asset from")
217 filepath
= self
.save_datablock(asset
)
219 functions
.asset_clear(context
, asset
)
221 self
.report({"ERROR"}, "Unexpected non-null user count for the asset")
224 bpy
.data
.actions
.remove(asset
)
226 context
.window_manager
.clipboard
= "%s%s" % (
227 self
.CLIPBOARD_ASSET_MARKER
,
231 asset_browser
.tag_redraw(context
.screen
)
232 self
.report({"INFO"}, "Pose Asset copied, use Paste As New Asset in any Asset Browser to paste")
235 def save_datablock(self
, action
: Action
) -> Path
:
236 tempdir
= Path(bpy
.app
.tempdir
)
237 filepath
= tempdir
/ "copied_asset.blend"
238 bpy
.data
.libraries
.write(
243 compress
=True, # Single-datablock blend file, likely little need to diff.
248 class POSELIB_OT_paste_asset(Operator
):
249 bl_idname
= "poselib.paste_asset"
250 bl_label
= "Paste As New Asset"
251 bl_description
= "Paste the Asset that was previously copied using Copy As Asset"
252 bl_options
= {"REGISTER", "UNDO"}
255 def poll(cls
, context
: Context
) -> bool:
256 clipboard
: str = context
.window_manager
.clipboard
259 marker
= POSELIB_OT_copy_as_asset
.CLIPBOARD_ASSET_MARKER
260 return clipboard
.startswith(marker
)
262 def execute(self
, context
: Context
) -> Set
[str]:
263 clipboard
= context
.window_manager
.clipboard
264 marker_len
= len(POSELIB_OT_copy_as_asset
.CLIPBOARD_ASSET_MARKER
)
265 filepath
= Path(clipboard
[marker_len
:])
267 assets
= functions
.load_assets_from(filepath
)
269 self
.report({"ERROR"}, "Did not find any assets on clipboard")
272 self
.report({"INFO"}, "Pasted %d assets" % len(assets
))
274 bpy
.ops
.file.refresh()
275 asset_browser_area
= asset_browser
.area_from_context(context
, 'ANIMATIONS')
276 if asset_browser_area
:
277 asset_browser
.activate_asset(assets
[0], asset_browser_area
, deferred
=True)
284 def poll(cls
, context
: Context
) -> bool:
287 and context
.object.mode
== "POSE" # This condition may not be desired.
288 and context
.asset_library_ref
289 and context
.asset_file_handle
292 return context
.asset_file_handle
.id_type
== 'ACTION'
294 def execute(self
, context
: Context
) -> Set
[str]:
295 asset
: FileSelectEntry
= context
.asset_file_handle
297 return self
.use_pose(context
, asset
.local_id
)
298 return self
._load
_and
_use
_pose
(context
)
300 def use_pose(self
, context
: Context
, asset
: bpy
.types
.ID
) -> Set
[str]:
301 # Implement in subclass.
304 def _load_and_use_pose(self
, context
: Context
) -> Set
[str]:
305 asset_library_ref
= context
.asset_library_ref
306 asset
= context
.asset_file_handle
307 asset_lib_path
= bpy
.types
.AssetHandle
.get_full_library_path(asset
, asset_library_ref
)
309 if not asset_lib_path
:
310 self
.report( # type: ignore
312 # TODO: Add some way to get the library name from the library reference (just asset_library_ref.name?).
313 f
"Selected asset {asset.name} could not be located inside the asset library",
316 if asset
.id_type
!= 'ACTION':
317 self
.report( # type: ignore
319 f
"Selected asset {asset.name} is not an Action",
323 with bpy
.types
.BlendData
.temp_data() as temp_data
:
324 with temp_data
.libraries
.load(asset_lib_path
) as (data_from
, data_to
):
325 data_to
.actions
= [asset
.name
]
327 action
: Action
= data_to
.actions
[0]
328 return self
.use_pose(context
, action
)
331 class POSELIB_OT_pose_asset_select_bones(PoseAssetUser
, Operator
):
332 bl_idname
= "poselib.pose_asset_select_bones"
333 bl_label
= "Select Bones"
334 bl_description
= "Select those bones that are used in this pose"
335 bl_options
= {"REGISTER", "UNDO"}
337 select
: BoolProperty(name
="Select", default
=True) # type: ignore
338 flipped
: BoolProperty(name
="Flipped", default
=False) # type: ignore
340 def use_pose(self
, context
: Context
, pose_asset
: Action
) -> Set
[str]:
341 arm_object
: Object
= context
.object
342 pose_usage
.select_bones(arm_object
, pose_asset
, select
=self
.select
, flipped
=self
.flipped
)
343 verb
= "Selected" if self
.select
else "Deselected"
344 self
.report({"INFO"}, f
"{verb} bones from {pose_asset.name}")
349 cls
, _context
: Context
, properties
: 'POSELIB_OT_pose_asset_select_bones'
351 if properties
.select
:
352 return cls
.bl_description
353 return cls
.bl_description
.replace("Select", "Deselect")
356 class POSELIB_OT_blend_pose_asset_for_keymap(Operator
):
357 bl_idname
= "poselib.blend_pose_asset_for_keymap"
358 bl_options
= {"REGISTER", "UNDO"}
360 _rna
= bpy
.ops
.poselib
.blend_pose_asset
.get_rna_type()
362 bl_description
= _rna
.description
366 def poll(cls
, context
: Context
) -> bool:
367 return bpy
.ops
.poselib
.blend_pose_asset
.poll(context
.copy())
369 def execute(self
, context
: Context
) -> Set
[str]:
370 flipped
= context
.window_manager
.poselib_flipped
371 return bpy
.ops
.poselib
.blend_pose_asset(context
.copy(), 'EXEC_DEFAULT', flipped
=flipped
)
373 def invoke(self
, context
: Context
, event
: Event
) -> Set
[str]:
374 flipped
= context
.window_manager
.poselib_flipped
375 return bpy
.ops
.poselib
.blend_pose_asset(context
.copy(), 'INVOKE_DEFAULT', flipped
=flipped
)
378 class POSELIB_OT_apply_pose_asset_for_keymap(Operator
):
379 bl_idname
= "poselib.apply_pose_asset_for_keymap"
380 bl_options
= {"REGISTER", "UNDO"}
382 _rna
= bpy
.ops
.poselib
.apply_pose_asset
.get_rna_type()
384 bl_description
= _rna
.description
388 def poll(cls
, context
: Context
) -> bool:
389 return bpy
.ops
.poselib
.apply_pose_asset
.poll(context
.copy())
391 def execute(self
, context
: Context
) -> Set
[str]:
392 flipped
= context
.window_manager
.poselib_flipped
393 return bpy
.ops
.poselib
.apply_pose_asset(context
.copy(), 'EXEC_DEFAULT', flipped
=flipped
)
396 class POSELIB_OT_convert_old_poselib(Operator
):
397 bl_idname
= "poselib.convert_old_poselib"
398 bl_label
= "Convert Old-Style Pose Library"
399 bl_description
= "Create a pose asset for each pose marker in the current action"
400 bl_options
= {"REGISTER", "UNDO"}
403 def poll(cls
, context
: Context
) -> bool:
404 action
= context
.object and context
.object.animation_data
and context
.object.animation_data
.action
406 cls
.poll_message_set("Active object has no Action")
408 if not action
.pose_markers
:
409 cls
.poll_message_set("Action %r is not a old-style pose library" % action
.name
)
413 def execute(self
, context
: Context
) -> Set
[str]:
414 from . import conversion
416 old_poselib
= context
.object.animation_data
.action
417 new_actions
= conversion
.convert_old_poselib(old_poselib
)
420 self
.report({'ERROR'}, "Unable to convert to pose assets")
423 self
.report({'INFO'}, "Converted %d poses to pose assets" % len(new_actions
))
428 ASSET_OT_assign_action
,
429 POSELIB_OT_apply_pose_asset_for_keymap
,
430 POSELIB_OT_blend_pose_asset_for_keymap
,
431 POSELIB_OT_convert_old_poselib
,
432 POSELIB_OT_copy_as_asset
,
433 POSELIB_OT_create_pose_asset
,
434 POSELIB_OT_paste_asset
,
435 POSELIB_OT_pose_asset_select_bones
,
436 POSELIB_OT_restore_previous_action
,
439 register
, unregister
= bpy
.utils
.register_classes_factory(classes
)