Add Chromium-only Blender WebEngine parity work

This commit is contained in:
mes123456
2026-08-12 04:47:48 -04:00
commit 9fd26010f6
18225 changed files with 11622124 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
# SPDX-FileCopyrightText: 2023 Blender Authors
#
# SPDX-License-Identifier: GPL-2.0-or-later
set(INC
../asset
../include
../space_graph
../../asset_system
../../makesrna
# RNA_prototypes.hh
${CMAKE_BINARY_DIR}/source/blender/makesrna
)
set(INC_SYS
)
set(SRC
anim_asset_ops.cc
anim_channels_defines.cc
anim_channels_edit.cc
anim_deps.cc
anim_draw.cc
anim_filter.cc
anim_ipo_utils.cc
anim_markers.cc
anim_motion_paths.cc
anim_ops.cc
drivers.cc
fmodifier_ui.cc
keyframes_draw.cc
keyframes_edit.cc
keyframes_general.cc
keyframes_keylist.cc
keyframing.cc
keyingsets.cc
time_scrub_ui.cc
transformable.cc
anim_intern.hh
keyframes_general_intern.hh
)
set(LIB
PRIVATE bf::blenkernel
PRIVATE bf::animrig
PRIVATE bf::blenlib
PRIVATE bf::blentranslation
PRIVATE bf::depsgraph
PRIVATE bf::dna
PRIVATE bf::gpu
PRIVATE bf::intern::clog
PRIVATE bf::intern::guardedalloc
PRIVATE bf::nodes
PRIVATE bf::sequencer
PRIVATE bf::windowmanager
)
if(WITH_PYTHON)
add_definitions(-DWITH_PYTHON)
endif()
blender_add_lib(bf_editor_animation "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
# RNA_prototypes.hh
add_dependencies(bf_editor_animation bf_rna)
if(WITH_GTESTS)
set(TEST_SRC
anim_draw_test.cc
anim_filter_test.cc
keyframes_general_test.cc
keyframes_keylist_test.cc
transformable_test.cc
)
set(TEST_INC
)
set(TEST_LIB
)
blender_add_test_suite_lib(editor_animation "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
endif()

View File

@@ -0,0 +1,866 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_listbase.h"
#include "BKE_asset.hh"
#include "BKE_asset_edit.hh"
#include "BKE_context.hh"
#include "BKE_fcurve.hh"
#include "BKE_global.hh"
#include "BKE_icons.hh"
#include "BKE_lib_id.hh"
#include "BKE_preferences.h"
#include "BKE_report.hh"
#include "BKE_screen.hh"
#include "DNA_asset_types.h"
#include "WM_api.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_prototypes.hh"
#include "ED_asset.hh"
#include "ED_asset_library.hh"
#include "ED_asset_list.hh"
#include "ED_asset_mark_clear.hh"
#include "ED_asset_menu_utils.hh"
#include "ED_asset_shelf.hh"
#include "ED_fileselect.hh"
#include "ED_screen.hh"
#include "ED_undo.hh"
#include "UI_interface_icons.hh"
#include "UI_resources.hh"
#include "BLT_translation.hh"
#include "ANIM_action.hh"
#include "ANIM_action_iterators.hh"
#include "ANIM_armature.hh"
#include "ANIM_keyframing.hh"
#include "ANIM_pose.hh"
#include "ANIM_rna.hh"
#include "AS_asset_catalog.hh"
#include "AS_asset_catalog_tree.hh"
#include "AS_asset_library.hh"
#include "AS_asset_representation.hh"
#include "anim_intern.hh"
namespace blender::ed::animrig {
static const EnumPropertyItem *rna_asset_library_reference_itemf(bContext * /*C*/,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
bool *r_free)
{
const EnumPropertyItem *items = ed::asset::library_reference_to_rna_enum_itemf(
/*include_readonly=*/false,
/*include_current_file=*/true,
/*include_remote_libraries=*/false,
/*include_separate_online_essentials=*/false);
*r_free = true;
BLI_assert(items != nullptr);
return items;
}
static Vector<RNAPath> construct_pose_rna_paths(const PointerRNA &bone_pointer)
{
BLI_assert(bone_pointer.type == RNA_PoseBone);
Vector<RNAPath> paths;
paths.append({"location"});
paths.append({"scale"});
bPoseChannel *pose_bone = static_cast<bPoseChannel *>(bone_pointer.data);
switch (pose_bone->rotmode) {
case ROT_MODE_QUAT:
paths.append({"rotation_quaternion"});
break;
case ROT_MODE_AXISANGLE:
paths.append({"rotation_axis_angle"});
break;
case ROT_MODE_XYZ:
case ROT_MODE_XZY:
case ROT_MODE_YXZ:
case ROT_MODE_YZX:
case ROT_MODE_ZXY:
case ROT_MODE_ZYX:
paths.append({"rotation_euler"});
default:
break;
}
paths.extend({{"bbone_curveinx"},
{"bbone_curveoutx"},
{"bbone_curveinz"},
{"bbone_curveoutz"},
{"bbone_rollin"},
{"bbone_rollout"},
{"bbone_scalein"},
{"bbone_scaleout"},
{"bbone_easein"},
{"bbone_easeout"}});
paths.extend(blender::animrig::get_keyable_id_property_paths(bone_pointer));
return paths;
}
static blender::animrig::Action &extract_pose(Main &bmain, const Span<Object *> pose_objects)
{
/* This currently only looks at the pose and not other things that could go onto different
* slots on the same action. */
using namespace blender::animrig;
Action &action = action_add(bmain, "pose_create");
Layer &layer = action.layer_add("pose");
Strip &strip = layer.strip_add(action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(action);
const KeyframeSettings key_settings = {BEZT_KEYTYPE_KEYFRAME, HD_AUTO, BEZT_IPO_BEZ};
for (Object *pose_object : pose_objects) {
BLI_assert(pose_object->pose);
Slot &slot = action.slot_add_for_id(pose_object->id);
const bArmature *armature = id_cast<bArmature *>(pose_object->data);
Set<RNAPath> existing_paths;
if (pose_object->adt && pose_object->adt->action &&
pose_object->adt->slot_handle != Slot::unassigned)
{
Action &pose_object_action = pose_object->adt->action->wrap();
const slot_handle_t pose_object_slot = pose_object->adt->slot_handle;
foreach_fcurve_in_action_slot(
pose_object_action, pose_object_slot, [&](const FCurve &fcurve) {
RNAPath existing_path = {fcurve.rna_path, std::nullopt, fcurve.array_index};
existing_paths.add(existing_path);
});
}
for (bPoseChannel &pose_bone : pose_object->pose->chanbase) {
if (!blender::animrig::bone_is_selected(armature,
{&pose_bone, pose_bone.bone_get(*pose_object)}))
{
continue;
}
PointerRNA bone_pointer = RNA_pointer_create_discrete(
&pose_object->id, RNA_PoseBone, &pose_bone);
Vector<RNAPath> rna_paths = construct_pose_rna_paths(bone_pointer);
for (const RNAPath &rna_path : rna_paths) {
PointerRNA resolved_pointer;
PropertyRNA *resolved_property;
if (!RNA_path_resolve(
&bone_pointer, rna_path.path.c_str(), &resolved_pointer, &resolved_property))
{
continue;
}
const Vector<float> values = blender::animrig::get_rna_values(&resolved_pointer,
resolved_property);
const std::optional<std::string> rna_path_id_to_prop = RNA_path_from_ID_to_property(
&resolved_pointer, resolved_property);
if (!rna_path_id_to_prop.has_value()) {
continue;
}
for (const int i : values.index_range()) {
if (RNA_property_is_idprop(resolved_property) &&
!existing_paths.contains({rna_path_id_to_prop.value(), std::nullopt, i}))
{
/* Skipping custom properties without animation. */
continue;
}
strip_data.keyframe_insert(
&bmain, slot, {rna_path_id_to_prop.value(), i}, {1, values[i]}, key_settings);
}
}
}
}
return action;
}
/**
* Check that the newly created asset is visible SOMEWHERE in Blender. If not already visible,
* open the asset shelf on the current 3D view. The reason for not always doing that is that it
* might be annoying in case you have 2 3D viewports open, but you want the asset shelf on only one
* of them, or you work out of the asset browser.
*/
static void ensure_asset_ui_visible(bContext &C)
{
ScrArea *current_area = CTX_wm_area(&C);
if (!current_area || current_area->type->spaceid != SPACE_VIEW3D) {
/* Opening the asset shelf will only work from the 3D viewport. */
return;
}
wmWindowManager *wm = CTX_wm_manager(&C);
for (wmWindow &win : wm->windows) {
const bScreen *screen = WM_window_get_active_screen(&win);
for (ScrArea &area : screen->areabase) {
if (area.type->spaceid == SPACE_FILE) {
SpaceFile *sfile = reinterpret_cast<SpaceFile *>(area.spacedata.first);
if (sfile->browse_mode == FILE_BROWSE_MODE_ASSETS) {
/* Asset Browser is open. */
return;
}
continue;
}
const ARegion *shelf_region = BKE_area_find_region_type(&area, RGN_TYPE_ASSET_SHELF);
if (!shelf_region) {
continue;
}
if (shelf_region->runtime->visible) {
/* A visible asset shelf was found. */
return;
}
}
}
/* At this point, no asset shelf or asset browser was visible anywhere. */
ARegion *shelf_region = BKE_area_find_region_type(current_area, RGN_TYPE_ASSET_SHELF);
if (!shelf_region) {
return;
}
shelf_region->flag &= ~RGN_FLAG_HIDDEN;
ED_region_visibility_change_update(&C, CTX_wm_area(&C), shelf_region);
}
static Vector<Object *> get_selected_pose_objects(bContext *C)
{
Vector<PointerRNA> selected_objects;
CTX_data_selected_objects(C, &selected_objects);
Vector<Object *> selected_pose_objects;
for (const PointerRNA &ptr : selected_objects) {
Object *object = reinterpret_cast<Object *>(ptr.owner_id);
if (!object->pose) {
continue;
}
selected_pose_objects.append(object);
}
Object *active_object = CTX_data_active_object(C);
/* The active object may not be selected, it should be added because you can still switch to pose
* mode. */
if (active_object && active_object->pose && !selected_pose_objects.contains(active_object)) {
selected_pose_objects.append(active_object);
}
return selected_pose_objects;
}
static wmOperatorStatus create_pose_asset_local(bContext *C,
wmOperator *op,
const StringRefNull name,
const AssetLibraryReference lib_ref)
{
Vector<Object *> selected_pose_objects = get_selected_pose_objects(C);
if (selected_pose_objects.is_empty()) {
return OPERATOR_CANCELLED;
}
Main *bmain = CTX_data_main(C);
/* Extract the pose into a new action. */
blender::animrig::Action &pose_action = extract_pose(*bmain, selected_pose_objects);
asset::mark_id(&pose_action.id);
if (!G.background) {
asset::generate_preview(C, &pose_action.id);
}
BKE_id_rename(*bmain, pose_action.id, name);
/* Add asset to catalog. */
char catalog_path_c[MAX_NAME];
RNA_string_get(op->ptr, "catalog_path", catalog_path_c);
AssetMetaData &meta_data = *pose_action.id.asset_data;
asset_system::AssetLibrary *library = AS_asset_library_load(bmain, lib_ref);
/* NOTE(@ChrisLend): I don't know if a local library can fail to load.
* Just being defensive here. */
BLI_assert(library);
if (catalog_path_c[0] && library) {
const asset_system::AssetCatalogPath catalog_path =
asset_system::AssetCatalogPath::from_user_input(catalog_path_c);
asset_system::AssetCatalog &catalog = asset::library_ensure_catalogs_in_path(*library,
catalog_path);
BKE_asset_metadata_catalog_id_set(&meta_data, catalog.catalog_id, catalog.simple_name.c_str());
}
ensure_asset_ui_visible(*C);
asset::shelf::show_catalog_in_visible_shelves(*C, catalog_path_c);
asset::refresh_asset_library(C, lib_ref);
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_ADDED, nullptr);
return OPERATOR_FINISHED;
}
static wmOperatorStatus create_pose_asset_user_library(bContext *C,
wmOperator *op,
const char name[MAX_NAME],
const AssetLibraryReference lib_ref)
{
BLI_assert(lib_ref.type == ASSET_LIBRARY_CUSTOM);
Main *bmain = CTX_data_main(C);
const bUserAssetLibrary *user_library = BKE_preferences_asset_library_find_index(
&U, lib_ref.custom_library_index);
BLI_assert_msg(user_library, "The passed lib_ref is expected to be a user library");
if (!user_library) {
return OPERATOR_CANCELLED;
}
BLI_assert_msg(!(user_library->flag & ASSET_LIBRARY_USE_REMOTE_URL),
"The passed lib_ref is expected to be an on disk library");
if (user_library->flag & ASSET_LIBRARY_USE_REMOTE_URL) {
return OPERATOR_CANCELLED;
}
asset_system::AssetLibrary *library = AS_asset_library_load(bmain, lib_ref);
if (!library) {
BKE_report(op->reports, RPT_ERROR, "Failed to load asset library");
return OPERATOR_CANCELLED;
}
Vector<Object *> selected_pose_objects = get_selected_pose_objects(C);
if (selected_pose_objects.is_empty()) {
return OPERATOR_CANCELLED;
}
/* Temporary action in current main that will be exported and later deleted. */
blender::animrig::Action &pose_action = extract_pose(*bmain, selected_pose_objects);
asset::mark_id(&pose_action.id);
if (!G.background) {
asset::generate_preview(C, &pose_action.id);
}
/* Add asset to catalog. */
char catalog_path_c[MAX_NAME];
RNA_string_get(op->ptr, "catalog_path", catalog_path_c);
AssetMetaData &meta_data = *pose_action.id.asset_data;
if (catalog_path_c[0]) {
const asset_system::AssetCatalogPath catalog_path =
asset_system::AssetCatalogPath::from_user_input(catalog_path_c);
const asset_system::AssetCatalog &catalog = asset::library_ensure_catalogs_in_path(
*library, catalog_path);
BKE_asset_metadata_catalog_id_set(&meta_data, catalog.catalog_id, catalog.simple_name.c_str());
}
AssetWeakReference pose_asset_reference;
const std::optional<std::string> final_full_asset_filepath = bke::asset_edit_id_save_as(
*bmain, pose_action.id, name, *user_library, pose_asset_reference, *op->reports);
library->catalog_service().write_to_disk(*final_full_asset_filepath);
ensure_asset_ui_visible(*C);
asset::shelf::show_catalog_in_visible_shelves(*C, catalog_path_c);
BKE_id_free(bmain, &pose_action.id);
asset::refresh_asset_library(C, lib_ref);
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_ADDED, nullptr);
return OPERATOR_FINISHED;
}
static wmOperatorStatus pose_asset_create_exec(bContext *C, wmOperator *op)
{
char name[MAX_NAME] = "";
PropertyRNA *name_prop = RNA_struct_find_property(op->ptr, "pose_name");
if (RNA_property_is_set(op->ptr, name_prop)) {
RNA_property_string_get(op->ptr, name_prop, name);
}
if (name[0] == '\0') {
BKE_report(op->reports, RPT_ERROR, "No name set");
return OPERATOR_CANCELLED;
}
const int enum_value = RNA_enum_get(op->ptr, "asset_library_reference");
const AssetLibraryReference lib_ref = asset::library_reference_from_enum_value(enum_value);
switch (lib_ref.type) {
case ASSET_LIBRARY_LOCAL:
return create_pose_asset_local(C, op, name, lib_ref);
case ASSET_LIBRARY_CUSTOM:
return create_pose_asset_user_library(C, op, name, lib_ref);
default:
/* Only local and custom libraries should be exposed in the enum. */
BLI_assert_unreachable();
break;
}
BKE_report(op->reports, RPT_ERROR, "Unexpected library type. Failed to create pose asset");
return OPERATOR_FINISHED;
}
static wmOperatorStatus pose_asset_create_invoke(bContext *C,
wmOperator *op,
const wmEvent * /*event*/)
{
/* If the library isn't saved from the operator's last execution, use the first library. */
if (!RNA_struct_property_is_set_ex(op->ptr, "asset_library_reference", false)) {
std::optional<AssetLibraryReference> dest_library_ref =
ed::asset::get_user_library_ref_for_save();
if (!dest_library_ref) {
BKE_report(op->reports, RPT_WARNING, "No editable asset library to save into");
return OPERATOR_CANCELLED;
}
RNA_enum_set(op->ptr,
"asset_library_reference",
asset::library_reference_to_enum_value(&*dest_library_ref));
}
return WM_operator_props_dialog_popup(C, op, 400, std::nullopt, IFACE_("Create"));
}
static bool pose_asset_create_poll(bContext *C)
{
if (!ED_operator_posemode_context(C)) {
return false;
}
return true;
}
static void visit_library_prop_catalogs_catalog_for_search_fn(
const bContext *C,
PointerRNA *ptr,
PropertyRNA * /*prop*/,
const char *edit_text,
FunctionRef<void(StringPropertySearchVisitParams)> visit_fn)
{
const int enum_value = RNA_enum_get(ptr, "asset_library_reference");
const AssetLibraryReference lib_ref = asset::library_reference_from_enum_value(enum_value);
asset::visit_library_catalogs_catalog_for_search(
*CTX_data_main(C), lib_ref, edit_text, visit_fn);
}
void POSELIB_OT_create_pose_asset(wmOperatorType *ot)
{
ot->name = "Create Pose Asset...";
ot->description = "Create a new asset from the selected bones in the scene";
ot->idname = "POSELIB_OT_create_pose_asset";
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
ot->exec = pose_asset_create_exec;
ot->invoke = pose_asset_create_invoke;
ot->poll = pose_asset_create_poll;
ot->prop = RNA_def_string(
ot->srna, "pose_name", nullptr, MAX_NAME, "Pose Name", "Name for the new pose asset");
PropertyRNA *prop = RNA_def_property(ot->srna, "asset_library_reference", PROP_ENUM, PROP_NONE);
RNA_def_enum_funcs(prop, rna_asset_library_reference_itemf);
RNA_def_property_enum_default(prop, ASSET_LIBRARY_LOCAL);
RNA_def_property_ui_text(prop, "Library", "Asset library used to store the new pose");
prop = RNA_def_string(
ot->srna, "catalog_path", nullptr, MAX_NAME, "Catalog", "Catalog to use for the new asset");
RNA_def_property_string_search_func_runtime(
prop, visit_library_prop_catalogs_catalog_for_search_fn, PROP_STRING_SEARCH_SUGGESTION);
}
enum AssetModifyMode {
MODIFY_ADJUST = 0,
MODIFY_REPLACE,
MODIFY_ADD,
MODIFY_REMOVE,
};
static const EnumPropertyItem prop_asset_overwrite_modes[] = {
{MODIFY_ADJUST,
"ADJUST",
0,
"Adjust",
"Update existing channels in the pose asset but don't remove or add any channels"},
{MODIFY_REPLACE,
"REPLACE",
0,
"Replace with Selection",
"Completely replace all channels in the pose asset with the current selection"},
{MODIFY_ADD,
"ADD",
0,
"Add Selected Bones",
"Add channels of the selection to the pose asset. Existing channels will be updated"},
{MODIFY_REMOVE,
"REMOVE",
0,
"Remove Selected Bones",
"Remove channels of the selection from the pose asset"},
{0, nullptr, 0, nullptr, nullptr},
};
/**
* Get the selected asset from the given `bContext`. If the asset is an Action, returns a pointer
* to that action, else returns a nullptr.
*
* Note that this may open another .blend file and import the Action, which means it should not be
* used in poll functions.
*
* \see #pose_asset_potentially_editable_poll() for use in poll functions.
* \see #is_pose_asset_blend_editable() to check if the asset is from an editable .asset.blend.
*/
static bAction *get_action_of_selected_asset(bContext *C)
{
const asset_system::AssetRepresentation *asset = CTX_wm_asset(C);
if (!asset) {
return nullptr;
}
if (asset->get_id_type() != ID_AC) {
return nullptr;
}
AssetWeakReference asset_reference = asset->make_weak_reference();
Main *bmain = CTX_data_main(C);
return reinterpret_cast<bAction *>(
bke::asset_edit_id_from_weak_reference(*bmain, ID_AC, asset_reference));
}
/**
* Check that the .asset.blend file that contains the Action is suitable for modification.
*
* Editable: return true
* Not: report and return false.
*/
static bool is_pose_asset_blend_editable(const bAction &action, ReportList *reports)
{
if (!bke::asset_edit_id_is_editable(action.id)) {
BKE_reportf(reports, RPT_ERROR, "Action is not editable");
return false;
}
if (!bke::asset_edit_id_is_writable(action.id)) {
BKE_reportf(reports, RPT_ERROR, "Asset blend file is not editable");
return false;
}
return true;
}
/**
* Return true when the active asset is a local ID or in an .asset.blend file.
*
* This does not load the actual asset data-block.
*/
static bool pose_asset_potentially_editable_poll(bContext *C)
{
const asset_system::AssetRepresentation *asset_handle = CTX_wm_asset(C);
if (!asset_handle || asset_handle->get_id_type() != ID_AC) {
CTX_wm_operator_poll_msg_set(C, "No selected pose asset");
return false;
}
if (asset_handle->is_local_id()) {
return true;
}
if (!asset_handle->is_potentially_editable_asset_blend()) {
CTX_wm_operator_poll_msg_set(C, "Asset blend file is not editable");
return false;
}
return true;
}
struct PathValue {
RNAPath rna_path;
float value;
};
static Vector<PathValue> generate_path_values(Object &pose_object)
{
Vector<PathValue> path_values;
const bArmature *armature = id_cast<bArmature *>(pose_object.data);
for (bPoseChannel &pose_bone : pose_object.pose->chanbase) {
if (!blender::animrig::bone_is_selected(armature,
{&pose_bone, pose_bone.bone_get(pose_object)}))
{
continue;
}
PointerRNA bone_pointer = RNA_pointer_create_discrete(
&pose_object.id, RNA_PoseBone, &pose_bone);
Vector<RNAPath> rna_paths = construct_pose_rna_paths(bone_pointer);
for (RNAPath &rna_path : rna_paths) {
PointerRNA resolved_pointer;
PropertyRNA *resolved_property;
if (!RNA_path_resolve(
&bone_pointer, rna_path.path.c_str(), &resolved_pointer, &resolved_property))
{
continue;
}
const std::optional<std::string> rna_path_id_to_prop = RNA_path_from_ID_to_property(
&resolved_pointer, resolved_property);
if (!rna_path_id_to_prop.has_value()) {
continue;
}
Vector<float> values = blender::animrig::get_rna_values(&resolved_pointer,
resolved_property);
int i = 0;
for (const float value : values) {
RNAPath path = {rna_path_id_to_prop.value(), std::nullopt, i};
path_values.append({path, value});
i++;
}
}
}
return path_values;
}
static inline void replace_pose_key(Main &bmain,
blender::animrig::StripKeyframeData &strip_data,
const blender::animrig::Slot &slot,
const float2 time_value,
const blender::animrig::FCurveDescriptor &fcurve_descriptor)
{
using namespace blender::animrig;
Channelbag &channelbag = strip_data.channelbag_for_slot_ensure(slot);
FCurve &fcurve = channelbag.fcurve_ensure(&bmain, fcurve_descriptor);
/* Clearing all keys beforehand in case the pose was not defined on frame defined in
* `time_value`. */
BKE_fcurve_delete_keys_all(fcurve);
const KeyframeSettings key_settings = {BEZT_KEYTYPE_KEYFRAME, HD_AUTO, BEZT_IPO_BEZ};
insert_vert_fcurve(&fcurve, time_value, key_settings, INSERTKEY_NOFLAGS);
}
static void update_pose_action_from_scene(Main *bmain,
blender::animrig::Action &pose_action,
Object &pose_object,
const AssetModifyMode mode)
{
using namespace blender::animrig;
/* The frame on which an FCurve has a key to define a pose. */
constexpr int pose_frame = 1;
if (pose_action.slot_array_num < 1) {
/* All actions should have slots at this point. */
BLI_assert_unreachable();
return;
}
Slot &slot = blender::animrig::get_best_pose_slot_for_id(pose_object.id, pose_action);
BLI_assert(pose_action.strip_keyframe_data().size() == 1);
BLI_assert(pose_action.layers().size() == 1);
StripKeyframeData *strip_data = pose_action.strip_keyframe_data()[0];
Vector<PathValue> path_values = generate_path_values(pose_object);
Set<RNAPath> existing_paths;
foreach_fcurve_in_action_slot(pose_action, slot.handle, [&](const FCurve &fcurve) {
existing_paths.add({fcurve.rna_path, std::nullopt, fcurve.array_index});
});
switch (mode) {
case MODIFY_ADJUST: {
for (const PathValue &path_value : path_values) {
/* Only updating existing channels. */
if (existing_paths.contains(path_value.rna_path)) {
replace_pose_key(*bmain,
*strip_data,
slot,
{pose_frame, path_value.value},
{path_value.rna_path.path, path_value.rna_path.index.value()});
}
}
break;
}
case MODIFY_ADD: {
for (const PathValue &path_value : path_values) {
replace_pose_key(*bmain,
*strip_data,
slot,
{pose_frame, path_value.value},
{path_value.rna_path.path, path_value.rna_path.index.value()});
}
break;
}
case MODIFY_REPLACE: {
Channelbag *channelbag = strip_data->channelbag_for_slot(slot.handle);
if (!channelbag) {
/* No channels to remove. */
return;
}
channelbag->fcurves_clear();
for (const PathValue &path_value : path_values) {
replace_pose_key(*bmain,
*strip_data,
slot,
{pose_frame, path_value.value},
{path_value.rna_path.path, path_value.rna_path.index.value()});
}
break;
}
case MODIFY_REMOVE: {
Channelbag *channelbag = strip_data->channelbag_for_slot(slot.handle);
if (!channelbag) {
/* No channels to remove. */
return;
}
Map<RNAPath, FCurve *> fcurve_map;
foreach_fcurve_in_action_slot(
pose_action, pose_action.slot_array[0]->handle, [&](FCurve &fcurve) {
fcurve_map.add({fcurve.rna_path, std::nullopt, fcurve.array_index}, &fcurve);
});
for (const PathValue &path_value : path_values) {
if (existing_paths.contains(path_value.rna_path)) {
FCurve *fcurve = fcurve_map.lookup(path_value.rna_path);
channelbag->fcurve_remove(*fcurve);
}
}
break;
}
}
}
static wmOperatorStatus pose_asset_modify_exec(bContext *C, wmOperator *op)
{
bAction *action = get_action_of_selected_asset(C);
BLI_assert_msg(action, "Poll should have checked action exists");
if (ID_IS_LINKED(action) && !is_pose_asset_blend_editable(*action, op->reports)) {
return OPERATOR_CANCELLED;
}
/* Get asset now. Asset browser might get tagged for refreshing through operations below, and not
* allow querying items from context until refreshed, see #140781. */
const asset_system::AssetRepresentation *asset = CTX_wm_asset(C);
Main *bmain = CTX_data_main(C);
Object *pose_object = CTX_data_active_object(C);
if (!pose_object || !pose_object->pose) {
return OPERATOR_CANCELLED;
}
AssetModifyMode mode = AssetModifyMode(RNA_enum_get(op->ptr, "mode"));
update_pose_action_from_scene(bmain, action->wrap(), *pose_object, mode);
if (ID_IS_LINKED(action)) {
/* Not needed for local assets. */
bke::asset_edit_id_save(*bmain, action->id, *op->reports);
}
else {
/* Only create undo-step for local actions. Undoing external files isn't supported. */
ED_undo_push_op(C, op);
}
asset::refresh_asset_library_from_asset(C, *asset);
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_EDITED, nullptr);
return OPERATOR_FINISHED;
}
static bool pose_asset_modify_poll(bContext *C)
{
if (!ED_operator_posemode_context(C)) {
CTX_wm_operator_poll_msg_set(C, "Pose assets can only be modified from Pose Mode");
return false;
}
return pose_asset_potentially_editable_poll(C);
}
static std::string pose_asset_modify_description(bContext * /* C */,
wmOperatorType * /* ot */,
PointerRNA *ptr)
{
const int mode = RNA_enum_get(ptr, "mode");
return TIP_(std::string(prop_asset_overwrite_modes[mode].description));
}
/* Calling it overwrite instead of save because we aren't actually saving an opened asset. */
void POSELIB_OT_asset_modify(wmOperatorType *ot)
{
ot->name = "Modify Pose Asset";
ot->description =
"Update the selected pose asset in the asset library from the currently selected bones. The "
"mode defines how the asset is updated";
ot->idname = "POSELIB_OT_asset_modify";
ot->exec = pose_asset_modify_exec;
ot->poll = pose_asset_modify_poll;
ot->get_description = pose_asset_modify_description;
RNA_def_enum(ot->srna,
"mode",
prop_asset_overwrite_modes,
MODIFY_ADJUST,
"Overwrite Mode",
"Specify which parts of the pose asset are overwritten");
}
static wmOperatorStatus pose_asset_delete_exec(bContext *C, wmOperator *op)
{
bAction *action = get_action_of_selected_asset(C);
if (!action) {
return OPERATOR_CANCELLED;
}
const asset_system::AssetRepresentation *asset = CTX_wm_asset(C);
if (ID_IS_LINKED(action) && !is_pose_asset_blend_editable(*action, op->reports)) {
return OPERATOR_CANCELLED;
}
std::optional<AssetLibraryReference> library_ref =
asset->owner_asset_library().library_reference();
if (ID_IS_LINKED(action)) {
bke::asset_edit_id_delete(*CTX_data_main(C), action->id, *op->reports);
}
else {
asset::clear_id(&action->id);
/* Only create undo-step for local actions. Undoing external files isn't supported. */
ED_undo_push_op(C, op);
}
asset::refresh_asset_library(C, library_ref.value());
WM_main_add_notifier(NC_ASSET | ND_ASSET_LIST | NA_REMOVED, nullptr);
return OPERATOR_FINISHED;
}
static wmOperatorStatus pose_asset_delete_invoke(bContext *C,
wmOperator *op,
const wmEvent * /*event*/)
{
/* Perform some checks that the 'exec' function also does, so that when things aren't editable,
* the user gets a message about this *before* having to confirm the deletion. */
bAction *action = get_action_of_selected_asset(C);
if (!action) {
/* TODO: if this ever happens, figure out how that happened, and see if more
* useful information can be included in the report. After all, the poll
* function already checks that the active asset exists and is an Action. */
BKE_report(op->reports, RPT_ERROR, "Could not load Action for the active asset");
return OPERATOR_CANCELLED;
}
if (ID_IS_LINKED(action) && !is_pose_asset_blend_editable(*action, op->reports)) {
return OPERATOR_CANCELLED;
}
return WM_operator_confirm_ex(
C,
op,
IFACE_("Delete Pose Asset"),
ID_IS_LINKED(action) ?
IFACE_("Permanently delete pose asset blend file? This cannot be undone.") :
IFACE_("The asset is local to the file. Deleting it will just clear the asset status."),
IFACE_("Delete"),
ui::AlertIcon::Warning,
false);
}
void POSELIB_OT_asset_delete(wmOperatorType *ot)
{
ot->name = "Delete Pose Asset";
ot->description = "Delete the selected Pose Asset";
ot->idname = "POSELIB_OT_asset_delete";
ot->poll = pose_asset_potentially_editable_poll;
ot->invoke = pose_asset_delete_invoke;
ot->exec = pose_asset_delete_exec;
}
} // namespace blender::ed::animrig

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,530 @@
/* SPDX-FileCopyrightText: 2008 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#include <cstring>
#include "MEM_guardedalloc.h"
#include "DNA_anim_types.h"
#include "DNA_armature_types.h"
#include "DNA_gpencil_legacy_types.h"
#include "DNA_grease_pencil_types.h"
#include "DNA_mask_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_sequence_types.h"
#include "DNA_space_types.h"
#include "DNA_windowmanager_types.h"
#include "BLI_listbase.h"
#include "BLI_set.hh"
#include "BLI_string.h"
#include "BLI_utildefines.h"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_context.hh"
#include "BKE_fcurve.hh"
#include "BKE_gpencil_legacy.h"
#include "BKE_grease_pencil.hh"
#include "BKE_screen.hh"
#include "BKE_workspace.hh"
#include "DEG_depsgraph.hh"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "SEQ_sequencer.hh"
#include "SEQ_utils.hh"
#include "ED_anim_api.hh"
#include "ANIM_action.hh"
namespace blender {
/* **************************** depsgraph tagging ******************************** */
void ANIM_list_elem_update(Main *bmain, Scene *scene, bAnimListElem *ale)
{
ID *id;
FCurve *fcu;
AnimData *adt;
id = ale->id;
if (!id) {
return;
}
/* tag AnimData for refresh so that other views will update in realtime with these changes */
adt = BKE_animdata_from_id(id);
if (adt) {
DEG_id_tag_update(id, ID_RECALC_ANIMATION);
if (adt->action != nullptr) {
DEG_id_tag_update(&adt->action->id, ID_RECALC_ANIMATION);
}
}
/* Tag copy on the main object if updating anything directly inside AnimData */
if (ELEM(ale->type, ANIMTYPE_ANIMDATA, ANIMTYPE_NLAACTION, ANIMTYPE_NLATRACK, ANIMTYPE_NLACURVE))
{
DEG_id_tag_update(id, ID_RECALC_ANIMATION);
return;
}
/* update data */
fcu = static_cast<FCurve *>((ale->datatype == ALE_FCURVE) ? ale->key_data : nullptr);
if (fcu && fcu->rna_path) {
/* If we have an fcurve, call the update for the property we
* are editing, this is then expected to do the proper redraws
* and depsgraph updates. */
PointerRNA ptr;
PropertyRNA *prop;
PointerRNA id_ptr = RNA_id_pointer_create(id);
if (RNA_path_resolve_property(&id_ptr, fcu->rna_path, &ptr, &prop)) {
RNA_property_update_main(bmain, scene, &ptr, prop);
}
}
else {
/* in other case we do standard depsgraph update, ideally
* we'd be calling property update functions here too ... */
DEG_id_tag_update(id, /* XXX: or do we want something more restrictive? */
ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION);
}
}
void ANIM_id_update(Main *bmain, ID *id)
{
if (id) {
DEG_id_tag_update_ex(bmain,
id, /* XXX: or do we want something more restrictive? */
ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY | ID_RECALC_ANIMATION);
}
}
/* **************************** animation data <-> data syncing ******************************** */
/* This code here is used to synchronize the
* - selection (to find selected data easier)
* - ... (insert other relevant items here later)
* status in relevant Blender data with the status stored in animation channels.
*
* This should be called in the refresh() callbacks for various editors in
* response to appropriate notifiers.
*/
/* perform syncing updates for Action Groups */
static void animchan_sync_group(bAnimContext *ac, bAnimListElem *ale, bActionGroup **active_agrp)
{
bActionGroup *agrp = static_cast<bActionGroup *>(ale->data);
ID *owner_id = ale->id;
/* major priority is selection status
* so we need both a group and an owner
*/
if (ELEM(nullptr, agrp, owner_id)) {
return;
}
/* for standard Objects, check if group is the name of some bone */
if (GS(owner_id->name) == ID_OB) {
Object *ob = reinterpret_cast<Object *>(owner_id);
/* check if there are bones, and whether the name matches any
* NOTE: this feature will only really work if groups by default contain the F-Curves
* for a single bone.
*/
if (ob->pose) {
bPoseChannel *pchan = BKE_pose_channel_find_name(ob->pose, agrp->name);
if (pchan) {
Bone *bone = pchan->bone_get(*ob);
/* if one matches, sync the selection status */
if (bone && (pchan->flag & POSE_SELECTED)) {
agrp->flag |= AGRP_SELECTED;
}
else {
agrp->flag &= ~AGRP_SELECTED;
}
/* also sync active group status */
bArmature *arm = id_cast<bArmature *>(ob->data);
if ((ob == ac->obact) && (bone == arm->act_bone)) {
/* if no previous F-Curve has active flag, then we're the first and only one to get it */
if (*active_agrp == nullptr) {
agrp->flag |= AGRP_ACTIVE;
*active_agrp = agrp;
}
else {
/* someone else has already taken it - set as not active */
agrp->flag &= ~AGRP_ACTIVE;
}
}
else {
/* this can't possibly be active now */
agrp->flag &= ~AGRP_ACTIVE;
}
/* sync bone color */
action_group_colors_set_from_posebone(agrp, {pchan, bone});
}
}
}
}
static void animchan_sync_fcurve_scene(bAnimListElem *ale)
{
ID *owner_id = ale->id;
BLI_assert(GS(owner_id->name) == ID_SCE);
Scene *scene = reinterpret_cast<Scene *>(owner_id);
FCurve *fcu = static_cast<FCurve *>(ale->data);
Strip *strip = nullptr;
/* Only affect if F-Curve involves sequence_editor.strips. */
char strip_name[sizeof(strip->name)];
if (!BLI_str_quoted_substr(fcu->rna_path, "strips_all[", strip_name, sizeof(strip_name))) {
return;
}
/* Check if this strip is selected. */
Editing *ed = seq::editing_get(scene);
if (ed == nullptr) {
/* The existence of the F-Curve doesn't imply the existence of the sequencer
* strip, or even the sequencer itself. */
return;
}
strip = seq::get_strip_by_name(ed->current_strips(), strip_name, false);
if (strip == nullptr) {
return;
}
/* update selection status */
if (strip->flag & SEQ_SELECT) {
fcu->flag |= FCURVE_SELECTED;
}
else {
fcu->flag &= ~FCURVE_SELECTED;
}
}
/* perform syncing updates for F-Curves */
static void animchan_sync_fcurve(bAnimListElem *ale)
{
FCurve *fcu = static_cast<FCurve *>(ale->data);
ID *owner_id = ale->id;
/* major priority is selection status, so refer to the checks done in `anim_filter.cc`
* #skip_fcurve_selected_data() for reference about what's going on here.
*/
if (ELEM(nullptr, fcu, fcu->rna_path, owner_id)) {
return;
}
switch (GS(owner_id->name)) {
case ID_SCE:
animchan_sync_fcurve_scene(ale);
break;
default:
break;
}
}
/* perform syncing updates for GPencil Layers */
static void animchan_sync_gplayer(bAnimListElem *ale)
{
bGPDlayer *gpl = static_cast<bGPDlayer *>(ale->data);
/* Make sure the selection flags agree with the "active" flag.
* The selection flags are used in the Dope-sheet only, whereas
* the active flag is used everywhere else. Hence, we try to
* sync these here so that it all seems to be have as the user
* expects - #50184
*
* Assume that we only really do this when the active status changes.
* (NOTE: This may prove annoying if it means selection is always lost)
*/
if (gpl->flag & GP_LAYER_ACTIVE) {
gpl->flag |= GP_LAYER_SELECT;
}
else {
gpl->flag &= ~GP_LAYER_SELECT;
}
}
/* ---------------- */
void ANIM_sync_animchannels_to_data(const bContext *C)
{
bAnimContext ac;
ListBaseT<bAnimListElem> anim_data = {nullptr, nullptr};
int filter;
bActionGroup *active_agrp = nullptr;
/* get animation context info for filtering the channels */
if (ANIM_animdata_get_context(C, &ac) == 0) {
return;
}
/* filter data */
/* NOTE: we want all channels, since we want to be able to set selection status on some of them
* even when collapsed... however,
* don't include duplicates so that selection statuses don't override each other.
*/
filter = ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_CHANNELS | ANIMFILTER_NODUPLIS;
ANIM_animdata_filter(
&ac, &anim_data, eAnimFilter_Flags(filter), ac.data, eAnimCont_Types(ac.datatype));
/* flush settings as appropriate depending on the types of the channels */
for (bAnimListElem &ale : anim_data) {
switch (ale.type) {
case ANIMTYPE_GROUP:
animchan_sync_group(&ac, &ale, &active_agrp);
break;
case ANIMTYPE_FCURVE:
animchan_sync_fcurve(&ale);
break;
case ANIMTYPE_GPLAYER:
animchan_sync_gplayer(&ale);
break;
case ANIMTYPE_GREASE_PENCIL_LAYER: {
using namespace blender::bke::greasepencil;
GreasePencil *grease_pencil = reinterpret_cast<GreasePencil *>(ale.id);
Layer *layer = static_cast<Layer *>(ale.data);
layer->set_selected(grease_pencil->is_layer_active(layer));
break;
}
case ANIMTYPE_NONE:
case ANIMTYPE_ANIMDATA:
case ANIMTYPE_SPECIALDATA__UNUSED:
case ANIMTYPE_SUMMARY:
case ANIMTYPE_SCENE:
case ANIMTYPE_OBJECT:
case ANIMTYPE_NLACONTROLS:
case ANIMTYPE_NLACURVE:
case ANIMTYPE_FILLACT_LAYERED:
case ANIMTYPE_ACTION_SLOT:
case ANIMTYPE_FILLACTD:
case ANIMTYPE_FILLDRIVERS:
case ANIMTYPE_DSMAT:
case ANIMTYPE_DSLAM:
case ANIMTYPE_DSCAM:
case ANIMTYPE_DSCACHEFILE:
case ANIMTYPE_DSCUR:
case ANIMTYPE_DSSKEY:
case ANIMTYPE_DSWOR:
case ANIMTYPE_DSNTREE:
case ANIMTYPE_DSPART:
case ANIMTYPE_DSMBALL:
case ANIMTYPE_DSARM:
case ANIMTYPE_DSMESH:
case ANIMTYPE_DSTEX:
case ANIMTYPE_DSLAT:
case ANIMTYPE_DSLINESTYLE:
case ANIMTYPE_DSSPK:
case ANIMTYPE_DSGPENCIL:
case ANIMTYPE_DSMCLIP:
case ANIMTYPE_DSHAIR:
case ANIMTYPE_DSPOINTCLOUD:
case ANIMTYPE_DSVOLUME:
case ANIMTYPE_DSLIGHTPROBE:
case ANIMTYPE_SHAPEKEY:
case ANIMTYPE_GREASE_PENCIL_DATABLOCK:
case ANIMTYPE_GREASE_PENCIL_LAYER_GROUP:
case ANIMTYPE_MASKDATABLOCK:
case ANIMTYPE_MASKLAYER:
case ANIMTYPE_NLATRACK:
case ANIMTYPE_NLAACTION:
case ANIMTYPE_PALETTE:
case ANIMTYPE_NUM_TYPES:
break;
}
}
ANIM_animdata_freelist(&anim_data);
}
void ANIM_animdata_update(bAnimContext *ac, ListBaseT<bAnimListElem> *anim_data)
{
for (bAnimListElem &ale : *anim_data) {
if (ale.type == ANIMTYPE_GPLAYER) {
bGPDlayer *gpl = static_cast<bGPDlayer *>(ale.data);
if (ale.update & ANIM_UPDATE_ORDER) {
ale.update &= ~ANIM_UPDATE_ORDER;
if (gpl) {
BKE_gpencil_layer_frames_sort(gpl, nullptr);
}
}
if (ale.update & ANIM_UPDATE_DEPS) {
ale.update &= ~ANIM_UPDATE_DEPS;
ANIM_list_elem_update(ac->bmain, ac->scene, &ale);
}
/* disable handles to avoid crash */
if (ale.update & ANIM_UPDATE_HANDLES) {
ale.update &= ~ANIM_UPDATE_HANDLES;
}
}
else if (ale.datatype == ALE_MASKLAY) {
MaskLayer *masklay = static_cast<MaskLayer *>(ale.data);
if (ale.update & ANIM_UPDATE_ORDER) {
ale.update &= ~ANIM_UPDATE_ORDER;
if (masklay) {
/* While correct & we could enable it: 'posttrans_mask_clean' currently
* both sorts and removes doubles, so this is not necessary here. */
// BKE_mask_layer_shape_sort(masklay);
}
}
if (ale.update & ANIM_UPDATE_DEPS) {
ale.update &= ~ANIM_UPDATE_DEPS;
ANIM_list_elem_update(ac->bmain, ac->scene, &ale);
}
/* Disable handles to avoid assert. */
if (ale.update & ANIM_UPDATE_HANDLES) {
ale.update &= ~ANIM_UPDATE_HANDLES;
}
}
else if (ale.datatype == ALE_FCURVE) {
FCurve *fcu = static_cast<FCurve *>(ale.key_data);
if (ale.update & ANIM_UPDATE_ORDER) {
ale.update &= ~ANIM_UPDATE_ORDER;
if (fcu) {
sort_time_fcurve(*fcu);
}
}
if (ale.update & ANIM_UPDATE_HANDLES) {
ale.update &= ~ANIM_UPDATE_HANDLES;
if (fcu) {
BKE_fcurve_handles_recalc(*fcu);
}
}
if (ale.update & ANIM_UPDATE_DEPS) {
ale.update &= ~ANIM_UPDATE_DEPS;
ANIM_list_elem_update(ac->bmain, ac->scene, &ale);
}
}
else if (ELEM(ale.type,
ANIMTYPE_ANIMDATA,
ANIMTYPE_NLAACTION,
ANIMTYPE_NLATRACK,
ANIMTYPE_NLACURVE))
{
if (ale.update & ANIM_UPDATE_DEPS) {
ale.update &= ~ANIM_UPDATE_DEPS;
ANIM_list_elem_update(ac->bmain, ac->scene, &ale);
}
}
else if (ELEM(ale.type,
ANIMTYPE_GREASE_PENCIL_LAYER,
ANIMTYPE_GREASE_PENCIL_LAYER_GROUP,
ANIMTYPE_GREASE_PENCIL_DATABLOCK))
{
if (ale.update & ANIM_UPDATE_DEPS) {
ale.update &= ~ANIM_UPDATE_DEPS;
ANIM_list_elem_update(ac->bmain, ac->scene, &ale);
}
/* Order appears to be already handled in `grease_pencil_layer_apply_trans_data` when
* translating. */
ale.update &= ~(ANIM_UPDATE_HANDLES | ANIM_UPDATE_ORDER);
}
else if (ale.update) {
#if 0
if (G.debug & G_DEBUG) {
printf("%s: Unhandled animchannel updates (%d) for type=%d (%p)\n",
__func__,
ale->update,
ale->type,
ale->data);
}
#endif
/* Prevent crashes in cases where it can't be handled */
ale.update = eAnim_Update_Flags(0);
}
BLI_assert(ale.update == 0);
}
}
void ANIM_animdata_freelist(ListBaseT<bAnimListElem> *anim_data)
{
#ifndef NDEBUG
bAnimListElem *ale, *ale_next;
for (ale = static_cast<bAnimListElem *>(anim_data->first); ale; ale = ale_next) {
ale_next = ale->next;
BLI_assert(ale->update == 0);
MEM_delete(ale);
}
anim_data->clear_no_delete();
#else
anim_data->free_no_destruct();
#endif
}
void ANIM_deselect_keys_in_animation_editors(bContext *C)
{
wmWindow *ctx_window = CTX_wm_window(C);
ScrArea *ctx_area = CTX_wm_area(C);
ARegion *ctx_region = CTX_wm_region(C);
Set<bAction *> dna_actions;
for (wmWindow &win : CTX_wm_manager(C)->windows) {
bScreen *screen = BKE_workspace_active_screen_get(win.workspace_hook);
for (ScrArea &area : screen->areabase) {
if (!ELEM(area.spacetype, SPACE_GRAPH, SPACE_ACTION)) {
continue;
}
ARegion *window_region = BKE_area_find_region_type(&area, RGN_TYPE_WINDOW);
if (!window_region) {
continue;
}
CTX_wm_window_set(C, &win);
CTX_wm_area_set(C, &area);
CTX_wm_region_set(C, window_region);
bAnimContext ac;
if (!ANIM_animdata_get_context(C, &ac)) {
continue;
}
ListBaseT<bAnimListElem> anim_data = {nullptr, nullptr};
eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_FCURVESONLY);
ANIM_animdata_filter(&ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype));
for (bAnimListElem &ale : anim_data) {
if (!ale.adt || !ale.adt->action) {
continue;
}
dna_actions.add(ale.adt->action);
}
ANIM_animdata_freelist(&anim_data);
}
}
CTX_wm_window_set(C, ctx_window);
CTX_wm_area_set(C, ctx_area);
CTX_wm_region_set(C, ctx_region);
for (bAction *dna_action : dna_actions) {
animrig::action_deselect_keys(dna_action->wrap());
}
}
} // namespace blender

