Add Chromium-only Blender WebEngine parity work
This commit is contained in:
@@ -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()
|
||||
866
blender-5.2.0/source/blender/editors/animation/anim_asset_ops.cc
Normal file
866
blender-5.2.0/source/blender/editors/animation/anim_asset_ops.cc
Normal 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
5773
blender-5.2.0/source/blender/editors/animation/anim_channels_edit.cc
Normal file
5773
blender-5.2.0/source/blender/editors/animation/anim_channels_edit.cc
Normal file
File diff suppressed because it is too large
Load Diff
530
blender-5.2.0/source/blender/editors/animation/anim_deps.cc
Normal file
530
blender-5.2.0/source/blender/editors/animation/anim_deps.cc
Normal 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
|
||||
855
blender-5.2.0/source/blender/editors/animation/anim_draw.cc
Normal file
855
blender-5.2.0/source/blender/editors/animation/anim_draw.cc
Normal 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(®ion->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
|
||||
@@ -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
|
||||
4101
blender-5.2.0/source/blender/editors/animation/anim_filter.cc
Normal file
4101
blender-5.2.0/source/blender/editors/animation/anim_filter.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
123
blender-5.2.0/source/blender/editors/animation/anim_intern.hh
Normal file
123
blender-5.2.0/source/blender/editors/animation/anim_intern.hh
Normal 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
|
||||
348
blender-5.2.0/source/blender/editors/animation/anim_ipo_utils.cc
Normal file
348
blender-5.2.0/source/blender/editors/animation/anim_ipo_utils.cc
Normal 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
|
||||
2155
blender-5.2.0/source/blender/editors/animation/anim_markers.cc
Normal file
2155
blender-5.2.0/source/blender/editors/animation/anim_markers.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
1642
blender-5.2.0/source/blender/editors/animation/anim_ops.cc
Normal file
1642
blender-5.2.0/source/blender/editors/animation/anim_ops.cc
Normal file
File diff suppressed because it is too large
Load Diff
1239
blender-5.2.0/source/blender/editors/animation/drivers.cc
Normal file
1239
blender-5.2.0/source/blender/editors/animation/drivers.cc
Normal file
File diff suppressed because it is too large
Load Diff
1060
blender-5.2.0/source/blender/editors/animation/fmodifier_ui.cc
Normal file
1060
blender-5.2.0/source/blender/editors/animation/fmodifier_ui.cc
Normal file
File diff suppressed because it is too large
Load Diff
928
blender-5.2.0/source/blender/editors/animation/keyframes_draw.cc
Normal file
928
blender-5.2.0/source/blender/editors/animation/keyframes_draw.cc
Normal 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
|
||||
1715
blender-5.2.0/source/blender/editors/animation/keyframes_edit.cc
Normal file
1715
blender-5.2.0/source/blender/editors/animation/keyframes_edit.cc
Normal file
File diff suppressed because it is too large
Load Diff
2222
blender-5.2.0/source/blender/editors/animation/keyframes_general.cc
Normal file
2222
blender-5.2.0/source/blender/editors/animation/keyframes_general.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
@@ -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
|
||||
1505
blender-5.2.0/source/blender/editors/animation/keyframes_keylist.cc
Normal file
1505
blender-5.2.0/source/blender/editors/animation/keyframes_keylist.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
1661
blender-5.2.0/source/blender/editors/animation/keyframing.cc
Normal file
1661
blender-5.2.0/source/blender/editors/animation/keyframing.cc
Normal file
File diff suppressed because it is too large
Load Diff
734
blender-5.2.0/source/blender/editors/animation/keyingsets.cc
Normal file
734
blender-5.2.0/source/blender/editors/animation/keyingsets.cc
Normal 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
|
||||
417
blender-5.2.0/source/blender/editors/animation/time_scrub_ui.cc
Normal file
417
blender-5.2.0/source/blender/editors/animation/time_scrub_ui.cc
Normal 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 = ®ion->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 = ®ion->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
|
||||
511
blender-5.2.0/source/blender/editors/animation/transformable.cc
Normal file
511
blender-5.2.0/source/blender/editors/animation/transformable.cc
Normal 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user