View File

@@ -0,0 +1,855 @@
/* SPDX-FileCopyrightText: 2008 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#include "BLI_sys_types.h"
#include "DNA_anim_types.h"
#include "DNA_gpencil_legacy_types.h"
#include "DNA_grease_pencil_types.h"
#include "DNA_object_types.h"
#include "DNA_scene_types.h"
#include "DNA_screen_types.h"
#include "DNA_sequence_types.h"
#include "DNA_space_types.h"
#include "DNA_userdef_types.h"
#include "DNA_workspace_types.h"
#include "BLI_listbase.h"
#include "BLI_math_rotation.h"
#include "BLI_math_vector.h"
#include "BLI_rect.h"
#include "BLI_utildefines.h"
#include "BKE_context.hh"
#include "BKE_curve.hh"
#include "BKE_fcurve.hh"
#include "BKE_global.hh"
#include "BKE_mask.hh"
#include "BKE_nla.hh"
#include "ED_anim_api.hh"
#include "ED_keyframes_edit.hh"
#include "ED_keyframes_keylist.hh"
#include "ED_sequencer.hh"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "UI_resources.hh"
#include "UI_view2d.hh"
#include "GPU_immediate.hh"
#include "GPU_state.hh"
#include "SEQ_time.hh"
#include <utility>
namespace blender {
/* *************************************************** */
/* CURRENT FRAME DRAWING */
void ANIM_draw_cfra(const bContext *C, View2D *v2d, short flag)
{
Scene *scene = CTX_data_scene(C);
const float time = scene->r.cfra + scene->r.subframe;
const float x = float(time * scene->r.framelen);
GPU_line_width((flag & DRAWCFRA_WIDE) ? 3.0 : 2.0);
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
/* Draw a light green line to indicate current frame */
immUniformThemeColor(TH_CFRAME);
immBegin(GPU_PRIM_LINES, 2);
immVertex2f(pos, x, v2d->cur.ymin - 500.0f); /* XXX arbitrary... want it go to bottom */
immVertex2f(pos, x, v2d->cur.ymax);
immEnd();
immUnbindProgram();
}
/* *************************************************** */
/* PREVIEW RANGE 'CURTAINS' */
/* NOTE: 'Preview Range' tools are defined in `anim_ops.cc`. */
void ANIM_draw_previewrange(const Scene *scene, View2D *v2d, int end_frame_width)
{
/* Only draw this if preview range is set. */
if (PRVRANGEON) {
GPU_blend(GPU_BLEND_ALPHA);
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
immUniformThemeColorShadeAlpha(TH_ANIM_PREVIEW_RANGE, -25, -30);
/* Only draw two separate 'curtains' if there's no overlap between them. */
if (scene->r.psfra < scene->r.pefra + end_frame_width) {
immRectf(pos, v2d->cur.xmin, v2d->cur.ymin, float(scene->r.psfra), v2d->cur.ymax);
immRectf(pos,
float(scene->r.pefra + end_frame_width),
v2d->cur.ymin,
v2d->cur.xmax,
v2d->cur.ymax);
}
else {
immRectf(pos, v2d->cur.xmin, v2d->cur.ymin, v2d->cur.xmax, v2d->cur.ymax);
}
immUnbindProgram();
GPU_blend(GPU_BLEND_NONE);
}
}
void ANIM_draw_scene_strip_range(const bContext *C, View2D *v2d)
{
SpaceAction *space_action = CTX_wm_space_action(C);
if (!space_action || (space_action->overlays.flag & ADS_OVERLAY_SHOW_OVERLAYS) == 0 ||
(space_action->overlays.flag & ADS_SHOW_SCENE_STRIP_FRAME_RANGE) == 0)
{
return;
}
WorkSpace *workspace = CTX_wm_workspace(C);
if (!workspace) {
return;
}
if ((workspace->flags & WORKSPACE_SYNC_SCENE_TIME) == 0) {
return;
}
const Scene *sequencer_scene = workspace->sequencer_scene;
if (!sequencer_scene) {
return;
}
const Strip *scene_strip = ed::vse::get_scene_strip_for_time_sync(sequencer_scene);
if (!scene_strip || !scene_strip->scene) {
return;
}
GPU_blend(GPU_BLEND_ALPHA);
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
immUniformThemeColorShadeAlpha(TH_ANIM_SCENE_STRIP_RANGE, -25, -30);
/* ..._handle are frames in "sequencer logic", meaning that on the right_handle point in time,
* the strip is not visible any more. The last visible frame of the strip is actually on
* (right_handle-1), hence the -1 when computing the end_frame. */
const float left_handle = scene_strip->left_handle();
const float right_handle = scene_strip->right_handle(sequencer_scene);
float start_frame = seq::give_frame_index(sequencer_scene, scene_strip, left_handle) +
scene_strip->scene->r.sfra + scene_strip->anim_startofs;
float end_frame = seq::give_frame_index(sequencer_scene, scene_strip, right_handle - 1) +
scene_strip->scene->r.sfra + scene_strip->anim_startofs;
/* This can happen when the strip time is reversed. */
if (start_frame > end_frame) {
std::swap(start_frame, end_frame);
}
immRectf(pos, v2d->cur.xmin, v2d->cur.ymin, start_frame, v2d->cur.ymax);
immRectf(pos, end_frame, v2d->cur.ymin, v2d->cur.xmax, v2d->cur.ymax);
immUnbindProgram();
GPU_blend(GPU_BLEND_NONE);
}
/* *************************************************** */
/* SCENE FRAME RANGE */
void ANIM_draw_framerange(Scene *scene, View2D *v2d)
{
/* draw darkened area outside of active timeline frame range */
GPU_blend(GPU_BLEND_ALPHA);
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
immUniformThemeColorShadeAlpha(TH_BACK, -25, -100);
if (scene->r.sfra < scene->r.efra) {
immRectf(pos, v2d->cur.xmin, v2d->cur.ymin, float(scene->r.sfra), v2d->cur.ymax);
immRectf(pos, float(scene->r.efra), v2d->cur.ymin, v2d->cur.xmax, v2d->cur.ymax);
}
else {
immRectf(pos, v2d->cur.xmin, v2d->cur.ymin, v2d->cur.xmax, v2d->cur.ymax);
}
GPU_blend(GPU_BLEND_NONE);
/* thin lines where the actual frames are */
immUniformThemeColorShade(TH_BACK, -60);
immBegin(GPU_PRIM_LINES, 4);
immVertex2f(pos, float(scene->r.sfra), v2d->cur.ymin);
immVertex2f(pos, float(scene->r.sfra), v2d->cur.ymax);
immVertex2f(pos, float(scene->r.efra), v2d->cur.ymin);
immVertex2f(pos, float(scene->r.efra), v2d->cur.ymax);
immEnd();
immUnbindProgram();
}
void ANIM_draw_action_framerange(
AnimData *adt, bAction *action, View2D *v2d, float ymin, float ymax)
{
if ((action->flag & ACT_FRAME_RANGE) == 0) {
return;
}
/* Compute the dimensions. */
CLAMP_MIN(ymin, v2d->cur.ymin);
CLAMP_MAX(ymax, v2d->cur.ymax);
if (ymin > ymax) {
return;
}
const float sfra = BKE_nla_tweakedit_remap(adt, action->frame_start, NLATIME_CONVERT_MAP);
const float efra = BKE_nla_tweakedit_remap(adt, action->frame_end, NLATIME_CONVERT_MAP);
/* Diagonal stripe filled area outside of the frame range. */
GPU_blend(GPU_BLEND_ALPHA);
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_2D_DIAG_STRIPES);
float color[4];
ui::theme::get_color_shade_alpha_4fv(TH_BACK, -40, -50, color);
immUniform4f("color1", color[0], color[1], color[2], color[3]);
immUniform4f("color2", 0.0f, 0.0f, 0.0f, 0.0f);
immUniform1i("size1", 2 * UI_SCALE_FAC);
immUniform1i("size2", 4 * UI_SCALE_FAC);
if (sfra < efra) {
immRectf(pos, v2d->cur.xmin, ymin, sfra, ymax);
immRectf(pos, efra, ymin, v2d->cur.xmax, ymax);
}
else {
immRectf(pos, v2d->cur.xmin, ymin, v2d->cur.xmax, ymax);
}
immUnbindProgram();
GPU_blend(GPU_BLEND_NONE);
/* Thin lines where the actual frames are. */
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
immUniformThemeColorShade(TH_BACK, -60);
GPU_line_width(1.0f);
immBegin(GPU_PRIM_LINES, 4);
immVertex2f(pos, sfra, ymin);
immVertex2f(pos, sfra, ymax);
immVertex2f(pos, efra, ymin);
immVertex2f(pos, efra, ymax);
immEnd();
immUnbindProgram();
}
/* *************************************************** */
/* NLA-MAPPING UTILITIES (required for drawing and also editing keyframes). */
bool ANIM_nla_mapping_allowed(const bAnimListElem *ale)
{
/* Historically, there was another check in the code that this function replaced:
* if (!ELEM(ac->datatype,
* ANIMCONT_ACTION,
* ANIMCONT_SHAPEKEY,
* ANIMCONT_DOPESHEET,
* ANIMCONT_FCURVES,
* ANIMCONT_NLA,
* ANIMCONT_CHANNEL,
* ANIMCONT_TIMELINE))
* {
* ... prevent NLA-remapping ...
* }
*
* I (Sybren) suspect that this was actually hiding some animation type check. When that code was
* written, I think there was no GreasePencil data showing in the regular Dope Sheet editor.
*/
switch (ale->type) {
case ANIMTYPE_NLACURVE:
/* NLA Control Curves occur on NLA strips,
* and shouldn't be subjected to this kind of mapping. */
return false;
case ANIMTYPE_FCURVE: {
/* The F-Curve data of a driver should never get NLA-remapped. */
FCurve *fcurve = static_cast<FCurve *>(ale->key_data);
return !fcurve->driver;
}
case ANIMTYPE_DSGPENCIL:
case ANIMTYPE_GPLAYER:
case ANIMTYPE_GREASE_PENCIL_DATABLOCK:
case ANIMTYPE_GREASE_PENCIL_LAYER_GROUP:
case ANIMTYPE_GREASE_PENCIL_LAYER:
/* Grease Pencil doesn't use the NLA, so don't bother remapping. */
return false;
case ANIMTYPE_MASKDATABLOCK:
case ANIMTYPE_MASKLAYER:
/* I (Sybren) don't _think_ masks can use the NLA. */
return false;
case ANIMTYPE_SUMMARY:
/* The summary line cannot do NLA remapping since it may contain multiple actions. */
return false;
default:
/* NLA time remapping is the default behavior, and only should be
* prohibited for the above types. */
return true;
}
}
float ANIM_nla_tweakedit_remap(bAnimListElem *ale,
const float cframe,
const eNlaTime_ConvertModes mode)
{
if (!ANIM_nla_mapping_allowed(ale)) {
return cframe;
}
return BKE_nla_tweakedit_remap(ale->adt, cframe, mode);
}
/* ------------------- */
/* Helper function for ANIM_nla_mapping_apply_fcurve() -> "restore",
* i.e. mapping points back to action-time. */
static short bezt_nlamapping_restore(KeyframeEditData *ked, BezTriple *bezt)
{
/* AnimData block providing scaling is stored in 'data', only_keys option is stored in i1 */
AnimData *adt = static_cast<AnimData *>(ked->data);
short only_keys = short(ked->i1);
/* adjust BezTriple handles only if allowed to */
if (only_keys == 0) {
bezt->vec[0][0] = BKE_nla_tweakedit_remap(adt, bezt->vec[0][0], NLATIME_CONVERT_UNMAP);
bezt->vec[2][0] = BKE_nla_tweakedit_remap(adt, bezt->vec[2][0], NLATIME_CONVERT_UNMAP);
}
bezt->vec[1][0] = BKE_nla_tweakedit_remap(adt, bezt->vec[1][0], NLATIME_CONVERT_UNMAP);
return 0;
}
/* helper function for ANIM_nla_mapping_apply_fcurve() -> "apply",
* i.e. mapping points to NLA-mapped global time */
static short bezt_nlamapping_apply(KeyframeEditData *ked, BezTriple *bezt)
{
/* AnimData block providing scaling is stored in 'data', only_keys option is stored in i1 */
AnimData *adt = static_cast<AnimData *>(ked->data);
short only_keys = short(ked->i1);
/* adjust BezTriple handles only if allowed to */
if (only_keys == 0) {
bezt->vec[0][0] = BKE_nla_tweakedit_remap(adt, bezt->vec[0][0], NLATIME_CONVERT_MAP);
bezt->vec[2][0] = BKE_nla_tweakedit_remap(adt, bezt->vec[2][0], NLATIME_CONVERT_MAP);
}
bezt->vec[1][0] = BKE_nla_tweakedit_remap(adt, bezt->vec[1][0], NLATIME_CONVERT_MAP);
return 0;
}
void ANIM_nla_mapping_apply_fcurve(AnimData *adt, FCurve *fcu, bool restore, bool only_keys)
{
if (adt == nullptr || adt->nla_tracks.is_empty()) {
return;
}
KeyframeEditData ked = {{nullptr}};
KeyframeEditFunc map_cb;
/* init edit data
* - AnimData is stored in 'data'
* - only_keys is stored in 'i1'
*/
ked.data = static_cast<void *>(adt);
ked.i1 = int(only_keys);
/* get editing callback */
if (restore) {
map_cb = bezt_nlamapping_restore;
}
else {
map_cb = bezt_nlamapping_apply;
}
/* apply to F-Curve */
ANIM_fcurve_keyframes_loop(&ked, fcu, nullptr, map_cb, nullptr);
}
void ANIM_nla_mapping_apply_if_needed_fcurve(bAnimListElem *ale,
FCurve *fcu,
const bool restore,
const bool only_keys)
{
if (!ANIM_nla_mapping_allowed(ale)) {
return;
}
ANIM_nla_mapping_apply_fcurve(ale->adt, fcu, restore, only_keys);
}
/* *************************************************** */
/* UNITS CONVERSION MAPPING (required for drawing and editing keyframes) */
short ANIM_get_normalization_flags(SpaceLink *space_link)
{
if (space_link->spacetype == SPACE_GRAPH) {
SpaceGraph *sipo = reinterpret_cast<SpaceGraph *>(space_link);
bool use_normalization = (sipo->flag & SIPO_NORMALIZE) != 0;
bool freeze_normalization = (sipo->flag & SIPO_NORMALIZE_FREEZE) != 0;
return use_normalization ? (ANIM_UNITCONV_NORMALIZE |
(freeze_normalization ? ANIM_UNITCONV_NORMALIZE_FREEZE : 0)) :
0;
}
return 0;
}
static void fcurve_scene_coord_range_get(Scene *scene,
const FCurve *fcu,
float *r_min_coord,
float *r_max_coord)
{
float min_coord = FLT_MAX;
float max_coord = -FLT_MAX;
const bool use_preview_only = PRVRANGEON;
if (fcu->bezt || fcu->fpt) {
int start = 0;
int end = fcu->totvert;
if (use_preview_only) {
if (fcu->bezt) {
/* Preview frame ranges need to be converted to bezt array indices. */
bool replace = false;
start = BKE_fcurve_bezt_binarysearch_index(
fcu->bezt, scene->r.psfra, fcu->totvert, &replace);
end = BKE_fcurve_bezt_binarysearch_index(
fcu->bezt, scene->r.pefra + 1, fcu->totvert, &replace);
}
else if (fcu->fpt) {
const int unclamped_start = int(scene->r.psfra - fcu->fpt[0].vec[0]);
start = max_ii(unclamped_start, 0);
end = min_ii(unclamped_start + (scene->r.pefra - scene->r.psfra) + 1, fcu->totvert);
}
}
if (fcu->bezt) {
const BezTriple *bezt = fcu->bezt + start;
for (int i = start; i < end; i++, bezt++) {
if (i == 0) {
/* We ignore extrapolation flags and handle here, and use the
* control point position only. so we normalize "interesting"
* part of the curve.
*
* Here we handle left extrapolation.
*/
max_coord = max_ff(max_coord, bezt->vec[1][1]);
min_coord = min_ff(min_coord, bezt->vec[1][1]);
}
else {
const BezTriple *prev_bezt = bezt - 1;
if (!ELEM(prev_bezt->ipo, BEZT_IPO_BEZ, BEZT_IPO_BACK, BEZT_IPO_ELASTIC)) {
/* The points on the curve will lie inside the start and end points.
* Calculate min/max using both previous and current CV.
*/
max_coord = max_ff(max_coord, bezt->vec[1][1]);
min_coord = min_ff(min_coord, bezt->vec[1][1]);
max_coord = max_ff(max_coord, prev_bezt->vec[1][1]);
min_coord = min_ff(min_coord, prev_bezt->vec[1][1]);
}
else {
const int resol = fcu->driver ?
32 :
min_ii(int(5.0f * len_v2v2(bezt->vec[1], prev_bezt->vec[1])),
32);
if (resol < 2) {
max_coord = max_ff(max_coord, prev_bezt->vec[1][1]);
min_coord = min_ff(min_coord, prev_bezt->vec[1][1]);
}
else {
if (!ELEM(prev_bezt->ipo, BEZT_IPO_BACK, BEZT_IPO_ELASTIC)) {
/* Calculate min/max using bezier forward differencing. */
float data[120];
float v1[2], v2[2], v3[2], v4[2];
v1[0] = prev_bezt->vec[1][0];
v1[1] = prev_bezt->vec[1][1];
v2[0] = prev_bezt->vec[2][0];
v2[1] = prev_bezt->vec[2][1];
v3[0] = bezt->vec[0][0];
v3[1] = bezt->vec[0][1];
v4[0] = bezt->vec[1][0];
v4[1] = bezt->vec[1][1];
BKE_fcurve_correct_bezpart(v1, v2, v3, v4);
BKE_curve_forward_diff_bezier(
v1[0], v2[0], v3[0], v4[0], data, resol, sizeof(float[3]));
BKE_curve_forward_diff_bezier(
v1[1], v2[1], v3[1], v4[1], data + 1, resol, sizeof(float[3]));
for (int j = 0; j <= resol; ++j) {
const float *fp = &data[j * 3];
max_coord = max_ff(max_coord, fp[1]);
min_coord = min_ff(min_coord, fp[1]);
}
}
else {
/* Calculate min/max using full fcurve evaluation.
* [slower than bezier forward differencing but evaluates Back/Elastic
* interpolation as well]. */
float step_size = (bezt->vec[1][0] - prev_bezt->vec[1][0]) / resol;
for (int j = 0; j <= resol; j++) {
float eval_time = prev_bezt->vec[1][0] + step_size * j;
float eval_value = evaluate_fcurve_only_curve(fcu, eval_time);
max_coord = max_ff(max_coord, eval_value);
min_coord = min_ff(min_coord, eval_value);
}
}
}
}
}
}
}
else if (fcu->fpt) {
const FPoint *fpt = fcu->fpt + start;
for (int i = start; i < end; ++i, ++fpt) {
min_coord = min_ff(min_coord, fpt->vec[1]);
max_coord = max_ff(max_coord, fpt->vec[1]);
}
}
}
if (r_min_coord) {
*r_min_coord = min_coord;
}
if (r_max_coord) {
*r_max_coord = max_coord;
}
}
static float normalization_factor_get(Scene *scene, FCurve *fcu, short flag, float *r_offset)
{
float factor = 1.0f, offset = 0.0f;
if (flag & ANIM_UNITCONV_RESTORE) {
if (r_offset) {
*r_offset = fcu->prev_offset;
}
return 1.0f / fcu->prev_norm_factor;
}
if (flag & ANIM_UNITCONV_NORMALIZE_FREEZE) {
if (r_offset) {
*r_offset = fcu->prev_offset;
}
if (fcu->prev_norm_factor == 0.0f) {
/* Happens when Auto Normalize was disabled before
* any curves were displayed.
*/
return 1.0f;
}
return fcu->prev_norm_factor;
}
if (G.moving & G_TRANSFORM_FCURVES) {
if (r_offset) {
*r_offset = fcu->prev_offset;
}
if (fcu->prev_norm_factor == 0.0f) {
/* Same as above. */
return 1.0f;
}
return fcu->prev_norm_factor;
}
fcu->prev_norm_factor = 1.0f;
float max_coord = -FLT_MAX;
float min_coord = FLT_MAX;
fcurve_scene_coord_range_get(scene, fcu, &min_coord, &max_coord);
/* We use an ULPS-based floating point comparison here, with the
* rationale that if there are too few possible values between
* `min_coord` and `max_coord`, then after display normalization it
* will certainly be a weird quantized experience for the user anyway. */
if (min_coord < max_coord && ulp_diff_ff(min_coord, max_coord) > 256) {
/* Normalize. */
const float range = max_coord - min_coord;
factor = 2.0f / range;
offset = -min_coord - range / 2.0f;
}
else {
/* Skip normalization in 2 cases. Either the y difference of all keyframes is too small to
* normalize or there are no keys at all in the range. In the first case, the curve should be
* brought to the 0 line. In the second case we cannot do that since we have no information. */
factor = 1.0f;
if (min_coord == FLT_MAX) {
offset = 0.0f;
}
else {
offset = -min_coord;
}
}
BLI_assert(factor != 0.0f);
if (r_offset) {
*r_offset = offset;
}
fcu->prev_norm_factor = factor;
fcu->prev_offset = offset;
return factor;
}
float ANIM_unit_mapping_get_factor(Scene *scene, ID *id, FCurve *fcu, short flag, float *r_offset)
{
if (flag & ANIM_UNITCONV_NORMALIZE) {
return normalization_factor_get(scene, fcu, flag, r_offset);
}
if (r_offset) {
*r_offset = 0.0f;
}
/* TODO: change the pointer parameters to references, as this function should not be called
* without an animated ID or a scene (to get the preferred units). */
if (!id || !fcu || !fcu->rna_path || !scene) {
/* Not enough information to do the remapping, so just show the data as-is. */
return 1.0f;
}
PointerRNA ptr;
PropertyRNA *prop;
PointerRNA id_ptr = RNA_id_pointer_create(id);
if (!RNA_path_resolve_property(&id_ptr, fcu->rna_path, &ptr, &prop)) {
/* Without resolving the property, its type & subtype are unknown; remapping is impossible. */
return 1.0f;
}
const PropertyUnit prop_unit = PropertyUnit(RNA_SUBTYPE_UNIT(RNA_property_subtype(prop)));
switch (prop_unit) {
case PROP_UNIT_ROTATION:
if (scene->unit.system_rotation == USER_UNIT_ROT_RADIANS) {
return 1.0f;
}
if (flag & ANIM_UNITCONV_RESTORE) {
return DEG2RADF(1.0f);
}
return RAD2DEGF(1.0f);
default:
/* TODO: other rotation types here as necessary */
break;
}
return 1.0f;
}
static bool find_prev_next_keyframes(bContext *C, int *r_nextfra, int *r_prevfra)
{
Scene *scene = CTX_data_scene(C);
Object *ob = CTX_data_active_object(C);
Mask *mask = CTX_data_edit_mask(C);
bDopeSheet ads = {nullptr};
AnimKeylist *keylist = ED_keylist_create();
const ActKeyColumn *aknext, *akprev;
float cfranext, cfraprev;
bool donenext = false, doneprev = false;
int nextcount = 0, prevcount = 0;
cfranext = cfraprev = float(scene->r.cfra);
/* Seed up dummy dope-sheet context with flags to perform necessary filtering. */
if ((scene->flag & SCE_KEYS_NO_SELONLY) == 0) {
/* only selected channels are included */
ads.filterflag |= ADS_FILTER_ONLYSEL;
}
/* populate tree with keyframe nodes */
scene_to_keylist(&ads, scene, keylist, 0, {-FLT_MAX, FLT_MAX});
gpencil_to_keylist(&ads, scene->gpd, keylist, false);
if (ob) {
ob_to_keylist(&ads, ob, keylist, 0, {-FLT_MAX, FLT_MAX});
gpencil_to_keylist(&ads, id_cast<bGPdata *>(ob->data), keylist, false);
}
if (mask) {
MaskLayer *masklay = BKE_mask_layer_active(mask);
mask_to_keylist(&ads, masklay, keylist);
}
ED_keylist_prepare_for_direct_access(keylist);
/* TODO(jbakker): Key-lists are ordered, no need to do any searching at all. */
/* find matching keyframe in the right direction */
do {
aknext = ED_keylist_find_next(keylist, cfranext);
if (aknext) {
if (scene->r.cfra == int(aknext->cfra)) {
/* make this the new starting point for the search and ignore */
cfranext = aknext->cfra;
}
else {
/* this changes the frame, so set the frame and we're done */
if (++nextcount == U.view_frame_keyframes) {
donenext = true;
}
}
cfranext = aknext->cfra;
}
} while ((aknext != nullptr) && (donenext == false));
do {
akprev = ED_keylist_find_prev(keylist, cfraprev);
if (akprev) {
if (scene->r.cfra == int(akprev->cfra)) {
/* make this the new starting point for the search */
}
else {
/* this changes the frame, so set the frame and we're done */
if (++prevcount == U.view_frame_keyframes) {
doneprev = true;
}
}
cfraprev = akprev->cfra;
}
} while ((akprev != nullptr) && (doneprev == false));
/* free temp stuff */
ED_keylist_free(keylist);
/* any success? */
if (doneprev || donenext) {
if (doneprev) {
*r_prevfra = cfraprev;
}
else {
*r_prevfra = scene->r.cfra - (cfranext - scene->r.cfra);
}
if (donenext) {
*r_nextfra = cfranext;
}
else {
*r_nextfra = scene->r.cfra + (scene->r.cfra - cfraprev);
}
return true;
}
return false;
}
void ANIM_center_frame(bContext *C, int smooth_viewtx)
{
const bool is_sequencer = CTX_wm_space_seq(C) != nullptr;
Scene *scene = is_sequencer ? CTX_data_sequencer_scene(C) : CTX_data_scene(C);
if (!scene) {
return;
}
ARegion *region = CTX_wm_region(C);
float w = BLI_rctf_size_x(&region->v2d.cur);
rctf newrct;
int nextfra, prevfra;
switch (U.view_frame_type) {
case ZOOM_FRAME_MODE_SECONDS: {
const float fps = scene->frames_per_second();
newrct.xmax = scene->r.cfra + U.view_frame_seconds * fps + 1;
newrct.xmin = scene->r.cfra - U.view_frame_seconds * fps - 1;
newrct.ymax = region->v2d.cur.ymax;
newrct.ymin = region->v2d.cur.ymin;
break;
}
/* hardest case of all, look for all keyframes around frame and display those */
case ZOOM_FRAME_MODE_KEYFRAMES:
if (find_prev_next_keyframes(C, &nextfra, &prevfra)) {
newrct.xmax = nextfra;
newrct.xmin = prevfra;
newrct.ymax = region->v2d.cur.ymax;
newrct.ymin = region->v2d.cur.ymin;
break;
}
/* else drop through, keep range instead */
ATTR_FALLTHROUGH;
case ZOOM_FRAME_MODE_KEEP_RANGE:
default:
newrct.xmax = scene->r.cfra + (w / 2);
newrct.xmin = scene->r.cfra - (w / 2);
newrct.ymax = region->v2d.cur.ymax;
newrct.ymin = region->v2d.cur.ymin;
break;
}
ui::view2d_smooth_view(C, region, &newrct, smooth_viewtx);
}
/* *************************************************** */
rctf ANIM_frame_range_view2d_add_xmargin(const View2D &view_2d, const rctf view_rect)
{
/* Keyframe diamonds seem to be drawn at 10 pixels wide, multiplied by the UI scale. */
const float keyframe_size = 10 * UI_SCALE_FAC;
const float margin_in_px = 4 * keyframe_size;
/* This cannot use view2d_scale_get_x(view_2d) because that would use the
* current scale of the view, and not the one we'd get once `view_rect` is
* applied. And this function should not assume that view_2d.cur == view_rect.
*
* As an added bonus, the division is inverted (compared to
* view2d_scale_get_x()) so that we can multiply with the result instead of
* doing yet another division. */
const float target_scale = BLI_rctf_size_x(&view_rect) / BLI_rcti_size_x(&view_2d.mask);
const float margin_in_frames = margin_in_px * target_scale;
/* Limit the margin to a maximum of 12.5% of the available size. This will
* make the margins smaller when the view gets smaller, but for large views
* still retain the fixed size calculated above */
const float margin_max = 0.125f * BLI_rctf_size_x(&view_rect);
const float margin = std::min(margin_in_frames, margin_max);
rctf rect_with_margin = view_rect;
rect_with_margin.xmin -= margin;
rect_with_margin.xmax += margin;
return rect_with_margin;
}
} // namespace blender

View File

@@ -0,0 +1,77 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "BLI_math_constants.h"
#include "ED_anim_api.hh"
#include "DNA_anim_types.h"
#include "BKE_fcurve.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "RNA_define.hh"
#include "testing/testing.h"
namespace blender::animrig::tests {
class AnimDrawTest : public bke::BlenderGTestBase {
public:
Main *bmain;
Object *object;
void SetUp() override
{
this->bmain = BKE_main_new();
this->object = BKE_id_new<Object>(this->bmain, "OBTestObject");
}
void TearDown() override
{
BKE_main_free(this->bmain);
RNA_exit();
}
};
TEST_F(AnimDrawTest, anim_unit_mapping_get_factor_not_normalizing)
{
FCurve *fcurve = MEM_new<FCurve>(__func__);
fcurve->array_index = 0;
/* Avoid creating a Scene via BKE_id_new<Scene>(this->bmain, "SCTestScene"); as that requires
* much more setup (appdirs, imbuf for color management, and maybe more). This test doesn't
* actually need a full Scene, it just needs its `units` field. */
Scene scene = {};
scene.unit.scale_length = 1.0f;
{ /* Rotation: Degrees. */
scene.unit.system_rotation = 0;
BKE_fcurve_rnapath_set(*fcurve, "rotation_euler");
EXPECT_FLOAT_EQ(RAD2DEGF(1.0f),
ANIM_unit_mapping_get_factor(&scene, &this->object->id, fcurve, 0, nullptr));
EXPECT_FLOAT_EQ(1.0f / RAD2DEGF(1.0f),
ANIM_unit_mapping_get_factor(
&scene, &this->object->id, fcurve, ANIM_UNITCONV_RESTORE, nullptr));
}
{ /* Rotation: Radians. */
scene.unit.system_rotation = USER_UNIT_ROT_RADIANS;
BKE_fcurve_rnapath_set(*fcurve, "rotation_euler");
EXPECT_FLOAT_EQ(1.0f,
ANIM_unit_mapping_get_factor(&scene, &this->object->id, fcurve, 0, nullptr));
EXPECT_FLOAT_EQ(1.0f,
ANIM_unit_mapping_get_factor(
&scene, &this->object->id, fcurve, ANIM_UNITCONV_RESTORE, nullptr));
}
BKE_fcurve_free(fcurve);
}
} // namespace blender::animrig::tests

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,278 @@
/* SPDX-FileCopyrightText: 2024 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
#include "ANIM_action.hh"
#include "ANIM_fcurve.hh"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_global.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "DNA_space_types.h"
#include "ED_anim_api.hh"
#include "BLI_listbase.h"
#include "testing/testing.h"
namespace blender::animrig::tests {
class ActionFilterTest : public bke::BlenderGTestBase {
public:
Main *bmain;
Action *action;
Object *cube;
Object *suzanne;
void SetUp() override
{
bmain = BKE_main_new();
G_MAIN = bmain; /* For BKE_animdata_free(). */
action = &BKE_id_new<bAction>(bmain, "ACÄnimåtië")->wrap();
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
suzanne = BKE_object_add_only_object(bmain, OB_EMPTY, "OBSuzanne");
}
void TearDown() override
{
BKE_main_free(bmain);
G_MAIN = nullptr;
}
};
TEST_F(ActionFilterTest, slots_expanded_or_not)
{
Slot &slot_cube = action->slot_add();
Slot &slot_suzanne = action->slot_add();
ASSERT_TRUE(assign_action(action, cube->id));
ASSERT_TRUE(assign_action(action, suzanne->id));
ASSERT_EQ(assign_action_slot(&slot_cube, cube->id), ActionSlotAssignmentResult::OK);
ASSERT_EQ(assign_action_slot(&slot_suzanne, suzanne->id), ActionSlotAssignmentResult::OK);
Layer &layer = action->layer_add("Kübus layer");
Strip &key_strip = layer.strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = key_strip.data<StripKeyframeData>(*action);
/* Create multiple FCurves for multiple Slots. */
const KeyframeSettings settings = get_keyframe_settings(false);
ASSERT_EQ(
SingleKeyingResult::SUCCESS,
strip_data.keyframe_insert(bmain, slot_cube, {"location", 0}, {1.0f, 0.25f}, settings));
ASSERT_EQ(
SingleKeyingResult::SUCCESS,
strip_data.keyframe_insert(bmain, slot_cube, {"location", 1}, {1.0f, 0.25f}, settings));
ASSERT_EQ(
SingleKeyingResult::SUCCESS,
strip_data.keyframe_insert(bmain, slot_suzanne, {"location", 0}, {1.0f, 0.25f}, settings));
ASSERT_EQ(
SingleKeyingResult::SUCCESS,
strip_data.keyframe_insert(bmain, slot_suzanne, {"location", 1}, {1.0f, 0.25f}, settings));
Channelbag *cube_channelbag = strip_data.channelbag_for_slot(slot_cube);
ASSERT_NE(nullptr, cube_channelbag);
FCurve *fcu_cube_loc_x = cube_channelbag->fcurve_find({"location", 0});
FCurve *fcu_cube_loc_y = cube_channelbag->fcurve_find({"location", 1});
ASSERT_NE(nullptr, fcu_cube_loc_x);
ASSERT_NE(nullptr, fcu_cube_loc_y);
/* Mock an bAnimContext for the Animation editor, with the above Animation showing. */
SpaceAction saction = {};
saction.ads.filterflag = eDopeSheet_FilterFlag(0);
bAnimContext ac = {nullptr};
ac.bmain = bmain;
ac.datatype = ANIMCONT_ACTION;
ac.data = action;
ac.spacetype = SPACE_ACTION;
ac.sl = reinterpret_cast<SpaceLink *>(&saction);
ac.obact = cube;
ac.active_action = action;
ac.active_action_user = &cube->id;
ac.ads = &saction.ads;
{ /* Test with collapsed slots. */
slot_cube.set_expanded(false);
slot_suzanne.set_expanded(false);
/* This should produce 2 slots and no FCurves. */
ListBaseT<bAnimListElem> anim_data = {nullptr, nullptr};
eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE |
ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS |
ANIMFILTER_LIST_CHANNELS);
const int num_entries = ANIM_animdata_filter(
&ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype));
EXPECT_EQ(2, num_entries);
EXPECT_EQ(2, anim_data.count());
ASSERT_GE(num_entries, 1)
<< "Missing 1st ANIMTYPE_ACTION_SLOT entry, stopping to prevent crash";
const bAnimListElem *first_ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 0));
EXPECT_EQ(ANIMTYPE_ACTION_SLOT, first_ale->type);
EXPECT_EQ(ALE_ACTION_SLOT, first_ale->datatype);
EXPECT_EQ(&cube->id, first_ale->id) << "id should be the animated ID (" << cube->id.name
<< ") but is (" << first_ale->id->name << ")";
EXPECT_EQ(cube->adt, first_ale->adt) << "adt should be the animated ID's animation data";
EXPECT_EQ(&action->id, first_ale->fcurve_owner_id) << "fcurve_owner_id should be the Action";
EXPECT_EQ(&action->id, first_ale->key_data) << "key_data should be the Action";
EXPECT_EQ(&slot_cube, first_ale->data);
EXPECT_EQ(slot_cube.slot_flags, first_ale->flag);
ASSERT_GE(num_entries, 2)
<< "Missing 2nd ANIMTYPE_ACTION_SLOT entry, stopping to prevent crash";
const bAnimListElem *second_ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 1));
EXPECT_EQ(ANIMTYPE_ACTION_SLOT, second_ale->type);
EXPECT_EQ(&slot_suzanne, second_ale->data);
/* Assume the rest is set correctly, as it's the same code as tested above. */
ANIM_animdata_freelist(&anim_data);
}
{ /* Test with one expanded and one collapsed slot. */
slot_cube.set_expanded(true);
slot_suzanne.set_expanded(false);
/* This should produce 2 slots and 2 FCurves. */
ListBaseT<bAnimListElem> anim_data = {nullptr, nullptr};
eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE |
ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS |
ANIMFILTER_LIST_CHANNELS);
const int num_entries = ANIM_animdata_filter(
&ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype));
EXPECT_EQ(4, num_entries);
EXPECT_EQ(4, anim_data.count());
/* First should be Cube slot. */
ASSERT_GE(num_entries, 1) << "Missing 1st ale, stopping to prevent crash";
const bAnimListElem *ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 0));
EXPECT_EQ(ANIMTYPE_ACTION_SLOT, ale->type);
EXPECT_EQ(&slot_cube, ale->data);
/* After that the Cube's FCurves. */
ASSERT_GE(num_entries, 2) << "Missing 2nd ale, stopping to prevent crash";
ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 1));
EXPECT_EQ(ANIMTYPE_FCURVE, ale->type);
EXPECT_EQ(fcu_cube_loc_x, ale->data);
EXPECT_EQ(slot_cube.handle, ale->slot_handle);
ASSERT_GE(num_entries, 3) << "Missing 3rd ale, stopping to prevent crash";
ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 2));
EXPECT_EQ(ANIMTYPE_FCURVE, ale->type);
EXPECT_EQ(fcu_cube_loc_y, ale->data);
EXPECT_EQ(slot_cube.handle, ale->slot_handle);
/* And finally the Suzanne slot. */
ASSERT_GE(num_entries, 4) << "Missing 4th ale, stopping to prevent crash";
ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 3));
EXPECT_EQ(ANIMTYPE_ACTION_SLOT, ale->type);
EXPECT_EQ(&slot_suzanne, ale->data);
ANIM_animdata_freelist(&anim_data);
}
{ /* Test one expanded and one collapsed slot, and one Slot and one FCurve selected. */
slot_cube.set_expanded(true);
slot_cube.set_selected(false);
slot_suzanne.set_expanded(false);
slot_suzanne.set_selected(true);
fcu_cube_loc_x->flag &= ~FCURVE_SELECTED;
fcu_cube_loc_y->flag |= FCURVE_SELECTED;
/* This should produce 1 slot and 1 FCurve. */
ListBaseT<bAnimListElem> anim_data = {nullptr, nullptr};
eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE |
ANIMFILTER_SEL | ANIMFILTER_FOREDIT | ANIMFILTER_NODUPLIS |
ANIMFILTER_LIST_CHANNELS);
const int num_entries = ANIM_animdata_filter(
&ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype));
EXPECT_EQ(2, num_entries);
EXPECT_EQ(2, anim_data.count());
/* First should be Cube's selected FCurve. */
const bAnimListElem *ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 0));
EXPECT_EQ(ANIMTYPE_FCURVE, ale->type);
EXPECT_EQ(fcu_cube_loc_y, ale->data);
/* Second the Suzanne slot, as that's the only selected slot. */
ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 1));
EXPECT_EQ(ANIMTYPE_ACTION_SLOT, ale->type);
EXPECT_EQ(&slot_suzanne, ale->data);
ANIM_animdata_freelist(&anim_data);
}
}
TEST_F(ActionFilterTest, layered_action_active_fcurves)
{
Slot &slot_cube = action->slot_add();
/* The Action+Slot has to be assigned to what the bAnimContext thinks is the active Object.
* See the BLI_assert_msg() call in the ANIMCONT_ACTION case of ANIM_animdata_filter(). */
ASSERT_EQ(assign_action_and_slot(action, &slot_cube, cube->id), ActionSlotAssignmentResult::OK);
Layer &layer = action->layer_add("Kübus layer");
Strip &key_strip = layer.strip_add(*action, Strip::Type::Keyframe);
StripKeyframeData &strip_data = key_strip.data<StripKeyframeData>(*action);
/* Create multiple FCurves. */
const KeyframeSettings settings = get_keyframe_settings(false);
ASSERT_EQ(
SingleKeyingResult::SUCCESS,
strip_data.keyframe_insert(bmain, slot_cube, {"location", 0}, {1.0f, 0.25f}, settings));
ASSERT_EQ(
SingleKeyingResult::SUCCESS,
strip_data.keyframe_insert(bmain, slot_cube, {"location", 1}, {1.0f, 0.25f}, settings));
/* Set one F-Curve as the active one, and the other as inactive. The latter is necessary because
* by default the first curve is automatically marked active, but that's too trivial a test case
* (it's too easy to mistakenly just return the first-seen F-Curve). */
Channelbag *cube_channelbag = strip_data.channelbag_for_slot(slot_cube);
ASSERT_NE(nullptr, cube_channelbag);
FCurve *fcurve_active = cube_channelbag->fcurve_find({"location", 1});
fcurve_active->flag |= FCURVE_ACTIVE;
FCurve *fcurve_other = cube_channelbag->fcurve_find({"location", 0});
fcurve_other->flag &= ~FCURVE_ACTIVE;
/* Mock an bAnimContext for the Action editor. */
SpaceAction saction = {};
saction.ads.filterflag = eDopeSheet_FilterFlag(0);
bAnimContext ac = {nullptr};
ac.bmain = bmain;
ac.datatype = ANIMCONT_ACTION;
ac.data = action;
ac.spacetype = SPACE_ACTION;
ac.sl = reinterpret_cast<SpaceLink *>(&saction);
ac.obact = cube;
ac.active_action = action;
ac.active_action_user = &cube->id;
ac.ads = &saction.ads;
{
/* This should produce just the active F-Curve. */
ListBaseT<bAnimListElem> anim_data = {nullptr, nullptr};
eAnimFilter_Flags filter = (ANIMFILTER_DATA_VISIBLE | ANIMFILTER_LIST_VISIBLE |
ANIMFILTER_FCURVESONLY | ANIMFILTER_ACTIVE);
const int num_entries = ANIM_animdata_filter(
&ac, &anim_data, filter, ac.data, eAnimCont_Types(ac.datatype));
EXPECT_EQ(1, num_entries);
EXPECT_EQ(1, anim_data.count());
const bAnimListElem *first_ale = static_cast<bAnimListElem *>(BLI_findlink(&anim_data, 0));
EXPECT_EQ(ANIMTYPE_FCURVE, first_ale->type);
EXPECT_EQ(ALE_FCURVE, first_ale->datatype);
EXPECT_EQ(fcurve_active, first_ale->data);
ANIM_animdata_freelist(&anim_data);
}
}
} // namespace blender::animrig::tests

View File

@@ -0,0 +1,123 @@
/* SPDX-FileCopyrightText: 2009 Blender Authors, Joshua Leung.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#pragma once
#include "DNA_listBase.h"
namespace blender {
struct wmOperatorType;
/* size of string buffers used for animation channel displayed names */
#define ANIM_CHAN_NAME_SIZE 256
struct KeyingSet;
/* KeyingSets/Keyframing Interface ------------- */
/** List of builtin KeyingSets (defined in `blender/animrig/keyingsets.cc`). */
extern ListBaseT<KeyingSet> builtin_keyingsets;
/* Operator Define Prototypes ------------------- */
/* -------------------------------------------------------------------- */
/** \name Main Keyframe Management operators
*
* These handle keyframes management from various spaces.
* They only make use of Keying Sets.
* \{ */
void ANIM_OT_keyframe_insert(wmOperatorType *ot);
void ANIM_OT_keyframe_delete(wmOperatorType *ot);
void ANIM_OT_keyframe_insert_by_name(wmOperatorType *ot);
void ANIM_OT_keyframe_delete_by_name(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Main Keyframe Management operators
*
* These handle keyframes management from various spaces.
* They will handle the menus required for each space.
* \{ */
void ANIM_OT_keyframe_insert_menu(wmOperatorType *ot);
void ANIM_OT_keyframe_delete_v3d(wmOperatorType *ot);
void ANIM_OT_keyframe_delete_vse(wmOperatorType *ot);
void ANIM_OT_keyframe_clear_v3d(wmOperatorType *ot);
void ANIM_OT_keyframe_clear_vse(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Keyframe management operators for UI buttons (RMB menu)
* \{ */
void ANIM_OT_keyframe_insert_button(wmOperatorType *ot);
void ANIM_OT_keyframe_delete_button(wmOperatorType *ot);
void ANIM_OT_keyframe_clear_button(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name KeyingSet management operators for UI buttons (RMB menu)
* \{ */
void ANIM_OT_keyingset_button_add(wmOperatorType *ot);
void ANIM_OT_keyingset_button_remove(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name KeyingSet management operators for RNA collections/UI buttons
* \{ */
void ANIM_OT_keying_set_add(wmOperatorType *ot);
void ANIM_OT_keying_set_remove(wmOperatorType *ot);
void ANIM_OT_keying_set_path_add(wmOperatorType *ot);
void ANIM_OT_keying_set_path_remove(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name KeyingSet general operators
* \{ */
void ANIM_OT_keying_set_active_set(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Driver management operators for UI buttons (RMB menu)
* \{ */
void ANIM_OT_driver_button_add(wmOperatorType *ot);
void ANIM_OT_driver_button_remove(wmOperatorType *ot);
void ANIM_OT_driver_button_edit(wmOperatorType *ot);
void ANIM_OT_copy_driver_button(wmOperatorType *ot);
void ANIM_OT_paste_driver_button(wmOperatorType *ot);
/** \} */
/* -------------------------------------------------------------------- */
/** \name Pose Asset operators
* \{ */
namespace ed::animrig {
void POSELIB_OT_create_pose_asset(wmOperatorType *ot);
void POSELIB_OT_asset_modify(wmOperatorType *ot);
void POSELIB_OT_asset_delete(wmOperatorType *ot);
void POSELIB_OT_screenshot_preview(wmOperatorType *ot);
} // namespace ed::animrig
/** \} */
} // namespace blender

View File

@@ -0,0 +1,348 @@
/* SPDX-FileCopyrightText: 2001-2002 NaN Holding BV. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
/* This file contains code for presenting F-Curves and other animation data
* in the UI (especially for use in the Animation Editors).
*
* -- Joshua Leung, Dec 2008
*/
#include "MEM_guardedalloc.h"
#include "BLI_math_color.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BLT_translation.hh"
#include "DNA_anim_types.h"
#include "DNA_modifier_types.h"
#include "DNA_node_types.h"
#include "BKE_node.hh"
#include "BKE_node_runtime.hh"
#include "RNA_access.hh"
#include "RNA_path.hh"
#include "RNA_prototypes.hh"
#include "ED_anim_api.hh"
#include "ANIM_action.hh"
#include <fmt/format.h>
#include <cstring>
namespace blender {
struct StructRNA;
/* ----------------------- Getter functions ----------------------- */
std::optional<int> getname_anim_fcurve(char *name, ID *id, FCurve *fcu)
{
/* Could make an argument, it's a documented limit at the moment. */
constexpr size_t name_maxncpy = 256;
/* Handle some nullptr cases. */
if (name == nullptr) {
/* A 'get name' function should be able to get the name, otherwise it's a bug. */
BLI_assert_unreachable();
return {};
}
if (fcu == nullptr) {
BLI_strncpy_utf8(name, RPT_("<invalid>"), name_maxncpy);
return {};
}
if (fcu->rna_path == nullptr) {
BLI_strncpy_utf8(name, RPT_("<no path>"), name_maxncpy);
return {};
}
if (id == nullptr) {
BLI_snprintf_utf8(name, name_maxncpy, "%s[%d]", fcu->rna_path, fcu->array_index);
return {};
}
PointerRNA id_ptr = RNA_id_pointer_create(id);
PointerRNA ptr;
PropertyRNA *prop;
if (!RNA_path_resolve_property(&id_ptr, fcu->rna_path, &ptr, &prop)) {
/* Could not resolve the path, so just use the path itself as 'name'. */
BLI_snprintf_utf8(name, name_maxncpy, "\"%s[%d]\"", fcu->rna_path, fcu->array_index);
/* Tag F-Curve as disabled - as not usable path. */
fcu->flag |= FCURVE_DISABLED;
return {};
}
const char *structname = nullptr, *propname = nullptr;
char arrayindbuf[16];
const char *arrayname = nullptr;
bool free_structname = false;
/* For now, name will consist of 3 parts: struct-name, property name, array index
* There are several options possible:
* 1) <struct-name>.<property-name>.<array-index>
* i.e. Bone1.Location.X, or Object.Location.X
* 2) <array-index> <property-name> (<struct name>)
* i.e. X Location (Bone1), or X Location (Object)
*
* Currently, option 2 is in use, to try and make it easier to quickly identify F-Curves
* (it does have problems with looking rather odd though).
* Option 1 is better in terms of revealing a consistent sense of hierarchy though,
* which isn't so clear with option 2.
*/
/* For struct-name:
* - As base, we use a custom name from the structs if one is available
* - However, if we're showing sub-data of bones
* (probably there will be other exceptions later).
* need to include that info too since it gets confusing otherwise.
* - If a pointer just refers to the ID-block, then don't repeat this info
* since this just introduces clutter.
*/
char pchanName[name_maxncpy], constName[name_maxncpy];
if (BLI_str_quoted_substr(fcu->rna_path, "bones[", pchanName, sizeof(pchanName)) &&
BLI_str_quoted_substr(fcu->rna_path, "constraints[", constName, sizeof(constName)))
{
structname = BLI_sprintfN("%s : %s", pchanName, constName);
free_structname = true;
}
else if (ptr.data != ptr.owner_id) {
PropertyRNA *nameprop = RNA_struct_name_property(ptr.type);
if (nameprop) {
structname = RNA_property_string_get_alloc(&ptr, nameprop, nullptr, 0, nullptr);
free_structname = true;
}
else {
structname = RNA_struct_ui_name(ptr.type);
}
/* For the sequencer, a strip's 'Transform' or 'Crop' is a nested (under Strip)
* struct, but displaying the struct name alone is no meaningful information
* (and also cannot be filtered well), same for modifiers.
* So display strip name alongside as well. */
if (GS(ptr.owner_id->name) == ID_SCE) {
char stripname[name_maxncpy];
if (BLI_str_quoted_substr(
fcu->rna_path, "sequence_editor.strips_all[", stripname, sizeof(stripname)))
{
if (strstr(fcu->rna_path, ".transform.") || strstr(fcu->rna_path, ".crop.") ||
strstr(fcu->rna_path, ".modifiers["))
{
const char *structname_all = BLI_sprintfN("%s : %s", stripname, structname);
if (free_structname) {
MEM_delete(structname);
}
structname = structname_all;
free_structname = true;
}
}
}
if (RNA_struct_is_a(ptr.type, RNA_NodeSocket)) {
/* Display the name/label of a node socket's node to allow distinguishing multiple nodes. */
BLI_assert(GS(ptr.owner_id->name) == ID_NT);
const bNodeTree *ntree = reinterpret_cast<const bNodeTree *>(ptr.owner_id);
const bNodeSocket *socket = static_cast<const bNodeSocket *>(ptr.data);
const bNode &node = bke::node_find_node(*ntree, *socket);
if (free_structname) {
MEM_delete(structname);
}
structname = node.label_or_name().c_str();
free_structname = false;
}
else if (RNA_struct_is_a(ptr.type, RNA_Node)) {
/* Display the label of the node if available to distinguish nodes like "Value". */
BLI_assert(GS(ptr.owner_id->name) == ID_NT);
const bNode *node = static_cast<const bNode *>(ptr.data);
if (free_structname) {
MEM_delete(structname);
}
structname = node->label_or_name().c_str();
free_structname = false;
}
}
propname = RNA_property_ui_name(prop);
if (RNA_struct_is_a(ptr.type, RNA_NodesModifier)) {
/* Display geometry node properties with node-tree socket labels. */
const NodesModifierData *nmd = static_cast<const NodesModifierData *>(ptr.data);
if (nmd->node_group && !ID_MISSING(nmd->node_group)) {
if (const bNodeTreeInterfaceSocket *input = bke::node_find_interface_input_by_identifier(
*nmd->node_group, propname))
{
propname = input->name;
}
}
}
else if (RNA_struct_is_a(ptr.type, RNA_NodeSocket)) {
/* Use the socket's name rather than the "Default Value" name of the socket's RNA property. */
const bNodeSocket *socket = static_cast<const bNodeSocket *>(ptr.data);
propname = socket->name;
}
/* Array Index - only if applicable */
if (RNA_property_array_check(prop)) {
char c = RNA_property_array_item_char(prop, fcu->array_index);
/* we need to write the index to a temp buffer (in py syntax) */
if (c) {
SNPRINTF_UTF8(arrayindbuf, "%c ", c);
}
else {
SNPRINTF_UTF8(arrayindbuf, "[%d]", fcu->array_index);
}
arrayname = &arrayindbuf[0];
}
else {
/* no array index */
arrayname = "";
}
/* putting this all together into the buffer */
/* XXX we need to check for invalid names...
* XXX the name length limit needs to be passed in or as some define */
if (structname) {
BLI_snprintf_utf8(name, name_maxncpy, "%s%s (%s)", arrayname, propname, structname);
}
else {
BLI_snprintf_utf8(name, name_maxncpy, "%s%s", arrayname, propname);
}
/* free temp name if nameprop is set */
if (free_structname) {
MEM_delete(structname);
}
/* Use the property's owner struct icon. */
return RNA_struct_ui_icon(ptr.type);
}
std::string getname_anim_fcurve_for_slot(Main &bmain, const animrig::Slot &slot, FCurve &fcurve)
{
/* TODO: Refactor to avoid this variable. */
constexpr size_t name_maxncpy = 256;
char name_buffer[name_maxncpy];
name_buffer[0] = '\0';
/* Check the Slot's users to see if we can find an ID* that can resolve the F-Curve. */
for (ID *user : slot.users(bmain)) {
const std::optional<int> icon = getname_anim_fcurve(name_buffer, user, &fcurve);
if (icon.has_value()) {
/* Managed to find a name! */
return name_buffer;
}
}
if (!slot.users(bmain).is_empty()) {
/* This slot is assigned to at least one ID, and still the property it animates could not be
* found. There is no use in continuing. */
fcurve.flag |= FCURVE_DISABLED;
return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index);
}
/* If this part of the code is hit, the slot is not assigned to anything. The remainder of
* this function is all a best-effort attempt. Because of that, it will not set the
* FCURVE_DISABLED flag on the F-Curve, as having unassigned animation data is not an error (and
* that flag indicates an error). */
/* Fall back to the ID type of the slot for simple properties. */
if (!slot.has_idtype()) {
/* The Slot has never been assigned to any ID, so we don't even know what type of ID it is
* meant for. */
return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index);
}
if (StringRef(fcurve.rna_path).find(".") != StringRef::not_found) {
/* Not a simple property, so bail out. This needs path resolution, which needs an ID*. */
return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index);
}
/* Find the StructRNA for this Slot's ID type. */
StructRNA *srna = ID_code_to_RNA_type(slot.idtype);
if (!srna) {
return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index);
}
/* Find the property. */
PropertyRNA *prop = RNA_struct_type_find_property(srna, fcurve.rna_path);
if (!prop) {
return fmt::format("\"{}[{}]\"", fcurve.rna_path, fcurve.array_index);
}
/* Property Name is straightforward */
const char *propname = RNA_property_ui_name(prop);
/* Array Index - only if applicable */
if (!RNA_property_array_check(prop)) {
return propname;
}
std::string arrayname;
char c = RNA_property_array_item_char(prop, fcurve.array_index);
if (c) {
arrayname = std::string(1, c);
}
else {
arrayname = fmt::format("[{}]", fcurve.array_index);
}
return arrayname + " " + propname;
}
/* ------------------------------- Color Codes for F-Curve Channels ---------------------------- */
/* step between the major distinguishable color bands of the primary colors */
#define HSV_BANDWIDTH 0.3f
/* used to determine the color of F-Curves with FCURVE_COLOR_AUTO_RAINBOW set */
// void fcurve_rainbow(uint cur, uint tot, float *out)
void getcolor_fcurve_rainbow(int cur, int tot, float out[3])
{
float hsv[3], fac;
int grouping;
/* we try to divide the color into groupings of n colors,
* where n is:
* 3 - for 'odd' numbers of curves - there should be a majority of triplets of curves
* 4 - for 'even' numbers of curves - there should be a majority of quartets of curves
* so the base color is simply one of the three primary colors
*/
grouping = (4 - (tot % 2));
hsv[0] = HSV_BANDWIDTH * float(cur % grouping);
/* 'Value' (i.e. darkness) needs to vary so that larger sets of three will be
* 'darker' (i.e. smaller value), so that they don't look that similar to previous ones.
* However, only a range of 0.3 to 1.0 is really usable to avoid clashing
* with some other stuff
*/
fac = (float(cur) / float(tot)) * 0.7f;
/* the base color can get offset a bit so that the colors aren't so identical */
hsv[0] += fac * HSV_BANDWIDTH;
if (hsv[0] > 1.0f) {
hsv[0] = fmod(hsv[0], 1.0f);
}
/* saturation adjustments for more visible range */
hsv[1] = ((hsv[0] > 0.5f) && (hsv[0] < 0.8f)) ? 0.5f : 0.6f;
/* value is fixed at 1.0f, otherwise we cannot clearly see the curves... */
hsv[2] = 1.0f;
/* finally, convert this to RGB colors */
hsv_to_rgb_v(hsv, out);
}
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,538 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup bke
*/
#include "MEM_guardedalloc.h"
#include <cstdlib>
#include "BLI_bounds.hh"
#include "BLI_listbase.h"
#include "BLI_listbase_wrapper.hh"
#include "BLI_math_matrix.h"
#include "BLI_math_matrix.hh"
#include "BLI_math_vector.h"
#include "BLI_string.h"
#include "DNA_anim_types.h"
#include "DNA_armature_types.h"
#include "DNA_scene_types.h"
#include "BKE_action.hh"
#include "BKE_anim_data.hh"
#include "BKE_camera.h"
#include "BKE_main.hh"
#include "BKE_scene.hh"
#include "DEG_depsgraph.hh"
#include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_query.hh"
#include "GPU_batch.hh"
#include "GPU_vertex_buffer.hh"
#include "ED_anim_api.hh"
#include "ED_keyframes_keylist.hh"
#include "ANIM_action.hh"
#include "ANIM_action_legacy.hh"
#include "ANIM_animdata.hh"
#include "ANIM_bone_collections.hh"
#include "CLG_log.h"
namespace blender {
static CLG_LogRef LOG = {"anim.motion_paths"};
/* Motion path needing to be baked (mpt). */
struct MPathTarget {
bMotionPath *mpath; /* Motion path in question. */
AnimKeylist *keylist; /* Temp, to know where the keyframes are. */
/* Original (Source Objects) */
Object *ob; /* Source Object */
bPoseChannel *pchan; /* Source pose-channel (if applicable). */
};
/* ........ */
Depsgraph *animviz_depsgraph_build(Main *bmain,
Scene *scene,
ViewLayer *view_layer,
const Span<MPathTarget *> targets)
{
/* Allocate dependency graph. */
Depsgraph *depsgraph = DEG_graph_new(bmain, scene, view_layer, DAG_EVAL_VIEWPORT);
/* Make a flat array of IDs for the DEG API. */
Array<ID *> ids(targets.size());
int current_id_index = 0;
for (const MPathTarget *mpt : targets) {
ids[current_id_index++] = &mpt->ob->id;
}
/* Build graph from all requested IDs. */
DEG_graph_build_from_ids(depsgraph, ids);
return depsgraph;
}
void animviz_build_motionpath_targets(Object *ob, Vector<MPathTarget *> &r_targets)
{
/* TODO: it would be nice in future to be able to update objects dependent on these bones too? */
MPathTarget *mpt;
/* Object itself first. */
if ((ob->avs.recalc & ANIMVIZ_RECALC_PATHS) && (ob->mpath)) {
/* New target for object. */
mpt = MEM_new_zeroed<MPathTarget>("MPathTarget Ob");
mpt->mpath = ob->mpath;
mpt->ob = ob;
r_targets.append(mpt);
}
/* Bones. */
if ((ob->pose) && (ob->pose->avs.recalc & ANIMVIZ_RECALC_PATHS)) {
bArmature *arm = id_cast<bArmature *>(ob->data);
for (bPoseChannel &pchan : ob->pose->chanbase) {
if (!pchan.mpath) {
continue;
}
Bone *bone = pchan.bone_get(*ob);
if (!bone || !ANIM_bone_in_visible_collection(arm, bone)) {
continue;
}
/* New target for bone. */
mpt = MEM_new_zeroed<MPathTarget>("MPathTarget PoseBone");
mpt->mpath = pchan.mpath;
mpt->ob = ob;
mpt->pchan = &pchan;
r_targets.append(mpt);
}
}
}
void animviz_free_motionpath_targets(Vector<MPathTarget *> &targets)
{
for (MPathTarget *mpt : targets) {
MEM_delete(mpt);
}
targets.clear_and_shrink();
}
/* ........ */
/* Perform baking for the targets on the current frame. */
static void motionpaths_calc_bake_targets(const Span<MPathTarget *> targets,
const int cframe,
Depsgraph *depsgraph,
Object *camera)
{
/* For each target, check if it can be baked on the current frame. */
for (const MPathTarget *mpt : targets) {
bMotionPath *mpath = mpt->mpath;
/* Current frame must be within the range the cache works for.
* - is inclusive of the first frame, but not the last otherwise we get buffer overruns.
*/
if ((cframe < mpath->start_frame) || (cframe >= mpath->end_frame)) {
continue;
}
/* Get the relevant cache vert to write to. */
bMotionPathVert *mpv = mpath->points + (cframe - mpath->start_frame);
Object *ob_eval = DEG_get_evaluated(depsgraph, mpt->ob);
/* Lookup evaluated pose channel, here because the depsgraph
* evaluation can change them so they are not cached in mpt. */
bPoseChannel *pchan_eval = nullptr;
if (mpt->pchan) {
pchan_eval = BKE_pose_channel_find_name(ob_eval->pose, mpt->pchan->name);
}
/* Pose-channel or object path baking? */
if (pchan_eval) {
/* Heads or tails. */
if (mpath->flag & MOTIONPATH_FLAG_BHEAD) {
copy_v3_v3(mpv->co, pchan_eval->pose_head);
}
else {
copy_v3_v3(mpv->co, pchan_eval->pose_tail);
}
/* Result must be in world-space. */
mul_m4_v3(ob_eval->object_to_world().ptr(), mpv->co);
}
else {
/* World-space object location. */
copy_v3_v3(mpv->co, ob_eval->object_to_world().location());
}
if (mpath->flag & MOTIONPATH_FLAG_BAKE_CAMERA && camera) {
Object *cam_eval = DEG_get_evaluated(depsgraph, camera);
/* Aka projection matrix. */
float4x4 window_matrix;
Scene *scene = DEG_get_input_scene(depsgraph);
BKE_camera_multiview_window_matrix(&scene->r, cam_eval, nullptr, window_matrix.ptr());
/* World to Object is the view matrix. */
float4x4 perspective_matrix = window_matrix * cam_eval->world_to_object();
const float4 co_clip_space = perspective_matrix *
float4(mpv->co[0], mpv->co[1], mpv->co[2], 1.0);
/* Storing the verts in NDC space which contains lens effects like sensor offset. See
* `overlay_motion_path.hh/motion_path_sync`. Negative w values are behind the camera, thus
* can't be correctly projected into the scene. Using abs(w) is consistent with
* `project_point` in shader code. */
const float3 co_ndc_space = float3(co_clip_space) /
math::max(math::abs(co_clip_space.w), 0.0001f);
copy_v3_v3(mpv->co, co_ndc_space);
}
float mframe = float(cframe);
/* Tag if it's a keyframe. */
if (ED_keylist_find_exact(mpt->keylist, mframe)) {
mpv->flag |= MOTIONPATH_VERT_KEY;
}
else {
mpv->flag &= ~MOTIONPATH_VERT_KEY;
}
/* Incremental update on evaluated object if possible, for fast updating
* while dragging in transform. */
bMotionPath *mpath_eval = nullptr;
if (mpt->pchan) {
mpath_eval = (pchan_eval) ? pchan_eval->mpath : nullptr;
}
else {
mpath_eval = ob_eval->mpath;
}
if (mpath_eval && mpath_eval->length == mpath->length) {
bMotionPathVert *mpv_eval = mpath_eval->points + (cframe - mpath_eval->start_frame);
*mpv_eval = *mpv;
GPU_VERTBUF_DISCARD_SAFE(mpath_eval->points_vbo);
GPU_BATCH_DISCARD_SAFE(mpath_eval->batch_line);
GPU_BATCH_DISCARD_SAFE(mpath_eval->batch_points);
}
}
}
/* Get pointer to animviz settings for the given target. */
static bAnimVizSettings *animviz_target_settings_get(const MPathTarget *mpt)
{
if (mpt->pchan != nullptr) {
return &mpt->ob->pose->avs;
}
return &mpt->ob->avs;
}
/* Returns the combined range of all `MPathTarget` start and end frames. */
static Bounds<int> motionpath_get_global_framerange(const Span<MPathTarget *> targets)
{
Bounds<int> frame_range = {INT_MAX, INT_MIN};
for (const MPathTarget *mpt : targets) {
frame_range.min = min_ii(frame_range.min, mpt->mpath->start_frame);
frame_range.max = max_ii(frame_range.max, mpt->mpath->end_frame);
}
return frame_range;
}
static int motionpath_get_prev_keyframe(MPathTarget *mpt,
AnimKeylist *keylist,
const int current_frame)
{
/* TODO(jbakker): Remove complexity, key-lists are ordered. */
if (current_frame <= mpt->mpath->start_frame) {
return mpt->mpath->start_frame;
}
float current_frame_float = current_frame;
const ActKeyColumn *ak = ED_keylist_find_prev(keylist, current_frame_float);
if (ak == nullptr) {
return mpt->mpath->start_frame;
}
return ak->cfra;
}
static int motionpath_get_prev_prev_keyframe(MPathTarget *mpt,
AnimKeylist *keylist,
const int current_frame)
{
int frame = motionpath_get_prev_keyframe(mpt, keylist, current_frame);
return motionpath_get_prev_keyframe(mpt, keylist, frame);
}
static int motionpath_get_next_keyframe(MPathTarget *mpt,
AnimKeylist *keylist,
const int current_frame)
{
if (current_frame >= mpt->mpath->end_frame) {
return mpt->mpath->end_frame;
}
float current_frame_float = current_frame;
const ActKeyColumn *ak = ED_keylist_find_next(keylist, current_frame_float);
if (ak == nullptr) {
return mpt->mpath->end_frame;
}
return ak->cfra;
}
static int motionpath_get_next_next_keyframe(MPathTarget *mpt,
AnimKeylist *keylist,
const int current_frame)
{
int frame = motionpath_get_next_keyframe(mpt, keylist, current_frame);
return motionpath_get_next_keyframe(mpt, keylist, frame);
}
static bool motionpath_check_can_use_keyframe_range(MPathTarget * /*mpt*/,
AnimData *adt,
const Span<FCurve *> fcurves)
{
if (adt == nullptr || fcurves.is_empty()) {
return false;
}
/* NOTE: We might needed to do a full frame range update if there is a specific setup of NLA
* or drivers or modifiers on the f-curves. */
return true;
}
static Bounds<int> motionpath_calculate_update_range(MPathTarget *mpt,
AnimData *adt,
const Span<FCurve *> fcurves,
const int current_frame)
{
/* If the current frame is outside of the configured motion path range we ignore update of this
* motion path by using invalid frame range where start frame is above the end frame. */
if (current_frame < mpt->mpath->start_frame || current_frame > mpt->mpath->end_frame) {
return {INT_MAX, INT_MIN};
}
/* Similar to the case when there is only a single keyframe: need to update en entire range to
* a constant value. */
if (!motionpath_check_can_use_keyframe_range(mpt, adt, fcurves)) {
return {mpt->mpath->start_frame, mpt->mpath->end_frame};
}
Bounds<int> frame_range = {INT_MAX, INT_MIN};
/* NOTE: Iterate over individual f-curves, and check their keyframes individually and pick a
* widest range from them. This is because it's possible to have more narrow keyframe on a
* channel which wasn't edited.
* Could be optimized further by storing some flags about which channels has been modified so
* we ignore all others (which can potentially make an update range unnecessary wide). */
for (FCurve *fcu : fcurves) {
AnimKeylist *keylist = ED_keylist_create();
fcurve_to_keylist(adt, fcu, keylist, 0, {-FLT_MAX, FLT_MAX}, true);
ED_keylist_prepare_for_direct_access(keylist);
int fcu_sfra = motionpath_get_prev_prev_keyframe(mpt, keylist, current_frame);
int fcu_efra = motionpath_get_next_next_keyframe(mpt, keylist, current_frame);
/* Extend range further, since acceleration compensation propagates even further away. */
if (fcu->auto_smoothing != FCURVE_SMOOTH_NONE) {
fcu_sfra = motionpath_get_prev_prev_keyframe(mpt, keylist, fcu_sfra);
fcu_efra = motionpath_get_next_next_keyframe(mpt, keylist, fcu_efra + 1);
}
if (fcu_sfra <= fcu_efra) {
frame_range.min = min_ii(frame_range.min, fcu_sfra);
frame_range.max = max_ii(frame_range.max, fcu_efra + 1);
}
ED_keylist_free(keylist);
}
return frame_range;
}
static void motionpath_free_free_tree_data(MutableSpan<MPathTarget *> targets)
{
for (MPathTarget *mpt : targets) {
ED_keylist_free(mpt->keylist);
}
}
void animviz_motionpath_compute_range(Object *ob, Scene *scene)
{
bAnimVizSettings *avs = ob->mode == OB_MODE_POSE ? &ob->pose->avs : &ob->avs;
if (avs->path_range == MOTIONPATH_RANGE_MANUAL) {
/* Don't touch manually-determined ranges. */
return;
}
const bool has_action = ob->adt && ob->adt->action;
if (avs->path_range == MOTIONPATH_RANGE_SCENE || !has_action ||
!animrig::legacy::assigned_action_has_keyframes(ob->adt))
{
/* Default to the scene (preview) range if there is no animation data to
* find selected keys in. */
avs->path_sf = scene->playback_start();
avs->path_ef = scene->playback_end();
return;
}
AnimKeylist *keylist = ED_keylist_create();
for (FCurve *fcu : animrig::fcurves_for_assigned_action(ob->adt)) {
fcurve_to_keylist(ob->adt, fcu, keylist, 0, {-FLT_MAX, FLT_MAX}, true);
}
Bounds<float> frame_range;
switch (avs->path_range) {
case MOTIONPATH_RANGE_KEYS_SELECTED:
if (ED_keylist_selected_keys_frame_range(keylist, &frame_range)) {
break;
}
ATTR_FALLTHROUGH; /* Fall through if there were no selected keys found. */
case MOTIONPATH_RANGE_KEYS_ALL:
ED_keylist_all_keys_frame_range(keylist, &frame_range);
break;
case MOTIONPATH_RANGE_MANUAL:
case MOTIONPATH_RANGE_SCENE:
BLI_assert_msg(false, "This should not happen, function should have exited earlier.");
};
avs->path_sf = frame_range.min;
avs->path_ef = frame_range.max;
ED_keylist_free(keylist);
}
static void build_keylist_for_target(MPathTarget &target, AnimKeylist &keylist)
{
/* For object level motion paths this is a nullptr in which case the filtering is ignored. */
bPoseChannel *pose_bone = target.pchan;
for (FCurve *fcu : animrig::fcurves_for_assigned_action(target.ob->adt)) {
if (pose_bone &&
!animrig::fcurve_matches_collection_path(*fcu, "pose.bones[", pose_bone->name))
{
continue;
}
/* When only updating a subset of the motion path we could pass a range here to improve
* performance. */
fcurve_to_keylist(target.ob->adt, fcu, &keylist, 0, {-FLT_MAX, FLT_MAX}, true);
}
}
void animviz_calc_motionpaths(Depsgraph *depsgraph,
Scene *scene,
MutableSpan<MPathTarget *> targets,
eAnimvizCalcRange range)
{
using namespace blender::animrig;
BLI_assert_msg(!DEG_is_active(depsgraph),
"Motion path calculation should always happen with a minimal depsgraph.");
if (targets.is_empty()) {
return;
}
/* The frame range to calculate. Inclusive/Exclusive. */
Bounds<int> frame_range = {INT_MAX, INT_MIN};
switch (range) {
case ANIMVIZ_CALC_RANGE_CHANGED:
/* Nothing to do here, will be handled later when iterating through the targets. */
break;
case ANIMVIZ_CALC_RANGE_FULL:
frame_range = motionpath_get_global_framerange(targets);
if (frame_range.is_empty()) {
return;
}
break;
}
for (MPathTarget *mpt : targets) {
AnimData *adt = BKE_animdata_from_id(&mpt->ob->id);
/* Build list of all keyframes in active action for object or pchan. */
mpt->keylist = ED_keylist_create();
Vector<FCurve *> fcurves;
if (adt && adt->action) {
/* Get pointer to animviz settings for each target. */
bAnimVizSettings *avs = animviz_target_settings_get(mpt);
/* For bones it is likely that all FCurves belong to a group named after the bone. Only
* checking FCurves of a given group can improve performance when building the keylist. */
if ((mpt->pchan) && (avs->path_viewflag & MOTIONPATH_VIEW_KFACT) == 0) {
Action &action = adt->action->wrap();
bActionGroup *agrp = nullptr;
Channelbag *cbag = channelbag_for_action_slot(action, adt->slot_handle);
agrp = cbag ? cbag->channel_group_find(mpt->pchan->name) : nullptr;
if (agrp) {
fcurves = listbase_to_vector<FCurve>(agrp->channels);
action_group_to_keylist(adt, agrp, mpt->keylist, 0, {-FLT_MAX, FLT_MAX});
}
}
else {
build_keylist_for_target(*mpt, *mpt->keylist);
}
}
ED_keylist_prepare_for_direct_access(mpt->keylist);
if (range == ANIMVIZ_CALC_RANGE_CHANGED) {
const Bounds<int> target_bounds = motionpath_calculate_update_range(
mpt, adt, fcurves, scene->r.cfra);
if (!target_bounds.is_empty()) {
frame_range.min = min_ii(frame_range.min, target_bounds.min);
frame_range.max = max_ii(frame_range.max, target_bounds.max);
}
}
}
if (frame_range.is_empty()) {
motionpath_free_free_tree_data(targets);
return;
}
/* Calculate path over requested range. */
CLOG_INFO(&LOG,
"Calculating MotionPaths between frames %d - %d (%d frames)",
frame_range.min,
frame_range.max,
frame_range.max - frame_range.min + 1);
for (int frame = frame_range.min; frame < frame_range.max; frame++) {
/* Update relevant data for new frame. */
DEG_evaluate_on_framechange(depsgraph, frame);
/* Perform baking for targets. */
motionpaths_calc_bake_targets(targets, frame, depsgraph, scene->camera);
}
/* Clear recalc flags from targets. */
for (MPathTarget *mpt : targets) {
bMotionPath *mpath = mpt->mpath;
/* Get pointer to animviz settings for each target. */
bAnimVizSettings *avs = animviz_target_settings_get(mpt);
/* Clear the flag requesting recalculation of targets. */
avs->recalc &= ~ANIMVIZ_RECALC_PATHS;
/* Clean temp data. */
ED_keylist_free(mpt->keylist);
/* Free previous batches to force update. */
GPU_VERTBUF_DISCARD_SAFE(mpath->points_vbo);
GPU_BATCH_DISCARD_SAFE(mpath->batch_line);
GPU_BATCH_DISCARD_SAFE(mpath->batch_points);
}
}
} // namespace blender

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,928 @@
/* SPDX-FileCopyrightText: 2009 Blender Authors, Joshua Leung. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
/* System includes ----------------------------------------------------- */
#include <cfloat>
#include "MEM_guardedalloc.h"
#include "BKE_grease_pencil.hh"
#include "BKE_library.hh"
#include "BLI_listbase.h"
#include "BLI_math_vector.h"
#include "BLI_rect.h"
#include "DNA_anim_types.h"
#include "DNA_gpencil_legacy_types.h"
#include "DNA_grease_pencil_types.h"
#include "DNA_mask_types.h"
#include "GPU_immediate.hh"
#include "GPU_shader_shared.hh"
#include "GPU_state.hh"
#include "UI_interface.hh"
#include "UI_resources.hh"
#include "UI_view2d.hh"
#include "ED_anim_api.hh"
#include "ED_keyframes_draw.hh"
#include "ED_keyframes_keylist.hh"
#include "ANIM_action.hh"
namespace blender {
/* *************************** Keyframe Drawing *************************** */
void draw_keyframe_shape(const float x,
const float y,
float size,
const bool sel,
const eBezTriple_KeyframeType key_type,
const eKeyframeShapeDrawOpts mode,
const float alpha,
const KeyframeShaderBindings *sh_bindings,
const short handle_type,
const short extreme_type)
{
bool draw_fill = ELEM(mode, KEYFRAME_SHAPE_INSIDE, KEYFRAME_SHAPE_BOTH);
bool draw_outline = ELEM(mode, KEYFRAME_SHAPE_FRAME, KEYFRAME_SHAPE_BOTH);
BLI_assert(draw_fill || draw_outline);
/* Adjust size of keyframe shape according to type of keyframe. */
switch (key_type) {
case BEZT_KEYTYPE_KEYFRAME:
break;
case BEZT_KEYTYPE_BREAKDOWN:
size *= 0.85f;
break;
case BEZT_KEYTYPE_MOVEHOLD:
size *= 0.925f;
break;
case BEZT_KEYTYPE_EXTREME:
size *= 1.2f;
break;
case BEZT_KEYTYPE_JITTER:
size *= 0.8f;
break;
case BEZT_KEYTYPE_GENERATED:
size *= 0.75;
break;
}
uchar fill_col[4];
uchar outline_col[4];
uint flags = 0;
/* draw! */
if (draw_fill) {
/* get interior colors from theme (for selected and unselected only) */
switch (key_type) {
case BEZT_KEYTYPE_BREAKDOWN:
ui::theme::get_color_3ubv(sel ? TH_KEYTYPE_BREAKDOWN_SELECT : TH_KEYTYPE_BREAKDOWN,
fill_col);
break;
case BEZT_KEYTYPE_EXTREME:
ui::theme::get_color_3ubv(sel ? TH_KEYTYPE_EXTREME_SELECT : TH_KEYTYPE_EXTREME, fill_col);
break;
case BEZT_KEYTYPE_JITTER:
ui::theme::get_color_3ubv(sel ? TH_KEYTYPE_JITTER_SELECT : TH_KEYTYPE_JITTER, fill_col);
break;
case BEZT_KEYTYPE_MOVEHOLD:
ui::theme::get_color_3ubv(sel ? TH_KEYTYPE_MOVEHOLD_SELECT : TH_KEYTYPE_MOVEHOLD,
fill_col);
break;
case BEZT_KEYTYPE_KEYFRAME:
ui::theme::get_color_3ubv(sel ? TH_KEYTYPE_KEYFRAME_SELECT : TH_KEYTYPE_KEYFRAME,
fill_col);
break;
case BEZT_KEYTYPE_GENERATED:
ui::theme::get_color_3ubv(sel ? TH_KEYTYPE_GENERATED_SELECT : TH_KEYTYPE_GENERATED,
fill_col);
break;
}
/* For effects like graying out protected/muted channels. The theme RNA/UI doesn't allow users
* to set the alpha. */
fill_col[3] = 255.0f * alpha;
if (!draw_outline) {
/* force outline color to match */
outline_col[0] = fill_col[0];
outline_col[1] = fill_col[1];
outline_col[2] = fill_col[2];
outline_col[3] = fill_col[3];
}
}
if (draw_outline) {
/* exterior - black frame */
ui::theme::get_color_4ubv(sel ? TH_KEYBORDER_SELECT : TH_KEYBORDER, outline_col);
outline_col[3] *= alpha;
if (!draw_fill) {
/* fill color needs to be (outline.rgb, 0) */
fill_col[0] = outline_col[0];
fill_col[1] = outline_col[1];
fill_col[2] = outline_col[2];
fill_col[3] = 0;
}
/* Handle type to outline shape. */
switch (handle_type) {
case KEYFRAME_HANDLE_AUTO_CLAMP:
flags = GPU_KEYFRAME_SHAPE_CIRCLE;
break; /* circle */
case KEYFRAME_HANDLE_AUTO:
flags = GPU_KEYFRAME_SHAPE_CIRCLE | GPU_KEYFRAME_SHAPE_INNER_DOT;
break; /* circle with dot */
case KEYFRAME_HANDLE_VECTOR:
flags = GPU_KEYFRAME_SHAPE_SQUARE;
break; /* square */
case KEYFRAME_HANDLE_ALIGNED:
flags = GPU_KEYFRAME_SHAPE_DIAMOND | GPU_KEYFRAME_SHAPE_CLIPPED_VERTICAL;
break; /* clipped diamond */
case KEYFRAME_HANDLE_FREE:
default:
flags = GPU_KEYFRAME_SHAPE_DIAMOND; /* diamond */
}
/* Extreme type to arrow-like shading. */
if (extreme_type & KEYFRAME_EXTREME_MAX) {
flags |= GPU_KEYFRAME_SHAPE_ARROW_END_MAX;
}
if (extreme_type & KEYFRAME_EXTREME_MIN) {
flags |= GPU_KEYFRAME_SHAPE_ARROW_END_MIN;
}
if (extreme_type & GPU_KEYFRAME_SHAPE_ARROW_END_MIXED) {
flags |= 0x400;
}
}
immAttr1f(sh_bindings->size_id, size);
immAttr4ubv(sh_bindings->color_id, fill_col);
immAttr4ubv(sh_bindings->outline_color_id, outline_col);
immAttr1u(sh_bindings->flags_id, flags);
immVertex2f(sh_bindings->pos_id, x, y);
}
/* Common attributes shared between the draw calls. */
struct DrawKeylistUIData {
float alpha;
float icon_size;
float half_icon_size;
float smaller_size;
float ipo_size;
float gpencil_size;
float screenspace_margin;
float sel_color[4];
float unsel_color[4];
float sel_mhcol[4];
float unsel_mhcol[4];
float ipo_color_linear[4];
float ipo_color_constant[4];
float ipo_color_other[4];
float ipo_color_mix[4];
/* Show interpolation and handle type? */
bool show_ipo;
};
static void channel_ui_data_init(DrawKeylistUIData *ctx,
View2D *v2d,
float yscale_fac,
bool channel_locked,
eSAction_Flag saction_flag)
{
/* locked channels are less strongly shown, as feedback for locked channels in DopeSheet */
/* TODO: allow this opacity factor to be themed? */
ctx->alpha = channel_locked ? 0.25f : 1.0f;
ctx->icon_size = U.widget_unit * 0.5f * yscale_fac;
ctx->half_icon_size = 0.5f * ctx->icon_size;
ctx->smaller_size = 0.35f * ctx->icon_size;
ctx->ipo_size = 0.1f * ctx->icon_size;
ctx->gpencil_size = ctx->smaller_size * 0.8f;
ctx->screenspace_margin = (0.35f * float(UI_UNIT_X)) / ui::view2d_scale_get_x(v2d);
ctx->show_ipo = (saction_flag & SACTION_SHOW_INTERPOLATION) != 0;
ui::theme::get_color_4fv(TH_LONGKEY_SELECT, ctx->sel_color);
ui::theme::get_color_4fv(TH_LONGKEY, ctx->unsel_color);
ui::theme::get_color_4fv(TH_DOPESHEET_IPOLINE, ctx->ipo_color_linear);
ui::theme::get_color_4fv(TH_DOPESHEET_IPOCONST, ctx->ipo_color_constant);
ui::theme::get_color_4fv(TH_DOPESHEET_IPOOTHER, ctx->ipo_color_other);
ui::theme::get_color_4fv(TH_KEYTYPE_KEYFRAME, ctx->ipo_color_mix);
ctx->sel_color[3] *= ctx->alpha;
ctx->unsel_color[3] *= ctx->alpha;
ctx->ipo_color_linear[3] *= ctx->alpha;
ctx->ipo_color_constant[3] *= ctx->alpha;
ctx->ipo_color_other[3] *= ctx->alpha;
ctx->ipo_color_mix[3] *= ctx->alpha * 0.5f;
copy_v4_v4(ctx->sel_mhcol, ctx->sel_color);
ctx->sel_mhcol[3] *= 0.8f;
copy_v4_v4(ctx->unsel_mhcol, ctx->unsel_color);
ctx->unsel_mhcol[3] *= 0.8f;
}
static void draw_keylist_block_gpencil(const DrawKeylistUIData *ctx,
const ActKeyColumn *ab,
float ypos)
{
ui::draw_roundbox_corner_set(ui::CNR_TOP_RIGHT | ui::CNR_BOTTOM_RIGHT);
float size = 1.0f;
switch (ab->next->key_type) {
case BEZT_KEYTYPE_BREAKDOWN:
case BEZT_KEYTYPE_MOVEHOLD:
case BEZT_KEYTYPE_JITTER:
case BEZT_KEYTYPE_GENERATED:
size *= 0.5f;
break;
case BEZT_KEYTYPE_KEYFRAME:
size *= 0.8f;
break;
case BEZT_KEYTYPE_EXTREME:
break;
}
rctf box;
box.xmin = ab->cfra;
box.xmax = min_ff(ab->next->cfra - (ctx->screenspace_margin * size), ab->next->cfra);
box.ymin = ypos - ctx->gpencil_size;
box.ymax = ypos + ctx->gpencil_size;
ui::draw_roundbox_4fv(
&box, true, 0.25f * float(UI_UNIT_X), (ab->block.sel) ? ctx->sel_mhcol : ctx->unsel_mhcol);
}
static void draw_keylist_block_moving_hold(const DrawKeylistUIData *ctx,
const ActKeyColumn *ab,
float ypos)
{
rctf box;
box.xmin = ab->cfra;
box.xmax = ab->next->cfra;
box.ymin = ypos - ctx->smaller_size;
box.ymax = ypos + ctx->smaller_size;
ui::draw_roundbox_4fv(&box, true, 3.0f, (ab->block.sel) ? ctx->sel_mhcol : ctx->unsel_mhcol);
}
static void draw_keylist_block_standard(const DrawKeylistUIData *ctx,
const ActKeyColumn *ab,
float ypos)
{
/* The bar needs to be an odd number of pixels high for proper alignment. */
const int height = int(0.45f * (ctx->icon_size)) * 2 - 1;
rctf box;
box.xmin = ab->cfra;
box.xmax = ab->next->cfra;
box.ymin = round(ypos - (float(height) * 0.5f));
box.ymax = box.ymin + height;
ui::draw_roundbox_4fv(&box, true, 3.0f, (ab->block.sel) ? ctx->sel_color : ctx->unsel_color);
}
static void draw_keylist_block_interpolation_line(const DrawKeylistUIData *ctx,
const ActKeyColumn *ab,
float ypos)
{
rctf box;
box.xmin = ab->cfra;
box.xmax = ab->next->cfra;
box.ymin = ypos - ctx->ipo_size;
box.ymax = ypos + ctx->ipo_size;
/* Color for interpolation lines based on their type */
const float *color = nullptr;
constexpr short IPO_FLAGS = ACTKEYBLOCK_FLAG_IPO_OTHER | ACTKEYBLOCK_FLAG_IPO_LINEAR |
ACTKEYBLOCK_FLAG_IPO_CONSTANT;
if (ab->block.conflict & IPO_FLAGS) {
/* This is a summary line that combines multiple interpolation modes. */
color = ctx->ipo_color_mix;
}
else if (ab->block.flag & ACTKEYBLOCK_FLAG_IPO_OTHER) {
color = ctx->ipo_color_other;
}
else if (ab->block.flag & ACTKEYBLOCK_FLAG_IPO_LINEAR) {
color = ctx->ipo_color_linear;
}
else if (ab->block.flag & ACTKEYBLOCK_FLAG_IPO_CONSTANT) {
color = ctx->ipo_color_constant;
}
else {
/* No line to draw. */
return;
}
ui::draw_roundbox_4fv(&box, true, 3.0f, color);
}
static void draw_keylist_block(const DrawKeylistUIData *ctx, const ActKeyColumn *ab, float ypos)
{
/* Draw grease pencil bars between keyframes. */
if ((ab->next != nullptr) && (ab->block.flag & ACTKEYBLOCK_FLAG_GPENCIL)) {
draw_keylist_block_gpencil(ctx, ab, ypos);
}
else {
/* Draw other types. */
draw_roundbox_corner_set(ui::CNR_NONE);
int valid_hold = actkeyblock_get_valid_hold(ab);
if (valid_hold != 0) {
if ((valid_hold & ACTKEYBLOCK_FLAG_STATIC_HOLD) == 0) {
/* draw "moving hold" long-keyframe block - slightly smaller */
draw_keylist_block_moving_hold(ctx, ab, ypos);
}
else {
/* draw standard long-keyframe block */
draw_keylist_block_standard(ctx, ab, ypos);
}
}
if (ctx->show_ipo && actkeyblock_is_valid(ab) && (ab->block.flag)) {
/* draw an interpolation line */
draw_keylist_block_interpolation_line(ctx, ab, ypos);
}
}
}
static void draw_keylist_blocks(const DrawKeylistUIData *ctx,
const ActKeyColumn *keys,
const int key_len,
float ypos)
{
for (int i = 0; i < key_len; i++) {
const ActKeyColumn *ab = &keys[i];
draw_keylist_block(ctx, ab, ypos);
}
}
static bool draw_keylist_is_visible_key(const View2D *v2d, const ActKeyColumn *ak)
{
return IN_RANGE_INCL(ak->cfra, v2d->cur.xmin, v2d->cur.xmax);
}
static void draw_keylist_keys(const DrawKeylistUIData *ctx,
View2D *v2d,
const KeyframeShaderBindings *sh_bindings,
const ActKeyColumn *keys,
const int key_len,
float ypos,
eSAction_Flag saction_flag)
{
short handle_type = KEYFRAME_HANDLE_NONE, extreme_type = KEYFRAME_EXTREME_NONE;
for (int i = 0; i < key_len; i++) {
const ActKeyColumn *ak = &keys[i];
if (draw_keylist_is_visible_key(v2d, ak)) {
if (ctx->show_ipo) {
handle_type = ak->handle_type;
}
if (saction_flag & SACTION_SHOW_EXTREMES) {
extreme_type = ak->extreme_type;
}
draw_keyframe_shape(ak->cfra,
ypos,
ctx->icon_size,
(ak->sel & SELECT),
eBezTriple_KeyframeType(ak->key_type),
KEYFRAME_SHAPE_BOTH,
ctx->alpha,
sh_bindings,
handle_type,
extreme_type);
}
}
}
/* *************************** Drawing Stack *************************** */
enum class ChannelType {
SUMMARY,
SCENE,
OBJECT,
FCURVE,
ACTION_LAYERED,
ACTION_SLOT,
ACTION_LEGACY,
ACTION_GROUP,
GREASE_PENCIL_CELS,
GREASE_PENCIL_GROUP,
GREASE_PENCIL_DATA,
GREASE_PENCIL_LAYER,
MASK_LAYER,
};
struct ChannelListElement {
ChannelListElement *next, *prev;
AnimKeylist *keylist;
ChannelType type;
float yscale_fac;
float ypos;
eSAction_Flag saction_flag;
bool channel_locked;
/* Currently only used for F-Curve channels, because some should be nla
* remapped but not others. All other channel types ignore this, as it's clear
* from the type whether they should be nla remapped or not. */
bool use_nla_remapping;
/* TODO: check which of these can be put into a `union`: */
bAnimContext *ac;
bDopeSheet *ads;
Scene *sce;
Object *ob;
ID *animated_id; /* The ID that adt (below) belongs to. */
AnimData *adt;
FCurve *fcu;
bAction *act;
animrig::Slot *action_slot;
bActionGroup *agrp;
bGPDlayer *gpl;
const GreasePencilLayer *grease_pencil_layer;
const GreasePencilLayerTreeGroup *grease_pencil_layer_group;
const GreasePencil *grease_pencil;
MaskLayer *masklay;
};
static void build_channel_keylist(ChannelListElement *elem, float2 range)
{
switch (elem->type) {
case ChannelType::SUMMARY: {
summary_to_keylist(elem->ac, elem->keylist, elem->saction_flag, range);
break;
}
case ChannelType::SCENE: {
scene_to_keylist(elem->ads, elem->sce, elem->keylist, elem->saction_flag, range);
break;
}
case ChannelType::OBJECT: {
ob_to_keylist(elem->ads, elem->ob, elem->keylist, elem->saction_flag, range);
break;
}
case ChannelType::FCURVE: {
fcurve_to_keylist(
elem->adt, elem->fcu, elem->keylist, elem->saction_flag, range, elem->use_nla_remapping);
break;
}
case ChannelType::ACTION_LAYERED: {
/* This is only called for action summaries in the Dope-sheet, *not* the
* Action Editor. Therefore despite the name `ACTION_LAYERED`, this is
* only used to show a *single slot* of the action: the slot used by the
* ID the action is listed under.
*
* Thus we use the same function as the `ChannelType::ACTION_SLOT` case
* below because in practice the only distinction between these cases is
* where they get the slot from. In this case, we get it from `elem`'s
* ADT. */
BLI_assert(elem->act);
BLI_assert(elem->adt);
action_slot_summary_to_keylist(elem->ac,
elem->animated_id,
elem->act->wrap(),
elem->adt->slot_handle,
elem->keylist,
elem->saction_flag,
range);
break;
}
case ChannelType::ACTION_SLOT: {
BLI_assert(elem->act);
BLI_assert(elem->action_slot);
action_slot_summary_to_keylist(elem->ac,
elem->animated_id,
elem->act->wrap(),
elem->action_slot->handle,
elem->keylist,
elem->saction_flag,
range);
break;
}
case ChannelType::ACTION_LEGACY: {
action_to_keylist(elem->adt, elem->act, elem->keylist, elem->saction_flag, range);
break;
}
case ChannelType::ACTION_GROUP: {
action_group_to_keylist(elem->adt, elem->agrp, elem->keylist, elem->saction_flag, range);
break;
}
case ChannelType::GREASE_PENCIL_CELS: {
grease_pencil_cels_to_keylist(
elem->adt, elem->grease_pencil_layer, elem->keylist, elem->saction_flag);
break;
}
case ChannelType::GREASE_PENCIL_GROUP: {
grease_pencil_layer_group_to_keylist(
elem->adt, elem->grease_pencil_layer_group, elem->keylist, elem->saction_flag);
break;
}
case ChannelType::GREASE_PENCIL_DATA: {
if (elem->ac->datatype != ANIMCONT_GPENCIL && elem->adt) {
action_to_keylist(elem->adt, elem->adt->action, elem->keylist, elem->saction_flag, range);
}
grease_pencil_data_block_to_keylist(
elem->adt, elem->grease_pencil, elem->keylist, elem->saction_flag, false);
break;
}
case ChannelType::GREASE_PENCIL_LAYER: {
gpl_to_keylist(elem->ads, elem->gpl, elem->keylist);
break;
}
case ChannelType::MASK_LAYER: {
mask_to_keylist(elem->ads, elem->masklay, elem->keylist);
break;
}
}
}
static void draw_channel_blocks(ChannelListElement *elem, View2D *v2d)
{
DrawKeylistUIData ctx;
channel_ui_data_init(&ctx, v2d, elem->yscale_fac, elem->channel_locked, elem->saction_flag);
const int key_len = ED_keylist_array_len(elem->keylist);
const ActKeyColumn *keys = ED_keylist_array(elem->keylist);
draw_keylist_blocks(&ctx, keys, key_len, elem->ypos);
}
static void draw_channel_keys(ChannelListElement *elem,
View2D *v2d,
const KeyframeShaderBindings *sh_bindings)
{
DrawKeylistUIData ctx;
channel_ui_data_init(&ctx, v2d, elem->yscale_fac, elem->channel_locked, elem->saction_flag);
const int key_len = ED_keylist_array_len(elem->keylist);
const ActKeyColumn *keys = ED_keylist_array(elem->keylist);
draw_keylist_keys(&ctx, v2d, sh_bindings, keys, key_len, elem->ypos, elem->saction_flag);
}
static void prepare_channel_for_drawing(ChannelListElement *elem)
{
ED_keylist_prepare_for_direct_access(elem->keylist);
}
/** List of channels that are actually drawn because they are in view. */
struct ChannelDrawList {
ListBaseT<ChannelListElement> channels;
};
ChannelDrawList *ED_channel_draw_list_create()
{
return MEM_new_zeroed<ChannelDrawList>(__func__);
}
static void channel_list_build_keylists(ChannelDrawList *channel_list, float2 range)
{
for (ChannelListElement &elem : channel_list->channels) {
build_channel_keylist(&elem, range);
prepare_channel_for_drawing(&elem);
}
}
static void channel_list_draw_blocks(ChannelDrawList *channel_list, View2D *v2d)
{
for (ChannelListElement &elem : channel_list->channels) {
draw_channel_blocks(&elem, v2d);
}
}
static int channel_visible_key_len(const View2D *v2d, const ListBaseT<ActKeyColumn> *keys)
{
/* count keys */
uint len = 0;
for (ActKeyColumn &ak : *keys) {
/* Optimization: if keyframe doesn't appear within 5 units (screenspace)
* in visible area, don't draw.
* This might give some improvements,
* since we current have to flip between view/region matrices.
*/
if (draw_keylist_is_visible_key(v2d, &ak)) {
len++;
}
}
return len;
}
static int channel_list_visible_key_len(const ChannelDrawList *channel_list, const View2D *v2d)
{
uint len = 0;
for (ChannelListElement &elem : channel_list->channels) {
const ListBaseT<ActKeyColumn> *keys = ED_keylist_listbase(elem.keylist);
len += channel_visible_key_len(v2d, keys);
}
return len;
}
static void channel_list_draw_keys(ChannelDrawList *channel_list, View2D *v2d)
{
const int visible_key_len = channel_list_visible_key_len(channel_list, v2d);
if (visible_key_len == 0) {
return;
}
GPU_blend(GPU_BLEND_ALPHA);
GPUVertFormat *format = immVertexFormat();
KeyframeShaderBindings sh_bindings;
sh_bindings.pos_id = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
sh_bindings.size_id = GPU_vertformat_attr_add(format, "size", gpu::VertAttrType::SFLOAT_32);
sh_bindings.color_id = GPU_vertformat_attr_add(
format, "color", gpu::VertAttrType::UNORM_8_8_8_8);
sh_bindings.outline_color_id = GPU_vertformat_attr_add(
format, "outlineColor", gpu::VertAttrType::UNORM_8_8_8_8);
sh_bindings.flags_id = GPU_vertformat_attr_add(format, "flags", gpu::VertAttrType::UINT_32);
GPU_program_point_size(true);
immBindBuiltinProgram(GPU_SHADER_KEYFRAME_SHAPE);
immUniform1f("outline_scale", 1.0f);
immUniform2f("ViewportSize", BLI_rcti_size_x(&v2d->mask) + 1, BLI_rcti_size_y(&v2d->mask) + 1);
immBegin(GPU_PRIM_POINTS, visible_key_len);
for (ChannelListElement &elem : channel_list->channels) {
draw_channel_keys(&elem, v2d, &sh_bindings);
}
immEnd();
GPU_program_point_size(false);
immUnbindProgram();
GPU_blend(GPU_BLEND_NONE);
}
static void channel_list_draw(ChannelDrawList *channel_list, View2D *v2d)
{
channel_list_draw_blocks(channel_list, v2d);
channel_list_draw_keys(channel_list, v2d);
}
void ED_channel_list_flush(ChannelDrawList *channel_list, View2D *v2d)
{
channel_list_build_keylists(channel_list, {v2d->cur.xmin, v2d->cur.xmax});
channel_list_draw(channel_list, v2d);
}
void ED_channel_list_free(ChannelDrawList *channel_list)
{
for (ChannelListElement &elem : channel_list->channels) {
ED_keylist_free(elem.keylist);
}
channel_list->channels.free_no_destruct();
MEM_delete(channel_list);
}
static ChannelListElement *channel_list_add_element(ChannelDrawList *channel_list,
ChannelType elem_type,
float ypos,
float yscale_fac,
eSAction_Flag saction_flag)
{
ChannelListElement *draw_elem = MEM_new_zeroed<ChannelListElement>(__func__);
BLI_addtail(&channel_list->channels, draw_elem);
draw_elem->type = elem_type;
draw_elem->keylist = ED_keylist_create();
draw_elem->ypos = ypos;
draw_elem->yscale_fac = yscale_fac;
draw_elem->saction_flag = saction_flag;
return draw_elem;
}
/* *************************** Channel Drawing Functions *************************** */
void ED_add_summary_channel(ChannelDrawList *channel_list,
bAnimContext *ac,
float ypos,
float yscale_fac,
int saction_flag)
{
saction_flag &= ~SACTION_SHOW_EXTREMES;
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::SUMMARY, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->ac = ac;
}
void ED_add_scene_channel(ChannelDrawList *channel_list,
bDopeSheet *ads,
Scene *sce,
float ypos,
float yscale_fac,
int saction_flag)
{
saction_flag &= ~SACTION_SHOW_EXTREMES;
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::SCENE, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->ads = ads;
draw_elem->sce = sce;
}
void ED_add_object_channel(ChannelDrawList *channel_list,
bDopeSheet *ads,
Object *ob,
float ypos,
float yscale_fac,
int saction_flag)
{
saction_flag &= ~SACTION_SHOW_EXTREMES;
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::OBJECT, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->ads = ads;
draw_elem->ob = ob;
}
void ED_add_fcurve_channel(ChannelDrawList *channel_list,
bAnimListElem *ale,
FCurve *fcu,
float ypos,
float yscale_fac,
int saction_flag)
{
const bool locked = (fcu->flag & FCURVE_PROTECTED) ||
((fcu->grp) && (fcu->grp->flag & AGRP_PROTECTED)) ||
((ale->adt && ale->adt->action) &&
(!ID_IS_EDITABLE(ale->adt->action) ||
ID_IS_OVERRIDE_LIBRARY(ale->adt->action)));
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::FCURVE, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->animated_id = ale->id;
draw_elem->adt = ale->adt;
draw_elem->fcu = fcu;
draw_elem->channel_locked = locked;
draw_elem->use_nla_remapping = ANIM_nla_mapping_allowed(ale);
}
void ED_add_action_group_channel(ChannelDrawList *channel_list,
bAnimListElem *ale,
bActionGroup *agrp,
float ypos,
float yscale_fac,
int saction_flag)
{
bool locked = (agrp->flag & AGRP_PROTECTED) ||
((ale->adt && ale->adt->action) &&
(!ID_IS_EDITABLE(ale->adt->action) || ID_IS_OVERRIDE_LIBRARY(ale->adt->action)));
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::ACTION_GROUP, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->animated_id = ale->id;
draw_elem->adt = ale->adt;
draw_elem->agrp = agrp;
draw_elem->channel_locked = locked;
}
void ED_add_action_layered_channel(ChannelDrawList *channel_list,
bAnimContext *ac,
bAnimListElem *ale,
bAction *action,
const float ypos,
const float yscale_fac,
int saction_flag)
{
BLI_assert(action);
const bool locked = (!ID_IS_EDITABLE(action) || ID_IS_OVERRIDE_LIBRARY(action));
saction_flag &= ~SACTION_SHOW_EXTREMES;
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::ACTION_LAYERED, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->ac = ac;
draw_elem->animated_id = ale->id;
draw_elem->adt = ale->adt;
draw_elem->act = action;
draw_elem->channel_locked = locked;
}
void ED_add_action_slot_channel(ChannelDrawList *channel_list,
bAnimContext *ac,
bAnimListElem *ale,
animrig::Action &action,
animrig::Slot &slot,
const float ypos,
const float yscale_fac,
int saction_flag)
{
const bool locked = (ID_IS_LINKED(&action) || ID_IS_OVERRIDE_LIBRARY(&action));
saction_flag &= ~SACTION_SHOW_EXTREMES;
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::ACTION_SLOT, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->ac = ac;
draw_elem->animated_id = ale->id;
draw_elem->adt = ale->adt;
draw_elem->act = &action;
draw_elem->action_slot = &slot;
draw_elem->channel_locked = locked;
}
void ED_add_grease_pencil_datablock_channel(ChannelDrawList *channel_list,
bAnimContext *ac,
bAnimListElem *ale,
const GreasePencil *grease_pencil,
const float ypos,
const float yscale_fac,
int saction_flag)
{
ChannelListElement *draw_elem = channel_list_add_element(channel_list,
ChannelType::GREASE_PENCIL_DATA,
ypos,
yscale_fac,
eSAction_Flag(saction_flag));
/* GreasePencil properties can be animated via an Action, so the GP-related
* animation data is not limited to GP drawings. */
draw_elem->animated_id = ale->id;
draw_elem->adt = ale->adt;
draw_elem->act = ale->adt ? ale->adt->action : nullptr;
draw_elem->grease_pencil = grease_pencil;
draw_elem->ac = ac;
}
void ED_add_grease_pencil_cels_channel(ChannelDrawList *channel_list,
bDopeSheet *ads,
const GreasePencilLayer *layer,
const float ypos,
const float yscale_fac,
int saction_flag)
{
ChannelListElement *draw_elem = channel_list_add_element(channel_list,
ChannelType::GREASE_PENCIL_CELS,
ypos,
yscale_fac,
eSAction_Flag(saction_flag));
draw_elem->ads = ads;
draw_elem->grease_pencil_layer = layer;
draw_elem->channel_locked = layer->wrap().is_locked();
}
void ED_add_grease_pencil_layer_group_channel(ChannelDrawList *channel_list,
bDopeSheet *ads,
const GreasePencilLayerTreeGroup *layer_group,
const float ypos,
const float yscale_fac,
int saction_flag)
{
ChannelListElement *draw_elem = channel_list_add_element(channel_list,
ChannelType::GREASE_PENCIL_GROUP,
ypos,
yscale_fac,
eSAction_Flag(saction_flag));
draw_elem->ads = ads;
draw_elem->grease_pencil_layer_group = layer_group;
draw_elem->channel_locked = layer_group->wrap().is_locked();
}
void ED_add_grease_pencil_layer_legacy_channel(ChannelDrawList *channel_list,
bDopeSheet *ads,
bGPDlayer *gpl,
float ypos,
float yscale_fac,
int saction_flag)
{
bool locked = (gpl->flag & GP_LAYER_LOCKED) != 0;
ChannelListElement *draw_elem = channel_list_add_element(channel_list,
ChannelType::GREASE_PENCIL_LAYER,
ypos,
yscale_fac,
eSAction_Flag(saction_flag));
draw_elem->ads = ads;
draw_elem->gpl = gpl;
draw_elem->channel_locked = locked;
}
void ED_add_mask_layer_channel(ChannelDrawList *channel_list,
bDopeSheet *ads,
MaskLayer *masklay,
float ypos,
float yscale_fac,
int saction_flag)
{
bool locked = (masklay->flag & MASK_LAYERFLAG_LOCKED) != 0;
ChannelListElement *draw_elem = channel_list_add_element(
channel_list, ChannelType::MASK_LAYER, ypos, yscale_fac, eSAction_Flag(saction_flag));
draw_elem->ads = ads;
draw_elem->masklay = masklay;
draw_elem->channel_locked = locked;
}
} // namespace blender

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,163 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#pragma once
#include "BLI_map.hh"
#include "BLI_set.hh"
#include "BLI_string_ref.hh"
#include "ANIM_action.hh"
#include <limits>
#include <optional>
#include <string>
namespace blender {
struct FCurve;
struct ID;
struct Main;
namespace ed::animation {
/**
* Global copy/paste buffer for multi-slotted keyframe data.
*
* All the animation data managed by this struct is copied into it, and thus
* owned by this struct.
*/
struct KeyframeCopyBuffer {
/**
* The copied keyframes, in a ChannelBag per slot.
*
* Note that the slot handles are arbitrary, and are likely different from the
* handles of the original slots (i.e. the ones that the copied F-Curves were
* for). This is to make it possible to copy from different Actions (like is
* possible on the dope sheet) and still distinguish between their slots.
*/
animrig::StripKeyframeData keyframe_data;
/**
* Slot identifier used for slotless keyframes.
*
* These are keyframes copied from F-Curves not owned by an Action, such as drivers and NLA
* control curves.
*/
static constexpr const char *SLOTLESS_SLOT_IDENTIFIER = "";
/**
* Just a more-or-less randomly chosen number to start at.
*
* Having this distinctly different from DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE
* makes it easier to spot bugs.
*/
static constexpr animrig::slot_handle_t DEFAULT_LAST_USED_SLOT_HANDLE = 0x1acca;
animrig::slot_handle_t last_used_slot_handle = DEFAULT_LAST_USED_SLOT_HANDLE;
/**
* Mapping from slot handles to their identifiers.
*
* Since the StripKeyframeData only stores slot handles, and not their
* identifiers, this has to be stored here. An alternative would be to store
* the copied data into an Action, but that would allow for multi-layer,
* multi-strip data which is overkill for the functionality needed here.
*/
Map<animrig::slot_handle_t, std::string> slot_identifiers;
/**
* Mapping from slot handles to the ID that they were copied from.
*
* Multiple IDs can be animated by a single slot, in which case an arbitrary
* one is stored here. This pointer is only used to resolve RNA paths to find the
* property name, and thus the exact ID doesn't matter much.
*
* TODO(@sybren): it would be better to track the ID name here, instead of the pointer.
* That'll make it safer to work with when pasting into another file, or after
* the copied-from ID has been deleted. For now I am trying to keep
* things feature-par with the original code this is replacing.
*/
Map<animrig::slot_handle_t, ID *> slot_animated_ids;
/**
* Pointers to F-Curves in this->keyframe_data that animate bones.
*
* This is mostly to indicate which F-Curves are flipped when pasting flipped.
*/
Set<const FCurve *> bone_fcurves;
/* The first and last frames that got copied. */
float first_frame = std::numeric_limits<float>::infinity();
float last_frame = -std::numeric_limits<float>::infinity();
/** The current scene frame when copying. Used for the 'relative' paste method. */
float current_frame = 0.0f;
KeyframeCopyBuffer() = default;
KeyframeCopyBuffer(const KeyframeCopyBuffer &other) = delete;
~KeyframeCopyBuffer() = default;
bool is_empty() const;
bool is_single_fcurve() const;
bool is_bone(const FCurve &fcurve) const;
int num_slots() const;
animrig::Channelbag *channelbag_for_slot(StringRef slot_identifier);
/**
* Print the contents of the copy buffer to stdout.
*/
void debug_print() const;
};
extern KeyframeCopyBuffer *keyframe_copy_buffer;
/**
* Flip bone names in the RNA path, returning the flipped path.
*
* Returns empty optional if the `rna_path` is not animating a bone,
* i.e. doesn't have the `pose.bones["` prefix.
*/
std::optional<std::string> flip_names(StringRefNull rna_path);
/**
* Most strict paste buffer matching method: exact matches on RNA path and array index only.
*/
bool pastebuf_match_path_full(Main *bmain,
const FCurve &fcurve_to_match,
const FCurve &fcurve_in_copy_buffer,
animrig::slot_handle_t slot_handle_in_copy_buffer,
bool from_single,
bool to_single,
bool flip);
/**
* Medium strict paste buffer matching method: match the property name (so not the entire RNA path)
* and the array index.
*/
bool pastebuf_match_path_property(Main *bmain,
const FCurve &fcurve_to_match,
const FCurve &fcurve_in_copy_buffer,
animrig::slot_handle_t slot_handle_in_copy_buffer,
bool from_single,
bool to_single,
bool flip);
/**
* Least strict paste buffer matching method: array indices only.
*/
bool pastebuf_match_index_only(Main *bmain,
const FCurve &fcurve_to_match,
const FCurve &fcurve_in_copy_buffer,
animrig::slot_handle_t slot_handle_in_copy_buffer,
bool from_single,
bool to_single,
bool flip);
} // namespace ed::animation
} // namespace blender

View File

@@ -0,0 +1,755 @@
/* SPDX-FileCopyrightText: 2025 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "testing/testing.h"
#include "keyframes_general_intern.hh"
#include "BLI_listbase.h"
#include "BLI_string.h"
#include "BLI_string_utf8.h"
#include "BKE_armature.hh"
#include "BKE_fcurve.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_anim_types.h"
#include "DNA_object_types.h"
#include "ED_keyframes_edit.hh"
namespace blender {
using namespace blender::animrig;
namespace ed::animation::tests {
namespace {
/* std::unique_ptr for FCurve. */
struct fcurve_deleter {
void operator()(FCurve *fcurve) const
{
/* If this F-Curve was registered as "bone", remove it from that registration as well. */
keyframe_copy_buffer->bone_fcurves.remove(fcurve);
BKE_fcurve_free(fcurve);
}
};
using FCurvePtr = std::unique_ptr<FCurve, fcurve_deleter>;
/**
* Create a "fake" F-Curve. It does not belong to any Action, and has no keys.
* It just has its RNA path and array index set.
*/
FCurvePtr fake_fcurve(const char *rna_path, const int array_index)
{
FCurve *fcurve = BKE_fcurve_create();
if (rna_path) {
fcurve->rna_path = BLI_strdup(rna_path);
}
fcurve->array_index = array_index;
return FCurvePtr(fcurve);
}
/**
* Create a fake F-Curve (see above), and pretend it's been added to the copy buffer.
*
* This doesn't really add the F-Curve to the copy buffer, but rather just manipulates
* `keyframe_copy_buffer->bone_fcurves` and `keyframe_copy_buffer->slot_animated_ids` so that the
* F-Curve matching functions can do their work.
*/
FCurvePtr fake_fcurve_in_buffer(const char *rna_path,
const int array_index,
const bool is_bone,
const slot_handle_t slot_handle = Slot::unassigned,
ID *owner_id = nullptr)
{
FCurvePtr fcurve_ptr = fake_fcurve(rna_path, array_index);
if (is_bone) {
keyframe_copy_buffer->bone_fcurves.add(fcurve_ptr.get());
}
if (owner_id) {
keyframe_copy_buffer->slot_animated_ids.add_overwrite(slot_handle, owner_id);
}
return fcurve_ptr;
}
} // namespace
/**
* Keyframe pasting test suite.
*
* Currently this just tests the name flipping & F-Curve matching, and not the actual copy-pasting.
*/
struct keyframes_paste : public testing::Test {
static void SetUpTestSuite()
{
bke::gtest_setup();
ANIM_fcurves_copybuf_reset();
}
static void TearDownTestSuite()
{
ANIM_fcurves_copybuf_free();
bke::gtest_teardown();
}
};
TEST_F(keyframes_paste, flip_names)
{
EXPECT_EQ(std::nullopt, flip_names("whatever")) << "not a bone prefix";
EXPECT_EQ("pose.bones[\"head\"]", flip_names("pose.bones[\"head\"]"))
<< "unflippable name should remain unchanged";
EXPECT_EQ("pose.bones[\"Arm_L\"]", flip_names("pose.bones[\"Arm_R\"]"))
<< "flippable name should be flipped";
EXPECT_EQ("pose.bones[\"Arm_L\"].rotation_euler",
flip_names("pose.bones[\"Arm_R\"].rotation_euler"))
<< "flippable name should be flipped";
}
TEST_F(keyframes_paste, pastebuf_match_path_full)
{
constexpr slot_handle_t unassigned = Slot::unassigned;
{ /* NULL RNA paths. */
ANIM_fcurves_copybuf_reset();
FCurvePtr fcurve_target = fake_fcurve(nullptr, 0);
FCurvePtr fcurve_in_buffer = fake_fcurve_in_buffer(nullptr, 0, false);
/* Little wrapper for #pastebuf_match_path_full() to make it easier to see
* the differences between the test-cases. */
auto call = [&](const bool from_single, const bool to_single, const bool flip) {
return pastebuf_match_path_full(
nullptr, *fcurve_target, *fcurve_in_buffer, unassigned, from_single, to_single, flip);
};
/* This only matches when `to_single` is true. */
EXPECT_FALSE(call(false, false, false));
EXPECT_FALSE(call(false, false, true));
EXPECT_TRUE(call(false, true, false));
EXPECT_TRUE(call(false, true, true));
EXPECT_FALSE(call(true, false, false));
EXPECT_FALSE(call(true, false, true));
EXPECT_TRUE(call(true, true, false));
EXPECT_TRUE(call(true, true, true));
}
{ /* Many to many, no flipping. */
ANIM_fcurves_copybuf_reset();
FCurvePtr fcurve = fake_fcurve("location", 0);
EXPECT_TRUE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 0, false),
unassigned,
false,
false,
false));
EXPECT_FALSE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 1, false),
unassigned,
false,
false,
false))
<< "array index mismatch";
EXPECT_FALSE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("rotation_euler", 0, false),
unassigned,
false,
false,
false))
<< "rna path mismatch";
}
/* Many to many, Flipping bone names. */
{
ANIM_fcurves_copybuf_reset();
const bool from_single = false;
const bool to_single = false;
const bool flip = true;
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is NOT bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is NOT bone";
EXPECT_FALSE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, but array index mismatch";
}
/* Many to single (so only array index matters), Flipping bone names requested (but won't happen
* because 'to single'). */
{
ANIM_fcurves_copybuf_reset();
const bool from_single = false;
const bool to_single = true;
const bool flip = true;
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_TRUE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is NOT bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"nose\"].rotation_euler", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is bone";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, but array index mismatch";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, but array index mismatch";
}
{ /* Single (so array indices won't matter) to Many, Flipping bone names requested. */
ANIM_fcurves_copybuf_reset();
const bool from_single = true;
const bool to_single = false;
const bool flip = true;
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_FALSE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is NOT bone";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"nose\"].rotation_euler", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is bone";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, but array index mismatch";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, but array index mismatch";
}
{
/* Single (so array indices won't matter) to Many, NOT flipping bone names. */
ANIM_fcurves_copybuf_reset();
const bool from_single = true;
const bool to_single = false;
const bool flip = false;
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_FALSE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is NOT bone";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"nose\"].rotation_euler", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, but array index mismatch";
EXPECT_FALSE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, but array index mismatch";
}
/* Single to Single (so nothing should matter), Flipping bone names requested. */
{
ANIM_fcurves_copybuf_reset();
const bool from_single = true;
const bool to_single = true;
const bool flip = true;
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_TRUE(pastebuf_match_path_full(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is NOT bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"nose\"].rotation_euler", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "rna path mismatch, ACI is bone";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.R\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, but array index mismatch";
EXPECT_TRUE(pastebuf_match_path_full(
nullptr,
*fcurve,
*fake_fcurve_in_buffer("pose.bones[\"hand.L\"].location", 1, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, but array index mismatch";
}
}
TEST_F(keyframes_paste, pastebuf_match_path_property)
{
constexpr slot_handle_t unassigned = Slot::unassigned;
Main *bmain = BKE_main_new();
ID *arm_ob_id;
{ /* Set up an armature, to test matching on property names. */
bArmature *armature = BKE_armature_add(bmain, "Armature");
for (const auto &bone_name : {"hand.L", "hand.R", "middle"}) {
Bone *bone = MEM_new<Bone>(__func__);
STRNCPY_UTF8(bone->name, bone_name);
BLI_addtail(&armature->bonebase, bone);
}
Object *armature_object = BKE_object_add_only_object(bmain, OB_ARMATURE, "Armature");
armature_object->data = id_cast<ID *>(armature);
BKE_pose_ensure(bmain, armature_object, armature, false);
arm_ob_id = &armature_object->id;
}
/* Wrapper function to create an F-Curve in the copy buffer, animating the armature object. */
const auto fake_armob_fcurve =
[&](const char *rna_path, const int array_index, const bool is_bone) {
return fake_fcurve_in_buffer(rna_path, array_index, is_bone, unassigned, arm_ob_id);
};
{ /* From Single Channel, so array indices are ignored. */
ANIM_fcurves_copybuf_reset();
const bool from_single = true;
const bool to_single = false; /* Doesn't matter, function under test doesn't use this. */
const bool flip = false; /* Doesn't matter, function under test doesn't use this. */
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].location", 2, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, other array index";
EXPECT_FALSE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].rotation_euler", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "same bone, other property";
EXPECT_FALSE(pastebuf_match_path_property(bmain,
*fcurve,
*fake_armob_fcurve("rotation_euler", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "other struct, same property name";
EXPECT_FALSE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"missing\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "nonexistent bone, but same property name";
/* This just tests the current functionality. This may not necessarily be
* correct / desired behavior. */
FCurvePtr fcurve_with_long_rna_path = fake_fcurve(
"pose.bones[\"hand.L\"].weirdly_long_location", 0);
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "property name suffix-match";
}
{ /* From Multiple Channels, so array indices matter. */
ANIM_fcurves_copybuf_reset();
const bool from_single = false;
const bool to_single = false; /* Doesn't matter, function under test doesn't use this. */
const bool flip = false; /* Doesn't matter, function under test doesn't use this. */
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, is bone";
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.R\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "flipped path match, is bone";
EXPECT_FALSE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].location", 2, true),
unassigned,
from_single,
to_single,
flip))
<< "original path match, other array index";
EXPECT_FALSE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].rotation_euler", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "same bone, other property";
EXPECT_FALSE(pastebuf_match_path_property(bmain,
*fcurve,
*fake_armob_fcurve("rotation_euler", 0, false),
unassigned,
from_single,
to_single,
flip))
<< "other struct, same property name";
EXPECT_FALSE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"missing\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "nonexistent bone, but same property name";
/* This just tests the current functionality. This may not necessarily be
* correct / desired behavior. */
FCurvePtr fcurve_with_long_rna_path = fake_fcurve(
"pose.bones[\"hand.L\"].weirdly_long_location", 0);
EXPECT_TRUE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_armob_fcurve("pose.bones[\"hand.L\"].location", 0, true),
unassigned,
from_single,
to_single,
flip))
<< "property name suffix-match";
}
{ /* Resilience against deleted IDs. */
ANIM_fcurves_copybuf_reset();
FCurvePtr fcurve = fake_fcurve("pose.bones[\"hand.L\"].location", 0);
Object *object_not_in_main = BKE_object_add_only_object(nullptr, OB_EMPTY, "non-main");
EXPECT_FALSE(pastebuf_match_path_property(
bmain,
*fcurve,
*fake_fcurve_in_buffer(
"pose.bones[\"hand.L\"].location", 0, true, unassigned, &object_not_in_main->id),
unassigned,
false,
false,
false))
<< "copying from deleted ID";
BKE_id_free(nullptr, &object_not_in_main->id);
}
BKE_main_free(bmain);
}
TEST_F(keyframes_paste, pastebuf_match_index_only)
{
constexpr slot_handle_t unassigned = Slot::unassigned;
ANIM_fcurves_copybuf_reset();
FCurvePtr fcurve = fake_fcurve("some_prop", 1);
EXPECT_TRUE(pastebuf_match_index_only(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 1, false),
unassigned,
false,
false,
false));
EXPECT_FALSE(pastebuf_match_index_only(nullptr,
*fcurve,
*fake_fcurve_in_buffer("location", 2, false),
unassigned,
false,
false,
false));
}
} // namespace ed::animation::tests
} // namespace blender

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,364 @@
/* SPDX-FileCopyrightText: 2023 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "testing/testing.h"
#include "ANIM_action.hh"
#include "ANIM_fcurve.hh"
#include "ED_anim_api.hh"
#include "ED_keyframes_keylist.hh"
#include "DNA_anim_types.h"
#include "DNA_curve_types.h"
#include "MEM_guardedalloc.h"
#include "BKE_action.hh"
#include "BKE_armature.hh"
#include "BKE_fcurve.hh"
#include "BKE_global.hh"
#include "BKE_gtest_base.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "BLI_listbase.h"
#include "BLI_string_utf8.h"
#include <functional>
#include <optional>
namespace blender::editor::animation::tests {
const float KEYLIST_NEAR_ERROR = 0.1;
const float FRAME_STEP = 0.005;
/* Build FCurve with keys on frames 10, 20, and 30. */
static void build_fcurve(FCurve &fcurve)
{
fcurve.totvert = 3;
fcurve.bezt = MEM_new_array_zeroed<BezTriple>(fcurve.totvert, "BezTriples");
fcurve.bezt[0].vec[1][0] = 10.0f;
fcurve.bezt[0].vec[1][1] = 1.0f;
fcurve.bezt[1].vec[1][0] = 20.0f;
fcurve.bezt[1].vec[1][1] = 2.0f;
fcurve.bezt[2].vec[1][0] = 30.0f;
fcurve.bezt[2].vec[1][1] = 1.0f;
}
static AnimKeylist *create_test_keylist()
{
FCurve *fcurve = BKE_fcurve_create();
build_fcurve(*fcurve);
AnimKeylist *keylist = ED_keylist_create();
fcurve_to_keylist(nullptr, fcurve, keylist, 0, {-FLT_MAX, FLT_MAX}, false);
BKE_fcurve_free(fcurve);
ED_keylist_prepare_for_direct_access(keylist);
return keylist;
}
static void assert_act_key_column(const ActKeyColumn *column,
const std::optional<float> expected_frame)
{
if (expected_frame.has_value()) {
ASSERT_NE(column, nullptr) << "Expected a frame to be found at " << *expected_frame;
EXPECT_NEAR(column->cfra, *expected_frame, KEYLIST_NEAR_ERROR);
}
else {
EXPECT_EQ(column, nullptr) << "Expected no frame to be found, but found " << column->cfra;
}
}
using KeylistFindFunction = std::function<const ActKeyColumn *(const AnimKeylist *, float)>;
static void check_keylist_find_range(const AnimKeylist *keylist,
KeylistFindFunction keylist_find_func,
const float frame_from,
const float frame_to,
const std::optional<float> expected_frame)
{
float cfra = frame_from;
for (; cfra < frame_to; cfra += FRAME_STEP) {
const ActKeyColumn *found = keylist_find_func(keylist, cfra);
assert_act_key_column(found, expected_frame);
}
}
static void check_keylist_find_next_range(const AnimKeylist *keylist,
const float frame_from,
const float frame_to,
const std::optional<float> expected_frame)
{
check_keylist_find_range(keylist, ED_keylist_find_next, frame_from, frame_to, expected_frame);
}
class KeyListTest : public bke::BlenderGTestBase {};
TEST_F(KeyListTest, find_next)
{
AnimKeylist *keylist = create_test_keylist();
check_keylist_find_next_range(keylist, 0.0f, 9.99f, 10.0f);
check_keylist_find_next_range(keylist, 10.0f, 19.99f, 20.0f);
check_keylist_find_next_range(keylist, 20.0f, 29.99f, 30.0f);
check_keylist_find_next_range(keylist, 30.0f, 39.99f, std::nullopt);
ED_keylist_free(keylist);
}
static void check_keylist_find_prev_range(const AnimKeylist *keylist,
const float frame_from,
const float frame_to,
const std::optional<float> expected_frame)
{
check_keylist_find_range(keylist, ED_keylist_find_prev, frame_from, frame_to, expected_frame);
}
TEST_F(KeyListTest, find_prev)
{
AnimKeylist *keylist = create_test_keylist();
check_keylist_find_prev_range(keylist, 0.0f, 10.00f, std::nullopt);
check_keylist_find_prev_range(keylist, 10.01f, 20.00f, 10.0f);
check_keylist_find_prev_range(keylist, 20.01f, 30.00f, 20.0f);
check_keylist_find_prev_range(keylist, 30.01f, 49.99f, 30.0f);
ED_keylist_free(keylist);
}
static void check_keylist_find_exact_range(const AnimKeylist *keylist,
const float frame_from,
const float frame_to,
const std::optional<float> expected_frame)
{
check_keylist_find_range(keylist, ED_keylist_find_exact, frame_from, frame_to, expected_frame);
}
TEST_F(KeyListTest, find_exact)
{
AnimKeylist *keylist = create_test_keylist();
check_keylist_find_exact_range(keylist, 0.0f, 9.99f, std::nullopt);
check_keylist_find_exact_range(keylist, 9.9901f, 10.01f, 10.0f);
check_keylist_find_exact_range(keylist, 10.01f, 19.99f, std::nullopt);
check_keylist_find_exact_range(keylist, 19.9901f, 20.01f, 20.0f);
check_keylist_find_exact_range(keylist, 20.01f, 29.99f, std::nullopt);
check_keylist_find_exact_range(keylist, 29.9901f, 30.01f, 30.0f);
check_keylist_find_exact_range(keylist, 30.01f, 49.99f, std::nullopt);
ED_keylist_free(keylist);
}
TEST_F(KeyListTest, find_closest)
{
AnimKeylist *keylist = create_test_keylist();
{
const ActKeyColumn *closest = ED_keylist_find_closest(keylist, -1);
EXPECT_EQ(closest->cfra, 10.0);
}
{
const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 10);
EXPECT_EQ(closest->cfra, 10.0);
}
{
const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 14.999);
EXPECT_EQ(closest->cfra, 10.0);
}
{
/* When the distance between key columns is equal, the previous column is chosen */
const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 15);
EXPECT_EQ(closest->cfra, 10.0);
}
{
const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 15.001);
EXPECT_EQ(closest->cfra, 20.0);
}
{
const ActKeyColumn *closest = ED_keylist_find_closest(keylist, 30.001);
EXPECT_EQ(closest->cfra, 30.0);
}
ED_keylist_free(keylist);
}
class KeylistSummaryTest : public bke::BlenderGTestBase {
public:
Main *bmain;
animrig::Action *action;
Object *cube;
Object *armature;
bArmature *armature_data;
Bone *bone1;
Bone *bone2;
SpaceAction saction = {};
bAnimContext ac = {nullptr};
void SetUp() override
{
bmain = BKE_main_new();
G_MAIN = bmain; /* For BKE_animdata_free(). */
action = &BKE_id_new<bAction>(bmain, "ACÄnimåtië")->wrap();
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
armature_data = BKE_armature_add(bmain, "ARArmature");
bone1 = MEM_new<Bone>("KeylistSummaryTest");
bone2 = MEM_new<Bone>("KeylistSummaryTest");
STRNCPY_UTF8(bone1->name, "Bone.001");
STRNCPY_UTF8(bone2->name, "Bone.002");
BLI_addtail(&armature_data->bonebase, bone1);
BLI_addtail(&armature_data->bonebase, bone2);
BKE_armature_bone_hash_make(armature_data);
armature = BKE_object_add_only_object(bmain, OB_ARMATURE, "OBArmature");
armature->data = id_cast<ID *>(armature_data);
BKE_pose_ensure(bmain, armature, armature_data, false);
/*
* Fill in the common bits for the mock bAnimContext, for an Action editor.
*
* Tests should fill in:
* - ac.obact
* - ac.active_action_user (= &ac.obact.id)
*/
saction.ads.filterflag = eDopeSheet_FilterFlag(0);
ac.bmain = bmain;
ac.datatype = ANIMCONT_ACTION;
ac.data = action;
ac.spacetype = SPACE_ACTION;
ac.sl = reinterpret_cast<SpaceLink *>(&saction);
ac.ads = &saction.ads;
ac.active_action = action;
}
void TearDown() override
{
ac.obact = nullptr;
ac.active_action = nullptr;
ac.active_action_user = nullptr;
BKE_main_free(bmain);
G_MAIN = nullptr;
}
};
TEST_F(KeylistSummaryTest, slot_summary_simple)
{
/* Test that a key summary is generated correctly for a slot that's animating
* an object's transforms. */
using namespace blender::animrig;
Slot &slot_cube = action->slot_add_for_id(cube->id);
ASSERT_EQ(ActionSlotAssignmentResult::OK, assign_action_and_slot(action, &slot_cube, cube->id));
Channelbag &channelbag = action_channelbag_ensure(*action, cube->id);
FCurve &loc_x = channelbag.fcurve_ensure(bmain, {"location", 0});
FCurve &loc_y = channelbag.fcurve_ensure(bmain, {"location", 1});
FCurve &loc_z = channelbag.fcurve_ensure(bmain, {"location", 2});
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&loc_x, {1.0, 0.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&loc_x, {2.0, 1.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&loc_y, {2.0, 2.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&loc_y, {3.0, 3.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&loc_z, {2.0, 4.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&loc_z, {5.0, 5.0}, {}, {}));
/* Generate slot summary keylist. */
AnimKeylist *keylist = ED_keylist_create();
ac.obact = cube;
ac.active_action_user = &cube->id;
action_slot_summary_to_keylist(
&ac, &cube->id, *action, slot_cube.handle, keylist, 0, {0.0, 6.0});
ED_keylist_prepare_for_direct_access(keylist);
const ActKeyColumn *col_0 = ED_keylist_find_exact(keylist, 0.0);
const ActKeyColumn *col_1 = ED_keylist_find_exact(keylist, 1.0);
const ActKeyColumn *col_2 = ED_keylist_find_exact(keylist, 2.0);
const ActKeyColumn *col_3 = ED_keylist_find_exact(keylist, 3.0);
const ActKeyColumn *col_4 = ED_keylist_find_exact(keylist, 4.0);
const ActKeyColumn *col_5 = ED_keylist_find_exact(keylist, 5.0);
const ActKeyColumn *col_6 = ED_keylist_find_exact(keylist, 6.0);
/* Check that we only have columns at the frames with keys. */
EXPECT_EQ(nullptr, col_0);
EXPECT_NE(nullptr, col_1);
EXPECT_NE(nullptr, col_2);
EXPECT_NE(nullptr, col_3);
EXPECT_EQ(nullptr, col_4);
EXPECT_NE(nullptr, col_5);
EXPECT_EQ(nullptr, col_6);
/* Check that the right number of keys are indicated in each column. */
EXPECT_EQ(1, col_1->totkey);
EXPECT_EQ(3, col_2->totkey);
EXPECT_EQ(1, col_3->totkey);
EXPECT_EQ(1, col_5->totkey);
ED_keylist_free(keylist);
}
TEST_F(KeylistSummaryTest, slot_summary_bone_selection)
{
/* Test that a key summary is generated correctly, excluding keys for
* unselected bones when filter-by-selection is on. */
using namespace blender::animrig;
Slot &slot_armature = action->slot_add_for_id(armature->id);
ASSERT_EQ(ActionSlotAssignmentResult::OK,
assign_action_and_slot(action, &slot_armature, armature->id));
Channelbag &channelbag = action_channelbag_ensure(*action, armature->id);
FCurve &bone1_loc_x = channelbag.fcurve_ensure(
bmain, {"pose.bones[\"Bone.001\"].location", 0, {}, {}, "Bone.001"});
FCurve &bone2_loc_x = channelbag.fcurve_ensure(
bmain, {"pose.bones[\"Bone.002\"].location", 0, {}, {}, "Bone.002"});
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&bone1_loc_x, {1.0, 0.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&bone1_loc_x, {2.0, 1.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&bone2_loc_x, {2.0, 2.0}, {}, {}));
ASSERT_EQ(SingleKeyingResult::SUCCESS, insert_vert_fcurve(&bone2_loc_x, {3.0, 3.0}, {}, {}));
/* Select only Bone.001. */
bPoseChannel *pose_bone1 = BKE_pose_channel_find_name(armature->pose, bone1->name);
ASSERT_NE(pose_bone1, nullptr);
pose_bone1->flag |= POSE_SELECTED;
bPoseChannel *pose_bone2 = BKE_pose_channel_find_name(armature->pose, bone2->name);
pose_bone2->flag &= ~POSE_SELECTED;
/* Generate slot summary keylist. */
AnimKeylist *keylist = ED_keylist_create();
saction.ads.filterflag = ADS_FILTER_ONLYSEL; /* Filter by selection. */
ac.obact = armature;
ac.active_action_user = &armature->id;
ac.filters.flag = eDopeSheet_FilterFlag(saction.ads.filterflag);
action_slot_summary_to_keylist(
&ac, &armature->id, *action, slot_armature.handle, keylist, 0, {0.0, 6.0});
ED_keylist_prepare_for_direct_access(keylist);
const ActKeyColumn *col_1 = ED_keylist_find_exact(keylist, 1.0);
const ActKeyColumn *col_2 = ED_keylist_find_exact(keylist, 2.0);
const ActKeyColumn *col_3 = ED_keylist_find_exact(keylist, 3.0);
/* Check that we only have columns at the frames with keys for Bone.001. */
EXPECT_NE(nullptr, col_1);
EXPECT_NE(nullptr, col_2);
EXPECT_EQ(nullptr, col_3);
/* Check that the right number of keys are indicated in each column. */
EXPECT_EQ(1, col_1->totkey);
EXPECT_EQ(1, col_2->totkey);
ED_keylist_free(keylist);
}
} // namespace blender::editor::animation::tests

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,734 @@
/* SPDX-FileCopyrightText: 2009 Blender Authors, Joshua Leung. All rights reserved.
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#include <cfloat>
#include <cstddef>
#include <cstring>
#include "MEM_guardedalloc.h"
#include "DNA_anim_types.h"
#include "DNA_scene_types.h"
#include "BLI_listbase.h"
#include "BKE_animsys.h"
#include "BKE_context.hh"
#include "BKE_report.hh"
#include "ANIM_keyframing.hh"
#include "ANIM_keyingsets.hh"
#include "ED_keyframing.hh"
#include "ED_screen.hh"
#include "UI_interface.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_enum_types.hh"
#include "RNA_path.hh"
#include "anim_intern.hh"
namespace blender {
/* ************************************************** */
/* KEYING SETS - OPERATORS (for use in UI panels) */
/* These operators are really duplication of existing functionality, but just for completeness,
* they're here too, and will give the basic data needed...
*/
/* poll callback for adding default KeyingSet */
static bool keyingset_poll_default_add(bContext *C)
{
/* As long as there's an active Scene, it's fine. */
return (CTX_data_scene(C) != nullptr);
}
/* Poll callback for editing active KeyingSet. */
static bool keyingset_poll_active_edit(bContext *C)
{
Scene *scene = CTX_data_scene(C);
if (scene == nullptr) {
return false;
}
/* There must be an active KeyingSet (and KeyingSets). */
return ((scene->active_keyingset > 0) && (scene->keyingsets.first));
}
/* poll callback for editing active KeyingSet Path */
static bool keyingset_poll_activePath_edit(bContext *C)
{
Scene *scene = CTX_data_scene(C);
if (scene == nullptr) {
return false;
}
if (scene->active_keyingset <= 0) {
return false;
}
KeyingSet *keyingset = static_cast<KeyingSet *>(
BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
/* there must be an active KeyingSet and an active path */
return ((keyingset) && (keyingset->paths.first) && (keyingset->active_path > 0));
}
/* Add a Default (Empty) Keying Set ------------------------- */
static wmOperatorStatus add_default_keyingset_exec(bContext *C, wmOperator * /*op*/)
{
Scene *scene = CTX_data_scene(C);
/* Validate flags
* - absolute KeyingSets should be created by default.
*/
const eKS_Settings flag = KEYINGSET_ABSOLUTE;
const eInsertKeyFlags keyingflag = animrig::get_keyframing_flags(scene);
/* Call the API func, and set the active keyingset index. */
BKE_keyingset_add(&scene->keyingsets, nullptr, nullptr, flag, keyingflag);
scene->active_keyingset = scene->keyingsets.count();
WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr);
return OPERATOR_FINISHED;
}
void ANIM_OT_keying_set_add(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Add Empty Keying Set";
ot->idname = "ANIM_OT_keying_set_add";
ot->description = "Add a new (empty) keying set to the active Scene";
/* Callbacks. */
ot->exec = add_default_keyingset_exec;
ot->poll = keyingset_poll_default_add;
}
/* Remove 'Active' Keying Set ------------------------- */
static wmOperatorStatus remove_active_keyingset_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
/* Verify the Keying Set to use:
* - use the active one
* - return error if it doesn't exist
*/
if (scene->active_keyingset == 0) {
BKE_report(op->reports, RPT_ERROR, "No active Keying Set to remove");
return OPERATOR_CANCELLED;
}
if (scene->active_keyingset < 0) {
BKE_report(op->reports, RPT_ERROR, "Cannot remove built in keying set");
return OPERATOR_CANCELLED;
}
KeyingSet *keyingset = static_cast<KeyingSet *>(
BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
/* Free KeyingSet's data, then remove it from the scene. */
BKE_keyingset_free_paths(keyingset);
BLI_freelinkN(&scene->keyingsets, keyingset);
/* The active one should now be the previously second-to-last one. */
scene->active_keyingset--;
WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr);
return OPERATOR_FINISHED;
}
void ANIM_OT_keying_set_remove(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Remove Active Keying Set";
ot->idname = "ANIM_OT_keying_set_remove";
ot->description = "Remove the active keying set";
/* Callbacks. */
ot->exec = remove_active_keyingset_exec;
ot->poll = keyingset_poll_active_edit;
}
/* Add Empty Keying Set Path ------------------------- */
static wmOperatorStatus add_empty_ks_path_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
/* Verify the Keying Set to use:
* - use the active one
* - return error if it doesn't exist
*/
if (scene->active_keyingset == 0) {
BKE_report(op->reports, RPT_ERROR, "No active Keying Set to add empty path to");
return OPERATOR_CANCELLED;
}
KeyingSet *keyingset = static_cast<KeyingSet *>(
BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
/* Don't use the API method for this, since that checks on values... */
KS_Path *keyingset_path = MEM_new<KS_Path>("KeyingSetPath Empty");
BLI_addtail(&keyingset->paths, keyingset_path);
keyingset->active_path = keyingset->paths.count();
keyingset_path->groupmode = KSP_GROUP_KSNAME; /* XXX? */
keyingset_path->idtype = ID_OB;
keyingset_path->flag = KSP_FLAG_WHOLE_ARRAY;
return OPERATOR_FINISHED;
}
void ANIM_OT_keying_set_path_add(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Add Empty Keying Set Path";
ot->idname = "ANIM_OT_keying_set_path_add";
ot->description = "Add empty path to active keying set";
/* Callbacks. */
ot->exec = add_empty_ks_path_exec;
ot->poll = keyingset_poll_active_edit;
}
/* Remove Active Keying Set Path ------------------------- */
static wmOperatorStatus remove_active_ks_path_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
KeyingSet *keyingset = static_cast<KeyingSet *>(
BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
/* If there is a KeyingSet, find the nominated path to remove. */
if (!keyingset) {
BKE_report(op->reports, RPT_ERROR, "No active Keying Set to remove a path from");
return OPERATOR_CANCELLED;
}
KS_Path *keyingset_path = static_cast<KS_Path *>(
BLI_findlink(&keyingset->paths, keyingset->active_path - 1));
if (!keyingset_path) {
BKE_report(op->reports, RPT_ERROR, "No active Keying Set path to remove");
return OPERATOR_CANCELLED;
}
/* Remove the active path from the KeyingSet. */
BKE_keyingset_free_path(keyingset, keyingset_path);
/* The active path should now be the previously second-to-last active one. */
keyingset->active_path--;
return OPERATOR_FINISHED;
}
void ANIM_OT_keying_set_path_remove(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Remove Active Keying Set Path";
ot->idname = "ANIM_OT_keying_set_path_remove";
ot->description = "Remove active Path from active keying set";
/* Callbacks. */
ot->exec = remove_active_ks_path_exec;
ot->poll = keyingset_poll_activePath_edit;
}
/* ************************************************** */
/* KEYING SETS - OPERATORS (for use in UI menus) */
/* Add to KeyingSet Button Operator ------------------------ */
static wmOperatorStatus add_keyingset_button_exec(bContext *C, wmOperator *op)
{
PropertyRNA *prop = nullptr;
PointerRNA ptr = {};
int index = 0;
eKSP_Settings pflag{};
if (!ui::context_active_but_prop_get(C, &ptr, &prop, &index)) {
/* Pass event on if no active button found. */
return (OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH);
}
/* Verify the Keying Set to use:
* - use the active one for now (more control over this can be added later)
* - add a new one if it doesn't exist
*/
KeyingSet *keyingset = nullptr;
Scene *scene = CTX_data_scene(C);
if (scene->active_keyingset == 0) {
/* Validate flags
* - absolute KeyingSets should be created by default
*/
const eKS_Settings flag = KEYINGSET_ABSOLUTE;
const eInsertKeyFlags keyingflag = animrig::get_keyframing_flags(scene);
/* Call the API func, and set the active keyingset index. */
keyingset = BKE_keyingset_add(
&scene->keyingsets, "ButtonKeyingSet", "Button Keying Set", flag, keyingflag);
scene->active_keyingset = scene->keyingsets.count();
}
else if (scene->active_keyingset < 0) {
BKE_report(op->reports, RPT_ERROR, "Cannot add property to built in keying set");
return OPERATOR_CANCELLED;
}
else {
keyingset = static_cast<KeyingSet *>(
BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
}
/* Check if property is able to be added. */
const bool all = RNA_boolean_get(op->ptr, "all");
bool changed = false;
if (ptr.owner_id && ptr.data && prop && RNA_property_anim_editable(&ptr, prop)) {
if (const std::optional<std::string> path = RNA_path_from_ID_to_property(&ptr, prop)) {
if (all) {
pflag |= KSP_FLAG_WHOLE_ARRAY;
/* We need to set the index for this to 0, even though it may break in some cases, this is
* necessary if we want the entire array for most cases to get included without the user
* having to worry about where they clicked.
*/
index = 0;
}
/* Add path to this setting. */
BKE_keyingset_add_path(
keyingset, ptr.owner_id, nullptr, path->c_str(), index, pflag, KSP_GROUP_KSNAME);
keyingset->active_path = keyingset->paths.count();
changed = true;
}
}
if (changed) {
WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr);
/* Show notification/report header, so that users notice that something changed. */
BKE_reportf(op->reports, RPT_INFO, "Property added to Keying Set: '%s'", keyingset->name);
}
return (changed) ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
}
void ANIM_OT_keyingset_button_add(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Add to Keying Set";
ot->idname = "ANIM_OT_keyingset_button_add";
ot->description = "Add current UI-active property to current keying set";
/* Callbacks. */
ot->exec = add_keyingset_button_exec;
// op->poll = ???
/* Flags. */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* Properties. */
RNA_def_boolean(ot->srna, "all", true, "All", "Add all elements of the array to a Keying Set");
}
/* Remove from KeyingSet Button Operator ------------------------ */
static wmOperatorStatus remove_keyingset_button_exec(bContext *C, wmOperator *op)
{
PropertyRNA *prop = nullptr;
PointerRNA ptr = {};
int index = 0;
if (!ui::context_active_but_prop_get(C, &ptr, &prop, &index)) {
/* Pass event on if no active button found. */
return (OPERATOR_CANCELLED | OPERATOR_PASS_THROUGH);
}
/* Verify the Keying Set to use:
* - use the active one for now (more control over this can be added later)
* - return error if it doesn't exist
*/
Scene *scene = CTX_data_scene(C);
if (scene->active_keyingset == 0) {
BKE_report(op->reports, RPT_ERROR, "No active Keying Set to remove property from");
return OPERATOR_CANCELLED;
}
if (scene->active_keyingset < 0) {
BKE_report(op->reports, RPT_ERROR, "Cannot remove property from built in keying set");
return OPERATOR_CANCELLED;
}
KeyingSet *keyingset = static_cast<KeyingSet *>(
BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
bool changed = false;
if (ptr.owner_id && ptr.data && prop) {
if (const std::optional<std::string> path = RNA_path_from_ID_to_property(&ptr, prop)) {
/* Try to find a path matching this description. */
KS_Path *keyingset_path = BKE_keyingset_find_path(
keyingset, ptr.owner_id, keyingset->name, path->c_str(), index, KSP_GROUP_KSNAME);
if (keyingset_path) {
BKE_keyingset_free_path(keyingset, keyingset_path);
changed = true;
}
}
}
if (changed) {
WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr);
/* Show warning. */
BKE_report(op->reports, RPT_INFO, "Property removed from keying set");
}
return (changed) ? OPERATOR_FINISHED : OPERATOR_CANCELLED;
}
void ANIM_OT_keyingset_button_remove(wmOperatorType *ot)
{
/* Identifiers. */
ot->name = "Remove from Keying Set";
ot->idname = "ANIM_OT_keyingset_button_remove";
ot->description = "Remove current UI-active property from current keying set";
/* Callbacks. */
ot->exec = remove_keyingset_button_exec;
// op->poll = ???
/* Flags. */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
}
/* ******************************************* */
/* Change Active KeyingSet Operator ------------------------ */
/* This operator checks if a menu should be shown
* for choosing the KeyingSet to make the active one. */
static wmOperatorStatus keyingset_active_menu_invoke(bContext *C,
wmOperator *op,
const wmEvent * /*event*/)
{
/* Call the menu, which will call this operator again, hence the canceled. */
ui::PopupMenu *pup = ui::popup_menu_begin(C, op->type->name, ICON_NONE);
ui::Layout &layout = *popup_menu_layout(pup);
layout.op_enum("ANIM_OT_keying_set_active_set", "type");
popup_menu_end(C, pup);
return OPERATOR_INTERFACE;
}
static wmOperatorStatus keyingset_active_menu_exec(bContext *C, wmOperator *op)
{
Scene *scene = CTX_data_scene(C);
const int type = RNA_enum_get(op->ptr, "type");
/* If type == 0, it will deselect any active keying set. */
scene->active_keyingset = type;
WM_event_add_notifier(C, NC_SCENE | ND_KEYINGSET, nullptr);
return OPERATOR_FINISHED;
}
/* Build the enum for all keyingsets except the active keyingset. */
static void build_keyingset_enum(bContext *C, EnumPropertyItem **item, int *totitem, bool *r_free)
{
/* user-defined Keying Sets
* - these are listed in the order in which they were defined for the active scene
*/
EnumPropertyItem item_tmp = {0};
Scene *scene = CTX_data_scene(C);
KeyingSet *keyingset;
int enum_index = 1;
if (scene->keyingsets.first) {
for (keyingset = static_cast<KeyingSet *>(scene->keyingsets.first); keyingset;
keyingset = keyingset->next, enum_index++)
{
if (ANIM_keyingset_context_ok_poll(C, keyingset)) {
item_tmp.identifier = keyingset->idname;
item_tmp.name = keyingset->name;
item_tmp.description = keyingset->description;
item_tmp.value = enum_index;
RNA_enum_item_add(item, totitem, &item_tmp);
}
}
RNA_enum_item_add_separator(item, totitem);
}
/* Builtin Keying Sets. */
enum_index = -1;
for (keyingset = static_cast<KeyingSet *>(builtin_keyingsets.first); keyingset;
keyingset = keyingset->next, enum_index--)
{
/* Only show KeyingSet if context is suitable. */
if (ANIM_keyingset_context_ok_poll(C, keyingset)) {
item_tmp.identifier = keyingset->idname;
item_tmp.name = keyingset->name;
item_tmp.description = keyingset->description;
item_tmp.value = enum_index;
RNA_enum_item_add(item, totitem, &item_tmp);
}
}
RNA_enum_item_end(item, totitem);
*r_free = true;
}
static const EnumPropertyItem *keyingset_set_active_enum_itemf(bContext *C,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
bool *r_free)
{
if (C == nullptr) {
return rna_enum_dummy_DEFAULT_items;
}
/* Active Keying Set.
* - only include entry if it exists
*/
Scene *scene = CTX_data_scene(C);
EnumPropertyItem *item = nullptr, item_tmp = {0};
int totitem = 0;
if (scene->active_keyingset) {
/* Active Keying Set. */
item_tmp.identifier = "__ACTIVE__";
item_tmp.name = "Clear Active Keying Set";
item_tmp.value = 0;
RNA_enum_item_add(&item, &totitem, &item_tmp);
RNA_enum_item_add_separator(&item, &totitem);
}
build_keyingset_enum(C, &item, &totitem, r_free);
return item;
}
void ANIM_OT_keying_set_active_set(wmOperatorType *ot)
{
PropertyRNA *prop;
/* Identifiers. */
ot->name = "Set Active Keying Set";
ot->idname = "ANIM_OT_keying_set_active_set";
ot->description = "Set a new active keying set";
/* Callbacks. */
ot->invoke = keyingset_active_menu_invoke;
ot->exec = keyingset_active_menu_exec;
ot->poll = ED_operator_areaactive;
/* Flags. */
ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO;
/* Keyingset to use (dynamic enum). */
prop = RNA_def_enum(
ot->srna, "type", rna_enum_dummy_DEFAULT_items, 0, "Keying Set", "The Keying Set to use");
RNA_def_enum_funcs(prop, keyingset_set_active_enum_itemf);
}
/* ******************************************* */
/* KEYING SETS API (for UI) */
/* Getters for Active/Indices ----------------------------- */
int ANIM_scene_get_keyingset_index(Scene *scene, KeyingSet *keyingset)
{
int index;
/* If no KeyingSet provided, have none. */
if (keyingset == nullptr) {
return 0;
}
/* Check if the KeyingSet exists in scene list. */
if (scene) {
/* Get index and if valid, return
* - (absolute) Scene KeyingSets are from (>= 1)
*/
index = BLI_findindex(&scene->keyingsets, keyingset);
if (index != -1) {
return (index + 1);
}
}
/* Still here, so try built-ins list too:
* - Built-ins are from (<= -1).
* - None/Invalid is (= 0).
*/
index = BLI_findindex(&builtin_keyingsets, keyingset);
if (index != -1) {
return -(index + 1);
}
return 0;
}
static void anim_keyingset_visit_for_search_impl(
const bContext *C,
FunctionRef<void(StringPropertySearchVisitParams)> visit_fn,
const bool use_poll)
{
/* Poll requires context. */
if (use_poll && (C == nullptr)) {
return;
}
Scene *scene = C ? CTX_data_scene(C) : nullptr;
/* Active Keying Set. */
if (!use_poll || (scene && scene->active_keyingset)) {
StringPropertySearchVisitParams visit_params{};
visit_params.text = "__ACTIVE__";
visit_params.info = "Active Keying Set";
visit_fn(visit_params);
}
/* User-defined Keying Sets. */
if (scene && scene->keyingsets.first) {
for (KeyingSet &keyingset : scene->keyingsets) {
if (use_poll && !ANIM_keyingset_context_ok_poll(const_cast<bContext *>(C), &keyingset)) {
continue;
}
StringPropertySearchVisitParams visit_params{};
visit_params.text = keyingset.idname;
visit_params.info = keyingset.name;
visit_fn(visit_params);
}
}
/* Builtin Keying Sets. */
for (KeyingSet &keyingset : builtin_keyingsets) {
if (use_poll && !ANIM_keyingset_context_ok_poll(const_cast<bContext *>(C), &keyingset)) {
continue;
}
StringPropertySearchVisitParams visit_params{};
visit_params.text = keyingset.idname;
visit_params.info = keyingset.name;
visit_fn(visit_params);
}
}
void ANIM_keyingset_visit_for_search(const bContext *C,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
const char * /*edit_text*/,
FunctionRef<void(StringPropertySearchVisitParams)> visit_fn)
{
anim_keyingset_visit_for_search_impl(C, visit_fn, false);
}
void ANIM_keyingset_visit_for_search_no_poll(
const bContext *C,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
const char * /*edit_text*/,
FunctionRef<void(StringPropertySearchVisitParams)> visit_fn)
{
anim_keyingset_visit_for_search_impl(C, visit_fn, true);
}
/* Menu of All Keying Sets ----------------------------- */
const EnumPropertyItem *ANIM_keying_sets_enum_itemf(bContext *C,
PointerRNA * /*ptr*/,
PropertyRNA * /*prop*/,
bool *r_free)
{
if (C == nullptr) {
return rna_enum_dummy_DEFAULT_items;
}
/* Active Keying Set
* - only include entry if it exists
*/
Scene *scene = CTX_data_scene(C);
EnumPropertyItem *item = nullptr, item_tmp = {0};
int totitem = 0;
if (scene->active_keyingset) {
/* Active Keying Set. */
item_tmp.identifier = "__ACTIVE__";
item_tmp.name = "Active Keying Set";
item_tmp.value = 0;
RNA_enum_item_add(&item, &totitem, &item_tmp);
RNA_enum_item_add_separator(&item, &totitem);
}
build_keyingset_enum(C, &item, &totitem, r_free);
return item;
}
KeyingSet *ANIM_keyingset_get_from_enum_type(Scene *scene, int type)
{
if (type == 0) {
type = scene->active_keyingset;
}
if (type > 0) {
return static_cast<KeyingSet *>(BLI_findlink(&scene->keyingsets, type - 1));
}
return static_cast<KeyingSet *>(BLI_findlink(&builtin_keyingsets, -type - 1));
}
KeyingSet *ANIM_keyingset_get_from_idname(Scene *scene, const char *idname)
{
KeyingSet *keyingset = static_cast<KeyingSet *>(
BLI_findstring(&scene->keyingsets, idname, offsetof(KeyingSet, idname)));
if (keyingset == nullptr) {
keyingset = static_cast<KeyingSet *>(
BLI_findstring(&builtin_keyingsets, idname, offsetof(KeyingSet, idname)));
}
return keyingset;
}
/* ******************************************* */
/* KEYFRAME MODIFICATION */
/* Polling API ----------------------------------------------- */
bool ANIM_keyingset_context_ok_poll(bContext *C, KeyingSet *keyingset)
{
if (keyingset->flag & KEYINGSET_ABSOLUTE) {
return true;
}
KeyingSetInfo *keyingset_info = animrig::keyingset_info_find_name(keyingset->typeinfo);
/* Get the associated 'type info' for this KeyingSet. */
if (keyingset_info == nullptr) {
return false;
}
/* TODO: check for missing callbacks! */
/* Check if it can be used in the current context. */
return keyingset_info->poll(keyingset_info, C);
}
} // namespace blender

View File

@@ -0,0 +1,417 @@
/* SPDX-FileCopyrightText: 2019 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#include "BKE_context.hh"
#include "BKE_scene.hh"
#include "GPU_immediate.hh"
#include "GPU_matrix.hh"
#include "GPU_state.hh"
#include "ED_time_scrub_ui.hh"
#include "WM_api.hh"
#include "WM_types.hh"
#include "UI_interface.hh"
#include "UI_interface_icons.hh"
#include "UI_interface_layout.hh"
#include "UI_resources.hh"
#include "UI_view2d.hh"
#include "DNA_scene_types.h"
#include "BLI_math_base.h"
#include "BLI_rect.h"
#include "BLI_string_utf8.h"
#include "BLI_timecode.h"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
namespace blender {
void ED_time_scrub_region_rect_get(const ARegion *region, rcti *r_rect)
{
r_rect->xmin = 0;
r_rect->xmax = region->winx;
r_rect->ymax = region->winy;
r_rect->ymin = r_rect->ymax - UI_TIME_SCRUB_MARGIN_Y;
}
static int get_centered_text_y(const rcti *rect)
{
return BLI_rcti_cent_y(rect) - UI_SCALE_FAC * 4;
}
static void draw_background(const rcti *rect)
{
uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
immUniformThemeColor(TH_TIME_SCRUB_BACKGROUND);
GPU_blend(GPU_BLEND_ALPHA);
immRectf(pos, rect->xmin, rect->ymin, rect->xmax, rect->ymax);
GPU_blend(GPU_BLEND_NONE);
immUnbindProgram();
}
static void get_current_time_str(
const Scene *scene, bool display_seconds, const float frame, char *r_str, uint str_maxncpy)
{
if (display_seconds) {
const float frame_len = scene->r.framelen > 0 ? scene->r.framelen : 1.0;
const float seconds = (frame / float(scene->frames_per_second())) / frame_len;
BLI_timecode_string_from_time(
r_str, str_maxncpy, -1, seconds, scene->frames_per_second(), U.timecode_style);
}
else if (scene->r.flag & SCER_SHOW_SUBFRAME) {
BLI_snprintf_utf8(r_str, str_maxncpy, "%.02f", frame);
}
else {
BLI_snprintf_utf8(r_str, str_maxncpy, "%d", int(frame));
}
}
struct PlayheadDimensions {
float text_width;
float text_padding;
float box_width;
float box_margin;
float shadow_width;
float tri_top;
float tri_half_width;
float tri_height;
};
static PlayheadDimensions get_playhead_dimensions(const Scene *scene,
const rcti *scrub_region_rect,
const float current_frame,
const bool display_seconds)
{
PlayheadDimensions dimensions;
constexpr int max_frame_string_len = 64;
char frame_str[max_frame_string_len];
get_current_time_str(scene, display_seconds, current_frame, frame_str, max_frame_string_len);
dimensions.text_width = ui::fontstyle_string_width(UI_FSTYLE_WIDGET, frame_str);
dimensions.text_padding = 4.0f * UI_SCALE_FAC;
const float box_min_width = 24.0f * UI_SCALE_FAC;
dimensions.box_width = std::max(dimensions.text_width + (2.0f * dimensions.text_padding),
box_min_width);
dimensions.box_margin = 2.0f * UI_SCALE_FAC;
dimensions.shadow_width = UI_SCALE_FAC;
dimensions.tri_top = ceil(scrub_region_rect->ymin + dimensions.box_margin);
dimensions.tri_half_width = 6.0f * UI_SCALE_FAC;
dimensions.tri_height = 6.0f * UI_SCALE_FAC;
return dimensions;
}
static void draw_playhead_stalk(const float region_x,
const rcti *scrub_region_rect,
const PlayheadDimensions &dimensions,
const float fg_color[4],
const float bg_color[4])
{
float shadow_width = dimensions.shadow_width;
/* Shadow for triangle below frame box. */
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
GPU_blend(GPU_BLEND_ALPHA);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
GPU_polygon_smooth(true);
immUniformColor4fv(bg_color);
immBegin(GPU_PRIM_TRIS, 3);
const float diag_offset = 0.4f * UI_SCALE_FAC;
immVertex2f(pos,
floor(region_x - dimensions.tri_half_width - shadow_width - diag_offset),
dimensions.shadow_width);
immVertex2f(pos,
floor(region_x + dimensions.tri_half_width + shadow_width + 1.0f + diag_offset),
dimensions.shadow_width);
immVertex2f(pos,
region_x + 0.5f,
dimensions.shadow_width - dimensions.tri_height - diag_offset - shadow_width);
immEnd();
immUnbindProgram();
GPU_polygon_smooth(false);
GPU_blend(GPU_BLEND_NONE);
rctf rect{};
/* Vertical line. */
if (UI_SCALE_FAC < 0.91f) {
shadow_width = 1.0f;
rect.xmin = floor(region_x) - shadow_width;
rect.xmax = rect.xmin + U.pixelsize + shadow_width + shadow_width;
}
else {
rect.xmin = floor(region_x - U.pixelsize) - shadow_width;
rect.xmax = floor(region_x + U.pixelsize + 1.0f) + shadow_width;
}
rect.ymin = 0.0f;
rect.ymax = scrub_region_rect->ymin;
ui::draw_roundbox_4fv_ex(&rect, fg_color, nullptr, 1.0f, bg_color, shadow_width, 0.0f);
}
static void draw_playhead_box(const float region_x,
const char frame_str[64],
const rcti *scrub_region_rect,
const PlayheadDimensions &dimensions,
const float fg_color[4],
const float bg_color[4])
{
rctf rect{};
draw_roundbox_corner_set(ui::CNR_ALL);
const float box_corner_radius = 4.0f * UI_SCALE_FAC;
rect.xmin = region_x - (dimensions.box_width / 2.0f);
rect.xmax = region_x + (dimensions.box_width / 2.0f) + 1.0f;
rect.ymin = floor(scrub_region_rect->ymin + (dimensions.box_margin - dimensions.shadow_width));
rect.ymax = ceil(scrub_region_rect->ymax - dimensions.box_margin + dimensions.shadow_width);
ui::draw_roundbox_4fv_ex(
&rect, fg_color, nullptr, 1.0f, bg_color, dimensions.shadow_width, box_corner_radius);
/* Frame number text. */
const uiFontStyle *fstyle = UI_FSTYLE_WIDGET;
uchar text_color[4];
ui::theme::get_color_4ubv(TH_HEADER_TEXT_HI, text_color);
const int y = BLI_rcti_cent_y(scrub_region_rect) - int(fstyle->points * UI_SCALE_FAC * 0.38f);
ui::fontstyle_draw_simple(
fstyle, region_x - (dimensions.text_width / 2.0f), y, frame_str, text_color);
}
/* Draws the little triangle at the bottom of the playhead. */
static void draw_playhead_tip(const float region_x,
const PlayheadDimensions &dimensions,
const float fg_color[4])
{
GPUVertFormat *format = immVertexFormat();
uint pos = GPU_vertformat_attr_add(format, "pos", gpu::VertAttrType::SFLOAT_32_32);
/* Triangular base under frame number. */
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
GPU_polygon_smooth(true);
GPU_blend(GPU_BLEND_ALPHA);
immBegin(GPU_PRIM_TRIS, 3);
immUniformColor4fv(fg_color);
immVertex2f(pos, region_x - dimensions.tri_half_width, dimensions.tri_top);
immVertex2f(pos, region_x + dimensions.tri_half_width + 1, dimensions.tri_top);
immVertex2f(pos, region_x + 0.5f, dimensions.tri_top - dimensions.tri_height);
immEnd();
immUnbindProgram();
GPU_polygon_smooth(false);
GPU_blend(GPU_BLEND_NONE);
}
/**
* Draw a playhead with reduced opacity at the given frame.
*/
static void draw_playhead_ghost(const float frame,
const Scene *scene,
const View2D *v2d,
const rcti *scrub_region_rect,
const bool display_seconds,
const bool display_stalk)
{
const float region_x = ui::view2d_view_to_region_x(v2d, frame);
PlayheadDimensions dimensions = get_playhead_dimensions(
scene, scrub_region_rect, frame, display_seconds);
float fg_color[4];
ui::theme::get_color_4fv(TH_CFRAME, fg_color);
float bg_color[4];
ui::theme::get_color_shade_4fv(TH_BACK, -20, bg_color);
fg_color[3] /= 2;
bg_color[3] /= 2;
if (display_stalk) {
draw_playhead_stalk(region_x, scrub_region_rect, dimensions, fg_color, bg_color);
}
constexpr int max_frame_string_len = 64;
char frame_str[max_frame_string_len];
get_current_time_str(scene, display_seconds, frame, frame_str, max_frame_string_len);
draw_playhead_box(region_x, frame_str, scrub_region_rect, dimensions, fg_color, bg_color);
if (display_stalk) {
draw_playhead_tip(region_x, dimensions, fg_color);
}
}
static void draw_current_frame(const Scene *scene,
bool display_seconds,
const View2D *v2d,
const rcti *scrub_region_rect,
bool display_stalk = true,
bool clamp_playhead = false)
{
const float current_frame = BKE_scene_frame_get(scene);
float region_x = ui::view2d_view_to_region_x(v2d, current_frame);
constexpr int max_frame_string_len = 64;
char frame_str[max_frame_string_len];
get_current_time_str(scene, display_seconds, current_frame, frame_str, max_frame_string_len);
PlayheadDimensions dimensions = get_playhead_dimensions(
scene, scrub_region_rect, current_frame, display_seconds);
if (clamp_playhead) {
region_x = math::clamp(region_x,
scrub_region_rect->xmin + dimensions.text_width / 2.0f,
scrub_region_rect->xmax - dimensions.text_width / 2.0f);
}
float fg_color[4];
ui::theme::get_color_4fv(TH_CFRAME, fg_color);
float bg_color[4];
ui::theme::get_color_shade_4fv(TH_BACK, -20, bg_color);
if (display_stalk) {
draw_playhead_stalk(region_x, scrub_region_rect, dimensions, fg_color, bg_color);
}
draw_playhead_box(region_x, frame_str, scrub_region_rect, dimensions, fg_color, bg_color);
if (display_stalk) {
draw_playhead_tip(region_x, dimensions, fg_color);
}
}
void ED_time_scrub_draw_current_frame(const ARegion *region,
const Scene *scene,
bool display_seconds,
bool display_stalk,
bool clamp_playhead)
{
const View2D *v2d = &region->v2d;
GPU_matrix_push_projection();
wmOrtho2_region_pixelspace(region);
rcti scrub_region_rect;
ED_time_scrub_region_rect_get(region, &scrub_region_rect);
if (scene->r.framelen != 1.0) {
/* In case the time scale feature is active, we draw a second playhead with less opacity to
* indicate the remapped time. */
const float ctime = BKE_scene_ctime_get(scene);
draw_playhead_ghost(ctime, scene, v2d, &scrub_region_rect, display_seconds, display_stalk);
}
draw_current_frame(
scene, display_seconds, v2d, &scrub_region_rect, display_stalk, clamp_playhead);
GPU_matrix_pop_projection();
}
void ED_time_scrub_draw(const ARegion *region,
const Scene *scene,
bool display_seconds,
bool discrete_frames,
const int base)
{
const View2D *v2d = &region->v2d;
GPU_matrix_push_projection();
wmOrtho2_region_pixelspace(region);
rcti scrub_region_rect;
ED_time_scrub_region_rect_get(region, &scrub_region_rect);
draw_background(&scrub_region_rect);
rcti numbers_rect = scrub_region_rect;
numbers_rect.ymin = get_centered_text_y(&scrub_region_rect) - 4 * UI_SCALE_FAC;
ui::view2d_draw_scale_x(region,
v2d,
&numbers_rect,
scene,
display_seconds,
!discrete_frames,
TH_TIME_SCRUB_TEXT,
base);
GPU_matrix_pop_projection();
}
rcti ED_time_scrub_clamp_scroller_mask(const rcti &scroller_mask)
{
rcti clamped_mask = scroller_mask;
clamped_mask.ymax -= UI_TIME_SCRUB_MARGIN_Y;
return clamped_mask;
}
bool ED_time_scrub_event_in_region(const ARegion *region, const wmEvent *event)
{
rcti rect = region->winrct;
rect.ymin = rect.ymax - UI_TIME_SCRUB_MARGIN_Y;
return BLI_rcti_isect_pt_v(&rect, event->xy);
}
bool ED_time_scrub_event_in_region_poll(const wmWindow * /*win*/,
const ScrArea * /*area*/,
const ARegion *region,
const wmEvent *event)
{
return ED_time_scrub_event_in_region(region, event);
}
void ED_time_scrub_channel_search_draw(const bContext *C, ARegion *region, bDopeSheet *dopesheet)
{
GPU_matrix_push_projection();
wmOrtho2_region_pixelspace(region);
rcti rect;
rect.xmin = 0;
rect.xmax = region->winx;
rect.ymin = region->winy - UI_TIME_SCRUB_MARGIN_Y;
rect.ymax = region->winy;
uint pos = GPU_vertformat_attr_add(immVertexFormat(), "pos", gpu::VertAttrType::SFLOAT_32_32);
immBindBuiltinProgram(GPU_SHADER_3D_UNIFORM_COLOR);
immUniformThemeColor(TH_BACK);
immRectf(pos, rect.xmin, rect.ymin, rect.xmax, rect.ymax);
immUnbindProgram();
PointerRNA ptr = RNA_pointer_create_discrete(&CTX_wm_screen(C)->id, RNA_DopeSheet, dopesheet);
const uiStyle *style = ui::style_get_dpi();
const float padding_x = 2 * UI_SCALE_FAC;
const float padding_y = UI_SCALE_FAC;
ui::Block *block = block_begin(C, region, __func__, ui::EmbossType::Emboss);
ui::Layout &layout = ui::block_layout(block,
ui::LayoutDirection::Vertical,
ui::LayoutType::Header,
rect.xmin + padding_x,
rect.ymin + UI_UNIT_Y + padding_y,
BLI_rcti_size_x(&rect) - 2 * padding_x,
1,
0,
style);
layout.scale_y_set((UI_UNIT_Y - padding_y) / UI_UNIT_Y);
ui::block_layout_set_current(block, &layout);
block_align_begin(block);
layout.prop(&ptr, "filter_text", UI_ITEM_NONE, "", ICON_NONE);
layout.prop(&ptr, "use_filter_invert", UI_ITEM_NONE, "", ICON_ARROW_LEFTRIGHT);
block_align_end(block);
ui::block_layout_resolve(block);
/* Make sure the events are consumed from the search and don't reach other UI blocks since this
* is drawn on top of animation-channels. */
block_flag_enable(block, ui::BLOCK_CLIP_EVENTS);
block_bounds_set_normal(block, 0);
block_end(C, block);
block_draw(C, block);
GPU_matrix_pop_projection();
}
} // namespace blender

View File

@@ -0,0 +1,511 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: GPL-2.0-or-later */
/** \file
* \ingroup edanimation
*/
#include "BLI_math_rotation.h"
#include "BLI_string.h"
#include "DNA_object_types.h"
#include "ANIM_rna.hh"
#include "RNA_access.hh"
#include "RNA_prototypes.hh"
#include "ED_anim_transformable.hh"
namespace blender::ed {
/**
* Returns true if the given property index matches the axis flag.
* Always returns true if no flag is set.
*/
static bool is_axis_mutable(const int index, const AxisMutable axis_flag)
{
/* AxisMutable happens to be set up in such a way that X, Y and Z correspond to bits 0, 1
* and 2. The AXIS_MUTABLE_ALL case has all these bits set. */
return axis_flag & (1 << index);
}
static TransformFloats copy_pointers_to_values(const Span<float *> value)
{
TransformFloats copy(value.size());
for (const int i : value.index_range()) {
copy[i] = *(value[i]);
}
return copy;
}
static void copy_span_into_mutable_span(const Span<float> value,
MutableSpan<float> target,
const AxisMutable axis_flag)
{
BLI_assert(target.size() == value.size());
for (const int i : value.index_range()) {
if (!is_axis_mutable(i, axis_flag)) {
continue;
}
target[i] = value[i];
}
}
/**
* Blend all values to a single target value. At factor 0, the given `values` are not modified.
*/
static void blend_linear(MutableSpan<float> values,
const float target,
const float factor,
const AxisMutable axis_flag)
{
for (const int i : values.index_range()) {
if (!is_axis_mutable(i, axis_flag)) {
continue;
}
values[i] += factor * (target - values[i]);
}
}
/**
* Blend the given `values` towards `target`. The indices are matched up and the Span lengths are
* expected to match.
*/
static void blend_linear(MutableSpan<float> values,
const Span<float> target,
const float factor,
const AxisMutable axis_flag)
{
BLI_assert(values.size() == target.size());
for (const int i : values.index_range()) {
if (!is_axis_mutable(i, axis_flag)) {
continue;
}
values[i] += factor * (target[i] - values[i]);
}
}
Array<float> property_interpolated(const Span<float> a, const Span<float> b, const float factor)
{
BLI_assert(a.size() == b.size());
Array<float> interpolated(a.size());
for (const int i : a.index_range()) {
interpolated[i] = interpf(b[i], a[i], factor);
}
return interpolated;
}
/* Since there can be more than one representation of rotation data, they are stored in an array.
* The enum is the index into that array. */
enum RotationModeIndices : uint8_t {
ROT_IDX_QUATERNION,
ROT_IDX_AXIS_ANGLE,
ROT_IDX_EULER,
/* Not a rotation mode, always keep last. */
ROT_IDX_MAX_ENUM,
};
Rotation Rotation::converted_to_mode(const eRotationModes mode) const
{
if (mode == this->mode) {
return *this;
}
float4 quat;
switch (this->mode) {
case ROT_MODE_QUAT:
copy_qt_qt(quat, this->values.data());
break;
case ROT_MODE_AXISANGLE:
axis_angle_to_quat(quat, &this->values[1], this->values[0]);
break;
default:
BLI_assert(this->mode <= ROT_MODE_ZYX);
eulO_to_quat(quat, this->values.data(), this->mode);
break;
}
Rotation converted;
converted.mode = mode;
switch (mode) {
case ROT_MODE_QUAT:
converted.values.reinitialize(4);
copy_qt_qt(converted.values.data(), quat);
break;
case ROT_MODE_AXISANGLE:
converted.values.reinitialize(4);
quat_to_axis_angle(&converted.values[1], &converted.values[0], quat);
break;
default:
/* TODO (christoph): pass in a reference rotation for the conversion to euler. */
BLI_assert(mode <= ROT_MODE_ZYX);
converted.values.reinitialize(3);
quat_to_eulO(converted.values.data(), mode, quat);
break;
}
return converted;
}
Rotation identity_rotation(const eRotationModes mode)
{
switch (mode) {
case ROT_MODE_QUAT:
return {{1, 0, 0, 0}, mode};
case ROT_MODE_AXISANGLE:
return {{0, 0, 1, 0}, mode};
default:
BLI_assert(mode <= ROT_MODE_ZYX);
return {{0, 0, 0}, mode};
}
}
static void interpolate_axis_angle(const float a_angle,
const float3 &a_axis,
const float b_angle,
const float3 &b_axis,
const float factor,
float *r_angle,
float r_axis[3])
{
float4 a_quat, b_quat;
axis_angle_to_quat(a_quat, a_axis, a_angle);
axis_angle_to_quat(b_quat, b_axis, b_angle);
float4 interpolated_quat;
interp_qt_qtqt(interpolated_quat, a_quat, b_quat, factor);
quat_to_axis_angle(r_axis, r_angle, interpolated_quat);
}
Rotation rotation_interpolated(const Rotation &a, const Rotation &b, const float factor)
{
/* Only different from `b` if the rotation mode does not match `a`. */
const Rotation b_aligned = b.converted_to_mode(a.mode);
Rotation interpolated;
interpolated.mode = a.mode;
interpolated.values.reinitialize(a.values.size());
switch (a.mode) {
case ROT_MODE_QUAT:
interp_qt_qtqt(interpolated.values.data(), a.values.data(), b_aligned.values.data(), factor);
break;
case ROT_MODE_AXISANGLE: {
interpolate_axis_angle(a.values[0],
&a.values[1],
b_aligned.values[0],
&b_aligned.values[1],
factor,
&interpolated.values[0],
&interpolated.values[1]);
break;
}
default:
/* Should axis angle use a different interpolation mode? */
for (const int i : interpolated.values.index_range()) {
interpolated.values[i] = interpf(b_aligned.values[i], a.values[i], factor);
}
break;
}
return interpolated;
}
static void build_rotations_array(
Array<TransformFloatPtrs> &rotations, float *euler, float *quat, float *axis, float *angle)
{
rotations.reinitialize(ROT_IDX_MAX_ENUM);
rotations[ROT_IDX_EULER] = TransformFloatPtrs(3);
for (const int i : IndexRange(3)) {
rotations[ROT_IDX_EULER][i] = &euler[i];
}
rotations[ROT_IDX_QUATERNION] = TransformFloatPtrs(4);
for (const int i : IndexRange(4)) {
rotations[ROT_IDX_QUATERNION][i] = &quat[i];
}
rotations[ROT_IDX_AXIS_ANGLE] = TransformFloatPtrs(4);
for (const int i : IndexRange(3)) {
rotations[ROT_IDX_AXIS_ANGLE][i + 1] = &axis[i];
}
rotations[ROT_IDX_AXIS_ANGLE][0] = angle;
}
AnimTransformable::AnimTransformable(Object &owner_id, bPoseChannel &pchan)
: type_(AnimTransformable::Type::POSE_BONE),
owner_id_(&owner_id.id),
data_(&pchan),
location_({pchan.loc, 3}),
rotation_mode_(&pchan.rotmode),
scale_({pchan.scale, 3})
{
build_rotations_array(rotations_, pchan.eul, pchan.quat, pchan.rotAxis, &pchan.rotAngle);
rna_path_from_id_ = animrig::get_pose_bone_rna_path(pchan);
}
AnimTransformable::AnimTransformable(Object &obj)
: type_(AnimTransformable::Type::OBJECT),
owner_id_(&obj.id),
data_(&obj),
location_({obj.loc, 3}),
rotation_mode_(reinterpret_cast<eRotationModes *>(&obj.rotmode)),
scale_({obj.scale, 3})
{
build_rotations_array(rotations_, obj.rot, obj.quat, obj.rotAxis, &obj.rotAngle);
rna_path_from_id_ = "";
}
template<> bPoseChannel *AnimTransformable::data<bPoseChannel *>() const
{
BLI_assert(type_ == Type::POSE_BONE);
return static_cast<bPoseChannel *>(data_);
}
StringRefNull AnimTransformable::rna_path() const
{
return rna_path_from_id_;
}
std::string AnimTransformable::rna_path_to_property(const PropertyType prop_type) const
{
/* Note that this assumes the property name for the underlying struct. If we add support for a
* struct where this doesn't match, the property names have to be moved to the constructor. */
StringRefNull property_name;
switch (prop_type) {
case PropertyType::LOCATION:
property_name = "location";
break;
case PropertyType::ROTATION:
property_name = animrig::get_rotation_mode_path(*rotation_mode_);
break;
case PropertyType::SCALE:
property_name = "scale";
break;
}
if (rna_path_from_id_.empty()) {
return std::string(property_name);
}
return fmt::format("{}.{}", rna_path_from_id_, property_name);
}
TransformFloats AnimTransformable::get_property(const PropertyType prop_type) const
{
switch (prop_type) {
case PropertyType::LOCATION:
return location_.as_span();
case PropertyType::ROTATION: {
const TransformFloatPtrs *rotation_array = get_rotation_array_from_mode(*rotation_mode_);
return copy_pointers_to_values(*rotation_array);
}
case PropertyType::SCALE:
return scale_.as_span();
}
BLI_assert_unreachable();
return {};
}
void AnimTransformable::set_property(const PropertyType prop_type,
const Span<float> values,
const AxisMutable axis_flag)
{
switch (prop_type) {
case PropertyType::LOCATION:
copy_span_into_mutable_span(values, location_, axis_flag);
break;
case PropertyType::ROTATION: {
const TransformFloatPtrs *rotation_array = get_rotation_array_from_mode(*rotation_mode_);
if (rotation_array->size() > values.size()) {
/* Trying to set a rotation with different mode. Use `set_rotation` instead. */
BLI_assert_unreachable();
return;
}
/* Axis flags don't work with quaternion rotations. */
BLI_assert((axis_flag == AXIS_MUTABLE_ALL) || (*rotation_mode_ != ROT_MODE_QUAT));
for (const int i : rotation_array->index_range()) {
if (!is_axis_mutable(i, axis_flag)) {
continue;
}
*(*rotation_array)[i] = values[i];
}
break;
}
case PropertyType::SCALE:
copy_span_into_mutable_span(values, scale_, axis_flag);
break;
}
}
void AnimTransformable::blend_property_to(const PropertyType prop_type,
const Span<float> target,
const float factor,
const AxisMutable axis_flag)
{
switch (prop_type) {
case PropertyType::LOCATION:
blend_linear(location_, target, factor, axis_flag);
break;
case PropertyType::ROTATION: {
const TransformFloatPtrs *rotation_array = get_rotation_array_from_mode(*rotation_mode_);
if (rotation_array->size() != target.size()) {
/* This doesn't catch all invalid cases. Differing euler rotation order or quaternion/axis
* angle will still have the same array size but blending will create bogus data. */
BLI_assert_msg(false, "Cannot do blending with differing rotation modes");
return;
}
Rotation rotation;
/* Assuming the rotation mode. See docstring of function. */
rotation.mode = *rotation_mode_;
rotation.values = target;
blend_rotation_to(rotation, factor, axis_flag);
break;
}
case PropertyType::SCALE:
blend_linear(scale_, target, factor, axis_flag);
break;
}
}
void AnimTransformable::blend_property_to(const PropertyType prop_type,
const float target,
const float factor,
const AxisMutable axis_flag)
{
switch (prop_type) {
case PropertyType::LOCATION:
blend_linear(location_, target, factor, axis_flag);
break;
case PropertyType::ROTATION: {
BLI_assert(*rotation_mode_ != ROT_MODE_QUAT);
const TransformFloatPtrs *rotation_array = get_rotation_array_from_mode(*rotation_mode_);
Rotation rotation;
/* Assuming the rotation mode. See docstring of function. */
rotation.mode = *rotation_mode_;
rotation.values.reinitialize(rotation_array->size());
rotation.values.fill(target);
blend_rotation_to(rotation, factor, axis_flag);
break;
}
case PropertyType::SCALE:
blend_linear(scale_, target, factor, axis_flag);
break;
}
}
const TransformFloatPtrs *AnimTransformable::get_rotation_array_from_mode(
const eRotationModes mode) const
{
const TransformFloatPtrs *rotations_array = nullptr;
switch (mode) {
case ROT_MODE_QUAT:
rotations_array = &rotations_[ROT_IDX_QUATERNION];
break;
case ROT_MODE_AXISANGLE:
rotations_array = &rotations_[ROT_IDX_AXIS_ANGLE];
break;
default:
BLI_assert(mode <= ROT_MODE_ZYX);
rotations_array = &rotations_[ROT_IDX_EULER];
break;
}
return rotations_array;
}
Rotation AnimTransformable::get_rotation() const
{
Rotation rotation;
rotation.mode = *rotation_mode_;
const TransformFloatPtrs *rotations_array = get_rotation_array_from_mode(rotation.mode);
BLI_assert(rotations_array != nullptr);
rotation.values = copy_pointers_to_values(*rotations_array);
return rotation;
}
void AnimTransformable::set_rotation(const Rotation &rotation)
{
const TransformFloatPtrs *rotations_array = get_rotation_array_from_mode(*rotation_mode_);
BLI_assert(rotations_array != nullptr);
if (rotation.mode == *rotation_mode_) {
/* Easy case, can just copy the values. */
for (const int i : rotations_array->index_range()) {
*(*rotations_array)[i] = rotation.values[i];
}
return;
}
Rotation rot_in_correct_mode = rotation.converted_to_mode(*rotation_mode_);
for (const int i : rotations_array->index_range()) {
*(*rotations_array)[i] = rot_in_correct_mode.values[i];
}
}
eRotationModes AnimTransformable::get_rotation_mode() const
{
return *rotation_mode_;
}
void AnimTransformable::blend_rotation_to(const Rotation &target,
const float factor,
const AxisMutable axis_flag)
{
/* If `target` matches the `current_mode`, the function will return `target` unmodified. */
Rotation compatible_rotation = target.converted_to_mode(*rotation_mode_);
const TransformFloatPtrs *rotations_array = get_rotation_array_from_mode(*rotation_mode_);
BLI_assert(rotations_array != nullptr);
TransformFloats result;
switch (*rotation_mode_) {
case ROT_MODE_QUAT: {
float4 current_quat;
for (const int i : IndexRange(4)) {
current_quat[i] = *((*rotations_array)[i]);
}
normalize_qt(current_quat);
normalize_qt(compatible_rotation.values.data());
result.reinitialize(4);
/* We are not using the axis flag here. Not sure how that would work with quaternions. */
interp_qt_qtqt(result.data(), current_quat, compatible_rotation.values.data(), factor);
break;
}
case ROT_MODE_AXISANGLE: {
result.reinitialize(4);
for (const int i : IndexRange(4)) {
result[i] = *((*rotations_array)[i]);
}
interpolate_axis_angle(result[0],
&result[1],
compatible_rotation.values[0],
&compatible_rotation.values[1],
factor,
&result[0],
&result[1]);
break;
}
default: {
BLI_assert(*rotation_mode_ <= ROT_MODE_ZYX);
result.reinitialize(3);
for (const int i : IndexRange(3)) {
result[i] = *((*rotations_array)[i]);
}
blend_linear(result, compatible_rotation.values, factor, axis_flag);
break;
}
}
BLI_assert(result.size() == rotations_array->size());
for (const int i : result.index_range()) {
*(*rotations_array)[i] = result[i];
}
}
} // namespace blender::ed

View File

@@ -0,0 +1,204 @@
/* SPDX-FileCopyrightText: 2026 Blender Authors
*
* SPDX-License-Identifier: Apache-2.0 */
#include "BLI_listbase.h"
#include "BLI_math_base.h"
#include "BLI_string.h"
#include "BKE_action.hh"
#include "BKE_armature.hh"
#include "BKE_idtype.hh"
#include "BKE_lib_id.hh"
#include "BKE_main.hh"
#include "BKE_object.hh"
#include "DNA_object_types.h"
#include "RNA_access.hh"
#include "RNA_define.hh"
#include "RNA_prototypes.hh"
#include "ED_anim_transformable.hh"
#include "CLG_log.h"
#include "testing/testing.h"
namespace blender::ed::tests {
class TransformableTest : public testing::Test {
public:
Main *bmain;
Object *armature_object;
bArmature *armature;
bPoseChannel *pose_bone;
static void SetUpTestSuite()
{
/* BKE_id_free() hits a code path that uses CLOG, which crashes if not initialized properly. */
CLG_init();
BKE_idtype_init();
RNA_init();
}
static void TearDownTestSuite()
{
CLG_exit();
RNA_exit();
}
void SetUp() override
{
bmain = BKE_main_new();
Bone *bone = MEM_new<Bone>("BONE");
STRNCPY(bone->name, "Bone");
armature = BKE_armature_add(bmain, "Armature");
BLI_addtail(&armature->bonebase, bone);
armature_object = BKE_object_add_only_object(bmain, OB_ARMATURE, "Armature");
armature_object->data = id_cast<ID *>(armature);
BKE_pose_ensure(bmain, armature_object, armature, false);
pose_bone = BKE_pose_channel_find_name(armature_object->pose, "Bone");
ASSERT_NE(pose_bone, nullptr);
}
void TearDown() override
{
BKE_main_free(bmain);
}
};
TEST_F(TransformableTest, transformable_get_values)
{
AnimTransformable transformable(*armature_object, *pose_bone);
EXPECT_STREQ(transformable.rna_path().c_str(), "pose.bones[\"Bone\"]");
Array<float> location = transformable.get_property(AnimTransformable::PropertyType::LOCATION);
Array<float> expected = {0, 0, 0};
EXPECT_EQ(expected, location);
pose_bone->loc[0] = 1;
expected = {0, 0, 0};
/* The returned values are a copy, changing the underlying data does not modify the array. */
EXPECT_EQ(expected, location);
location = transformable.get_property(AnimTransformable::PropertyType::LOCATION);
expected = {1, 0, 0};
EXPECT_EQ(expected, location);
Array<float> rotation_values = transformable.get_property(
AnimTransformable::PropertyType::ROTATION);
EXPECT_EQ(pose_bone->rotmode, ROT_MODE_QUAT);
EXPECT_EQ(transformable.get_rotation_mode(), pose_bone->rotmode);
EXPECT_EQ(rotation_values.size(), 4);
}
TEST_F(TransformableTest, transformable_rotation)
{
AnimTransformable transformable(*armature_object, *pose_bone);
Rotation rotation = transformable.get_rotation();
/* The rotation is always returned in the mode of the Transformable. */
EXPECT_EQ(rotation.mode, transformable.get_rotation_mode());
EXPECT_EQ(rotation.mode, ROT_MODE_QUAT);
Array<float> expected = {1, 0, 0, 0};
EXPECT_EQ(expected, rotation.values);
pose_bone->rotmode = ROT_MODE_XYZ;
pose_bone->eul[0] = 3.14;
transformable.set_rotation(rotation);
/* Even though the rotation is a quaternion, setting it to the Transformable that is a bone
* with xyz euler still works. The rotation is converted to the correct mode of the
* Transformable. */
expected = {0, 0, 0};
EXPECT_NEAR_SPAN(expected.as_span(), Span<float>(pose_bone->eul, 3), 0.001);
}
TEST_F(TransformableTest, transformable_blend_to)
{
AnimTransformable transformable(*armature_object, *pose_bone);
transformable.blend_property_to(
AnimTransformable::PropertyType::LOCATION, {1, 0, 0}, 0.0f, AXIS_MUTABLE_ALL);
Array<float> expected = {0, 0, 0};
/* A blend factor of 0 keeps the current values. */
EXPECT_NEAR_SPAN(expected.as_span(),
transformable.get_property(AnimTransformable::PropertyType::LOCATION).as_span(),
0.001);
transformable.blend_property_to(
AnimTransformable::PropertyType::LOCATION, {1, 0, 0}, 0.1f, AXIS_MUTABLE_ALL);
expected = {0.1f, 0, 0};
/* Blending linearly to 1. */
EXPECT_NEAR_SPAN(expected.as_span(),
transformable.get_property(AnimTransformable::PropertyType::LOCATION).as_span(),
0.001);
transformable.blend_property_to(
AnimTransformable::PropertyType::LOCATION, {1, 0, 0}, 1.0f, AXIS_MUTABLE_ALL);
expected = {1.0f, 0, 0};
/* Blending linearly to 1. */
EXPECT_NEAR_SPAN(expected.as_span(),
transformable.get_property(AnimTransformable::PropertyType::LOCATION).as_span(),
0.001);
}
TEST_F(TransformableTest, transformable_blend_rotation_to)
{
AnimTransformable transformable(*armature_object, *pose_bone);
/* There is a special function for rotations that does spherical interpolation for
* quaternions. */
EXPECT_EQ(pose_bone->rotmode, ROT_MODE_QUAT);
/* A 90 degree rotation on X. */
Rotation rot_90_x = {{0.707107f, 0.707107f, 0, 0}, ROT_MODE_QUAT};
transformable.blend_rotation_to(rot_90_x, 0.5f, AXIS_MUTABLE_ALL);
Rotation current_rotation = transformable.get_rotation();
EXPECT_NEAR(current_rotation.values[0], 0.92387f, 0.001);
EXPECT_NEAR(current_rotation.values[1], 0.38268f, 0.001);
/* Checking that the result is different from linear interpolation. */
EXPECT_NE(current_rotation.values[0], interpf(0.707107f, 1.0f, 0.5f));
EXPECT_NE(current_rotation.values[1], interpf(0.707107f, 0.0f, 0.5f));
transformable.set_rotation(identity_rotation(ROT_MODE_QUAT));
/* Using the generic blend function assumes that the given values are in the rotation mode that
* the object is currently in. As long as that is the case it will work as expected. */
transformable.blend_property_to(
AnimTransformable::PropertyType::ROTATION, rot_90_x.values, 0.5f, AXIS_MUTABLE_ALL);
EXPECT_NEAR(current_rotation.values[0], 0.92387f, 0.001);
EXPECT_NEAR(current_rotation.values[1], 0.38268f, 0.001);
}
TEST_F(TransformableTest, transformable_axis_constraints)
{
/* It is possible to only set and blend certain axes. This is a feature of the pose slide code
* and had to be added to transformables. */
AnimTransformable transformable(*armature_object, *pose_bone);
transformable.set_property(AnimTransformable::PropertyType::LOCATION, {1, 1, 1}, AXIS_MUTABLE_X);
Array<float> expected = {1, 0, 0};
EXPECT_NEAR_SPAN(expected.as_span(),
transformable.get_property(AnimTransformable::PropertyType::LOCATION).as_span(),
0.001);
transformable.set_property(AnimTransformable::PropertyType::LOCATION,
{2, 2, 2},
AxisMutable(AXIS_MUTABLE_X | AXIS_MUTABLE_Y));
expected = {2, 2, 0};
EXPECT_NEAR_SPAN(expected.as_span(),
transformable.get_property(AnimTransformable::PropertyType::LOCATION).as_span(),
0.001);
transformable.blend_property_to(AnimTransformable::PropertyType::LOCATION,
{3, 3, 3},
1.0f,
AxisMutable(AXIS_MUTABLE_Y | AXIS_MUTABLE_Z));
expected = {2, 3, 3};
EXPECT_NEAR_SPAN(expected.as_span(),
transformable.get_property(AnimTransformable::PropertyType::LOCATION).as_span(),
0.001);
}
} // namespace blender::ed::tests