Add Chromium-only Blender WebEngine parity work
This commit is contained in:
177
blender-5.2.0/source/blender/CMakeLists.txt
Normal file
177
blender-5.2.0/source/blender/CMakeLists.txt
Normal file
@@ -0,0 +1,177 @@
|
||||
# SPDX-FileCopyrightText: 2006 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(SRC_DNA_INC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_ID.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_ID_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_action_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_anim_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_anim_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_armature_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_asset_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_attribute_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_boid_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_brush_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_brush_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_cachefile_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_camera_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_cloth_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_collection_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_color_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_colorband_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_constraint_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_curve_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_curve_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_curveprofile_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_curves_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_customdata_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_defs.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_documentation.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_dynamicpaint_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_effect_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_fileglobal_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_fluid_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_freestyle_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_genfile.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_gpencil_legacy_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_gpencil_modifier_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_gpu_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_grease_pencil_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_image_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_image_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_key_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_lattice_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_layer_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_light_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_lightprobe_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_lineart_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_linestyle_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_listBase.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_mask_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_material_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_mesh_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_meshdata_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_meta_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_modifier_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_modifier_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_movieclip_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_nla_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_node_tree_interface_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_node_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_object_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_object_fluidsim_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_object_force_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_object_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_outliner_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_packedFile_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_particle_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_pointcache_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_pointcloud_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_rigidbody_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_scene_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_scene_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_screen_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_sdna_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_sequence_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_session_uid_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_shader_fx_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_sound_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_space_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_space_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_speaker_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_text_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_texture_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_theme_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_tracking_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_userdef_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_userdef_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_uuid_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_vec_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_vfont_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_view2d_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_view3d_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_view3d_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_viewer_path_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_volume_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_windowmanager_enums.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_windowmanager_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_workspace_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_world_types.h
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_xr_types.h
|
||||
)
|
||||
|
||||
# Utility & other headers.
|
||||
set(SRC_DNA_OTHER_INC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_array_utils.hh
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_print.hh
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_sdna_pointers.hh
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_sdna_type_ids.hh
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/makesdna/DNA_vec_defaults.h
|
||||
)
|
||||
|
||||
add_subdirectory(datatoc)
|
||||
add_subdirectory(gpu/shader_tool)
|
||||
add_subdirectory(editors)
|
||||
add_subdirectory(windowmanager)
|
||||
add_subdirectory(animrig)
|
||||
add_subdirectory(asset_system)
|
||||
add_subdirectory(blenkernel)
|
||||
add_subdirectory(blenlib)
|
||||
add_subdirectory(bmesh)
|
||||
add_subdirectory(draw)
|
||||
add_subdirectory(draw/intern/shaders)
|
||||
add_subdirectory(draw/engines/eevee/shaders)
|
||||
add_subdirectory(draw/engines/gpencil/shaders)
|
||||
add_subdirectory(draw/engines/overlay/shaders)
|
||||
add_subdirectory(draw/engines/workbench/shaders)
|
||||
add_subdirectory(render)
|
||||
add_subdirectory(blenfont)
|
||||
add_subdirectory(blentranslation)
|
||||
add_subdirectory(blenloader)
|
||||
add_subdirectory(blenloader_core)
|
||||
if(WITH_WEB)
|
||||
add_subdirectory(web_engine)
|
||||
endif()
|
||||
add_subdirectory(depsgraph)
|
||||
add_subdirectory(ikplugin)
|
||||
add_subdirectory(simulation)
|
||||
add_subdirectory(geometry)
|
||||
add_subdirectory(gpu)
|
||||
add_subdirectory(gpu/shaders)
|
||||
add_subdirectory(gpu/tests/shaders)
|
||||
add_subdirectory(imbuf)
|
||||
add_subdirectory(imbuf/intern/oiio)
|
||||
add_subdirectory(imbuf/intern/openexr)
|
||||
add_subdirectory(nodes)
|
||||
add_subdirectory(modifiers)
|
||||
add_subdirectory(sequencer)
|
||||
add_subdirectory(shader_fx)
|
||||
add_subdirectory(io)
|
||||
add_subdirectory(functions)
|
||||
add_subdirectory(makesdna)
|
||||
add_subdirectory(makesrna)
|
||||
if(NOT WITH_WEB)
|
||||
add_subdirectory(compositor)
|
||||
add_subdirectory(compositor/shaders)
|
||||
endif()
|
||||
|
||||
if(WITH_BLENDER_THUMBNAILER)
|
||||
add_subdirectory(blendthumb)
|
||||
endif()
|
||||
|
||||
if(WITH_IMAGE_CINEON)
|
||||
add_subdirectory(imbuf/intern/cineon)
|
||||
endif()
|
||||
|
||||
if(WITH_PYTHON)
|
||||
add_subdirectory(python)
|
||||
endif()
|
||||
|
||||
if(WITH_FREESTYLE)
|
||||
add_subdirectory(freestyle)
|
||||
endif()
|
||||
|
||||
if(WITH_CPU_CHECK)
|
||||
add_subdirectory(cpucheck)
|
||||
endif()
|
||||
1977
blender-5.2.0/source/blender/animrig/ANIM_action.hh
Normal file
1977
blender-5.2.0/source/blender/animrig/ANIM_action.hh
Normal file
File diff suppressed because it is too large
Load Diff
122
blender-5.2.0/source/blender/animrig/ANIM_action_iterators.hh
Normal file
122
blender-5.2.0/source/blender/animrig/ANIM_action_iterators.hh
Normal file
@@ -0,0 +1,122 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functionality to iterate an Action in various ways.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "DNA_action_types.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct FCurve;
|
||||
struct PointerRNA;
|
||||
struct PropertyRNA;
|
||||
namespace animrig {
|
||||
class Action;
|
||||
class Layer;
|
||||
class Strip;
|
||||
class Channelbag;
|
||||
} // namespace animrig
|
||||
|
||||
namespace animrig {
|
||||
|
||||
using slot_handle_t = decltype(blender::ActionSlot::handle);
|
||||
|
||||
/**
|
||||
* Iterates over all FCurves of the Action and executes the callback on it.
|
||||
* Works on layered and legacy actions.
|
||||
*/
|
||||
void foreach_fcurve_in_action(Action &action, FunctionRef<void(FCurve &fcurve)> callback);
|
||||
|
||||
/**
|
||||
* Iterates over all FCurves of the given slot handle in the Action and executes the callback on
|
||||
* it. This includes data that may be locked. If you want to limit this to editable data only use
|
||||
* `foreach_fcurve_in_action_slot_editable`.
|
||||
*
|
||||
* \note This could almost take a `const Action &` except for the fact that we do need to iterate
|
||||
* all FCurves in an editable way sometimes, e.g. for the pose library code.
|
||||
*
|
||||
* \note Use lambdas to have access to specific data in the callback.
|
||||
*/
|
||||
void foreach_fcurve_in_action_slot(Action &action,
|
||||
slot_handle_t handle,
|
||||
FunctionRef<void(FCurve &fcurve)> callback);
|
||||
|
||||
/**
|
||||
* Like `foreach_fcurve_in_action_slot` except any data that is not editable is skipped.
|
||||
*/
|
||||
void foreach_fcurve_in_action_slot_editable(Action &action,
|
||||
slot_handle_t handle,
|
||||
FunctionRef<void(FCurve &fcurve)> callback);
|
||||
|
||||
/**
|
||||
* Call the given callback for each Action + Slot that this ID uses.
|
||||
*
|
||||
* The following cases are visited:
|
||||
* - Direct Action+Slot assignment.
|
||||
* - NLA strips.
|
||||
* - Action Constraints, both on Object and Pose Bone level.
|
||||
*
|
||||
* \param callback: The function to call for each Action+Slot used. Even when there is no slot
|
||||
* assigned, this function will be called (but then with slot_handle = Slot::unassigned). The
|
||||
* callback should return `true` to continue the foreach loop, or return `false` to stop it.
|
||||
*
|
||||
* \returns Whether the foreach loop came to a natural end. So returns `false` when the callback
|
||||
* returned `false`, and `true` otherwise.
|
||||
*/
|
||||
bool foreach_action_slot_use(
|
||||
const ID &animated_id,
|
||||
FunctionRef<bool(const Action &action, slot_handle_t slot_handle)> callback);
|
||||
|
||||
/**
|
||||
* Essentially the same as foreach_action_slot_use(), except that it provides
|
||||
* the ID as well as pointers via which the callback can modify which
|
||||
* Action/slot is assigned.
|
||||
*
|
||||
* The ID passed to the callback is always the same `animated_id` as is passed
|
||||
* to this function. The actions & slots passed to the callback are *not*
|
||||
* necessarily the direct action & slot of that ID: they can also be the action
|
||||
* & slot of an Action Constraint or NLA Strip owned by the ID.
|
||||
*
|
||||
* \see animrig::generic_assign_action
|
||||
* \see animrig::generic_assign_action_slot
|
||||
* \see animrig::generic_assign_action_slot_handle
|
||||
*/
|
||||
bool foreach_action_slot_use_with_references(
|
||||
ID &animated_id,
|
||||
FunctionRef<bool(ID &animated_id,
|
||||
bAction *&action_ptr_ref,
|
||||
slot_handle_t &slot_handle_ref,
|
||||
char *last_slot_identifier)> callback);
|
||||
|
||||
/**
|
||||
* Essentially the same as foreach_action_slot_use(), except that it provides
|
||||
* the ID as well as the RNA properties via which the callback can modify which
|
||||
* Action/slot is assigned.
|
||||
*
|
||||
* The ID passed to the callback is always the same `animated_id` as is passed
|
||||
* to this function. The actions & slots passed to the callback are *not*
|
||||
* necessarily the direct action & slot of that ID: they can also be the action
|
||||
* & slot of an Action Constraint or NLA Strip owned by the ID.
|
||||
*
|
||||
* \note this function CANNOT be used to change which Action is assigned, as that makes the
|
||||
* PointerRNA/PropertyRNA values invalid.
|
||||
*
|
||||
* \see foreach_action_slot_use_with_references
|
||||
*/
|
||||
bool foreach_action_slot_use_with_rna(ID &animated_id,
|
||||
FunctionRef<bool(ID &animated_id,
|
||||
bAction *action,
|
||||
PointerRNA &action_slot_owner_ptr,
|
||||
PropertyRNA &action_slot_prop,
|
||||
char *last_slot_identifier)> callback);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
75
blender-5.2.0/source/blender/animrig/ANIM_action_legacy.hh
Normal file
75
blender-5.2.0/source/blender/animrig/ANIM_action_legacy.hh
Normal file
@@ -0,0 +1,75 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions for backward compatibility with the legacy Action API.
|
||||
*
|
||||
* It should be possible to remove these functions (and their callers) in
|
||||
* Blender 5.0, when we can remove the legacy API altogether.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
|
||||
namespace blender::animrig::legacy {
|
||||
|
||||
constexpr const char *DEFAULT_LEGACY_SLOT_NAME = "Legacy Slot";
|
||||
constexpr const char *DEFAULT_LEGACY_LAYER_NAME = "Legacy Layer";
|
||||
|
||||
/**
|
||||
* Return all F-Curves in the Action.
|
||||
*
|
||||
* It will return all F-Curves for all slots/layers/strips.
|
||||
*
|
||||
* The use of this function is an indicator for code that might have to be
|
||||
* inspected to see if this is _really_ the desired behavior, or whether the
|
||||
* F-Curves for a specific slot/layer/strip should be used instead.
|
||||
*
|
||||
* \see #animrig::legacy::fcurves_for_action_slot
|
||||
*/
|
||||
Vector<const FCurve *> fcurves_all(const bAction *action);
|
||||
Vector<FCurve *> fcurves_all(bAction *action);
|
||||
|
||||
/**
|
||||
* Return whether the action (+slot), if any, assigned to `adt` has keyframes.
|
||||
* This only considers the assigned slot.
|
||||
*
|
||||
* A null `adt` or a lack of assigned action are both handled, and are
|
||||
* considered to mean no key frames (and thus will return false).
|
||||
*/
|
||||
bool assigned_action_has_keyframes(AnimData *adt);
|
||||
|
||||
/**
|
||||
* Return all Channel Groups in the Action.
|
||||
* This will return all channel groups for all slots/layers/strips.
|
||||
*
|
||||
* \see #animrig::legacy::channel_groups_for_assigned_slot
|
||||
*/
|
||||
Vector<bActionGroup *> channel_groups_all(bAction *action);
|
||||
|
||||
/**
|
||||
* Return all Channel Groups for the assigned Action Slot.
|
||||
*
|
||||
* \see #animrig::legacy::channel_groups_all
|
||||
*/
|
||||
Vector<bActionGroup *> channel_groups_for_assigned_slot(AnimData *adt);
|
||||
|
||||
/**
|
||||
* Remove all F-Curves whose RNA path starts with the given prefix from an Action Slot.
|
||||
*
|
||||
* \param rna_path_prefix: All F-Curves whose RNA path start with this string will get removed.
|
||||
* Note that there is no other semantics here, so `prefix = "rotation"` will remove
|
||||
* "rotation_euler" as well. The prefix may not be an empty string.
|
||||
*
|
||||
* \return true if any were removed, false otherwise.
|
||||
*/
|
||||
bool action_fcurves_remove(bAction &action,
|
||||
slot_handle_t slot_handle,
|
||||
StringRefNull rna_path_prefix);
|
||||
|
||||
} // namespace blender::animrig::legacy
|
||||
94
blender-5.2.0/source/blender/animrig/ANIM_animdata.hh
Normal file
94
blender-5.2.0/source/blender/animrig/ANIM_animdata.hh
Normal file
@@ -0,0 +1,94 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to work with AnimData.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct ID;
|
||||
struct Main;
|
||||
|
||||
struct AnimData;
|
||||
struct FCurve;
|
||||
struct bAction;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
class Action;
|
||||
|
||||
/**
|
||||
* Get (or add relevant data to be able to do so) the Active Action for the given
|
||||
* Animation Data block, given an ID block where the Animation Data should reside.
|
||||
*/
|
||||
bAction *id_action_ensure(Main *bmain, ID *id);
|
||||
|
||||
/**
|
||||
* Delete the F-Curve from the given AnimData block (if possible),
|
||||
* as appropriate according to animation context.
|
||||
*
|
||||
* \note This function cannot be used to delete F-Curves from an NLA strip's Action.
|
||||
*/
|
||||
void animdata_fcurve_delete(AnimData *adt, FCurve *fcu);
|
||||
|
||||
/**
|
||||
* Unlink the action from animdata if it's empty.
|
||||
*
|
||||
* If the action has no F-Curves, unlink it from AnimData if it did not
|
||||
* come from a NLA Strip being tweaked.
|
||||
*/
|
||||
bool animdata_remove_empty_action(AnimData *adt);
|
||||
|
||||
/**
|
||||
* Build a Vector of IDs that are related to the given ID. Related things are e.g. Object<->Data,
|
||||
* Mesh<->Material and so on. The exact relationships are defined per ID type. Only relationships
|
||||
* of 1:1 are traced. The case of multiple users for 1 ID is treated as not related.
|
||||
* The returned Vector always contains the passed ID as the first index as such will never be
|
||||
* empty.
|
||||
*/
|
||||
Vector<ID *> find_related_ids(Main &bmain, ID &id);
|
||||
|
||||
/**
|
||||
* Compatibility helper function for `BKE_animadata_fcurve_find_by_rna_path()`.
|
||||
*
|
||||
* Searches each layer (top to bottom) to find an FCurve that matches the given
|
||||
* RNA path & index.
|
||||
*
|
||||
* \see BKE_animadata_fcurve_find_by_rna_path
|
||||
*
|
||||
* \note The returned FCurve should NOT be used for keyframe manipulation. Its
|
||||
* existence is an indicator for "this property is animated".
|
||||
*
|
||||
* \note This function assumes that `adt->action` actually points to a layered
|
||||
* Action. It is a bug to call this with a legacy Action, or without one.
|
||||
*
|
||||
* This function should probably be limited to the active layer (for the given
|
||||
* property, once pinning to layers is there), so that the "this is keyed" color
|
||||
* is more accurate.
|
||||
*
|
||||
* Again, this is just to hook up the layered Action to the old Blender UI code.
|
||||
*/
|
||||
const FCurve *fcurve_find_by_rna_path(const AnimData &adt,
|
||||
StringRefNull rna_path,
|
||||
int array_index);
|
||||
|
||||
/**
|
||||
* Return the F-Curves for the assigned Action Slot.
|
||||
*
|
||||
* If `adt` is `nullptr` or there is no Action assigned (i.e. `adt->action == nullptr`), an empty
|
||||
* Vector is returned.
|
||||
*/
|
||||
Span<FCurve *> fcurves_for_assigned_action(AnimData *adt);
|
||||
Span<const FCurve *> fcurves_for_assigned_action(const AnimData *adt);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
107
blender-5.2.0/source/blender/animrig/ANIM_armature.hh
Normal file
107
blender-5.2.0/source/blender/animrig/ANIM_armature.hh
Normal file
@@ -0,0 +1,107 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to deal with Armatures.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ANIM_bone_collections.hh"
|
||||
|
||||
#include "BKE_pose.hh"
|
||||
#include "DNA_armature_types.h"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
constexpr char bone_default_name[] = "Bone";
|
||||
|
||||
/**
|
||||
* Returns true if the given Bone is visible. This includes bone collection visibility.
|
||||
*/
|
||||
inline bool bone_is_visible(const bArmature *armature, const Bone *bone)
|
||||
{
|
||||
const bool bone_itself_visible = (bone->flag & BONE_HIDDEN_P) == 0;
|
||||
return bone_itself_visible && ANIM_bone_in_visible_collection(armature, bone);
|
||||
}
|
||||
|
||||
inline bool bone_is_visible(const bArmature *armature, const bke::PChanBoneConst pchanbone)
|
||||
{
|
||||
const bool bone_itself_visible = (pchanbone.pchan->drawflag & PCHAN_DRAW_HIDDEN) == 0;
|
||||
return bone_itself_visible && ANIM_bone_in_visible_collection(armature, pchanbone.bone);
|
||||
}
|
||||
|
||||
inline bool bone_is_visible(const bArmature *armature, const EditBone *ebone)
|
||||
{
|
||||
const bool bone_itself_visible = (ebone->flag & BONE_HIDDEN_A) == 0;
|
||||
return bone_itself_visible && ANIM_bonecoll_is_visible_editbone(armature, ebone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the bone is selected. This includes a visibility check
|
||||
* because invisible bones cannot be selected, no matter their flag.
|
||||
*/
|
||||
inline bool bone_is_selected(const bArmature *armature, const Bone *bone)
|
||||
{
|
||||
return (bone->flag & BONE_SELECTED) && bone_is_visible(armature, bone);
|
||||
}
|
||||
|
||||
inline bool bone_is_selected(const bArmature *armature, const bke::PChanBoneConst pchanbone)
|
||||
{
|
||||
return (pchanbone.pchan->flag & POSE_SELECTED) && bone_is_visible(armature, pchanbone);
|
||||
}
|
||||
|
||||
inline bool bone_is_selected(const bArmature *armature, const EditBone *ebone)
|
||||
{
|
||||
return (ebone->flag & BONE_SELECTED) && bone_is_visible(armature, ebone);
|
||||
}
|
||||
|
||||
inline bool bone_is_selectable(const bArmature *armature, const bke::PChanBoneConst pchanbone)
|
||||
{
|
||||
return bone_is_visible(armature, pchanbone) && !(pchanbone.bone->flag & BONE_UNSELECTABLE);
|
||||
}
|
||||
inline bool bone_is_selectable(const bArmature *armature, const bPoseChannel *pchan)
|
||||
{
|
||||
return bone_is_selectable(armature, {pchan, pchan->bone_get(*armature)});
|
||||
}
|
||||
|
||||
inline bool bone_is_selectable(const bArmature *armature, const Bone *bone)
|
||||
{
|
||||
return bone_is_visible(armature, bone) && !(bone->flag & BONE_UNSELECTABLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selection and deselection happens with the POSE_SELECTED_ALL flag which includes body tip and
|
||||
* root. While tip and root are not individually selectable in pose mode, these flags carry over to
|
||||
* edit mode.
|
||||
*/
|
||||
inline void bone_select(bPoseChannel *pchan)
|
||||
{
|
||||
pchan->flag |= POSE_SELECTED_ALL;
|
||||
}
|
||||
|
||||
inline void bone_deselect(bPoseChannel *pchan)
|
||||
{
|
||||
pchan->flag &= ~POSE_SELECTED_ALL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterates all descendents of the given pose bone including the bone itself. Iterates breadth
|
||||
* first.
|
||||
*/
|
||||
void pose_bone_descendent_iterator(Object &pose_ob,
|
||||
bPoseChannel &pchan,
|
||||
FunctionRef<void(bPoseChannel &child_bone)> callback);
|
||||
|
||||
/**
|
||||
* Iterates all descendents of the given pose bone depth first. The traversal for a branch is
|
||||
* stopped if the callback returns false. Returns true if the iteration completed or false if it
|
||||
* was stopped before visiting all bones.
|
||||
*/
|
||||
bool pose_bone_descendent_depth_iterator(Object &pose_ob,
|
||||
bPoseChannel &pchan,
|
||||
FunctionRef<bool(bPoseChannel &child_bone)> callback);
|
||||
} // namespace blender::animrig
|
||||
55
blender-5.2.0/source/blender/animrig/ANIM_armature_iter.hh
Normal file
55
blender-5.2.0/source/blender/animrig/ANIM_armature_iter.hh
Normal file
@@ -0,0 +1,55 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Iterators for armatures.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef __cplusplus
|
||||
# error This is a C++ header.
|
||||
#endif
|
||||
|
||||
#include "DNA_armature_types.h"
|
||||
|
||||
#include "BLI_listbase_wrapper.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
/**
|
||||
* Call `callback(bone)` for each bone in the list of bones.
|
||||
*
|
||||
* Bones are visited in depth-first order.
|
||||
*
|
||||
* TODO: extend the callback with a `bool` return value to indicate whether the
|
||||
* loop should continue or stop.
|
||||
*/
|
||||
template<typename CB> static void ANIM_armature_foreach_bone(ListBaseT<Bone> *bones, CB callback)
|
||||
{
|
||||
for (Bone *bone : ListBaseWrapper<Bone>(bones)) {
|
||||
callback(bone);
|
||||
ANIM_armature_foreach_bone(&bone->childbase, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call `callback(bone)` for each bone in the list of bones.
|
||||
*
|
||||
* Bones are visited in depth-first order.
|
||||
*
|
||||
* TODO: extend the callback with a `bool` return value to indicate whether the
|
||||
* loop should continue or stop.
|
||||
*/
|
||||
template<typename CB>
|
||||
static void ANIM_armature_foreach_bone(const ListBaseT<Bone> *bones, CB callback)
|
||||
{
|
||||
for (const Bone *bone : ConstListBaseWrapper<Bone>(bones)) {
|
||||
callback(bone);
|
||||
ANIM_armature_foreach_bone(&bone->childbase, callback);
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace blender::animrig
|
||||
474
blender-5.2.0/source/blender/animrig/ANIM_bone_collections.hh
Normal file
474
blender-5.2.0/source/blender/animrig/ANIM_bone_collections.hh
Normal file
@@ -0,0 +1,474 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief C++ functions to deal with Armature collections (i.e. the successor of bone layers).
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef __cplusplus
|
||||
# error This is a C++ header.
|
||||
#endif
|
||||
|
||||
#include "BLI_map.hh"
|
||||
|
||||
#include "BKE_armature.hh"
|
||||
|
||||
#include "DNA_action_types.h"
|
||||
#include "DNA_armature_types.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct bArmature;
|
||||
struct Bone;
|
||||
struct BoneCollection;
|
||||
struct bPoseChannel;
|
||||
struct EditBone;
|
||||
|
||||
/**
|
||||
* Construct a new #BoneCollection with the given name.
|
||||
*
|
||||
* The caller owns the returned pointer.
|
||||
*
|
||||
* You don't typically use this function directly, but rather create a bone collection on a
|
||||
* bArmature.
|
||||
*
|
||||
* \see #armature_bonecoll_new
|
||||
*/
|
||||
BoneCollection *ANIM_bonecoll_new(const char *name) ATTR_WARN_UNUSED_RESULT;
|
||||
|
||||
/**
|
||||
* Free the bone collection.
|
||||
*
|
||||
* You don't typically need this function, unless you created a bone collection outside the scope
|
||||
* of a bArmature. Normally bone collections are owned (and thus managed) by the armature.
|
||||
*
|
||||
* \see ANIM_armature_bonecoll_remove
|
||||
*
|
||||
* \param do_id_user_count: Whether to update user counts for IDs referenced from IDProperties of
|
||||
* the bone collection. Needs to be false when freeing an evaluated copy, true otherwise.
|
||||
*/
|
||||
void ANIM_bonecoll_free(BoneCollection *bcoll, bool do_id_user_count = true);
|
||||
|
||||
/**
|
||||
* Recalculate the armature & bone runtime data.
|
||||
*
|
||||
* TODO: move to BKE?
|
||||
*/
|
||||
void ANIM_armature_runtime_refresh(bArmature *armature);
|
||||
|
||||
/**
|
||||
* Free armature & bone runtime data.
|
||||
* TODO: move to BKE?
|
||||
*/
|
||||
void ANIM_armature_runtime_free(bArmature *armature);
|
||||
|
||||
/**
|
||||
* Add a new bone collection to the given armature.
|
||||
*
|
||||
* \param parent_index: Index into the Armature's `collections_array`. -1 adds it
|
||||
* as a root (i.e. parentless) collection.
|
||||
*
|
||||
* The Armature owns the returned pointer.
|
||||
*/
|
||||
BoneCollection *ANIM_armature_bonecoll_new(bArmature *armature,
|
||||
const char *name,
|
||||
int parent_index = -1);
|
||||
|
||||
/**
|
||||
* Add a bone collection to the Armature.
|
||||
*
|
||||
* If `anchor` is null or isn't found, this inserts the copy at the start
|
||||
* of the collection array.
|
||||
*
|
||||
* NOTE: this should not typically be used. It is only used by the library overrides system to
|
||||
* apply override operations.
|
||||
*/
|
||||
BoneCollection *ANIM_armature_bonecoll_insert_copy_after(bArmature *armature_dst,
|
||||
const bArmature *armature_src,
|
||||
const BoneCollection *anchor_in_dst,
|
||||
const BoneCollection *bcoll_to_copy);
|
||||
|
||||
/**
|
||||
* Remove the bone collection at `index` from the armature.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_remove_from_index(bArmature *armature, const int index);
|
||||
|
||||
/**
|
||||
* Remove a bone collection from the armature.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_remove(bArmature *armature, BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Set the given bone collection as the active one.
|
||||
*
|
||||
* Pass `nullptr` to clear the active bone collection.
|
||||
*
|
||||
* The bone collection MUST already be owned by this armature. If it is not,
|
||||
* this function will simply clear the active bone collection.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_active_set(bArmature *armature, BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Set the bone collection with the given index as the active one.
|
||||
*
|
||||
* Pass -1 to clear the active bone collection.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_active_index_set(bArmature *armature, int bone_collection_index);
|
||||
|
||||
/**
|
||||
* Set the bone collection with the given name as the active one.
|
||||
*
|
||||
* Pass an empty name to clear the active bone collection. A non-existent name will also cause the
|
||||
* active bone collection to be cleared.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_active_name_set(bArmature *armature, const char *name);
|
||||
|
||||
/**
|
||||
* Refresh the Armature runtime info about the active bone collection.
|
||||
*
|
||||
* The ground truth for the active bone collection is the collection's name,
|
||||
* whereas the runtime info also contains the active collection's index and
|
||||
* pointer. This function updates the runtime info to point to the named
|
||||
* collection. If that named collection cannot be found, the name will be
|
||||
* cleared.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_active_runtime_refresh(bArmature *armature);
|
||||
|
||||
/**
|
||||
* Determine whether the given bone collection is editable.
|
||||
*
|
||||
* Bone collections are editable when they are local, so either on a local Armature or added to a
|
||||
* linked Armature via a library override in the local file.
|
||||
*/
|
||||
bool ANIM_armature_bonecoll_is_editable(const bArmature *armature, const BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Move the bone collection at from_index to its sibling at to_index.
|
||||
*
|
||||
* The element at `to_index` is shifted to make space; it is not overwritten.
|
||||
* This shift happens towards `from_index`.
|
||||
*
|
||||
* This operation does not change the total number of elements in the array.
|
||||
*
|
||||
* \return true if the collection was successfully moved, false otherwise.
|
||||
* The latter happens if either index is out of bounds, or if the indices
|
||||
* are equal.
|
||||
*
|
||||
* \note This function is limited to moving between siblings of the bone
|
||||
* collection at `from_index`.
|
||||
*
|
||||
* \note This function ensures that the element at index `from_index` (before
|
||||
* the call) will end up at `to_index` (after the call). The element at
|
||||
* `to_index` before the call will shift towards `from_index`; in other words,
|
||||
* depending on the direction of movement, the moved element will end up either
|
||||
* before or after that one.
|
||||
*
|
||||
* TODO: add ASCII-art illustration of left & right movement.
|
||||
*
|
||||
* \see animrig::armature_bonecoll_move_to_parent() to move bone
|
||||
* collections between different parents.
|
||||
*/
|
||||
bool ANIM_armature_bonecoll_move_to_index(bArmature *armature, int from_index, int to_index);
|
||||
|
||||
enum class MoveLocation {
|
||||
Before, /* Move to before the item at the given index. */
|
||||
After, /* Move to after the item at the given index. */
|
||||
};
|
||||
|
||||
int ANIM_armature_bonecoll_move_before_after_index(bArmature *armature,
|
||||
int from_index,
|
||||
int to_index,
|
||||
MoveLocation before_after);
|
||||
|
||||
/**
|
||||
* Move the bone collection by \a step places up/down.
|
||||
*
|
||||
* \return whether the move actually happened.
|
||||
*
|
||||
* \note This function is limited to moving between siblings of the bone
|
||||
* collection at `from_index`.
|
||||
*
|
||||
* \see animrig::armature_bonecoll_move_to_parent() to move bone
|
||||
* collections between different parents.
|
||||
*/
|
||||
bool ANIM_armature_bonecoll_move(bArmature *armature, BoneCollection *bcoll, int step);
|
||||
|
||||
BoneCollection *ANIM_armature_bonecoll_get_by_name(bArmature *armature,
|
||||
const char *name) ATTR_WARN_UNUSED_RESULT;
|
||||
|
||||
/** Scan the bone collections to find the one with the given name.
|
||||
*
|
||||
* \return the index of the bone collection, or -1 if not found.
|
||||
*/
|
||||
int ANIM_armature_bonecoll_get_index_by_name(bArmature *armature,
|
||||
const char *name) ATTR_WARN_UNUSED_RESULT;
|
||||
|
||||
void ANIM_armature_bonecoll_name_set(bArmature *armature, BoneCollection *bcoll, const char *name);
|
||||
|
||||
/**
|
||||
* Show this bone collection.
|
||||
*
|
||||
* This marks the bone collection as 'visible'. Whether it is effectively
|
||||
* visible also depends on the visibility state of its ancestors. */
|
||||
void ANIM_bonecoll_show(bArmature *armature, BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Hide this bone collection.
|
||||
*
|
||||
* This marks the bone collection as 'hidden'. This also effectively hides its descendants,
|
||||
* regardless of their visibility state. */
|
||||
void ANIM_bonecoll_hide(bArmature *armature, BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Show or hide this bone collection.
|
||||
*
|
||||
* Calling this with a hard-coded `is_visible` parameter is equivalent to
|
||||
* calling the dedicated show/hide functions. Prefer the dedicated functions for
|
||||
* clarity.
|
||||
*
|
||||
* \see ANIM_bonecoll_show
|
||||
* \see ANIM_bonecoll_hide
|
||||
*/
|
||||
void ANIM_armature_bonecoll_is_visible_set(bArmature *armature,
|
||||
BoneCollection *bcoll,
|
||||
bool is_visible);
|
||||
|
||||
/**
|
||||
* Set or clear this bone collection's solo flag.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_solo_set(bArmature *armature, BoneCollection *bcoll, bool is_solo);
|
||||
|
||||
/**
|
||||
* Refresh the ARM_BCOLL_SOLO_ACTIVE flag.
|
||||
*/
|
||||
void ANIM_armature_refresh_solo_active(bArmature *armature);
|
||||
|
||||
/**
|
||||
* Determine whether this bone collection is visible, taking into account the visibility of its
|
||||
* ancestors and the "solo" flags that are in use.
|
||||
*/
|
||||
bool ANIM_armature_bonecoll_is_visible_effectively(const bArmature *armature,
|
||||
const BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Expand or collapse a bone collection in the tree view.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_is_expanded_set(BoneCollection *bcoll, bool is_expanded);
|
||||
|
||||
/**
|
||||
* Assign the bone to the bone collection.
|
||||
*
|
||||
* No-op if the bone is already a member of the collection.
|
||||
*
|
||||
* \return true if the bone was actually assigned, false if not (f.e. when it already was assigned
|
||||
* previously).
|
||||
*/
|
||||
bool ANIM_armature_bonecoll_assign(BoneCollection *bcoll, Bone *bone);
|
||||
bool ANIM_armature_bonecoll_assign_editbone(BoneCollection *bcoll, EditBone *ebone);
|
||||
/**
|
||||
* Keep existing membership on the destination but assign additional ones from source.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_assign_from_other_editbone(EditBone *dst, EditBone *src);
|
||||
bool ANIM_armature_bonecoll_assign_and_move(BoneCollection *bcoll, Bone *bone);
|
||||
bool ANIM_armature_bonecoll_assign_and_move_editbone(BoneCollection *bcoll, EditBone *ebone);
|
||||
bool ANIM_armature_bonecoll_unassign(BoneCollection *bcoll, Bone *bone);
|
||||
bool ANIM_armature_bonecoll_unassign_editbone(BoneCollection *bcoll, EditBone *ebone);
|
||||
void ANIM_armature_bonecoll_unassign_all(Bone *bone);
|
||||
void ANIM_armature_bonecoll_unassign_all_editbone(EditBone *ebone);
|
||||
|
||||
/* Assign the edit bone to the armature's active collection. */
|
||||
void ANIM_armature_bonecoll_assign_active(const bArmature *armature, EditBone *ebone);
|
||||
|
||||
/**
|
||||
* Return whether the Armature's active bone is assigned to the given bone collection.
|
||||
*/
|
||||
bool ANIM_armature_bonecoll_contains_active_bone(const bArmature *armature,
|
||||
const BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Reconstruct the bone collection memberships, based on the bone runtime data.
|
||||
*
|
||||
* This is needed to transition out of armature edit mode. That removes all bones, and
|
||||
* recreates them from the edit-bones.
|
||||
*/
|
||||
void ANIM_armature_bonecoll_reconstruct(bArmature *armature);
|
||||
|
||||
/*
|
||||
* Armature/Bone Layer abstractions. These functions are intended as the sole
|
||||
* accessors for `bone->layer`, `armature->layer`, etc. to get a grip on which
|
||||
* queries & operations are performed.
|
||||
*
|
||||
* The functions are named "bonecoll" (short for "bone collection"), as that's
|
||||
* the soon-to-be-introduced replacement for armature layers. This API is the
|
||||
* first step towards replacement.
|
||||
*/
|
||||
|
||||
/** Return true when any of the bone's collections is visible. */
|
||||
bool ANIM_bone_in_visible_collection(const bArmature *armature, const Bone *bone);
|
||||
|
||||
/**
|
||||
* Returns true when the edit-bone's collection is visible.
|
||||
*
|
||||
* \note This alone is not enough to check bone visibility since the user may have hidden the bone.
|
||||
* Use `animrig::bone_is_visible` to check bone visibility.
|
||||
*/
|
||||
bool ANIM_bonecoll_is_visible_editbone(const bArmature *armature, const EditBone *ebone);
|
||||
|
||||
inline bool ANIM_bonecoll_is_visible_pchan(const bArmature *armature, const bPoseChannel *pchan)
|
||||
{
|
||||
return ANIM_bone_in_visible_collection(armature, pchan->bone_get(*armature));
|
||||
}
|
||||
|
||||
inline bool ANIM_bonecoll_is_visible_actbone(const bArmature *armature)
|
||||
{
|
||||
return ANIM_bone_in_visible_collection(armature, armature->act_bone);
|
||||
}
|
||||
|
||||
void ANIM_armature_bonecoll_show_all(bArmature *armature);
|
||||
void ANIM_armature_bonecoll_hide_all(bArmature *armature);
|
||||
|
||||
void ANIM_armature_bonecoll_show_from_bone(bArmature *armature, const Bone *bone);
|
||||
void ANIM_armature_bonecoll_show_from_ebone(bArmature *armature, const EditBone *ebone);
|
||||
void ANIM_armature_bonecoll_show_from_pchan(bArmature *armature, const bPoseChannel *pchan);
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/**
|
||||
* Return the index of the given collection in the armature's collection array,
|
||||
* or -1 if not found.
|
||||
*/
|
||||
int armature_bonecoll_find_index(const bArmature *armature, const BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Return the index of the given bone collection's parent, or -1 if it has no parent.
|
||||
*/
|
||||
int armature_bonecoll_find_parent_index(const bArmature *armature, int bcoll_index);
|
||||
|
||||
/**
|
||||
* Find the child number of this bone collection.
|
||||
*
|
||||
* This is the offset of this collection relative to the parent's first child.
|
||||
* In other words, the first child has number 0, second child has number 1, etc.
|
||||
*
|
||||
* This requires a scan of the array, hence the function is called 'find' and not 'get'.
|
||||
*/
|
||||
int armature_bonecoll_child_number_find(const bArmature *armature, const BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* Move this bone collection to a new child number.
|
||||
*
|
||||
* \return the new absolute index of the bone collection, or -1 if the new child number was not
|
||||
* valid.
|
||||
*
|
||||
* \see armature_bonecoll_child_number_find
|
||||
*/
|
||||
int armature_bonecoll_child_number_set(bArmature *armature,
|
||||
BoneCollection *bcoll,
|
||||
int new_child_number);
|
||||
|
||||
bool armature_bonecoll_is_root(const bArmature *armature, int bcoll_index);
|
||||
|
||||
bool armature_bonecoll_is_child_of(const bArmature *armature,
|
||||
int potential_parent_index,
|
||||
int potential_child_index);
|
||||
|
||||
bool armature_bonecoll_is_descendant_of(const bArmature *armature,
|
||||
int potential_parent_index,
|
||||
int potential_descendant_index);
|
||||
|
||||
bool bonecoll_has_children(const BoneCollection *bcoll);
|
||||
|
||||
/**
|
||||
* For each bone collection in the destination armature, copy its #BONE_COLLECTION_EXPANDED flag
|
||||
* from the corresponding bone collection in the source armature.
|
||||
*
|
||||
* This is used in the handling of undo steps, to ensure that undo'ing does _not_
|
||||
* modify this flag.
|
||||
*/
|
||||
void bonecolls_copy_expanded_flag(Span<BoneCollection *> bcolls_dest,
|
||||
Span<const BoneCollection *> bcolls_source);
|
||||
|
||||
/**
|
||||
* Move a bone collection from one parent to another.
|
||||
*
|
||||
* \param from_bcoll_index: Index of the bone collection to move.
|
||||
* \param to_child_num: Gap index of where to insert the collection; 0 to make it
|
||||
* the first child, and parent->child_count to make it the last child. -1 also
|
||||
* works as an indicator for the last child, as that makes it possible to call
|
||||
* this function without requiring the caller to find the BoneCollection* of the
|
||||
* parent.
|
||||
* \param from_parent_index: Index of its current parent (-1 if it is a root collection).
|
||||
* \param to_parent_index: Index of the new parent (-1 if it is to become a root collection).
|
||||
* \return the collection's new index in the collections_array.
|
||||
*/
|
||||
int armature_bonecoll_move_to_parent(bArmature *armature,
|
||||
int from_bcoll_index,
|
||||
int to_child_num,
|
||||
int from_parent_index,
|
||||
int to_parent_index);
|
||||
|
||||
/* --------------------------------------------------------------------
|
||||
* The following functions are only used by edit-mode Armature undo:
|
||||
*/
|
||||
|
||||
/**
|
||||
* Duplicates a list of BoneCollections for edit-mode undo purposes, and
|
||||
* returns original-to-duplicate remapping data.
|
||||
*
|
||||
* IMPORTANT: this discards membership data in the duplicate collections.
|
||||
* This is because this function is only intended to be used with
|
||||
* edit-mode Armatures, where the membership information in collections
|
||||
* is not definitive, instead being stored in the EditBones. The
|
||||
* assumption is that the membership information in the collections will
|
||||
* be rebuilt from the EditBones when leaving edit mode.
|
||||
*
|
||||
* The source and destination each have two parts: a heap-allocated array of
|
||||
* `BoneCollection *`, and an integer that keeps track of that array's length.
|
||||
* The destination parameters are pointers to those components, so they can
|
||||
* be modified. The destination array should be empty and unallocated.
|
||||
*
|
||||
* \param bcoll_array_dst, bcoll_array_dst_num: the destination BoneCollection
|
||||
* array and array size.
|
||||
* \param bcoll_array_src, bcoll_array_src_num: the source BoneCollection array
|
||||
* and array size.
|
||||
* \param do_id_user: when true, increments the user count of IDs that
|
||||
* the BoneCollections' custom properties point to, if any.
|
||||
*
|
||||
* \return a map from pointers-to-the-original-collections to
|
||||
* pointers-to-the-duplicate-collections. This can be used to remap
|
||||
* collection pointers in other data, such as EditBones.
|
||||
*/
|
||||
Map<BoneCollection *, BoneCollection *> ANIM_bonecoll_array_copy_no_membership(
|
||||
BoneCollection ***bcoll_array_dst,
|
||||
int *bcoll_array_dst_num,
|
||||
BoneCollection **bcoll_array_src,
|
||||
int bcoll_array_src_num,
|
||||
bool do_id_user);
|
||||
/**
|
||||
* Frees a list of BoneCollections.
|
||||
*
|
||||
* IMPORTANT: although there is nothing about this function that
|
||||
* fundamentally prevents it from being used generally, other data
|
||||
* structures like Armature runtime data and EditBones often store
|
||||
* direct pointers to BoneCollections, which this function does NOT
|
||||
* handle. Prefer using higher-level functions to remove BoneCollections
|
||||
* from Armatures.
|
||||
*
|
||||
* \param bcoll_array: pointer to the heap-allocated array of `BoneCollection *`
|
||||
* to be freed.
|
||||
* \param bcoll_array_num: pointer to the integer that tracks the length of
|
||||
* bcoll_array.
|
||||
* \param do_id_user: when true, decrements the user count of IDs that
|
||||
* the BoneCollections' custom properties point to, if any.
|
||||
*/
|
||||
void ANIM_bonecoll_array_free(BoneCollection ***bcoll_array,
|
||||
int *bcoll_array_num,
|
||||
bool do_id_user);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
53
blender-5.2.0/source/blender/animrig/ANIM_bonecolor.hh
Normal file
53
blender-5.2.0/source/blender/animrig/ANIM_bonecolor.hh
Normal file
@@ -0,0 +1,53 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief C++ part of the BoneColor DNA struct.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef __cplusplus
|
||||
# error This is a C++ header.
|
||||
#endif
|
||||
|
||||
#include "BKE_pose.hh"
|
||||
|
||||
#include "DNA_armature_types.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct bPoseChannel;
|
||||
struct ThemeWireColor;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/** C++ wrapper for the DNA BoneColor struct. */
|
||||
class BoneColor : public blender::BoneColor {
|
||||
public:
|
||||
BoneColor();
|
||||
BoneColor(const BoneColor &other);
|
||||
~BoneColor();
|
||||
|
||||
const ThemeWireColor *effective_color() const;
|
||||
|
||||
/* Support for storing in a #Set<BoneColor>. */
|
||||
bool operator==(const BoneColor &other) const;
|
||||
bool operator!=(const BoneColor &other) const;
|
||||
uint64_t hash() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the effective BoneColor of this pose bone.
|
||||
*
|
||||
* This returns the pose bone's own color, unless it's set to "default", then it defaults to the
|
||||
* armature bone color.
|
||||
*/
|
||||
const BoneColor &ANIM_bonecolor_posebone_get(bke::PChanBoneConst pchanbone);
|
||||
|
||||
}; // namespace animrig
|
||||
|
||||
} // namespace blender
|
||||
28
blender-5.2.0/source/blender/animrig/ANIM_driver.hh
Normal file
28
blender-5.2.0/source/blender/animrig/ANIM_driver.hh
Normal file
@@ -0,0 +1,28 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to work with drivers.
|
||||
*/
|
||||
|
||||
#include "RNA_types.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct AnimationEvalContext;
|
||||
struct FCurve;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/** Evaluates the driver on the frame given in `anim_eval_context` and returns the value. Returns 0
|
||||
* if the RNA path can't be resolved. */
|
||||
float evaluate_driver_from_rna_pointer(const AnimationEvalContext *anim_eval_context,
|
||||
PointerRNA *ptr,
|
||||
PropertyRNA *prop,
|
||||
const FCurve *fcu);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
167
blender-5.2.0/source/blender/animrig/ANIM_evaluation.hh
Normal file
167
blender-5.2.0/source/blender/animrig/ANIM_evaluation.hh
Normal file
@@ -0,0 +1,167 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Developers
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Layered Action evaluation.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
/* Identifies the property that an evaluated animation value is for.
|
||||
*
|
||||
* This could be replaced with either `FCurveIdentifier` or `RNAPath`. However,
|
||||
* `FCurveIdentifier` is semantically meant to represent an fcurve itself rather
|
||||
* than the property an fcurve might be for, and moreover not all animation will
|
||||
* necessarily come from fcurves in the future anyway. `RNAPath` would be more
|
||||
* semantically appropriate, but it stores a full copy of the string component
|
||||
* of the path, and here we want to be lighter than that and use a string
|
||||
* reference.
|
||||
*/
|
||||
class PropIdentifier {
|
||||
public:
|
||||
/**
|
||||
* Reference to the RNA path of the property.
|
||||
*
|
||||
* This string is typically owned by the FCurve that animates the property.
|
||||
*/
|
||||
StringRefNull rna_path;
|
||||
int array_index;
|
||||
|
||||
PropIdentifier() = default;
|
||||
|
||||
PropIdentifier(const StringRefNull rna_path, const int array_index)
|
||||
: rna_path(rna_path), array_index(array_index)
|
||||
{
|
||||
}
|
||||
|
||||
bool operator==(const PropIdentifier &other) const
|
||||
{
|
||||
return rna_path == other.rna_path && array_index == other.array_index;
|
||||
}
|
||||
bool operator!=(const PropIdentifier &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
uint64_t hash() const
|
||||
{
|
||||
return get_default_hash(rna_path, array_index);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The evaluated value for an animated property, along with its RNA pointer.
|
||||
*/
|
||||
class AnimatedProperty {
|
||||
public:
|
||||
float value;
|
||||
PathResolvedRNA prop_rna;
|
||||
|
||||
AnimatedProperty(const float value, const PathResolvedRNA &prop_rna)
|
||||
: value(value), prop_rna(prop_rna)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
/* Result of FCurve evaluation for an action slot.
|
||||
* Mapping from property identifier to its float value.
|
||||
*
|
||||
* Can be fed to the evaluation of the next layer, mixed with another strip, or
|
||||
* used to modify actual RNA properties.
|
||||
*
|
||||
* TODO: see if this is efficient, and contains enough info, for mixing. For now
|
||||
* this just captures the FCurve evaluation result, but doesn't have any info
|
||||
* about how to do the mixing (LERP, quaternion SLERP, etc.).
|
||||
*/
|
||||
class EvaluationResult {
|
||||
protected:
|
||||
using EvaluationMap = Map<PropIdentifier, AnimatedProperty>;
|
||||
EvaluationMap result_;
|
||||
|
||||
public:
|
||||
EvaluationResult() = default;
|
||||
|
||||
operator bool() const
|
||||
{
|
||||
return !this->is_empty();
|
||||
}
|
||||
bool is_empty() const
|
||||
{
|
||||
return result_.is_empty();
|
||||
}
|
||||
|
||||
/**
|
||||
* When the expected count of entries is known, reserving with size instead of growing on demand
|
||||
* is more performant.
|
||||
*/
|
||||
void reserve(const int64_t size)
|
||||
{
|
||||
result_.reserve(size);
|
||||
};
|
||||
|
||||
void store(const StringRefNull rna_path,
|
||||
const int array_index,
|
||||
const float value,
|
||||
const PathResolvedRNA &prop_rna)
|
||||
{
|
||||
PropIdentifier key(rna_path, array_index);
|
||||
AnimatedProperty anim_prop(value, prop_rna);
|
||||
result_.add_overwrite(key, anim_prop);
|
||||
}
|
||||
|
||||
AnimatedProperty value(const StringRefNull rna_path, const int array_index) const
|
||||
{
|
||||
PropIdentifier key(rna_path, array_index);
|
||||
return result_.lookup(key);
|
||||
}
|
||||
|
||||
const AnimatedProperty *lookup_ptr(const PropIdentifier &key) const
|
||||
{
|
||||
return result_.lookup_ptr(key);
|
||||
}
|
||||
AnimatedProperty *lookup_ptr(const PropIdentifier &key)
|
||||
{
|
||||
return result_.lookup_ptr(key);
|
||||
}
|
||||
|
||||
EvaluationMap::ItemIterator items() const
|
||||
{
|
||||
return result_.items();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Evaluate the given action for the given slot and animated ID.
|
||||
*
|
||||
* This does *not* apply the resulting values to the ID. Instead, it returns
|
||||
* the resulting values in an `EvaluationResult`.
|
||||
*/
|
||||
EvaluationResult evaluate_action(PointerRNA &animated_id_ptr,
|
||||
Action &action,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context);
|
||||
|
||||
/**
|
||||
* Top level animation evaluation function.
|
||||
*
|
||||
* Animate the given ID, using the layered Action and the given slot.
|
||||
*
|
||||
* \param flush_to_original: when true, look up the original data-block (assuming
|
||||
* the given one is an evaluated copy) and update that too.
|
||||
*/
|
||||
void evaluate_and_apply_action(PointerRNA &animated_id_ptr,
|
||||
Action &action,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context,
|
||||
bool flush_to_original);
|
||||
|
||||
} // namespace blender::animrig
|
||||
180
blender-5.2.0/source/blender/animrig/ANIM_fcurve.hh
Normal file
180
blender-5.2.0/source/blender/animrig/ANIM_fcurve.hh
Normal file
@@ -0,0 +1,180 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to modify FCurves.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
#include "ANIM_keyframing.hh"
|
||||
|
||||
#include "BLI_math_vector_types.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct AnimData;
|
||||
struct FCurve;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/**
|
||||
* All the information needed to look up or create an FCurve.
|
||||
*
|
||||
* The `std::optional<>` fields are only used for creation. The mandatory fields
|
||||
* are used for both creation and lookup.
|
||||
*/
|
||||
struct FCurveDescriptor {
|
||||
StringRefNull rna_path;
|
||||
int array_index;
|
||||
std::optional<PropertyType> prop_type;
|
||||
std::optional<PropertySubType> prop_subtype;
|
||||
std::optional<StringRefNull> channel_group;
|
||||
};
|
||||
|
||||
/** This is used to pass in the settings for a keyframe into a function. */
|
||||
struct KeyframeSettings {
|
||||
eBezTriple_KeyframeType keyframe_type;
|
||||
eBezTriple_Handle handle;
|
||||
eBezTriple_Interpolation interpolation;
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to generate the KeyframeSettings struct.
|
||||
*
|
||||
* \param from_userprefs: if true read the user preferences for the settings, else return static
|
||||
* defaults.
|
||||
*/
|
||||
KeyframeSettings get_keyframe_settings(bool from_userprefs);
|
||||
|
||||
/**
|
||||
* Return the first fcurve in `fcurves` that matches `fcurve_descriptor`.
|
||||
*
|
||||
* If no matching fcurve is found, returns nullptr.
|
||||
*/
|
||||
const FCurve *fcurve_find(Span<const FCurve *> fcurves, const FCurveDescriptor &fcurve_descriptor);
|
||||
FCurve *fcurve_find(Span<FCurve *> fcurves, const FCurveDescriptor &fcurve_descriptor);
|
||||
|
||||
/**
|
||||
* Create an fcurve for a specific channel, pre-set-up with default flags and
|
||||
* interpolation mode.
|
||||
*
|
||||
* If the channel's property subtype is provided, the fcurve will also be set to
|
||||
* the correct color mode based on user preferences.
|
||||
*/
|
||||
FCurve *create_fcurve_for_channel(const FCurveDescriptor &fcurve_descriptor);
|
||||
|
||||
/**
|
||||
* Determine the F-Curve flags suitable for animating an RNA property of the given type.
|
||||
*/
|
||||
eFCurve_Flags fcurve_flags_for_property_type(PropertyType prop_type);
|
||||
|
||||
/** Initialize the given BezTriple with default values. */
|
||||
void initialize_bezt(BezTriple *beztr,
|
||||
float2 position,
|
||||
const KeyframeSettings &settings,
|
||||
eFCurve_Flags fcu_flags);
|
||||
|
||||
/**
|
||||
* Delete the keyframe at `time` on `fcurve` if a key exists there.
|
||||
*
|
||||
* This does NOT delete the FCurve if it ends up empty. That is for the caller to do.
|
||||
*
|
||||
* \note `time` is in fcurve time, not scene time. Any time remapping must be
|
||||
* done prior to calling this function.
|
||||
*
|
||||
* \return True if a keyframe was found at `time` and deleted, false otherwise.
|
||||
*/
|
||||
bool fcurve_delete_keyframe_at_time(FCurve *fcurve, float time);
|
||||
|
||||
/**
|
||||
* Deletes the keyframe at `cfra` on `fcu` if a key exists there, and deletes
|
||||
* the fcurve if it was the only keyframe.
|
||||
*
|
||||
* \note For fcurves on legacy actions only. More specifically, this assumes
|
||||
* that the fcurve lives on `adt->action` and that `adt->action` is a legacy
|
||||
* action.
|
||||
*
|
||||
* \note The caller needs to run #BKE_nla_tweakedit_remap to get NLA relative frame.
|
||||
* The caller should also check #BKE_fcurve_is_protected before keying.
|
||||
*/
|
||||
bool delete_keyframe_fcurve_legacy(AnimData *adt, FCurve *fcu, float cfra);
|
||||
|
||||
/**
|
||||
* \brief Lesser Key-framing API call.
|
||||
*
|
||||
* Use this when validation of necessary animation data isn't necessary as it already
|
||||
* exists, and there is a #BezTriple that can be directly copied into the array.
|
||||
*
|
||||
* This function adds a given #BezTriple to an F-Curve. It will allocate
|
||||
* memory for the array if needed, and will insert the #BezTriple into a
|
||||
* suitable place in chronological order.
|
||||
*
|
||||
* \returns The index of the keyframe array into which the bezt has been added.
|
||||
*
|
||||
* \note Any recalculate of the F-Curve that needs to be done will need to be done by the caller.
|
||||
*/
|
||||
int insert_bezt_fcurve(FCurve *fcu, const BezTriple *bezt, eInsertKeyFlags flag);
|
||||
|
||||
/**
|
||||
* \brief Main Key-framing API call.
|
||||
*
|
||||
* Use this when validation of necessary animation data isn't necessary as it
|
||||
* already exists. It will insert a keyframe using the current value being keyframed.
|
||||
*
|
||||
* This function is a wrapper for #insert_bezt_fcurve(), and should be used when
|
||||
* adding a new keyframe to a curve, when the keyframe doesn't exist anywhere else yet.
|
||||
*
|
||||
* \returns Either success or an indicator of why keying failed.
|
||||
*
|
||||
* \param flag: Optional flags (#eInsertKeyFlags) for controlling how keys get added
|
||||
* and/or whether updates get done.
|
||||
*/
|
||||
SingleKeyingResult insert_vert_fcurve(FCurve *fcu,
|
||||
const float2 position,
|
||||
const KeyframeSettings &settings,
|
||||
eInsertKeyFlags flag);
|
||||
|
||||
/**
|
||||
* \param sample_rate: indicates how many samples per frame should be generated.
|
||||
* \param samples: Is expected to be an array large enough to hold `sample_count`.
|
||||
*/
|
||||
void sample_fcurve_segment(
|
||||
const FCurve *fcu, float start_frame, float sample_rate, float *samples, int sample_count);
|
||||
|
||||
enum class BakeCurveRemove {
|
||||
NONE = 0,
|
||||
IN_RANGE = 1,
|
||||
OUT_RANGE = 2,
|
||||
ALL = 3,
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates keyframes in the given range at the given step interval.
|
||||
* \param range: start and end frame to bake. Is inclusive on both ends.
|
||||
* \param remove_existing: choice which keys to remove in relation to the given range.
|
||||
*/
|
||||
void bake_fcurve(FCurve *fcu, int2 range, float step, BakeCurveRemove remove_existing);
|
||||
|
||||
/**
|
||||
* Fill the space between selected keyframes with keyframes on full frames.
|
||||
* E.g. With a key selected on frame 1 and 3 it will insert a key on frame 2.
|
||||
*/
|
||||
void bake_fcurve_segments(FCurve *fcu);
|
||||
|
||||
/**
|
||||
* Checks if some F-Curve has a keyframe for a given frame.
|
||||
* \note Used for the buttons to check for keyframes.
|
||||
*
|
||||
* \param frame: The frame on which to check for a keyframe. A binary search with a threshold is
|
||||
* used to find the key, so the float doesn't need to match exactly.
|
||||
*/
|
||||
bool fcurve_frame_has_keyframe(const FCurve *fcu, float frame);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
312
blender-5.2.0/source/blender/animrig/ANIM_keyframing.hh
Normal file
312
blender-5.2.0/source/blender/animrig/ANIM_keyframing.hh
Normal file
@@ -0,0 +1,312 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to insert, delete or modify keyframes.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "RNA_path.hh"
|
||||
#include "RNA_types.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct ID;
|
||||
struct Main;
|
||||
struct Scene;
|
||||
|
||||
struct AnimationEvalContext;
|
||||
struct NlaKeyframingContext;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/**
|
||||
* Represents a single success/failure in the keyframing process.
|
||||
*
|
||||
* What is considered "single" depends on the level at which the failure
|
||||
* happens. For example, it can be at the level of a single key on a single
|
||||
* fcurve, all the way up to the level of an entire ID not being animatable.
|
||||
* Both are considered "single" events.
|
||||
*/
|
||||
enum class SingleKeyingResult {
|
||||
SUCCESS = 0,
|
||||
/* TODO: remove `UNKNOWN_FAILURE` and replace all usages with proper, specific
|
||||
* cases. This is needed right now as a stop-gap while progressively moving
|
||||
* the keyframing code over to propagate errors properly. */
|
||||
UNKNOWN_FAILURE,
|
||||
CANNOT_CREATE_FCURVE,
|
||||
FCURVE_NOT_KEYFRAMEABLE,
|
||||
NO_KEY_NEEDED,
|
||||
UNABLE_TO_INSERT_TO_NLA_STACK,
|
||||
ID_NOT_EDITABLE,
|
||||
ID_NOT_ANIMATABLE,
|
||||
NO_VALID_LAYER,
|
||||
NO_VALID_STRIP,
|
||||
NO_VALID_SLOT,
|
||||
CANNOT_RESOLVE_PATH,
|
||||
/* Make sure to always keep this at the end of the enum. */
|
||||
_KEYING_RESULT_MAX,
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a message into the ReportList depending on which SingleKeyingResult is passed.
|
||||
* This should only be used when a single keyframe is being added per user action. Otherwise use
|
||||
* CombinedKeyingResult to avoid spamming the user with messages.
|
||||
*/
|
||||
void generate_single_keying_result_report(const SingleKeyingResult result, ReportList *reports);
|
||||
|
||||
/**
|
||||
* Class for tracking the result of inserting keyframes. Tracks how often each of
|
||||
* `SingleKeyingResult` has happened.
|
||||
* */
|
||||
class CombinedKeyingResult {
|
||||
private:
|
||||
/* The index to the array maps a `SingleKeyingResult` to the number of times this result has
|
||||
* occurred. */
|
||||
std::array<int, size_t(SingleKeyingResult::_KEYING_RESULT_MAX)> result_counter;
|
||||
|
||||
public:
|
||||
CombinedKeyingResult();
|
||||
|
||||
/**
|
||||
* Increase the count of the given `SingleKeyingResult` by `count`.
|
||||
*/
|
||||
void add(SingleKeyingResult result, int count = 1);
|
||||
|
||||
/* Add values of the given result to this result. */
|
||||
void merge(const CombinedKeyingResult &other);
|
||||
|
||||
int get_count(const SingleKeyingResult result) const;
|
||||
|
||||
bool has_errors() const;
|
||||
|
||||
void generate_reports(ReportList *reports, eReportType report_level = RPT_ERROR);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return the default channel group name for the given RNA pointer and property
|
||||
* path, or none if it has no default.
|
||||
*
|
||||
* For example, for object location/rotation/scale this returns the standard
|
||||
* "Object Transforms" channel group name.
|
||||
*/
|
||||
std::optional<StringRefNull> default_channel_group_for_path(const PointerRNA *animated_struct,
|
||||
StringRef prop_rna_path);
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
|
||||
/**
|
||||
* Return whether key insertion functions are allowed to create new fcurves,
|
||||
* according to the given flags.
|
||||
*
|
||||
* Specifically, both `INSERTKEY_REPLACE` and `INSERTKEY_AVAILABLE` prohibit the
|
||||
* creation of new F-Curves.
|
||||
*/
|
||||
bool key_insertion_may_create_fcurve(eInsertKeyFlags insert_key_flags);
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Key-Framing Management
|
||||
* \{ */
|
||||
|
||||
/** Ensure FCurve flags are correct for the property type it animates. */
|
||||
void update_autoflags_fcurve_direct(FCurve *fcu, PropertyType prop_type);
|
||||
|
||||
/**
|
||||
* Return all values of the given PropertyRNA.
|
||||
* In case the property is an array, the length of the vector is the same as the length of the
|
||||
* array. Otherwise the vector has just 1 element.
|
||||
* All property types are cast to float.
|
||||
*/
|
||||
Vector<float> get_property_values(PointerRNA *ptr, PropertyRNA *prop, bool visual_key);
|
||||
|
||||
/**
|
||||
* \brief Main key-frame insertion API.
|
||||
*
|
||||
* Insert keys for `struct_pointer`, for all paths in `rna_paths`. Any necessary
|
||||
* animation data (AnimData, Action, ...) is created if it doesn't already
|
||||
* exist.
|
||||
*
|
||||
* Note that this function was created as part of an ongoing refactor by merging
|
||||
* two other functions that were *almost* identical to each other. There are
|
||||
* still things left over from that which can and should be improved (such as
|
||||
* the partially redundant `scene_frame` and `anim_eval_context`parameters).
|
||||
* Additionally, it's a bit of a mega-function now, and can probably be stripped
|
||||
* down to a clearer core functionality.
|
||||
*
|
||||
* \param struct_pointer: RNA pointer to the struct to be keyed. This is often
|
||||
* an ID, but not necessarily. For example, pose bones are also common. Note
|
||||
* that if you have an `ID` and want to pass it here for keying, you can create
|
||||
* the `PointerRNA` for it with `RNA_id_pointer_create()`.
|
||||
*
|
||||
* \param channel_group: the channel group to put any newly created fcurves
|
||||
* under. If not given, the standard groups are used.
|
||||
*
|
||||
* \param rna_paths: the RNA paths to key. These paths are relative to
|
||||
* `struct_pointer`. Note that for paths to array properties, if the array index
|
||||
* is specified then only that element is keyed, but if the index is not
|
||||
* specified then *all* array elements are keyed.
|
||||
*
|
||||
* \param scene_frame: the frame to insert the keys at. This is in scene time,
|
||||
* not NLA mapped (NLA mapping is already handled internally by this function).
|
||||
* If not given, the evaluation time from `anim_eval_context` is used instead.
|
||||
*
|
||||
* \returns A summary of the successful and failed keyframe insertions, with
|
||||
* reasons for the failures.
|
||||
*/
|
||||
CombinedKeyingResult insert_keyframes(Main *bmain,
|
||||
PointerRNA *struct_pointer,
|
||||
std::optional<StringRefNull> channel_group,
|
||||
const Span<RNAPath> rna_paths,
|
||||
std::optional<float> scene_frame,
|
||||
const AnimationEvalContext &anim_eval_context,
|
||||
eBezTriple_KeyframeType key_type,
|
||||
eInsertKeyFlags insert_key_flags);
|
||||
|
||||
/**
|
||||
* \brief Secondary Insert Key-framing API call.
|
||||
*
|
||||
* Retrieves the value of the PropertyRNA and sets a key at `fcurve_frame`. If the property is an
|
||||
* array, the FCurve's array_index is used to find the element's value.
|
||||
*
|
||||
* \warning This bypasses all animation layer and strip logic. Use with caution. If unsure, use
|
||||
* `insert_keyframes` instead.
|
||||
*
|
||||
* \param fcurve_frame: The frame at which to insert the keyframe. No time remapping is done on the
|
||||
* frame.
|
||||
*
|
||||
* \param keytype: The "keyframe type" (eBezTriple_KeyframeType), as shown in the Dope Sheet.
|
||||
*
|
||||
* \param flag: Used for special settings that alter the behavior of the keyframe insertion.
|
||||
* These include the 'visual' key-framing modes, quick refresh,
|
||||
* and extra keyframe filtering.
|
||||
*
|
||||
* \note this function no longer deals with the NLA. For NLA support use
|
||||
* animrig::nla::insert_keyframe_direct Other high level keyframing functions still work with the
|
||||
* NLA. See `insert_keyframes`.
|
||||
*
|
||||
* \return Success.
|
||||
*/
|
||||
SingleKeyingResult insert_keyframe_direct(PointerRNA &ptr,
|
||||
PropertyRNA &prop,
|
||||
FCurve &fcu,
|
||||
float fcurve_frame,
|
||||
eBezTriple_KeyframeType keytype,
|
||||
eInsertKeyFlags flag);
|
||||
|
||||
/**
|
||||
* \brief Main Delete Key-Framing API call.
|
||||
*
|
||||
* Use this to delete keyframe on current frame for relevant channel.
|
||||
* Will perform checks just in case.
|
||||
* \return The number of key-frames deleted.
|
||||
*/
|
||||
int delete_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_path, float cfra);
|
||||
|
||||
/**
|
||||
* Main Keyframing API call:
|
||||
* Use this when validation of necessary animation data isn't necessary as it
|
||||
* already exists. It will clear the current buttons fcurve(s).
|
||||
*
|
||||
* \return The number of f-curves removed.
|
||||
*/
|
||||
int clear_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_path);
|
||||
|
||||
/** Check if a flag is set for keyframing (per scene takes precedence). */
|
||||
bool is_keying_flag(const Scene *scene, eKeying_Flag flag);
|
||||
|
||||
/**
|
||||
* Checks whether a keyframe exists for the given ID-block one the given frame.
|
||||
*
|
||||
* \param frame: The frame on which to check for a keyframe. This uses a threshold so the float
|
||||
* doesn't need to match exactly.
|
||||
*/
|
||||
bool id_frame_has_keyframe(ID *id, float frame);
|
||||
|
||||
/**
|
||||
* Get the settings for key-framing from the given scene.
|
||||
*/
|
||||
eInsertKeyFlags get_keyframing_flags(Scene *scene);
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Auto keyframing
|
||||
* Notes:
|
||||
* - All the defines for this (User-Pref settings and Per-Scene settings)
|
||||
* are defined in DNA_userdef_types.h
|
||||
* - Scene settings take precedence over those for user-preferences, with old files
|
||||
* inheriting user-preferences settings for the scene settings
|
||||
* - "On/Off + Mode" are stored per Scene, but "settings" are currently stored as user-preferences.
|
||||
* \{ */
|
||||
|
||||
/** Check if auto-key-framing is enabled (per scene takes precedence). */
|
||||
bool is_autokey_on(const Scene *scene);
|
||||
|
||||
/** Check the mode for auto-keyframing (per scene takes precedence). */
|
||||
bool is_autokey_mode(const Scene *scene, eAutokey_Mode mode);
|
||||
|
||||
/**
|
||||
* Auto-keyframing feature - checks for whether anything should be done for the current frame.
|
||||
*/
|
||||
bool autokeyframe_cfra_can_key(const Scene *scene, ID *id);
|
||||
|
||||
/**
|
||||
* Insert keyframes on the given object `ob` based on the auto-keying settings.
|
||||
*
|
||||
* \param rna_paths: Only inserts keys on those RNA paths.
|
||||
*/
|
||||
void autokeyframe_object(bContext *C, const Scene *scene, Object *ob, Span<RNAPath> rna_paths);
|
||||
/**
|
||||
* Auto-keyframing feature - for objects
|
||||
*
|
||||
* \note Context may not always be available,
|
||||
* so must check before using it as it's a luxury for a few cases.
|
||||
*/
|
||||
bool autokeyframe_object(bContext *C, Scene *scene, Object *ob, KeyingSet *ks);
|
||||
bool autokeyframe_pchan(bContext *C, Scene *scene, Object *ob, bPoseChannel *pchan, KeyingSet *ks);
|
||||
/**
|
||||
* Auto-keyframing feature - for poses/pose-channels
|
||||
*
|
||||
* \param rna_paths: Only inserts keys on those RNA paths.
|
||||
* \param targetless_ik: Has targetless ik been done on any channels?
|
||||
*
|
||||
* \note Context may not always be available,
|
||||
* so must check before using it as it's a luxury for a few cases.
|
||||
*/
|
||||
void autokeyframe_pose_channel(bContext *C,
|
||||
Scene *scene,
|
||||
Object *ob,
|
||||
bPoseChannel *pose_channel,
|
||||
Span<RNAPath> rna_paths,
|
||||
short targetless_ik);
|
||||
/**
|
||||
* Use for auto-key-framing.
|
||||
* \param only_if_property_keyed: if true, auto-key-framing only creates keyframes on already keyed
|
||||
* properties. This is by design when using buttons. For other callers such as gizmos or sequencer
|
||||
* preview transform, creating new animation/keyframes also on non-keyed properties is desired.
|
||||
*/
|
||||
bool autokeyframe_property(bContext *C,
|
||||
Scene *scene,
|
||||
PointerRNA *ptr,
|
||||
PropertyRNA *prop,
|
||||
int rnaindex,
|
||||
float cfra,
|
||||
bool only_if_property_keyed);
|
||||
|
||||
/** \} */
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
194
blender-5.2.0/source/blender/animrig/ANIM_keyingsets.hh
Normal file
194
blender-5.2.0/source/blender/animrig/ANIM_keyingsets.hh
Normal file
@@ -0,0 +1,194 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functionality to interact with keying sets.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "RNA_types.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct KeyingSet;
|
||||
struct ExtensionRNA;
|
||||
/* Forward declaration for this struct which is declared a bit later. */
|
||||
struct KeyingSetInfo;
|
||||
struct bContext;
|
||||
struct ID;
|
||||
struct Scene;
|
||||
struct PointerRNA;
|
||||
|
||||
enum eInsertKeyFlags : short;
|
||||
|
||||
/* Names for builtin keying sets so we don't confuse these with labels/text,
|
||||
* defined in python script: `keyingsets_builtins.py`. */
|
||||
|
||||
static constexpr const char *ANIM_KS_LOCATION_ID = "Location";
|
||||
static constexpr const char *ANIM_KS_ROTATION_ID = "Rotation";
|
||||
static constexpr const char *ANIM_KS_SCALING_ID = "Scaling";
|
||||
static constexpr const char *ANIM_KS_LOC_ROT_SCALE_ID = "LocRotScale";
|
||||
static constexpr const char *ANIM_KS_LOC_ROT_SCALE_CPROP_ID = "LocRotScaleCProp";
|
||||
static constexpr const char *ANIM_KS_AVAILABLE_ID = "Available";
|
||||
static constexpr const char *ANIM_KS_WHOLE_CHARACTER_ID = "WholeCharacter";
|
||||
static constexpr const char *ANIM_KS_WHOLE_CHARACTER_SELECTED_ID = "WholeCharacterSelected";
|
||||
|
||||
/** Polling Callback for KeyingSets. */
|
||||
using cbKeyingSet_Poll = bool (*)(KeyingSetInfo *ksi, bContext *C);
|
||||
/** Context Iterator Callback for KeyingSets. */
|
||||
using cbKeyingSet_Iterator = void (*)(KeyingSetInfo *ksi, bContext *C, KeyingSet *ks);
|
||||
/** Property Specifier Callback for KeyingSets (called from iterators) */
|
||||
using cbKeyingSet_Generate = void (*)(KeyingSetInfo *ksi,
|
||||
bContext *C,
|
||||
KeyingSet *ks,
|
||||
PointerRNA *ptr);
|
||||
|
||||
/** Callback info for 'Procedural' KeyingSets to use. */
|
||||
struct KeyingSetInfo {
|
||||
KeyingSetInfo *next, *prev;
|
||||
|
||||
/* info */
|
||||
/** Identifier used for class name, which KeyingSet instances reference as "Type-info Name". */
|
||||
char idname[/*MAX_NAME*/ 64];
|
||||
/** identifier so that user can hook this up to a KeyingSet (used as label). */
|
||||
char name[/*MAX_NAME*/ 64];
|
||||
/** Short help/description. */
|
||||
char description[/*RNA_DYN_DESCR_MAX*/ 1024];
|
||||
/** Keying settings. */
|
||||
eInsertKeyFlags keyingflag;
|
||||
|
||||
/* polling callbacks */
|
||||
/** callback for polling the context for whether the right data is available. */
|
||||
cbKeyingSet_Poll poll;
|
||||
|
||||
/* generate callbacks */
|
||||
/**
|
||||
* Iterator to use to go through collections of data in context
|
||||
* - this callback is separate from the 'adding' stage, allowing
|
||||
* BuiltIn KeyingSets to be manually specified to use.
|
||||
*/
|
||||
cbKeyingSet_Iterator iter;
|
||||
/** Generator to use to add properties based on the data found by iterator. */
|
||||
cbKeyingSet_Generate generate;
|
||||
|
||||
/** RNA integration. */
|
||||
ExtensionRNA rna_ext;
|
||||
};
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/** Mode for modify_keyframes. */
|
||||
enum class ModifyKeyMode {
|
||||
INSERT = 0,
|
||||
/* Not calling it just `DELETE` because that interferes with a macro on windows. */
|
||||
DELETE_KEY,
|
||||
};
|
||||
|
||||
/** Return codes for errors (with Relative KeyingSets). */
|
||||
enum class ModifyKeyReturn {
|
||||
SUCCESS = 0,
|
||||
/** Context info was invalid for using the Keying Set. */
|
||||
INVALID_CONTEXT = -1,
|
||||
/** There isn't any type-info for generating paths from context. */
|
||||
MISSING_TYPEINFO = -2,
|
||||
};
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Keyingset Usage
|
||||
* \{ */
|
||||
|
||||
/**
|
||||
* Given a #KeyingSet and context info, validate Keying Set's paths.
|
||||
* This is only really necessary with relative/built-in KeyingSets
|
||||
* where their list of paths is dynamically generated based on the
|
||||
* current context info.
|
||||
*
|
||||
* \note Passing sources as pointer because it can be a nullptr.
|
||||
*/
|
||||
ModifyKeyReturn validate_keyingset(bContext *C, Vector<PointerRNA> *sources, KeyingSet *keyingset);
|
||||
|
||||
/**
|
||||
* Use the specified #KeyingSet and context info (if required)
|
||||
* to add/remove various Keyframes on the specified frame.
|
||||
*
|
||||
* Modify keyframes for the channels specified by the KeyingSet.
|
||||
* This takes into account many of the different combinations of using KeyingSets.
|
||||
*
|
||||
* \returns the number of channels that key-frames were added or
|
||||
* an #ModifyKeyReturn error (always a negative number).
|
||||
*/
|
||||
int apply_keyingset(bContext *C,
|
||||
Vector<PointerRNA> *sources,
|
||||
KeyingSet *keyingset,
|
||||
ModifyKeyMode mode,
|
||||
float cfra);
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Queries
|
||||
* \{ */
|
||||
|
||||
/**
|
||||
* Find builtin #KeyingSet by name.
|
||||
*
|
||||
* \return The first builtin #KeyingSet with the given name
|
||||
*/
|
||||
KeyingSet *builtin_keyingset_get_named(const char name[]);
|
||||
|
||||
/**
|
||||
* Find KeyingSet type info given a name.
|
||||
*/
|
||||
KeyingSetInfo *keyingset_info_find_name(const char name[]);
|
||||
|
||||
/**
|
||||
* Check if the ID appears in the paths specified by the #KeyingSet.
|
||||
*/
|
||||
bool keyingset_find_id(KeyingSet *keyingset, ID *id);
|
||||
|
||||
/**
|
||||
* Get Keying Set to use for Auto-Key-Framing some transforms.
|
||||
*/
|
||||
KeyingSet *get_keyingset_for_autokeying(const Scene *scene, const char *transformKSName);
|
||||
|
||||
/**
|
||||
* Get the active Keying Set for the given scene.
|
||||
*/
|
||||
KeyingSet *scene_get_active_keyingset(const Scene *scene);
|
||||
|
||||
/** \} */
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Creation, Destruction
|
||||
* \{ */
|
||||
|
||||
/**
|
||||
* Add the given KeyingSetInfo to the list of type infos,
|
||||
* and create an appropriate builtin set too.
|
||||
*/
|
||||
void keyingset_info_register(KeyingSetInfo *keyingset_info);
|
||||
/**
|
||||
* Remove the given #KeyingSetInfo from the list of type infos,
|
||||
* and also remove the builtin set if appropriate.
|
||||
*/
|
||||
void keyingset_info_unregister(Main *bmain, KeyingSetInfo *keyingset_info);
|
||||
|
||||
void keyingset_infos_exit();
|
||||
|
||||
/**
|
||||
* Add another data source for Relative Keying Sets to be evaluated with.
|
||||
*/
|
||||
void relative_keyingset_add_source(Vector<PointerRNA> &sources,
|
||||
ID *id,
|
||||
StructRNA *srna,
|
||||
void *data);
|
||||
void relative_keyingset_add_source(Vector<PointerRNA> &sources, ID *id);
|
||||
|
||||
/** \} */
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
70
blender-5.2.0/source/blender/animrig/ANIM_nla.hh
Normal file
70
blender-5.2.0/source/blender/animrig/ANIM_nla.hh
Normal file
@@ -0,0 +1,70 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors. All rights reserved.
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct ID;
|
||||
struct NlaStrip;
|
||||
|
||||
namespace animrig::nla {
|
||||
|
||||
/**
|
||||
* Assign the Action to this NLA strip.
|
||||
*
|
||||
* Similar to animrig::assign_action(), this tries to find a suitable slot.
|
||||
*
|
||||
* \see animrig::assign_action
|
||||
*
|
||||
* \returns whether the assignment was ok.
|
||||
*/
|
||||
bool assign_action(NlaStrip &strip, Action &action, ID &animated_id);
|
||||
|
||||
void unassign_action(NlaStrip &strip, ID &animated_id);
|
||||
|
||||
/**
|
||||
* Assign a slot to the NLA strip.
|
||||
*
|
||||
* The strip should already have an Action assigned to it, and the given Slot should belong to that
|
||||
* Action.
|
||||
*
|
||||
* \param slot_to_assign: the slot to assign, or nullptr to un-assign the current slot.
|
||||
*/
|
||||
ActionSlotAssignmentResult assign_action_slot(NlaStrip &strip,
|
||||
Slot *slot_to_assign,
|
||||
ID &animated_id);
|
||||
|
||||
ActionSlotAssignmentResult assign_action_slot_handle(NlaStrip &strip,
|
||||
slot_handle_t slot_handle,
|
||||
ID &animated_id);
|
||||
|
||||
/**
|
||||
* Keyframing function taking the NLA into account.
|
||||
*
|
||||
* Inserts a key into the given FCurve with the current value of the PropertyRNA remapped through
|
||||
* the NLA stack.
|
||||
*
|
||||
* With the removal of the NLA any calls to this can be redirected to
|
||||
* animrig::insert_keyframe_direct.
|
||||
*
|
||||
* \warning This bypasses all animation layer and strip logic. Use with caution. If unsure, use
|
||||
* `insert_keyframes` instead.
|
||||
*/
|
||||
bool insert_keyframe_direct(ReportList *reports,
|
||||
PointerRNA ptr,
|
||||
PropertyRNA *prop,
|
||||
FCurve *fcu,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
eBezTriple_KeyframeType keytype,
|
||||
NlaKeyframingContext *nla_context,
|
||||
eInsertKeyFlags flag);
|
||||
} // namespace animrig::nla
|
||||
} // namespace blender
|
||||
73
blender-5.2.0/source/blender/animrig/ANIM_pose.hh
Normal file
73
blender-5.2.0/source/blender/animrig/ANIM_pose.hh
Normal file
@@ -0,0 +1,73 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to work with animation poses.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "BLI_span.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct AnimationEvalContext;
|
||||
struct Object;
|
||||
struct bAction;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/**
|
||||
* Evaluate the action and apply it to the pose. Ignore selection state of the bones.
|
||||
*/
|
||||
void pose_apply_action_all_bones(Object *ob,
|
||||
bAction *action,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context);
|
||||
|
||||
/**
|
||||
* Evaluate the action and blend the result into the current pose based on `blend_factor`.
|
||||
* Only FCurves that relate to selected bones are evaluated.
|
||||
*/
|
||||
void pose_apply_action_blend(Object *ob,
|
||||
bAction *action,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
float blend_factor);
|
||||
|
||||
/**
|
||||
* Like `pose_apply_action_blend` but applies to all bones regardless of selection.
|
||||
*/
|
||||
void pose_apply_action_blend_all_bones(Object *ob,
|
||||
bAction *action,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
float blend_factor);
|
||||
|
||||
/**
|
||||
* Apply the given Action to all objects of the Span.
|
||||
* The slot is chosen automatically, see `get_best_pose_slot_for_id`.
|
||||
*/
|
||||
void pose_apply_action(Span<Object *> objects,
|
||||
Action &pose_action,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
float blend_factor);
|
||||
/**
|
||||
* Return true if any bone is selected. This is useful to decide if all bones should be affected
|
||||
* or not.
|
||||
*/
|
||||
bool any_bone_selected(Span<const Object *> objects);
|
||||
|
||||
/**
|
||||
* Get the best slot to read pose data from for the given ID.
|
||||
* Will always return a Slot as it falls back to the first Slot.
|
||||
*
|
||||
* Assumes that the Action has at least one Slot.
|
||||
*/
|
||||
Slot &get_best_pose_slot_for_id(const ID &id, Action &pose_data);
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
98
blender-5.2.0/source/blender/animrig/ANIM_rna.hh
Normal file
98
blender-5.2.0/source/blender/animrig/ANIM_rna.hh
Normal file
@@ -0,0 +1,98 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Helper functions for animation to interact with the RNA system.
|
||||
*/
|
||||
|
||||
#include "BLI_array.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
#include "DNA_action_types.h"
|
||||
#include "RNA_types.hh"
|
||||
|
||||
#include "RNA_path.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct PointerRNA;
|
||||
struct PropertyRNA;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/** Get the values of the given property. Casts non-float properties to float. */
|
||||
Vector<float> get_rna_values(PointerRNA *ptr, PropertyRNA *prop);
|
||||
|
||||
/** Get the rna path for the given rotation mode. */
|
||||
StringRefNull get_rotation_mode_path(eRotationModes rotation_mode);
|
||||
|
||||
/**
|
||||
* Returns the full pose bone rna path. For example "pose.bones["bone_name"]".
|
||||
*/
|
||||
std::string get_pose_bone_rna_path(const bPoseChannel &pose_bone);
|
||||
|
||||
/**
|
||||
* Returns the name of the pose bone encoded in this rna path.
|
||||
* If the given path is not to a pose bone, `std::nullopt` will be returned.
|
||||
* This function does the unescaping of the string, which is why it has to return
|
||||
* a copy of the string, and not just a StringRef.
|
||||
*/
|
||||
std::optional<std::string> pose_bone_name_from_rna_path(StringRefNull rna_path);
|
||||
|
||||
/**
|
||||
* Given an RNA path to a rotation property, return the corresponding rotation mode.
|
||||
*
|
||||
* \returns the rotation mode of the given rna path or a nullopt if the `rna_path` is not for a
|
||||
* rotation property.
|
||||
*
|
||||
* \note that this returns ROT_MODE_EUL for any euler rotation mode since it cannot determine the
|
||||
* rotation order.
|
||||
*
|
||||
* \note that this function assumes that the rna_path is syntactically valid.
|
||||
*/
|
||||
std::optional<eRotationModes> get_rotation_mode_from_path(StringRefNull rna_path);
|
||||
|
||||
/**
|
||||
* Given a PointerRNA return the rotation mode of the data it points to.
|
||||
*
|
||||
* \returns the rotation mode of the given rna pointer or a nullopt if the data has no rotation
|
||||
* mode.
|
||||
*/
|
||||
std::optional<eRotationModes> get_rotation_mode_from_rna_pointer(const PointerRNA &ptr);
|
||||
|
||||
/**
|
||||
* Given an RNA path, check if it is a path to a rotation property.
|
||||
*
|
||||
* \returns true if the given rna path is for a rotation property.
|
||||
*/
|
||||
bool is_rotation_path(StringRefNull rna_path);
|
||||
|
||||
/**
|
||||
* Returns a Vector of ID properties on the given pointer that can be animated. Not all pointer
|
||||
* types are supported. Unsupported pointer types will return an empty vector.
|
||||
*/
|
||||
Vector<RNAPath> get_keyable_id_property_paths(const PointerRNA &ptr);
|
||||
|
||||
/**
|
||||
* Returns the given property values as a float array. In case the property is not an array, the
|
||||
* array length is 1. Casts non-float values to float.
|
||||
* Calling this with unsupported property types is invalid and returns an array of length 0.
|
||||
*
|
||||
* \note Only PROP_BOOLEAN, PROP_INT and PROP_FLOAT are supported.
|
||||
*/
|
||||
Array<float> rna_property_get_as_float(PointerRNA &ptr, PropertyRNA &prop);
|
||||
|
||||
/**
|
||||
* Sets the given property to the given `values`. The size of values has to match the property
|
||||
* array length. In case the property is not an array, only the first index is used. This is an
|
||||
* abstraction around RNA properties to deal with them as float regardless of their actual type.
|
||||
*
|
||||
* \note Only PROP_BOOLEAN, PROP_INT and PROP_FLOAT are supported.
|
||||
*/
|
||||
void rna_property_set_as_float(PointerRNA &ptr, PropertyRNA &prop, Span<float> values);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
108
blender-5.2.0/source/blender/animrig/ANIM_versioning.hh
Normal file
108
blender-5.2.0/source/blender/animrig/ANIM_versioning.hh
Normal file
@@ -0,0 +1,108 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Versioning of old animation data. Most animation versioning code lives
|
||||
* in the versioning_xxx.cc files, but some is broken out and placed here.
|
||||
*/
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct bAction;
|
||||
struct BlendFileReadReport;
|
||||
struct ID;
|
||||
struct Main;
|
||||
struct ReportList;
|
||||
struct FCurve;
|
||||
|
||||
namespace animrig {
|
||||
class Action;
|
||||
}
|
||||
|
||||
namespace animrig::versioning {
|
||||
|
||||
/**
|
||||
* Return whether an action is layered (as opposed to legacy).
|
||||
*
|
||||
* This will return false for both Animato and pre-Animato actions. It is used
|
||||
* during file read and versioning to determine how forward-compatible and
|
||||
* legacy data should be handled.
|
||||
*/
|
||||
bool action_is_layered(const bAction &dna_action);
|
||||
|
||||
/**
|
||||
* Convert all legacy (Animato) Actions to slotted Actions, in-place.
|
||||
*
|
||||
* This function does *not* work on pre-Animato actions.
|
||||
*/
|
||||
void convert_legacy_animato_actions(Main &bmain);
|
||||
|
||||
/**
|
||||
* Convert legacy (Animato) Action to slotted Action, in-place.
|
||||
*
|
||||
* \note This function does *not* work on pre-Animato actions.
|
||||
*
|
||||
* This always creates a slot and a layer for the Action, even when the Action doesn't actually
|
||||
* contain any animation data. This ensures that versioned Actions all look the same, and there's
|
||||
* just less variations to keep track of. */
|
||||
void convert_legacy_animato_action(bAction &dna_action);
|
||||
|
||||
/**
|
||||
* Go over all animated IDs, and tag them whenever they use a legacy Action.
|
||||
*
|
||||
* \see convert_legacy_action_assignments
|
||||
*/
|
||||
void tag_action_users_for_slotted_actions_conversion(Main &bmain);
|
||||
|
||||
/**
|
||||
* Tag this ID so it'll get its legacy Action assignment converted.
|
||||
*
|
||||
* \see convert_legacy_action_assignments
|
||||
*/
|
||||
void tag_action_user_for_slotted_actions_conversion(ID &animated_id);
|
||||
|
||||
/**
|
||||
* Convert the Action assignments of all animated IDs.
|
||||
*
|
||||
* For all IDs that use an Action, this also picks an Action Slot to ensure the ID is still
|
||||
* animated.
|
||||
*
|
||||
* This only visits IDs tagged by #tag_action_users_for_slotted_actions_conversion.
|
||||
*/
|
||||
void convert_legacy_action_assignments(Main &bmain, ReportList *reports);
|
||||
|
||||
/**
|
||||
* Reconstruct channel pointers.
|
||||
* Assumes that the groups referred to by the FCurves are already in act->groups.
|
||||
* Reorders the main channel list to match group order.
|
||||
*
|
||||
* Only used in versioning code since this only works with legacy actions which
|
||||
* no longer exist in new files.
|
||||
*/
|
||||
void action_groups_reconstruct(bAction *act);
|
||||
|
||||
/**
|
||||
* This should only be used on legacy actions (i.e. not layered) in versioning code from
|
||||
* before 4.4.0. Anything after that should use `BKE_fcurves_id_cb`.
|
||||
*/
|
||||
void fcurves_id_cb(ID *id, const FunctionRef<void(ID *, FCurve *)> func);
|
||||
/**
|
||||
* This should only be used on legacy actions (i.e. not layered) in versioning code from
|
||||
* before 4.4.0. Anything after that should use `BKE_fcurves_main_cb`.
|
||||
*/
|
||||
void fcurves_main_cb(Main *bmain, const FunctionRef<void(ID *, FCurve *)> func);
|
||||
|
||||
/**
|
||||
* Return all FCurves of the given legacy action. This will return an empty Vector for layered
|
||||
* actions.
|
||||
*/
|
||||
Vector<FCurve *> fcurves_for_legacy_action(bAction *action);
|
||||
|
||||
} // namespace animrig::versioning
|
||||
} // namespace blender
|
||||
37
blender-5.2.0/source/blender/animrig/ANIM_visualkey.hh
Normal file
37
blender-5.2.0/source/blender/animrig/ANIM_visualkey.hh
Normal file
@@ -0,0 +1,37 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Functions to work with the visual keying system.
|
||||
*/
|
||||
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct PointerRNA;
|
||||
struct PropertyRNA;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/**
|
||||
* This helper function determines if visual-keyframing should be used when
|
||||
* inserting keyframes for the given channel. As visual-keyframing only works
|
||||
* on Object and Pose-Channel blocks, this should only get called for those
|
||||
* block-types, when using "standard" keying but 'Visual Keying' option in Auto-Keying
|
||||
* settings is on.
|
||||
*/
|
||||
bool visualkey_can_use(PointerRNA *ptr, PropertyRNA *prop);
|
||||
|
||||
/**
|
||||
* This helper function extracts the value to use for visual-keyframing
|
||||
* In the event that it is not possible to perform visual keying, try to fall-back
|
||||
* to using the default method. Assumes that all data it has been passed is valid.
|
||||
*/
|
||||
Vector<float> visualkey_get_values(PointerRNA *ptr, PropertyRNA *prop);
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
98
blender-5.2.0/source/blender/animrig/CMakeLists.txt
Normal file
98
blender-5.2.0/source/blender/animrig/CMakeLists.txt
Normal file
@@ -0,0 +1,98 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
PUBLIC .
|
||||
intern
|
||||
|
||||
../editors/include
|
||||
../makesrna
|
||||
# RNA_prototypes.hh
|
||||
${CMAKE_BINARY_DIR}/source/blender/makesrna
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
intern/action.cc
|
||||
intern/action_iterators.cc
|
||||
intern/action_legacy.cc
|
||||
intern/action_runtime.cc
|
||||
intern/action_selection.cc
|
||||
intern/anim_rna.cc
|
||||
intern/animdata.cc
|
||||
intern/armature.cc
|
||||
intern/bone_collections.cc
|
||||
intern/bonecolor.cc
|
||||
intern/driver.cc
|
||||
intern/evaluation.cc
|
||||
intern/fcurve.cc
|
||||
intern/keyframing.cc
|
||||
intern/keyframing_auto.cc
|
||||
intern/keyingsets.cc
|
||||
intern/nla.cc
|
||||
intern/pose.cc
|
||||
intern/versioning.cc
|
||||
intern/visualkey.cc
|
||||
|
||||
ANIM_action.hh
|
||||
ANIM_action_iterators.hh
|
||||
ANIM_action_legacy.hh
|
||||
ANIM_animdata.hh
|
||||
ANIM_armature.hh
|
||||
ANIM_armature_iter.hh
|
||||
ANIM_bone_collections.hh
|
||||
ANIM_bonecolor.hh
|
||||
ANIM_driver.hh
|
||||
ANIM_evaluation.hh
|
||||
ANIM_fcurve.hh
|
||||
ANIM_keyframing.hh
|
||||
ANIM_keyingsets.hh
|
||||
ANIM_nla.hh
|
||||
ANIM_pose.hh
|
||||
ANIM_rna.hh
|
||||
ANIM_versioning.hh
|
||||
ANIM_visualkey.hh
|
||||
intern/action_runtime.hh
|
||||
intern/bone_collections_internal.hh
|
||||
intern/evaluation_internal.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::blenkernel
|
||||
bf::blenlib
|
||||
PRIVATE bf::blenloader
|
||||
PRIVATE bf::blentranslation
|
||||
PRIVATE bf::depsgraph
|
||||
bf::dna
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
PRIVATE bf::intern::atomic
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::windowmanager
|
||||
)
|
||||
|
||||
blender_add_lib(bf_animrig "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
add_library(bf::animrig ALIAS bf_animrig)
|
||||
|
||||
if(WITH_GTESTS)
|
||||
set(TEST_INC
|
||||
)
|
||||
set(TEST_SRC
|
||||
intern/action_iterators_test.cc
|
||||
intern/action_legacy_test.cc
|
||||
intern/action_test.cc
|
||||
intern/anim_rna_test.cc
|
||||
intern/bone_collections_test.cc
|
||||
intern/evaluation_test.cc
|
||||
intern/keyframing_test.cc
|
||||
intern/nla_test.cc
|
||||
intern/pose_test.cc
|
||||
intern/versioning_test.cc
|
||||
)
|
||||
set(TEST_LIB
|
||||
PRIVATE bf::animrig
|
||||
)
|
||||
blender_add_test_suite_lib(animrig "${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}")
|
||||
endif()
|
||||
3070
blender-5.2.0/source/blender/animrig/intern/action.cc
Normal file
3070
blender-5.2.0/source/blender/animrig/intern/action.cc
Normal file
File diff suppressed because it is too large
Load Diff
280
blender-5.2.0/source/blender/animrig/intern/action_iterators.cc
Normal file
280
blender-5.2.0/source/blender/animrig/intern/action_iterators.cc
Normal file
@@ -0,0 +1,280 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_iterators.hh"
|
||||
|
||||
#include "BLI_assert.h"
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_nla.hh"
|
||||
|
||||
#include "DNA_constraint_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
void foreach_fcurve_in_action(Action &action, FunctionRef<void(FCurve &fcurve)> callback)
|
||||
{
|
||||
for (Layer *layer : action.layers()) {
|
||||
for (Strip *strip : layer->strips()) {
|
||||
if (strip->type() != Strip::Type::Keyframe) {
|
||||
continue;
|
||||
}
|
||||
for (Channelbag *bag : strip->data<StripKeyframeData>(action).channelbags()) {
|
||||
for (FCurve *fcu : bag->fcurves()) {
|
||||
callback(*fcu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void foreach_fcurve_in_action_slot_editable(Action &action,
|
||||
slot_handle_t handle,
|
||||
FunctionRef<void(FCurve &fcurve)> callback)
|
||||
{
|
||||
/* Once layers can be locked, this needs to be checked here. */
|
||||
assert_baklava_phase_1_invariants(action);
|
||||
for (Layer *layer : action.layers()) {
|
||||
for (Strip *strip : layer->strips()) {
|
||||
if (strip->type() != Strip::Type::Keyframe) {
|
||||
continue;
|
||||
}
|
||||
for (Channelbag *bag : strip->data<StripKeyframeData>(action).channelbags()) {
|
||||
if (bag->slot_handle != handle) {
|
||||
continue;
|
||||
}
|
||||
for (FCurve *fcu : bag->fcurves()) {
|
||||
BLI_assert(fcu != nullptr);
|
||||
if (fcu->flag & FCURVE_PROTECTED) {
|
||||
continue;
|
||||
}
|
||||
callback(*fcu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void foreach_fcurve_in_action_slot(Action &action,
|
||||
slot_handle_t handle,
|
||||
FunctionRef<void(FCurve &fcurve)> callback)
|
||||
{
|
||||
for (Layer *layer : action.layers()) {
|
||||
for (Strip *strip : layer->strips()) {
|
||||
if (strip->type() != Strip::Type::Keyframe) {
|
||||
continue;
|
||||
}
|
||||
for (Channelbag *bag : strip->data<StripKeyframeData>(action).channelbags()) {
|
||||
if (bag->slot_handle != handle) {
|
||||
continue;
|
||||
}
|
||||
for (FCurve *fcu : bag->fcurves()) {
|
||||
BLI_assert(fcu != nullptr);
|
||||
callback(*fcu);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool foreach_action_slot_use(
|
||||
const ID &animated_id,
|
||||
FunctionRef<bool(const Action &action, slot_handle_t slot_handle)> callback)
|
||||
{
|
||||
|
||||
const auto forward_to_callback = [&](ID & /* animated_id */,
|
||||
bAction *&action_ptr_ref,
|
||||
const slot_handle_t &slot_handle_ref,
|
||||
char * /*last_slot_identifier*/) -> bool {
|
||||
if (!action_ptr_ref) {
|
||||
return true;
|
||||
}
|
||||
return callback(const_cast<const Action &>(action_ptr_ref->wrap()), slot_handle_ref);
|
||||
};
|
||||
|
||||
return foreach_action_slot_use_with_references(const_cast<ID &>(animated_id),
|
||||
forward_to_callback);
|
||||
}
|
||||
|
||||
bool foreach_action_slot_use_with_references(
|
||||
ID &animated_id,
|
||||
FunctionRef<bool(ID &animated_id,
|
||||
bAction *&action_ptr_ref,
|
||||
slot_handle_t &slot_handle_ref,
|
||||
char *last_slot_identifier)> callback)
|
||||
{
|
||||
AnimData *adt = BKE_animdata_from_id(&animated_id);
|
||||
|
||||
if (adt) {
|
||||
if (adt->action) {
|
||||
/* Direct assignment. */
|
||||
if (!callback(animated_id, adt->action, adt->slot_handle, adt->last_slot_identifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* NLA strips. */
|
||||
const bool looped_until_last_strip = bke::nla::foreach_strip_adt(*adt, [&](NlaStrip *strip) {
|
||||
if (strip->act) {
|
||||
if (!callback(
|
||||
animated_id, strip->act, strip->action_slot_handle, strip->last_slot_identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!looped_until_last_strip) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* The rest of the code deals with constraints, so only relevant when this is an Object. */
|
||||
if (GS(animated_id.name) != ID_OB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Object &object = reinterpret_cast<const Object &>(animated_id);
|
||||
|
||||
/**
|
||||
* Visit a constraint, and call the callback if it's an Action constraint.
|
||||
*
|
||||
* \returns whether to continue looping over possible uses of Actions, i.e.
|
||||
* the return value of the callback.
|
||||
*/
|
||||
auto visit_constraint = [&](const bConstraint &constraint) -> bool {
|
||||
if (constraint.type != CONSTRAINT_TYPE_ACTION) {
|
||||
return true;
|
||||
}
|
||||
bActionConstraint *constraint_data = static_cast<bActionConstraint *>(constraint.data);
|
||||
if (!constraint_data->act) {
|
||||
return true;
|
||||
}
|
||||
return callback(animated_id,
|
||||
constraint_data->act,
|
||||
constraint_data->action_slot_handle,
|
||||
constraint_data->last_slot_identifier);
|
||||
};
|
||||
|
||||
/* Visit Object constraints. */
|
||||
for (bConstraint &con : object.constraints) {
|
||||
if (!visit_constraint(con)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Visit Pose Bone constraints. */
|
||||
if (object.type == OB_ARMATURE) {
|
||||
for (bPoseChannel &pchan : object.pose->chanbase) {
|
||||
for (bConstraint &con : pchan.constraints) {
|
||||
if (!visit_constraint(con)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool foreach_action_slot_use_with_rna(ID &animated_id,
|
||||
FunctionRef<bool(ID &animated_id,
|
||||
bAction *action,
|
||||
PointerRNA &action_slot_ptr,
|
||||
PropertyRNA &action_slot_prop,
|
||||
char *last_slot_identifier)> callback)
|
||||
{
|
||||
/* This function has to copy the logic of #foreach_action_slot_use_with_references(),
|
||||
* as it needs to know where exactly those pointers came from. */
|
||||
|
||||
AnimData *adt = BKE_animdata_from_id(&animated_id);
|
||||
|
||||
if (adt) {
|
||||
if (adt->action) {
|
||||
/* Direct assignment. */
|
||||
PointerRNA ptr = RNA_pointer_create_discrete(&animated_id, RNA_AnimData, adt);
|
||||
PropertyRNA *prop = RNA_struct_find_property(&ptr, "action_slot");
|
||||
if (!callback(animated_id, adt->action, ptr, *prop, adt->last_slot_identifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* NLA strips. */
|
||||
const bool looped_until_last_strip = bke::nla::foreach_strip_adt(*adt, [&](NlaStrip *strip) {
|
||||
if (strip->act) {
|
||||
PointerRNA ptr = RNA_pointer_create_discrete(&animated_id, RNA_NlaStrip, strip);
|
||||
PropertyRNA *prop = RNA_struct_find_property(&ptr, "action_slot");
|
||||
|
||||
if (!callback(animated_id, strip->act, ptr, *prop, strip->last_slot_identifier)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
if (!looped_until_last_strip) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* The rest of the code deals with constraints, so only relevant when this is an Object. */
|
||||
if (GS(animated_id.name) != ID_OB) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Object &object = reinterpret_cast<const Object &>(animated_id);
|
||||
|
||||
/**
|
||||
* Visit a constraint, and call the callback if it's an Action constraint.
|
||||
*
|
||||
* \returns whether to continue looping over possible uses of Actions, i.e.
|
||||
* the return value of the callback.
|
||||
*/
|
||||
auto visit_constraint = [&](bConstraint &constraint) -> bool {
|
||||
if (constraint.type != CONSTRAINT_TYPE_ACTION) {
|
||||
return true;
|
||||
}
|
||||
bActionConstraint *constraint_data = static_cast<bActionConstraint *>(constraint.data);
|
||||
if (!constraint_data->act) {
|
||||
return true;
|
||||
}
|
||||
|
||||
PointerRNA ptr = RNA_pointer_create_discrete(&animated_id, RNA_ActionConstraint, &constraint);
|
||||
PropertyRNA *prop = RNA_struct_find_property(&ptr, "action_slot");
|
||||
|
||||
return callback(
|
||||
animated_id, constraint_data->act, ptr, *prop, constraint_data->last_slot_identifier);
|
||||
};
|
||||
|
||||
/* Visit Object constraints. */
|
||||
for (bConstraint &con : object.constraints) {
|
||||
if (!visit_constraint(con)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Visit Pose Bone constraints. */
|
||||
if (object.type == OB_ARMATURE) {
|
||||
for (bPoseChannel &pchan : object.pose->chanbase) {
|
||||
for (bConstraint &con : pchan.constraints) {
|
||||
if (!visit_constraint(con)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
@@ -0,0 +1,178 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_iterators.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 "RNA_access.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::tests {
|
||||
class ActionIteratorsTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
Main *bmain;
|
||||
Action *action;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
bmain = BKE_main_new();
|
||||
action = BKE_id_new<Action>(bmain, "ACLayeredAction");
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ActionIteratorsTest, iterate_all_fcurves_of_slot)
|
||||
{
|
||||
Slot &cube_slot = action->slot_add();
|
||||
Slot &monkey_slot = action->slot_add();
|
||||
|
||||
/* Try iterating an empty action. */
|
||||
Vector<const FCurve *> no_fcurves;
|
||||
foreach_fcurve_in_action_slot(
|
||||
*action, cube_slot.handle, [&](const FCurve &fcurve) { no_fcurves.append(&fcurve); });
|
||||
|
||||
ASSERT_TRUE(no_fcurves.is_empty());
|
||||
|
||||
Layer &layer = action->layer_add("Layer One");
|
||||
Strip &strip = layer.strip_add(*action, Strip::Type::Keyframe);
|
||||
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
|
||||
const KeyframeSettings settings = get_keyframe_settings(false);
|
||||
|
||||
/* Insert 3 FCurves for each slot. */
|
||||
for (int i = 0; i < 3; i++) {
|
||||
SingleKeyingResult result_cube = strip_data.keyframe_insert(
|
||||
bmain, cube_slot, {"location", i}, {1.0f, 0.0f}, settings);
|
||||
ASSERT_EQ(SingleKeyingResult::SUCCESS, result_cube)
|
||||
<< "Expected keyframe insertion to be successful";
|
||||
|
||||
SingleKeyingResult result_monkey = strip_data.keyframe_insert(
|
||||
bmain, monkey_slot, {"rotation", i}, {1.0f, 0.0f}, settings);
|
||||
ASSERT_EQ(SingleKeyingResult::SUCCESS, result_monkey)
|
||||
<< "Expected keyframe insertion to be successful";
|
||||
}
|
||||
|
||||
/* Get all FCurves. */
|
||||
Vector<const FCurve *> cube_fcurves;
|
||||
foreach_fcurve_in_action_slot(
|
||||
*action, cube_slot.handle, [&](const FCurve &fcurve) { cube_fcurves.append(&fcurve); });
|
||||
|
||||
ASSERT_EQ(cube_fcurves.size(), 3);
|
||||
for (const FCurve *fcurve : cube_fcurves) {
|
||||
ASSERT_STREQ(fcurve->rna_path, "location");
|
||||
}
|
||||
|
||||
/* Get only FCurves with index 0 which should be 1. */
|
||||
Vector<const FCurve *> monkey_fcurves;
|
||||
foreach_fcurve_in_action_slot(*action, monkey_slot.handle, [&](const FCurve &fcurve) {
|
||||
if (fcurve.array_index == 0) {
|
||||
monkey_fcurves.append(&fcurve);
|
||||
}
|
||||
});
|
||||
|
||||
ASSERT_EQ(monkey_fcurves.size(), 1);
|
||||
ASSERT_STREQ(monkey_fcurves[0]->rna_path, "rotation");
|
||||
|
||||
/* Slots handles are just numbers. Passing in a slot handle that doesn't exist should return
|
||||
* nothing. */
|
||||
Vector<const FCurve *> invalid_slot_fcurves;
|
||||
foreach_fcurve_in_action_slot(
|
||||
*action, monkey_slot.handle + cube_slot.handle, [&](const FCurve &fcurve) {
|
||||
invalid_slot_fcurves.append(&fcurve);
|
||||
});
|
||||
ASSERT_TRUE(invalid_slot_fcurves.is_empty());
|
||||
}
|
||||
|
||||
TEST_F(ActionIteratorsTest, foreach_action_slot_use_with_references)
|
||||
{
|
||||
/* Create a cube and assign the Action + a slot. */
|
||||
Object *cube = BKE_id_new<Object>(bmain, "OBCube");
|
||||
Slot *slot_cube = assign_action_ensure_slot_for_keying(*action, cube->id);
|
||||
ASSERT_NE(slot_cube, nullptr);
|
||||
|
||||
/* Create another Action with slot to assign. */
|
||||
Action &other_action = BKE_id_new<bAction>(bmain, "ACAnotherAction")->wrap();
|
||||
Slot &another_slot = other_action.slot_add();
|
||||
|
||||
std::optional<ActionSlotAssignmentResult> slot_assignment_result;
|
||||
|
||||
bool all_assigns_ok = true;
|
||||
const auto assign_other_action = [&](ID & /* animated_id */,
|
||||
bAction *&action_ptr_ref,
|
||||
slot_handle_t &slot_handle_ref,
|
||||
char *last_slot_identifier) -> bool {
|
||||
/* Assign the other Action. */
|
||||
all_assigns_ok &= generic_assign_action(
|
||||
cube->id, &other_action, action_ptr_ref, slot_handle_ref, last_slot_identifier);
|
||||
|
||||
/* Assign the slot of the other Action. */
|
||||
slot_assignment_result = generic_assign_action_slot(
|
||||
&another_slot, cube->id, action_ptr_ref, slot_handle_ref, last_slot_identifier);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
foreach_action_slot_use_with_references(cube->id, assign_other_action);
|
||||
ASSERT_TRUE(all_assigns_ok);
|
||||
|
||||
/* Check the result, the slot assignment should have been changed. */
|
||||
ASSERT_TRUE(slot_assignment_result.has_value());
|
||||
EXPECT_EQ(ActionSlotAssignmentResult::OK, slot_assignment_result.value());
|
||||
|
||||
std::optional<std::pair<Action *, Slot *>> action_and_slot = get_action_slot_pair(cube->id);
|
||||
|
||||
ASSERT_TRUE(action_and_slot.has_value());
|
||||
EXPECT_EQ(&other_action, action_and_slot->first)
|
||||
<< "Expected Action " << other_action.id.name << " but found "
|
||||
<< action_and_slot->first->id.name;
|
||||
EXPECT_EQ(&another_slot, action_and_slot->second)
|
||||
<< "Expected Slot " << another_slot.identifier << " but found "
|
||||
<< action_and_slot->second->identifier;
|
||||
}
|
||||
|
||||
TEST_F(ActionIteratorsTest, foreach_action_slot_use_with_rna)
|
||||
{
|
||||
/* Create a cube and assign the Action + a slot. */
|
||||
Object *cube = BKE_id_new<Object>(bmain, "OBCube");
|
||||
Slot *slot_cube = assign_action_ensure_slot_for_keying(*action, cube->id);
|
||||
ASSERT_NE(slot_cube, nullptr);
|
||||
Slot &another_slot = action->slot_add();
|
||||
|
||||
const auto assign_other_slot = [&](ID & /* animated_id */,
|
||||
bAction *action,
|
||||
PointerRNA &action_slot_owner_ptr,
|
||||
PropertyRNA &action_slot_prop,
|
||||
char * /*last_slot_identifier*/) -> bool {
|
||||
PointerRNA rna_slot = RNA_pointer_create_discrete(&action->id, RNA_ActionSlot, &another_slot);
|
||||
RNA_property_pointer_set(&action_slot_owner_ptr, &action_slot_prop, rna_slot, nullptr);
|
||||
return true;
|
||||
};
|
||||
|
||||
foreach_action_slot_use_with_rna(cube->id, assign_other_slot);
|
||||
|
||||
/* Check the result, the slot assignment should have been changed. */
|
||||
std::optional<std::pair<Action *, Slot *>> action_and_slot = get_action_slot_pair(cube->id);
|
||||
|
||||
ASSERT_TRUE(action_and_slot.has_value());
|
||||
EXPECT_EQ(action, action_and_slot->first)
|
||||
<< "Expected Action " << action->id.name << " but found " << action_and_slot->first->id.name;
|
||||
EXPECT_EQ(&another_slot, action_and_slot->second)
|
||||
<< "Expected Slot " << another_slot.identifier << " but found "
|
||||
<< action_and_slot->second->identifier;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::tests
|
||||
158
blender-5.2.0/source/blender/animrig/intern/action_legacy.cc
Normal file
158
blender-5.2.0/source/blender/animrig/intern/action_legacy.cc
Normal file
@@ -0,0 +1,158 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_legacy.hh"
|
||||
|
||||
#include "BLI_listbase_wrapper.hh"
|
||||
|
||||
#include "BKE_fcurve.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
namespace blender::animrig::legacy {
|
||||
|
||||
/* Lots of template args to support transparent non-const and const versions. */
|
||||
template<typename ActionType,
|
||||
typename FCurveType,
|
||||
typename LayerType,
|
||||
typename StripType,
|
||||
typename StripKeyframeDataType,
|
||||
typename ChannelbagType>
|
||||
static Vector<FCurveType *> fcurves_all_templated(ActionType &action)
|
||||
{
|
||||
Vector<FCurveType *> all_fcurves;
|
||||
for (LayerType *layer : action.layers()) {
|
||||
for (StripType *strip : layer->strips()) {
|
||||
switch (strip->type()) {
|
||||
case Strip::Type::Keyframe: {
|
||||
StripKeyframeDataType &strip_data = strip->template data<StripKeyframeData>(action);
|
||||
for (ChannelbagType *bag : strip_data.channelbags()) {
|
||||
for (FCurveType *fcurve : bag->fcurves()) {
|
||||
all_fcurves.append(fcurve);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return all_fcurves;
|
||||
}
|
||||
|
||||
Vector<FCurve *> fcurves_all(bAction *action)
|
||||
{
|
||||
if (!action) {
|
||||
return {};
|
||||
}
|
||||
return fcurves_all_templated<Action, FCurve, Layer, Strip, StripKeyframeData, Channelbag>(
|
||||
action->wrap());
|
||||
}
|
||||
|
||||
Vector<const FCurve *> fcurves_all(const bAction *action)
|
||||
{
|
||||
if (!action) {
|
||||
return {};
|
||||
}
|
||||
return fcurves_all_templated<const Action,
|
||||
const FCurve,
|
||||
const Layer,
|
||||
const Strip,
|
||||
const StripKeyframeData,
|
||||
const Channelbag>(action->wrap());
|
||||
}
|
||||
|
||||
/* Lots of template args to support transparent non-const and const versions. */
|
||||
template<typename ActionType,
|
||||
typename FCurveType,
|
||||
typename LayerType,
|
||||
typename StripType,
|
||||
typename StripKeyframeDataType,
|
||||
typename ChannelbagType>
|
||||
static Vector<FCurveType *> fcurves_for_action_slot_templated(ActionType &action,
|
||||
const slot_handle_t slot_handle)
|
||||
{
|
||||
Vector<FCurveType *> as_vector(animrig::fcurves_for_action_slot(action, slot_handle));
|
||||
return as_vector;
|
||||
}
|
||||
|
||||
bool assigned_action_has_keyframes(AnimData *adt)
|
||||
{
|
||||
if (adt == nullptr || adt->action == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Action &action = adt->action->wrap();
|
||||
return action.has_keyframes(adt->slot_handle);
|
||||
}
|
||||
|
||||
Vector<bActionGroup *> channel_groups_all(bAction *action)
|
||||
{
|
||||
if (!action) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Action &action_wrap = action->wrap();
|
||||
Vector<bActionGroup *> all_groups;
|
||||
for (Layer *layer : action_wrap.layers()) {
|
||||
for (Strip *strip : layer->strips()) {
|
||||
switch (strip->type()) {
|
||||
case Strip::Type::Keyframe: {
|
||||
StripKeyframeData &strip_data = strip->template data<StripKeyframeData>(action_wrap);
|
||||
for (Channelbag *bag : strip_data.channelbags()) {
|
||||
all_groups.extend(bag->channel_groups());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return all_groups;
|
||||
}
|
||||
|
||||
Vector<bActionGroup *> channel_groups_for_assigned_slot(AnimData *adt)
|
||||
{
|
||||
if (!adt || !adt->action) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Action &action = adt->action->wrap();
|
||||
Channelbag *bag = channelbag_for_action_slot(action, adt->slot_handle);
|
||||
if (!bag) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Vector<bActionGroup *> slot_groups(bag->channel_groups());
|
||||
return slot_groups;
|
||||
}
|
||||
|
||||
bool action_fcurves_remove(bAction &action,
|
||||
const slot_handle_t slot_handle,
|
||||
const StringRefNull rna_path_prefix)
|
||||
{
|
||||
BLI_assert(!rna_path_prefix.is_empty());
|
||||
if (rna_path_prefix.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Channelbag *bag = channelbag_for_action_slot(action.wrap(), slot_handle);
|
||||
if (!bag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool any_removed = false;
|
||||
for (int64_t fcurve_index = 0; fcurve_index < bag->fcurve_array_num; fcurve_index++) {
|
||||
FCurve *fcurve = bag->fcurve(fcurve_index);
|
||||
if (!fcurve->rna_path) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (STRPREFIX(fcurve->rna_path, rna_path_prefix.c_str())) {
|
||||
bag->fcurve_remove_by_index(fcurve_index);
|
||||
fcurve_index--;
|
||||
any_removed = true;
|
||||
}
|
||||
}
|
||||
return any_removed;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::legacy
|
||||
@@ -0,0 +1,116 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_legacy.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 "BLI_listbase.h"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::tests {
|
||||
class ActionLegacyTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
Main *bmain;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
bmain = BKE_main_new();
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
|
||||
bAction *create_empty_action()
|
||||
{
|
||||
return BKE_id_new<bAction>(bmain, "ACAction");
|
||||
}
|
||||
|
||||
FCurve *fcurve_add_legacy(bAction *action, const StringRefNull rna_path, const int array_index)
|
||||
{
|
||||
FCurve *fcurve = MEM_new<FCurve>(__func__);
|
||||
BKE_fcurve_rnapath_set(*fcurve, rna_path);
|
||||
fcurve->array_index = array_index;
|
||||
BLI_addtail(&action->curves, fcurve);
|
||||
return fcurve;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ActionLegacyTest, fcurves_all)
|
||||
{
|
||||
{ /* nil pointer. */
|
||||
bAction *action = nullptr;
|
||||
Vector<FCurve *> fcurves = legacy::fcurves_all(action);
|
||||
EXPECT_TRUE(fcurves.is_empty());
|
||||
}
|
||||
|
||||
{ /* Empty Action. */
|
||||
Vector<FCurve *> fcurves = legacy::fcurves_all(create_empty_action());
|
||||
EXPECT_TRUE(fcurves.is_empty());
|
||||
}
|
||||
Action &action = create_empty_action()->wrap();
|
||||
Slot &slot1 = action.slot_add();
|
||||
Slot &slot2 = action.slot_add();
|
||||
|
||||
action.layer_keystrip_ensure();
|
||||
StripKeyframeData &key_data = action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
|
||||
FCurve &fcurve1 = key_data.channelbag_for_slot_ensure(slot1).fcurve_ensure(bmain,
|
||||
{"location", 1});
|
||||
FCurve &fcurve2 = key_data.channelbag_for_slot_ensure(slot2).fcurve_ensure(bmain, {"scale", 2});
|
||||
|
||||
Vector<FCurve *> fcurves_expect = {&fcurve1, &fcurve2};
|
||||
EXPECT_EQ(fcurves_expect, legacy::fcurves_all(&action));
|
||||
}
|
||||
|
||||
TEST_F(ActionLegacyTest, action_fcurves_remove)
|
||||
{
|
||||
{ /* Empty Action. */
|
||||
bAction *action = create_empty_action();
|
||||
EXPECT_FALSE(legacy::action_fcurves_remove(*action, Slot::unassigned, "rotation"));
|
||||
}
|
||||
/* Create an Action with two slots, to check that the 2nd slot is not affected
|
||||
* by removal from the 1st. */
|
||||
Action &action = create_empty_action()->wrap();
|
||||
Slot &slot_1 = action.slot_add();
|
||||
Slot &slot_2 = action.slot_add();
|
||||
|
||||
action.layer_keystrip_ensure();
|
||||
StripKeyframeData *strip_data = action.strip_keyframe_data()[0];
|
||||
Channelbag &bag_1 = strip_data->channelbag_for_slot_ensure(slot_1);
|
||||
Channelbag &bag_2 = strip_data->channelbag_for_slot_ensure(slot_2);
|
||||
|
||||
/* Add some F-Curves to each channelbag. */
|
||||
FCurve &fcurve_loc_x = bag_1.fcurve_ensure(nullptr, {"location", 0});
|
||||
bag_1.fcurve_ensure(nullptr, {"rotation_euler", 2});
|
||||
bag_1.fcurve_ensure(nullptr, {"rotation_mode", 0});
|
||||
FCurve &fcurve_loc_y = bag_1.fcurve_ensure(nullptr, {"location", 1});
|
||||
|
||||
bag_2.fcurve_ensure(nullptr, {"location", 0});
|
||||
bag_2.fcurve_ensure(nullptr, {"rotation_euler", 2});
|
||||
bag_2.fcurve_ensure(nullptr, {"rotation_mode", 0});
|
||||
bag_2.fcurve_ensure(nullptr, {"location", 1});
|
||||
|
||||
/* Check that removing from slot_1 works as expected. */
|
||||
EXPECT_TRUE(legacy::action_fcurves_remove(action, slot_1.handle, "rotation"));
|
||||
|
||||
Vector<FCurve *> fcurves_bag_1_expect = {&fcurve_loc_x, &fcurve_loc_y};
|
||||
EXPECT_EQ(fcurves_bag_1_expect.as_span(),
|
||||
animrig::fcurves_for_action_slot(action, slot_1.handle));
|
||||
|
||||
EXPECT_EQ(4, bag_2.fcurves().size())
|
||||
<< "Expected all F-Curves for slot 2 to be there after manipulating slot 1";
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::tests
|
||||
103
blender-5.2.0/source/blender/animrig/intern/action_runtime.cc
Normal file
103
blender-5.2.0/source/blender/animrig/intern/action_runtime.cc
Normal file
@@ -0,0 +1,103 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Internal C++ functions to deal with Actions, Slots, and their runtime data.
|
||||
*/
|
||||
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_global.hh"
|
||||
#include "BKE_lib_query.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_nla.hh"
|
||||
#include "BKE_node.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_iterators.hh"
|
||||
|
||||
#include "action_runtime.hh"
|
||||
|
||||
namespace blender::animrig::internal {
|
||||
|
||||
void rebuild_slot_user_cache(Main &bmain)
|
||||
{
|
||||
/* Loop over all Actions and clear their slots' user cache. */
|
||||
for (bAction &dna_action : bmain.actions) {
|
||||
Action &action = dna_action.wrap();
|
||||
for (Slot *slot : action.slots()) {
|
||||
BLI_assert_msg(slot->runtime, "Slot::runtime should always be allocated");
|
||||
slot->runtime->users.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark all Slots as clear. This is a bit of a lie, because the code below still has to run.
|
||||
* However, this is a necessity to make the `slot.users_add(*id)` call work without triggering
|
||||
* an infinite recursion.
|
||||
*
|
||||
* The alternative would be to go around the `slot.users_add()` function and access the
|
||||
* runtime directly, but this is IMO a bit cleaner. */
|
||||
bmain.is_action_slot_to_id_map_dirty = false;
|
||||
|
||||
/* Visit any ID to see which Action+Slot it is using. Returns whether the ID
|
||||
* was visited for the first time. */
|
||||
Set<ID *> visited_ids;
|
||||
auto visit_id = [&visited_ids](ID *id) -> bool {
|
||||
BLI_assert(id);
|
||||
|
||||
if (!visited_ids.add(id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach_action_slot_use(*id, [&](const Action &action, slot_handle_t slot_handle) {
|
||||
const Slot *slot = action.slot_for_handle(slot_handle);
|
||||
if (!slot) {
|
||||
return true;
|
||||
}
|
||||
/* Constant cast because the `foreach` produces const Actions, and I (Sybren)
|
||||
* didn't want to make a non-const duplicate. */
|
||||
const_cast<Slot *>(slot)->users_add(*id);
|
||||
return true;
|
||||
});
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Loop over all IDs to cache their slot usage. */
|
||||
ListBaseT<ID> *ids_of_idtype;
|
||||
ID *id;
|
||||
FOREACH_MAIN_LISTBASE_BEGIN (&bmain, ids_of_idtype) {
|
||||
/* Check whether this ID type can be animated. If not, just skip all IDs of this type. */
|
||||
id = static_cast<ID *>(ids_of_idtype->first);
|
||||
if (!id || !id_type_can_have_animdata(GS(id->name))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
FOREACH_MAIN_LISTBASE_ID_BEGIN (ids_of_idtype, id) {
|
||||
BLI_assert(id_can_have_animdata(id));
|
||||
|
||||
/* Process the ID itself. */
|
||||
if (!visit_id(id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Process embedded IDs, as these are not listed in bmain, but still can
|
||||
* have their own Action+Slot. Unfortunately there is no generic looper
|
||||
* for embedded IDs. At this moment the only animatable embedded ID is a
|
||||
* node tree. */
|
||||
bNodeTree *node_tree = bke::node_tree_from_id(id);
|
||||
if (node_tree) {
|
||||
visit_id(&node_tree->id);
|
||||
}
|
||||
}
|
||||
FOREACH_MAIN_LISTBASE_ID_END;
|
||||
}
|
||||
FOREACH_MAIN_LISTBASE_END;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::internal
|
||||
@@ -0,0 +1,60 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Internal C++ functions to deal with Actions, Slots, and their runtime data.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct ID;
|
||||
struct Main;
|
||||
|
||||
namespace animrig {
|
||||
|
||||
/**
|
||||
* Not placed in the 'internal' namespace, as this type is forward-declared in
|
||||
* DNA_action_types.h, and that shouldn't reference the internal namespace.
|
||||
*/
|
||||
class SlotRuntime {
|
||||
public:
|
||||
/**
|
||||
* Cache of pointers to the IDs that are animated by this slot.
|
||||
*
|
||||
* Note that this is a vector for simplicity, as the majority of the slots
|
||||
* will have zero or one user. Semantically it's treated as a set: order
|
||||
* doesn't matter, and it has no duplicate entries.
|
||||
*
|
||||
* \note This is NOT thread-safe.
|
||||
*/
|
||||
Vector<ID *> users;
|
||||
};
|
||||
|
||||
namespace internal {
|
||||
|
||||
/**
|
||||
* Rebuild the #SlotRuntime::users cache of all Slots in all Action for a specific `bmain`.
|
||||
*
|
||||
* The reason that all slot users are re-cached at once is two-fold:
|
||||
*
|
||||
* 1. Regardless of how many slot caches are rebuilt, this function will need
|
||||
* to loop over all IDs anyway.
|
||||
* 2. Deletion of IDs may be hard to detect otherwise. This is a bit of a weak
|
||||
* argument, as if this is not implemented properly (i.e. not un-assigning
|
||||
* the Action first), the 'dirty' flag will also not be set, and thus a
|
||||
* rebuild will not be triggered. In any case, because the rebuild is global,
|
||||
* any subsequent call at least ensures correctness even with such bugs.
|
||||
*/
|
||||
void rebuild_slot_user_cache(Main &bmain);
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,39 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "DNA_action_types.h"
|
||||
#include "DNA_anim_types.h"
|
||||
|
||||
#include "BLI_set.hh"
|
||||
|
||||
#include "BKE_fcurve.hh"
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_legacy.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
void action_deselect_keys(Action &action)
|
||||
{
|
||||
for (FCurve *fcu : legacy::fcurves_all(&action)) {
|
||||
BKE_fcurve_deselect_all_keys(*fcu);
|
||||
}
|
||||
}
|
||||
|
||||
void deselect_keys_actions(Span<bAction *> actions)
|
||||
{
|
||||
Set<bAction *> visited_actions;
|
||||
for (bAction *action : actions) {
|
||||
if (!visited_actions.add(action)) {
|
||||
continue;
|
||||
}
|
||||
action_deselect_keys(action->wrap());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
2170
blender-5.2.0/source/blender/animrig/intern/action_test.cc
Normal file
2170
blender-5.2.0/source/blender/animrig/intern/action_test.cc
Normal file
File diff suppressed because it is too large
Load Diff
345
blender-5.2.0/source/blender/animrig/intern/anim_rna.cc
Normal file
345
blender-5.2.0/source/blender/animrig/intern/anim_rna.cc
Normal file
@@ -0,0 +1,345 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "ANIM_rna.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_math_base.h"
|
||||
#include "BLI_string.h"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_path.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
Vector<float> get_rna_values(PointerRNA *ptr, PropertyRNA *prop)
|
||||
{
|
||||
Vector<float> values;
|
||||
if (RNA_property_array_check(prop)) {
|
||||
const int length = RNA_property_array_length(ptr, prop);
|
||||
|
||||
switch (RNA_property_type(prop)) {
|
||||
case PROP_BOOLEAN: {
|
||||
bool *tmp_bool = MEM_new_array_uninitialized<bool>(length, __func__);
|
||||
RNA_property_boolean_get_array(ptr, prop, tmp_bool);
|
||||
for (int i = 0; i < length; i++) {
|
||||
values.append(float(tmp_bool[i]));
|
||||
}
|
||||
MEM_delete(tmp_bool);
|
||||
break;
|
||||
}
|
||||
case PROP_INT: {
|
||||
int *tmp_int = MEM_new_array_uninitialized<int>(length, __func__);
|
||||
RNA_property_int_get_array(ptr, prop, tmp_int);
|
||||
for (int i = 0; i < length; i++) {
|
||||
values.append(float(tmp_int[i]));
|
||||
}
|
||||
MEM_delete(tmp_int);
|
||||
break;
|
||||
}
|
||||
case PROP_FLOAT: {
|
||||
values.reinitialize(length);
|
||||
RNA_property_float_get_array(ptr, prop, values.data());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
values.reinitialize(length);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch (RNA_property_type(prop)) {
|
||||
case PROP_BOOLEAN:
|
||||
values.append(float(RNA_property_boolean_get(ptr, prop)));
|
||||
break;
|
||||
case PROP_INT:
|
||||
values.append(float(RNA_property_int_get(ptr, prop)));
|
||||
break;
|
||||
case PROP_FLOAT:
|
||||
values.append(RNA_property_float_get(ptr, prop));
|
||||
break;
|
||||
case PROP_ENUM:
|
||||
values.append(float(RNA_property_enum_get(ptr, prop)));
|
||||
break;
|
||||
default:
|
||||
values.append(0.0f);
|
||||
}
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
constexpr const char *pose_bone_path_prefix = "pose.bones[\"";
|
||||
constexpr int pose_bone_path_prefix_length = std::char_traits<char>::length(pose_bone_path_prefix);
|
||||
|
||||
std::string get_pose_bone_rna_path(const bPoseChannel &pose_bone)
|
||||
{
|
||||
char name_esc[sizeof(pose_bone.name) * 2];
|
||||
BLI_str_escape(name_esc, pose_bone.name, sizeof(name_esc));
|
||||
return fmt::format("{}{}\"]", pose_bone_path_prefix, name_esc);
|
||||
}
|
||||
|
||||
std::optional<std::string> pose_bone_name_from_rna_path(const StringRefNull rna_path)
|
||||
{
|
||||
if (rna_path.size() < pose_bone_path_prefix_length ||
|
||||
!rna_path.startswith(pose_bone_path_prefix))
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const char *name_esc = rna_path.data() + pose_bone_path_prefix_length;
|
||||
const char *name_esc_end = BLI_str_escape_find_quote(name_esc);
|
||||
if (!name_esc_end) {
|
||||
return std::nullopt;
|
||||
}
|
||||
char name[MAXBONENAME];
|
||||
const size_t name_esc_len = size_t(name_esc_end - name_esc);
|
||||
if (name_esc_len >= sizeof(name)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
BLI_str_unescape(name, name_esc, name_esc_len);
|
||||
return name;
|
||||
}
|
||||
|
||||
StringRefNull get_rotation_mode_path(const eRotationModes rotation_mode)
|
||||
{
|
||||
switch (rotation_mode) {
|
||||
case ROT_MODE_QUAT:
|
||||
return "rotation_quaternion";
|
||||
case ROT_MODE_AXISANGLE:
|
||||
return "rotation_axis_angle";
|
||||
default:
|
||||
return "rotation_euler";
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<eRotationModes> get_rotation_mode_from_path(const StringRefNull rna_path)
|
||||
{
|
||||
/* Accounting for the difference between objects and bones where the latter is e.g.
|
||||
* `pose.bones["foo"].rotation_euler`. Assumes that rfind returns -1 if the string
|
||||
* is not found. */
|
||||
const int start_of_propname = rna_path.rfind(".") + 1;
|
||||
if (!rna_path.substr(start_of_propname, rna_path.size()).startswith("rotation_")) {
|
||||
return std::nullopt;
|
||||
}
|
||||
/* We already know that "rotation_" is in the rna_path, we can skip the full check for
|
||||
* "rotation_quaternion", "rotation_euler" or "rotation_axis_angle". */
|
||||
if (rna_path.endswith("quaternion")) {
|
||||
return ROT_MODE_QUAT;
|
||||
}
|
||||
else if (rna_path.endswith("euler")) {
|
||||
/* Cannot determine the rotation order from the path alone. */
|
||||
return ROT_MODE_EUL;
|
||||
}
|
||||
else if (rna_path.endswith("axis_angle")) {
|
||||
return ROT_MODE_AXISANGLE;
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<eRotationModes> get_rotation_mode_from_rna_pointer(const PointerRNA &ptr)
|
||||
{
|
||||
if (ptr.type == RNA_PoseBone) {
|
||||
bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr.data);
|
||||
return eRotationModes(pchan->rotmode);
|
||||
}
|
||||
if (ptr.type == RNA_Object) {
|
||||
Object *ob = static_cast<Object *>(ptr.data);
|
||||
return eRotationModes(ob->rotmode);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bool is_rotation_path(const StringRefNull rna_path)
|
||||
{
|
||||
return get_rotation_mode_from_path(rna_path).has_value();
|
||||
}
|
||||
|
||||
static bool is_idproperty_keyable(const IDProperty *id_prop, PointerRNA *ptr, PropertyRNA *prop)
|
||||
{
|
||||
/* While you can cast the IDProperty* to a PropertyRNA* and pass it to the RNA_* functions, this
|
||||
* does not work because it will not have the right flags set. Instead the resolved
|
||||
* PointerRNA and PropertyRNA need to be passed. */
|
||||
if (!RNA_property_anim_editable(ptr, prop)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ELEM(id_prop->type,
|
||||
eIDPropertyType::IDP_BOOLEAN,
|
||||
eIDPropertyType::IDP_INT,
|
||||
eIDPropertyType::IDP_FLOAT,
|
||||
eIDPropertyType::IDP_DOUBLE))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (id_prop->type == eIDPropertyType::IDP_ARRAY) {
|
||||
if (ELEM(id_prop->subtype,
|
||||
eIDPropertyType::IDP_BOOLEAN,
|
||||
eIDPropertyType::IDP_INT,
|
||||
eIDPropertyType::IDP_FLOAT,
|
||||
eIDPropertyType::IDP_DOUBLE))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<RNAPath> get_keyable_id_property_paths(const PointerRNA &ptr)
|
||||
{
|
||||
IDProperty *properties;
|
||||
|
||||
if (ptr.type == RNA_PoseBone) {
|
||||
const bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr.data);
|
||||
properties = pchan->prop;
|
||||
}
|
||||
else if (ptr.type == RNA_Object) {
|
||||
const Object *ob = static_cast<Object *>(ptr.data);
|
||||
properties = ob->id.properties;
|
||||
}
|
||||
else {
|
||||
/* Pointer type not supported. */
|
||||
return {};
|
||||
}
|
||||
|
||||
if (!properties) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Vector<RNAPath> paths;
|
||||
for (const IDProperty &id_prop : properties->data.group) {
|
||||
PointerRNA resolved_ptr;
|
||||
PropertyRNA *resolved_prop;
|
||||
std::string path = id_prop.name;
|
||||
/* Resolving the path twice, once as RNA property (without brackets, `"propname"`),
|
||||
* and once as ID property (with brackets, `["propname"]`).
|
||||
* This is required to support IDProperties that have been defined as part of an add-on.
|
||||
* Those need to be animated through an RNA path without the brackets. */
|
||||
bool is_resolved = RNA_path_resolve_property(
|
||||
&ptr, path.c_str(), &resolved_ptr, &resolved_prop);
|
||||
/* ID properties can be named the same as internal properties, for example `scale`. In that
|
||||
* case they would resolve, but it wouldn't be the correct property. `RNA_property_is_runtime`
|
||||
* catches that case. */
|
||||
if (!is_resolved || !RNA_property_is_runtime(resolved_prop)) {
|
||||
char name_escaped[MAX_IDPROP_NAME * 2];
|
||||
BLI_str_escape(name_escaped, id_prop.name, sizeof(name_escaped));
|
||||
path = fmt::format("[\"{}\"]", name_escaped);
|
||||
is_resolved = RNA_path_resolve_property(&ptr, path.c_str(), &resolved_ptr, &resolved_prop);
|
||||
}
|
||||
if (!is_resolved) {
|
||||
continue;
|
||||
}
|
||||
if (is_idproperty_keyable(&id_prop, &resolved_ptr, resolved_prop)) {
|
||||
paths.append({path});
|
||||
}
|
||||
}
|
||||
return paths;
|
||||
}
|
||||
|
||||
Array<float> rna_property_get_as_float(PointerRNA &ptr, PropertyRNA &prop)
|
||||
{
|
||||
const bool is_array = RNA_property_array_check(&prop);
|
||||
Array<float> values;
|
||||
if (is_array) {
|
||||
values.reinitialize(RNA_property_array_length(&ptr, &prop));
|
||||
}
|
||||
else {
|
||||
values.reinitialize(1);
|
||||
}
|
||||
switch (RNA_property_type(&prop)) {
|
||||
case PROP_BOOLEAN:
|
||||
if (is_array) {
|
||||
for (const int i : values.index_range()) {
|
||||
values[i] = RNA_property_boolean_get_index(&ptr, &prop, i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
values[0] = RNA_property_boolean_get(&ptr, &prop);
|
||||
}
|
||||
break;
|
||||
|
||||
case PROP_INT:
|
||||
if (is_array) {
|
||||
for (const int i : values.index_range()) {
|
||||
values[i] = RNA_property_int_get_index(&ptr, &prop, i);
|
||||
}
|
||||
}
|
||||
else {
|
||||
values[0] = RNA_property_int_get(&ptr, &prop);
|
||||
}
|
||||
break;
|
||||
|
||||
case PROP_FLOAT:
|
||||
if (is_array) {
|
||||
RNA_property_float_get_array(&ptr, &prop, values.data());
|
||||
}
|
||||
else {
|
||||
values[0] = RNA_property_float_get(&ptr, &prop);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* Unsupported property type. */
|
||||
return {};
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
void rna_property_set_as_float(PointerRNA &ptr, PropertyRNA &prop, const Span<float> values)
|
||||
{
|
||||
const bool is_array = RNA_property_array_check(&prop);
|
||||
if (is_array && RNA_property_array_length(&ptr, &prop) != values.size()) {
|
||||
/* Array length has to match. */
|
||||
BLI_assert_unreachable();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (RNA_property_type(&prop)) {
|
||||
case PROP_BOOLEAN:
|
||||
if (is_array) {
|
||||
for (const int i : values.index_range()) {
|
||||
RNA_property_boolean_set_index(&ptr, &prop, i, values[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
RNA_property_boolean_set(&ptr, &prop, values[0]);
|
||||
}
|
||||
break;
|
||||
case PROP_INT:
|
||||
if (is_array) {
|
||||
for (const int i : values.index_range()) {
|
||||
RNA_property_int_set_index(&ptr, &prop, i, values[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
RNA_property_int_set(&ptr, &prop, values[0]);
|
||||
}
|
||||
break;
|
||||
case PROP_FLOAT:
|
||||
if (is_array) {
|
||||
RNA_property_float_set_array(&ptr, &prop, values.data());
|
||||
}
|
||||
else {
|
||||
RNA_property_float_set(&ptr, &prop, values[0]);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
/* Unsupported property type. */
|
||||
BLI_assert_unreachable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
43
blender-5.2.0/source/blender/animrig/intern/anim_rna_test.cc
Normal file
43
blender-5.2.0/source/blender/animrig/intern/anim_rna_test.cc
Normal file
@@ -0,0 +1,43 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_rna.hh"
|
||||
|
||||
#include "BKE_gtest_base.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::tests {
|
||||
|
||||
class AnimRnaTest : public bke::BlenderGTestBase {};
|
||||
|
||||
TEST_F(AnimRnaTest, is_rotation_path)
|
||||
{
|
||||
EXPECT_TRUE(is_rotation_path("rotation_euler"));
|
||||
EXPECT_TRUE(is_rotation_path("pose.bones[\"test\"].rotation_euler"));
|
||||
|
||||
EXPECT_FALSE(is_rotation_path("xrotation_euler"));
|
||||
EXPECT_FALSE(is_rotation_path("rotation_euler2"));
|
||||
EXPECT_FALSE(is_rotation_path("[\"rotation_euler\"]"));
|
||||
EXPECT_FALSE(is_rotation_path("pose.bones[\"test\"][\"rotation_euler\"]"));
|
||||
}
|
||||
|
||||
TEST_F(AnimRnaTest, rotation_mode_from_path)
|
||||
{
|
||||
EXPECT_EQ(ROT_MODE_QUAT, get_rotation_mode_from_path("rotation_quaternion").value());
|
||||
EXPECT_EQ(ROT_MODE_EUL, get_rotation_mode_from_path("rotation_euler").value());
|
||||
EXPECT_EQ(ROT_MODE_EUL,
|
||||
get_rotation_mode_from_path("pose.bones[\"test\"].rotation_euler").value());
|
||||
EXPECT_EQ(ROT_MODE_AXISANGLE, get_rotation_mode_from_path("rotation_axis_angle").value());
|
||||
|
||||
EXPECT_EQ(std::nullopt, get_rotation_mode_from_path("scale"));
|
||||
EXPECT_EQ(std::nullopt, get_rotation_mode_from_path("xrotation_euler"));
|
||||
EXPECT_EQ(std::nullopt, get_rotation_mode_from_path("rotation_euler2"));
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::tests
|
||||
366
blender-5.2.0/source/blender/animrig/intern/animdata.cc
Normal file
366
blender-5.2.0/source/blender/animrig/intern/animdata.cc
Normal file
@@ -0,0 +1,366 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_animdata.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BKE_key.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_material.hh"
|
||||
#include "BKE_node.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_build.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_key_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_particle_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Public F-Curves API
|
||||
* \{ */
|
||||
|
||||
/* Find the users of the given ID within the objects of `bmain` and add non-duplicates to the end
|
||||
* of `related_ids`. */
|
||||
static void add_object_data_users(const Main &bmain, const ID &id, Vector<ID *> &related_ids)
|
||||
{
|
||||
if (ID_REAL_USERS(&id) != 1) {
|
||||
/* Only find objects if this ID is only used once. */
|
||||
return;
|
||||
}
|
||||
|
||||
Object *ob;
|
||||
ID *object_id;
|
||||
FOREACH_MAIN_LISTBASE_ID_BEGIN (&bmain.objects, object_id) {
|
||||
ob = reinterpret_cast<Object *>(object_id);
|
||||
if (ob->data != &id) {
|
||||
continue;
|
||||
}
|
||||
related_ids.append_non_duplicates(&ob->id);
|
||||
}
|
||||
FOREACH_MAIN_LISTBASE_ID_END;
|
||||
}
|
||||
|
||||
Vector<ID *> find_related_ids(Main &bmain, ID &id)
|
||||
{
|
||||
Vector<ID *> related_ids({&id});
|
||||
|
||||
/* `related_ids` can grow during an iteration if the ID of the current iteration has associated
|
||||
* code that defines relationships. */
|
||||
for (int i = 0; i < related_ids.size(); i++) {
|
||||
ID *related_id = related_ids[i];
|
||||
|
||||
if (related_id->flag & ID_FLAG_EMBEDDED_DATA) {
|
||||
/* No matter the type of embedded ID, their owner can always be added to the related IDs. */
|
||||
|
||||
/* User counting is irrelevant for the logic here, because embedded IDs cannot be shared.
|
||||
* Embedded IDs do exist (sometimes) with a non-zero user count, hence the assertion that the
|
||||
* user count is not greater than 1. */
|
||||
BLI_assert(ID_REAL_USERS(related_id) <= 1);
|
||||
ID *owner_id = BKE_id_owner_get(related_id);
|
||||
/* Embedded IDs should always have an owner. */
|
||||
BLI_assert(owner_id != nullptr);
|
||||
related_ids.append_non_duplicates(owner_id);
|
||||
}
|
||||
|
||||
/* No action found on current ID, add related IDs to the ID Vector. */
|
||||
switch (GS(related_id->name)) {
|
||||
case ID_OB: {
|
||||
Object *ob = reinterpret_cast<Object *>(related_id);
|
||||
if (!ob->data) {
|
||||
break;
|
||||
}
|
||||
ID *data = ob->data;
|
||||
if (ID_REAL_USERS(data) == 1) {
|
||||
related_ids.append_non_duplicates(data);
|
||||
}
|
||||
for (ParticleSystem &particle_system : ob->particlesystem) {
|
||||
if (!particle_system.part) {
|
||||
continue;
|
||||
}
|
||||
if (ID_REAL_USERS(&particle_system.part->id) != 1) {
|
||||
continue;
|
||||
}
|
||||
related_ids.append_non_duplicates(&particle_system.part->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ID_KE: {
|
||||
/* Shape-keys. */
|
||||
Key *key = reinterpret_cast<Key *>(related_id);
|
||||
/* Shape-keys are not embedded but there is currently no way to reuse them. */
|
||||
BLI_assert(ID_REAL_USERS(related_id) == 1);
|
||||
related_ids.append_non_duplicates(key->from);
|
||||
break;
|
||||
}
|
||||
|
||||
case ID_MA: {
|
||||
/* Explicitly not relating materials and material users. */
|
||||
Material *mat = reinterpret_cast<Material *>(related_id);
|
||||
if (mat->nodetree && ID_REAL_USERS(&mat->nodetree->id) == 1) {
|
||||
related_ids.append_non_duplicates(&mat->nodetree->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case ID_PA: {
|
||||
if (ID_REAL_USERS(related_id) != 1) {
|
||||
continue;
|
||||
}
|
||||
Object *ob;
|
||||
ID *object_id;
|
||||
/* Find users of this particle setting. */
|
||||
FOREACH_MAIN_LISTBASE_ID_BEGIN (&bmain.objects, object_id) {
|
||||
ob = reinterpret_cast<Object *>(object_id);
|
||||
bool object_uses_particle_settings = false;
|
||||
for (ParticleSystem &particle_system : ob->particlesystem) {
|
||||
if (!particle_system.part) {
|
||||
continue;
|
||||
}
|
||||
if (&particle_system.part->id != related_id) {
|
||||
continue;
|
||||
}
|
||||
object_uses_particle_settings = true;
|
||||
break;
|
||||
}
|
||||
if (object_uses_particle_settings) {
|
||||
related_ids.append_non_duplicates(&ob->id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
FOREACH_MAIN_LISTBASE_ID_END;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
/* Just check if the ID is used as object data somewhere. */
|
||||
add_object_data_users(bmain, *related_id, related_ids);
|
||||
bNodeTree *node_tree = bke::node_tree_from_id(related_id);
|
||||
if (node_tree && ID_REAL_USERS(&node_tree->id) == 1) {
|
||||
related_ids.append_non_duplicates(&node_tree->id);
|
||||
}
|
||||
|
||||
Key *key = BKE_key_from_id(related_id);
|
||||
if (key) {
|
||||
/* No check for multi user because the shape-key cannot be shared. */
|
||||
BLI_assert(ID_REAL_USERS(&key->id) == 1);
|
||||
related_ids.append_non_duplicates(&key->id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return related_ids;
|
||||
}
|
||||
|
||||
/* Find an action on an ID that is related to the given ID. Related things are e.g. Object<->Data,
|
||||
* Mesh<->Material and so on. */
|
||||
static bAction *find_related_action(Main &bmain, ID &id)
|
||||
{
|
||||
Vector<ID *> related_ids = find_related_ids(bmain, id);
|
||||
|
||||
for (ID *related_id : related_ids) {
|
||||
Action *action = get_action(*related_id);
|
||||
if (action && BKE_id_is_editable(&bmain, &action->id)) {
|
||||
/* Returning the first action found means highest priority has the action closest in the
|
||||
* relationship graph. */
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bAction *id_action_ensure(Main *bmain, ID *id)
|
||||
{
|
||||
AnimData *adt = BKE_animdata_ensure_id(id);
|
||||
if (adt == nullptr) {
|
||||
printf("ERROR: data-block type is not animatable (ID = %s)\n", (id) ? (id->name) : "<None>");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* init action if none available yet */
|
||||
/* TODO: need some wizardry to handle NLA stuff correct */
|
||||
if (adt->action == nullptr) {
|
||||
bAction *action = find_related_action(*bmain, *id);
|
||||
|
||||
if (action == nullptr) {
|
||||
/* init action name from name of ID block */
|
||||
char actname[sizeof(id->name) - 2];
|
||||
if (id->flag & ID_FLAG_EMBEDDED_DATA) {
|
||||
/* When the ID is embedded, use the name of the owner ID for clarity. */
|
||||
ID *owner_id = BKE_id_owner_get(id);
|
||||
/* If the ID is embedded it should have an owner. */
|
||||
BLI_assert(owner_id != nullptr);
|
||||
SNPRINTF_UTF8(actname, DATA_("%sAction"), owner_id->name + 2);
|
||||
}
|
||||
else if (GS(id->name) == ID_KE) {
|
||||
Key *key = reinterpret_cast<Key *>(id);
|
||||
SNPRINTF_UTF8(actname, DATA_("%sAction"), key->from->name + 2);
|
||||
}
|
||||
else {
|
||||
SNPRINTF_UTF8(actname, DATA_("%sAction"), id->name + 2);
|
||||
}
|
||||
|
||||
/* create action */
|
||||
action = BKE_action_add(bmain, actname);
|
||||
|
||||
/* Decrement the default-1 user count, as assigning it will increase it again. */
|
||||
BLI_assert(action->id.us == 1);
|
||||
id_us_min(&action->id);
|
||||
}
|
||||
|
||||
/* Assigning the Action should always work here. The only reason it wouldn't, is when a legacy
|
||||
* Action of the wrong ID type is assigned, but since in this branch of the code we're only
|
||||
* dealing with either new or layered Actions, this will never fail. */
|
||||
const bool ok = animrig::assign_action(action, {*id, *adt});
|
||||
BLI_assert_msg(ok, "Expecting Action assignment to work here");
|
||||
UNUSED_VARS_NDEBUG(ok);
|
||||
|
||||
/* Tag depsgraph to be rebuilt to include time dependency. */
|
||||
DEG_relations_tag_update(bmain);
|
||||
}
|
||||
|
||||
DEG_id_tag_update(&adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
|
||||
/* return the action */
|
||||
return adt->action;
|
||||
}
|
||||
|
||||
void animdata_fcurve_delete(AnimData *adt, FCurve *fcu)
|
||||
{
|
||||
/* - If no AnimData, we've got nowhere to remove the F-Curve from
|
||||
* (this doesn't guarantee that the F-Curve is in there, but at least we tried).
|
||||
* - If no F-Curve, there is nothing to remove
|
||||
*/
|
||||
if (ELEM(nullptr, adt, fcu)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool is_driver = fcu->driver != nullptr;
|
||||
if (is_driver) {
|
||||
BLI_remlink(&adt->drivers, fcu);
|
||||
}
|
||||
else if (adt->action) {
|
||||
Action &action = adt->action->wrap();
|
||||
action_fcurve_remove(action, *fcu);
|
||||
/* Return early to avoid the call to BKE_fcurve_free because the fcu has already been freed
|
||||
* by action_fcurve_remove. */
|
||||
return;
|
||||
}
|
||||
else {
|
||||
BLI_assert_unreachable();
|
||||
}
|
||||
|
||||
BKE_fcurve_free(fcu);
|
||||
}
|
||||
|
||||
bool animdata_remove_empty_action(AnimData *adt)
|
||||
{
|
||||
if (adt->action != nullptr) {
|
||||
bAction *act = adt->action;
|
||||
DEG_id_tag_update(&act->id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
Action &action = act->wrap();
|
||||
if (action.is_empty() && (adt->flag & ADT_NLA_EDIT_ON) == 0) {
|
||||
id_us_min(&act->id);
|
||||
adt->action = nullptr;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
const FCurve *fcurve_find_by_rna_path(const AnimData &adt,
|
||||
const StringRefNull rna_path,
|
||||
const int array_index)
|
||||
{
|
||||
BLI_assert(adt.action);
|
||||
if (!adt.action) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const Action &action = adt.action->wrap();
|
||||
|
||||
const Slot *slot = action.slot_for_handle(adt.slot_handle);
|
||||
if (!slot) {
|
||||
/* No need to inspect anything if this ID does not have an Action Slot. */
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* No check for the slot's ID type. Not only do we not have the actual ID
|
||||
* to do this check, but also, since the Action and the slot have been
|
||||
* assigned, just trust that it's valid. */
|
||||
|
||||
/* Iterate the layers top-down, as higher-up animation overrides (or at least can override)
|
||||
* lower-down animation. */
|
||||
for (int layer_idx = action.layer_array_num - 1; layer_idx >= 0; layer_idx--) {
|
||||
const Layer *layer = action.layer(layer_idx);
|
||||
|
||||
/* TODO: refactor this into something nicer once we have different strip types. */
|
||||
for (const Strip *strip : layer->strips()) {
|
||||
switch (strip->type()) {
|
||||
case Strip::Type::Keyframe: {
|
||||
const StripKeyframeData &strip_data = strip->data<StripKeyframeData>(action);
|
||||
const Channelbag *channelbag_for_slot = strip_data.channelbag_for_slot(*slot);
|
||||
if (!channelbag_for_slot) {
|
||||
continue;
|
||||
}
|
||||
const FCurve *fcu = channelbag_for_slot->fcurve_find({rna_path, array_index});
|
||||
if (!fcu) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* This code assumes that there is only one strip, and that it's infinite. When that
|
||||
* changes, this code needs to be expanded to check for strip boundaries. */
|
||||
return fcu;
|
||||
}
|
||||
}
|
||||
/* Explicit lack of 'default' clause, to get compiler warnings when strip types are added. */
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
Span<FCurve *> fcurves_for_assigned_action(AnimData *adt)
|
||||
{
|
||||
if (!adt || !adt->action) {
|
||||
return {};
|
||||
}
|
||||
return fcurves_for_action_slot(adt->action->wrap(), adt->slot_handle);
|
||||
}
|
||||
|
||||
Span<const FCurve *> fcurves_for_assigned_action(const AnimData *adt)
|
||||
{
|
||||
if (!adt || !adt->action) {
|
||||
return {};
|
||||
}
|
||||
return fcurves_for_action_slot(const_cast<const bAction *>(adt->action)->wrap(),
|
||||
adt->slot_handle);
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
76
blender-5.2.0/source/blender/animrig/intern/armature.cc
Normal file
76
blender-5.2.0/source/blender/animrig/intern/armature.cc
Normal file
@@ -0,0 +1,76 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_armature.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_pose.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
void pose_bone_descendent_iterator(Object &pose_ob,
|
||||
bPoseChannel &pchan,
|
||||
FunctionRef<void(bPoseChannel &child_bone)> callback)
|
||||
{
|
||||
/* Needed for fast name lookups. */
|
||||
BKE_pose_channels_hash_ensure(pose_ob.pose);
|
||||
|
||||
int i = 0;
|
||||
/* This is not using an std::deque because the implementation of that has issues on windows. */
|
||||
Vector<bPoseChannel *> descendants = {&pchan};
|
||||
while (i < descendants.size()) {
|
||||
bPoseChannel *descendant = descendants[i];
|
||||
i++;
|
||||
callback(*descendant);
|
||||
Bone *descendant_bone = descendant->bone_get(pose_ob);
|
||||
for (Bone &child_bone : descendant_bone->childbase) {
|
||||
bPoseChannel *child_pose_bone = BKE_pose_channel_find_name(pose_ob.pose, child_bone.name);
|
||||
if (!child_pose_bone) {
|
||||
/* Can happen if the pose is not rebuilt. */
|
||||
BLI_assert_unreachable();
|
||||
continue;
|
||||
}
|
||||
descendants.append(child_pose_bone);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
static bool pose_depth_iterator_recursive(Object &pose_ob,
|
||||
bke::PChanBone pchanbone,
|
||||
FunctionRef<bool(bPoseChannel &child_bone)> callback)
|
||||
{
|
||||
if (!callback(*pchanbone.pchan)) {
|
||||
return false;
|
||||
}
|
||||
bool success = true;
|
||||
for (Bone &child_bone : pchanbone.bone->childbase) {
|
||||
bPoseChannel *child_pose_bone = BKE_pose_channel_find_name(pose_ob.pose, child_bone.name);
|
||||
if (!child_pose_bone) {
|
||||
BLI_assert_unreachable();
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
success &= pose_depth_iterator_recursive(pose_ob, {child_pose_bone, &child_bone}, callback);
|
||||
}
|
||||
return success;
|
||||
}
|
||||
|
||||
bool pose_bone_descendent_depth_iterator(Object &pose_ob,
|
||||
bPoseChannel &pchan,
|
||||
FunctionRef<bool(bPoseChannel &child_bone)> callback)
|
||||
{
|
||||
/* Needed for fast name lookups. */
|
||||
BKE_pose_channels_hash_ensure(pose_ob.pose);
|
||||
return pose_depth_iterator_recursive(pose_ob, {&pchan, pchan.bone_get(pose_ob)}, callback);
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
1607
blender-5.2.0/source/blender/animrig/intern/bone_collections.cc
Normal file
1607
blender-5.2.0/source/blender/animrig/intern/bone_collections.cc
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*
|
||||
* \brief Internal C++ functions to deal with bone collections. These are mostly here for internal
|
||||
* use in `bone_collections.cc` and have them testable by unit tests.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct bArmature;
|
||||
struct BoneCollection;
|
||||
|
||||
namespace animrig::internal {
|
||||
|
||||
/**
|
||||
* Move a block of BoneCollections in the Armature's `collections_array`, from
|
||||
* `start_index` to `start_index + direction`.
|
||||
*
|
||||
* The move operation is actually implemented as a rotation, so that no
|
||||
* `BoneCollection*` is lost. In other words, one of these operations is
|
||||
* performed, depending on `direction`. Here `B` indicates an element in the
|
||||
* moved block, and `X` indicates the rotated element.
|
||||
*
|
||||
* direction = +1: [. . . X B B B B . . .] -> [. . . B B B B X . . .]
|
||||
* direction = -1: [. . . B B B B X . . .] -> [. . . X B B B B . . .]
|
||||
*
|
||||
* This function does not alter the length of `collections_array`.
|
||||
* It only performs the rotation, and updates any `child_index` when they
|
||||
* reference elements of the moved block.
|
||||
*
|
||||
* It also does not touch any `child_count` properties of bone collections.
|
||||
* Updating those, as well as any references to the rotated element, is the
|
||||
* responsibility of the caller.
|
||||
*
|
||||
* \param direction: Must be either -1 or 1.
|
||||
*/
|
||||
void bonecolls_rotate_block(bArmature *armature, int start_index, int count, int direction);
|
||||
|
||||
/**
|
||||
* Move a bone collection to another index.
|
||||
*
|
||||
* This is implemented via a call to #bonecolls_rotate_block, so all the
|
||||
* documentation of that function (including its invariants and caveats) applies
|
||||
* here too.
|
||||
*/
|
||||
void bonecolls_move_to_index(bArmature *armature, int from_index, int to_index);
|
||||
|
||||
/**
|
||||
* Find the given bone collection in the armature's collections, and return its index.
|
||||
*
|
||||
* The bone collection is only searched for at the given index, index+1, and index-1.
|
||||
*
|
||||
* If the bone collection cannot be found, -1 is returned.
|
||||
*/
|
||||
int bonecolls_find_index_near(bArmature *armature, BoneCollection *bcoll, int index);
|
||||
|
||||
void bonecolls_debug_list(const bArmature *armature);
|
||||
|
||||
/**
|
||||
* Unassign all (edit)bones from this bone collection, and free it.
|
||||
*
|
||||
* Note that this does NOT take care of updating the collection hierarchy information. See
|
||||
* #ANIM_armature_bonecoll_remove_from_index and #ANIM_armature_bonecoll_remove for that.
|
||||
*/
|
||||
void bonecoll_unassign_and_free(bArmature *armature, BoneCollection *bcoll);
|
||||
|
||||
} // namespace animrig::internal
|
||||
} // namespace blender
|
||||
1630
blender-5.2.0/source/blender/animrig/intern/bone_collections_test.cc
Normal file
1630
blender-5.2.0/source/blender/animrig/intern/bone_collections_test.cc
Normal file
File diff suppressed because it is too large
Load Diff
98
blender-5.2.0/source/blender/animrig/intern/bonecolor.cc
Normal file
98
blender-5.2.0/source/blender/animrig/intern/bonecolor.cc
Normal file
@@ -0,0 +1,98 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_bonecolor.hh"
|
||||
|
||||
#include "BLI_hash.hh"
|
||||
|
||||
#include "DNA_action_types.h"
|
||||
|
||||
#include "UI_resources.hh"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
BoneColor::BoneColor()
|
||||
{
|
||||
this->palette_index = 0;
|
||||
}
|
||||
BoneColor::BoneColor(const BoneColor &other)
|
||||
{
|
||||
this->palette_index = other.palette_index;
|
||||
std::memcpy(&this->custom, &other.custom, sizeof(this->custom));
|
||||
}
|
||||
BoneColor::~BoneColor() = default;
|
||||
|
||||
const ThemeWireColor *BoneColor::effective_color() const
|
||||
{
|
||||
const int8_t color_index = this->palette_index;
|
||||
if (color_index == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
if (color_index < 0) {
|
||||
return &this->custom;
|
||||
}
|
||||
|
||||
const bTheme *btheme = ui::theme::theme_get();
|
||||
return &btheme->tarm[(color_index - 1)];
|
||||
}
|
||||
|
||||
bool BoneColor::operator==(const BoneColor &other) const
|
||||
{
|
||||
if (palette_index != other.palette_index) {
|
||||
return false;
|
||||
}
|
||||
if (palette_index == -1) {
|
||||
/* Explicitly compare each field, skipping the DNA padding fields. */
|
||||
/* TODO: maybe there is already a DNA-level-comparison function for this? */
|
||||
|
||||
/* The last byte of the colors isn't used, but it's still in memory. The annoying thing is that
|
||||
* values are inconsistently either 0 or 255 depending on how the color was set, and there is
|
||||
* no way to influence this with the color picker in the GUI. So, just skip the last byte in
|
||||
* the comparisons. */
|
||||
return std::memcmp(custom.solid, other.custom.solid, sizeof(custom.solid) - 1) == 0 &&
|
||||
std::memcmp(custom.select, other.custom.select, sizeof(custom.select) - 1) == 0 &&
|
||||
std::memcmp(custom.active, other.custom.active, sizeof(custom.active) - 1) == 0 &&
|
||||
custom.flag == other.custom.flag;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool BoneColor::operator!=(const BoneColor &other) const
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
uint64_t BoneColor::hash() const
|
||||
{
|
||||
if (palette_index >= 0) {
|
||||
/* Theme colors are simple. */
|
||||
return get_default_hash(palette_index);
|
||||
}
|
||||
|
||||
/* For custom colors, hash everything together. */
|
||||
|
||||
/* The last byte of the color is skipped, as it is inconsistent (see note above). */
|
||||
const uint64_t hash_solid = get_default_hash(custom.solid[0], custom.solid[1], custom.solid[2]);
|
||||
const uint64_t hash_select = get_default_hash(
|
||||
custom.select[0], custom.select[1], custom.select[2]);
|
||||
const uint64_t hash_active = get_default_hash(
|
||||
custom.active[0], custom.active[1], custom.active[2]);
|
||||
return get_default_hash(hash_solid, hash_select, hash_active, custom.flag);
|
||||
}
|
||||
|
||||
const BoneColor &ANIM_bonecolor_posebone_get(const bke::PChanBoneConst pchanbone)
|
||||
{
|
||||
if (pchanbone.pchan->color.palette_index == 0) {
|
||||
return pchanbone.bone->color.wrap();
|
||||
}
|
||||
return pchanbone.pchan->color.wrap();
|
||||
}
|
||||
|
||||
}; // namespace blender::animrig
|
||||
28
blender-5.2.0/source/blender/animrig/intern/driver.cc
Normal file
28
blender-5.2.0/source/blender/animrig/intern/driver.cc
Normal file
@@ -0,0 +1,28 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_driver.hh"
|
||||
#include "BKE_fcurve_driver.h"
|
||||
#include "DNA_anim_types.h"
|
||||
#include "RNA_access.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
float evaluate_driver_from_rna_pointer(const AnimationEvalContext *anim_eval_context,
|
||||
PointerRNA *ptr,
|
||||
PropertyRNA *prop,
|
||||
const FCurve *fcu)
|
||||
{
|
||||
PathResolvedRNA anim_rna;
|
||||
if (!RNA_path_resolved_create(ptr, prop, fcu->array_index, &anim_rna)) {
|
||||
return 0.0f;
|
||||
}
|
||||
return evaluate_driver(&anim_rna, fcu->driver, fcu->driver, anim_eval_context);
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
331
blender-5.2.0/source/blender/animrig/intern/evaluation.cc
Normal file
331
blender-5.2.0/source/blender/animrig/intern/evaluation.cc
Normal file
@@ -0,0 +1,331 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Developers
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "ANIM_evaluation.hh"
|
||||
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_fcurve.hh"
|
||||
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_math_base.hh"
|
||||
#include "BLI_task.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "evaluation_internal.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"anim.evaluation"};
|
||||
|
||||
namespace animrig {
|
||||
|
||||
using namespace internal;
|
||||
|
||||
/**
|
||||
* Blend the intermediate_result into the final_result based on the layer
|
||||
* weight and mix mode.
|
||||
*/
|
||||
void blend_layer_results(EvaluationResult &final_result,
|
||||
const EvaluationResult &intermediate_result,
|
||||
const Layer ¤t_layer);
|
||||
|
||||
/**
|
||||
* Apply the result of the animation evaluation to the given data-block.
|
||||
*
|
||||
* \param flush_to_original: when true, look up the original data-block (assuming the given one is
|
||||
* an evaluated copy) and update that too.
|
||||
*/
|
||||
void apply_evaluation_result(const EvaluationResult &evaluation_result,
|
||||
PointerRNA &animated_id_ptr,
|
||||
bool flush_to_original);
|
||||
|
||||
EvaluationResult evaluate_action(PointerRNA &animated_id_ptr,
|
||||
Action &action,
|
||||
const slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context)
|
||||
{
|
||||
EvaluationResult result;
|
||||
|
||||
/* Evaluate each layer in order. */
|
||||
for (Layer *layer : action.layers()) {
|
||||
if (layer->influence <= 0.0f) {
|
||||
/* Don't bother evaluating layers without influence. */
|
||||
continue;
|
||||
}
|
||||
|
||||
EvaluationResult layer_result = evaluate_layer(
|
||||
animated_id_ptr, action, *layer, slot_handle, anim_eval_context);
|
||||
if (!layer_result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
/* Simple case: no results so far, so just use this layer as-is. There is
|
||||
* nothing to blend/combine with, so ignore the influence and combination
|
||||
* options. */
|
||||
result = std::move(layer_result);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Complex case: blend this layer's result into combined result. */
|
||||
blend_layer_results(result, layer_result, *layer);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void evaluate_and_apply_action(PointerRNA &animated_id_ptr,
|
||||
Action &action,
|
||||
const slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context,
|
||||
const bool flush_to_original)
|
||||
{
|
||||
EvaluationResult evaluation_result = evaluate_action(
|
||||
animated_id_ptr, action, slot_handle, anim_eval_context);
|
||||
if (!evaluation_result) {
|
||||
return;
|
||||
}
|
||||
|
||||
apply_evaluation_result(evaluation_result, animated_id_ptr, flush_to_original);
|
||||
}
|
||||
|
||||
/* Copy of the same-named function in anim_sys.cc, with the check on action groups removed. */
|
||||
static bool is_fcurve_evaluatable(const FCurve *fcu)
|
||||
{
|
||||
if (fcu->rna_path == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Not checking for FCURVE_DISABLED here, because those FCurves may still be evaluatable for
|
||||
* other users of the same slot. See #135666. This is safe to do since this function isn't called
|
||||
* for drivers. */
|
||||
if (fcu->flag & FCURVE_MUTED) {
|
||||
return false;
|
||||
}
|
||||
if (BKE_fcurve_is_empty(fcu)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Copy of the same-named function in anim_sys.cc, but with the special handling for NLA strips
|
||||
* removed. */
|
||||
static void animsys_construct_orig_pointer_rna(const PointerRNA *ptr, PointerRNA *ptr_orig)
|
||||
{
|
||||
*ptr_orig = *ptr;
|
||||
/* Original note from anim_sys.cc:
|
||||
* -----------
|
||||
* NOTE: nlastrip_evaluate_controls() creates PointerRNA with ID of nullptr. Technically, this is
|
||||
* not a valid pointer, but there are exceptions in various places of this file which handles
|
||||
* such pointers.
|
||||
* We do special trickery here as well, to quickly go from evaluated to original NlaStrip.
|
||||
* -----------
|
||||
* And this is all not ported to the new layered animation system. */
|
||||
BLI_assert_msg(ptr->owner_id, "NLA support was not ported to the layered animation system");
|
||||
ptr_orig->owner_id = ptr_orig->owner_id->orig_id;
|
||||
ptr_orig->data = ptr_orig->owner_id;
|
||||
}
|
||||
|
||||
/* Copy of the same-named function in anim_sys.cc. */
|
||||
static void animsys_write_orig_anim_rna(PointerRNA *ptr,
|
||||
const char *rna_path,
|
||||
const int array_index,
|
||||
const float value)
|
||||
{
|
||||
PointerRNA ptr_orig;
|
||||
animsys_construct_orig_pointer_rna(ptr, &ptr_orig);
|
||||
|
||||
PathResolvedRNA orig_anim_rna;
|
||||
/* TODO(sergey): Should be possible to cache resolved path in dependency graph somehow. */
|
||||
if (BKE_animsys_rna_path_resolve(&ptr_orig, rna_path, array_index, &orig_anim_rna)) {
|
||||
BKE_animsys_write_to_rna_path(&orig_anim_rna, value);
|
||||
}
|
||||
}
|
||||
|
||||
static EvaluationResult evaluate_keyframe_data(PointerRNA &animated_id_ptr,
|
||||
StripKeyframeData &strip_data,
|
||||
const slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &offset_eval_context)
|
||||
{
|
||||
Channelbag *channelbag_for_slot = strip_data.channelbag_for_slot(slot_handle);
|
||||
if (!channelbag_for_slot) {
|
||||
return {};
|
||||
}
|
||||
|
||||
Span<FCurve *> fcurves = channelbag_for_slot->fcurves();
|
||||
/* Stores true for FCurves that have been evaluated. Not using BitVector because writing to it
|
||||
* from threads will introduce race conditions.*/
|
||||
Array<bool> valid(fcurves.size(), false);
|
||||
Array<float> results(fcurves.size());
|
||||
Array<PathResolvedRNA> resolved_rna(fcurves.size());
|
||||
|
||||
threading::parallel_for(fcurves.index_range(), 512, [&](const IndexRange range) {
|
||||
for (const int i : range) {
|
||||
FCurve *fcu = fcurves[i];
|
||||
if (!is_fcurve_evaluatable(fcu)) {
|
||||
continue;
|
||||
}
|
||||
/* Resolve the RNA path to skip unresolvable properties. It's faster to do that in a thread
|
||||
* and store the result for later. */
|
||||
PathResolvedRNA &anim_rna = resolved_rna[i];
|
||||
if (!BKE_animsys_rna_path_resolve(
|
||||
&animated_id_ptr, fcu->rna_path, fcu->array_index, &anim_rna))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
BLI_assert(fcu->driver == nullptr);
|
||||
/* Not using calculate_fcurve because FCurves of channelbags are not drivers. */
|
||||
results[i] = evaluate_fcurve(fcu, offset_eval_context.eval_time);
|
||||
valid[i] = true;
|
||||
}
|
||||
});
|
||||
|
||||
EvaluationResult evaluation_result;
|
||||
evaluation_result.reserve(fcurves.size());
|
||||
for (const int i : fcurves.index_range()) {
|
||||
if (!valid[i]) {
|
||||
continue;
|
||||
}
|
||||
FCurve *fcu = fcurves[i];
|
||||
PathResolvedRNA &anim_rna = resolved_rna[i];
|
||||
/* This part is not threadsafe. */
|
||||
evaluation_result.store(fcu->rna_path, fcu->array_index, results[i], anim_rna);
|
||||
}
|
||||
|
||||
return evaluation_result;
|
||||
}
|
||||
|
||||
void apply_evaluation_result(const EvaluationResult &evaluation_result,
|
||||
PointerRNA &animated_id_ptr,
|
||||
const bool flush_to_original)
|
||||
{
|
||||
for (const auto &channel_result : evaluation_result.items()) {
|
||||
const PropIdentifier &prop_ident = channel_result.key;
|
||||
const AnimatedProperty &anim_prop = channel_result.value;
|
||||
const float animated_value = anim_prop.value;
|
||||
PathResolvedRNA anim_rna = anim_prop.prop_rna;
|
||||
|
||||
BKE_animsys_write_to_rna_path(&anim_rna, animated_value);
|
||||
|
||||
if (flush_to_original) {
|
||||
/* Convert the StringRef to a `const char *`, as the rest of the RNA path handling code in
|
||||
* BKE still uses `char *` instead of `StringRef`. */
|
||||
animsys_write_orig_anim_rna(
|
||||
&animated_id_ptr, prop_ident.rna_path.c_str(), prop_ident.array_index, animated_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static EvaluationResult evaluate_strip(PointerRNA &animated_id_ptr,
|
||||
Action &owning_action,
|
||||
Strip &strip,
|
||||
const slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context)
|
||||
{
|
||||
AnimationEvalContext offset_eval_context = anim_eval_context;
|
||||
/* Positive offset means the entire strip is pushed "to the right", so
|
||||
* evaluation needs to happen further "to the left". */
|
||||
offset_eval_context.eval_time -= strip.frame_offset;
|
||||
|
||||
switch (strip.type()) {
|
||||
case Strip::Type::Keyframe: {
|
||||
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(owning_action);
|
||||
return evaluate_keyframe_data(animated_id_ptr, strip_data, slot_handle, offset_eval_context);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void blend_layer_results(EvaluationResult &final_result,
|
||||
const EvaluationResult &intermediate_result,
|
||||
const Layer ¤t_layer)
|
||||
{
|
||||
/* TODO?: store the layer results sequentially, so that we can step through
|
||||
* them in parallel, instead of iterating over one and doing map lookups on
|
||||
* the other. */
|
||||
|
||||
for (const auto &channel_result : intermediate_result.items()) {
|
||||
const PropIdentifier &prop_ident = channel_result.key;
|
||||
AnimatedProperty *last_prop = final_result.lookup_ptr(prop_ident);
|
||||
const AnimatedProperty &anim_prop = channel_result.value;
|
||||
|
||||
if (!last_prop) {
|
||||
/* Nothing to blend with, so just take (influence * value). */
|
||||
final_result.store(prop_ident.rna_path,
|
||||
prop_ident.array_index,
|
||||
anim_prop.value * current_layer.influence,
|
||||
anim_prop.prop_rna);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* TODO: move this to a separate function. And write more smartness for rotations. */
|
||||
switch (current_layer.mix_mode()) {
|
||||
case Layer::MixMode::Replace:
|
||||
last_prop->value = anim_prop.value * current_layer.influence;
|
||||
break;
|
||||
case Layer::MixMode::Offset:
|
||||
last_prop->value = math::interpolate(
|
||||
current_layer.influence, last_prop->value, anim_prop.value);
|
||||
break;
|
||||
case Layer::MixMode::Add:
|
||||
last_prop->value += anim_prop.value * current_layer.influence;
|
||||
break;
|
||||
case Layer::MixMode::Subtract:
|
||||
last_prop->value -= anim_prop.value * current_layer.influence;
|
||||
break;
|
||||
case Layer::MixMode::Multiply:
|
||||
last_prop->value *= anim_prop.value * current_layer.influence;
|
||||
break;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
namespace internal {
|
||||
|
||||
EvaluationResult evaluate_layer(PointerRNA &animated_id_ptr,
|
||||
Action &owning_action,
|
||||
Layer &layer,
|
||||
const slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context)
|
||||
{
|
||||
/* TODO: implement cross-blending between overlapping strips. For now, this is not supported.
|
||||
* Instead, the first strong result is taken (see below), and if that is not available, the last
|
||||
* weak result will be used.
|
||||
*
|
||||
* Weak result: obtained from evaluating the final frame of the strip.
|
||||
* Strong result: any result that is not a weak result. */
|
||||
EvaluationResult last_weak_result;
|
||||
|
||||
for (Strip *strip : layer.strips()) {
|
||||
if (!strip->contains_frame(anim_eval_context.eval_time)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Cannot use const here because the std::move would not work otherwise. */
|
||||
EvaluationResult strip_result = evaluate_strip(
|
||||
animated_id_ptr, owning_action, *strip, slot_handle, anim_eval_context);
|
||||
if (!strip_result) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool is_weak_result = strip->is_last_frame(anim_eval_context.eval_time);
|
||||
if (is_weak_result) {
|
||||
/* Keep going until a strong result is found. */
|
||||
last_weak_result = std::move(strip_result);
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Found a strong result, just return it. */
|
||||
return strip_result;
|
||||
}
|
||||
|
||||
return last_weak_result;
|
||||
}
|
||||
|
||||
} // namespace internal
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,30 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Developers
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ANIM_evaluation.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Action;
|
||||
struct Layer;
|
||||
struct AnimationEvalContext;
|
||||
struct PointerRNA;
|
||||
|
||||
namespace animrig::internal {
|
||||
|
||||
/**
|
||||
* Evaluate the animation data on the given layer, for the given slot. This
|
||||
* just returns the evaluation result, without taking any other layers,
|
||||
* blending, influence, etc. into account.
|
||||
*/
|
||||
EvaluationResult evaluate_layer(PointerRNA &animated_id_ptr,
|
||||
Action &owning_action,
|
||||
Layer &layer,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext &anim_eval_context);
|
||||
|
||||
} // namespace animrig::internal
|
||||
} // namespace blender
|
||||
310
blender-5.2.0/source/blender/animrig/intern/evaluation_test.cc
Normal file
310
blender-5.2.0/source/blender/animrig/intern/evaluation_test.cc
Normal file
@@ -0,0 +1,310 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_evaluation.hh"
|
||||
#include "evaluation_internal.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_gtest_base.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_prototypes.hh"
|
||||
|
||||
#include "BLI_math_base.h"
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::tests {
|
||||
|
||||
using namespace blender::animrig::internal;
|
||||
|
||||
class AnimationEvaluationTest : public bke::BlenderGTestBase {
|
||||
protected:
|
||||
Main *bmain;
|
||||
Action *action;
|
||||
Object *cube;
|
||||
Slot *slot;
|
||||
Layer *layer;
|
||||
|
||||
KeyframeSettings settings = get_keyframe_settings(false);
|
||||
AnimationEvalContext anim_eval_context = {};
|
||||
PointerRNA cube_rna_ptr;
|
||||
|
||||
public:
|
||||
void SetUp() override
|
||||
{
|
||||
bmain = BKE_main_new();
|
||||
action = BKE_id_new<Action>(bmain, "ACÄnimåtië");
|
||||
|
||||
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
|
||||
|
||||
slot = &action->slot_add();
|
||||
ASSERT_EQ(assign_action_and_slot(action, slot, cube->id), ActionSlotAssignmentResult::OK);
|
||||
|
||||
layer = &action->layer_add("Kübus layer");
|
||||
|
||||
/* Make it easier to predict test values. */
|
||||
settings.interpolation = BEZT_IPO_LIN;
|
||||
|
||||
cube_rna_ptr = RNA_pointer_create_discrete(&cube->id, RNA_Object, &cube->id);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
|
||||
/** Evaluate the layer, and return result for the given property. */
|
||||
std::optional<float> evaluate_single_property(const StringRefNull rna_path,
|
||||
const int array_index,
|
||||
const float eval_time)
|
||||
{
|
||||
anim_eval_context.eval_time = eval_time;
|
||||
EvaluationResult result = evaluate_layer(
|
||||
cube_rna_ptr, *action, *layer, slot->handle, anim_eval_context);
|
||||
|
||||
const AnimatedProperty *loc0_result = result.lookup_ptr(PropIdentifier(rna_path, array_index));
|
||||
if (!loc0_result) {
|
||||
return {};
|
||||
}
|
||||
return loc0_result->value;
|
||||
}
|
||||
|
||||
/** Evaluate the layer, and test that the given property evaluates to the expected value. */
|
||||
testing::AssertionResult test_evaluate_layer(const StringRefNull rna_path,
|
||||
const int array_index,
|
||||
const float2 eval_time__expect_value)
|
||||
{
|
||||
const float eval_time = eval_time__expect_value[0];
|
||||
const float expect_value = eval_time__expect_value[1];
|
||||
|
||||
const std::optional<float> opt_eval_value = evaluate_single_property(
|
||||
rna_path, array_index, eval_time);
|
||||
if (!opt_eval_value) {
|
||||
return testing::AssertionFailure()
|
||||
<< rna_path << "[" << array_index << "] should have been animated";
|
||||
}
|
||||
|
||||
const float eval_value = *opt_eval_value;
|
||||
const uint diff_ulps = ulp_diff_ff(expect_value, eval_value);
|
||||
if (diff_ulps >= 4) {
|
||||
return testing::AssertionFailure()
|
||||
<< std::endl
|
||||
<< " " << rna_path << "[" << array_index
|
||||
<< "] evaluation did not produce the expected result:" << std::endl
|
||||
<< " evaluated to: " << testing::PrintToString(eval_value) << std::endl
|
||||
<< " expected : " << testing::PrintToString(expect_value) << std::endl;
|
||||
}
|
||||
|
||||
return testing::AssertionSuccess();
|
||||
};
|
||||
|
||||
/** Evaluate the layer, and test that the given property is not part of the result. */
|
||||
testing::AssertionResult test_evaluate_layer_no_result(const StringRefNull rna_path,
|
||||
const int array_index,
|
||||
const float eval_time)
|
||||
{
|
||||
const std::optional<float> eval_value = evaluate_single_property(
|
||||
rna_path, array_index, eval_time);
|
||||
if (eval_value) {
|
||||
return testing::AssertionFailure()
|
||||
<< std::endl
|
||||
<< " " << rna_path << "[" << array_index
|
||||
<< "] evaluation should NOT produce a value:" << std::endl
|
||||
<< " evaluated to: " << testing::PrintToString(*eval_value) << std::endl;
|
||||
}
|
||||
|
||||
return testing::AssertionSuccess();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AnimationEvaluationTest, evaluate_layer__keyframes)
|
||||
{
|
||||
Strip &strip = layer->strip_add(*action, Strip::Type::Keyframe);
|
||||
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
|
||||
|
||||
/* Set some keys. */
|
||||
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.1f}, settings);
|
||||
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 47.5f}, settings);
|
||||
strip_data.keyframe_insert(bmain, *slot, {"rotation_euler", 1}, {1.0f, 0.0f}, settings);
|
||||
strip_data.keyframe_insert(bmain, *slot, {"rotation_euler", 1}, {5.0f, 3.14f}, settings);
|
||||
|
||||
/* Set the animated properties to some values. These should not be overwritten
|
||||
* by the evaluation itself. */
|
||||
cube->loc[0] = 3.0f;
|
||||
cube->loc[1] = 2.0f;
|
||||
cube->loc[2] = 7.0f;
|
||||
cube->rot[0] = 3.0f;
|
||||
cube->rot[1] = 2.0f;
|
||||
cube->rot[2] = 7.0f;
|
||||
|
||||
/* Evaluate. */
|
||||
anim_eval_context.eval_time = 3.0f;
|
||||
EvaluationResult result = evaluate_layer(
|
||||
cube_rna_ptr, *action, *layer, slot->handle, anim_eval_context);
|
||||
|
||||
/* Check the result. */
|
||||
ASSERT_FALSE(result.is_empty());
|
||||
AnimatedProperty *loc0_result = result.lookup_ptr(PropIdentifier("location", 0));
|
||||
ASSERT_NE(nullptr, loc0_result) << "location[0] should have been animated";
|
||||
EXPECT_EQ(47.3f, loc0_result->value);
|
||||
|
||||
EXPECT_EQ(3.0f, cube->loc[0]) << "Evaluation should not modify the animated ID";
|
||||
EXPECT_EQ(2.0f, cube->loc[1]) << "Evaluation should not modify the animated ID";
|
||||
EXPECT_EQ(7.0f, cube->loc[2]) << "Evaluation should not modify the animated ID";
|
||||
EXPECT_EQ(3.0f, cube->rot[0]) << "Evaluation should not modify the animated ID";
|
||||
EXPECT_EQ(2.0f, cube->rot[1]) << "Evaluation should not modify the animated ID";
|
||||
EXPECT_EQ(7.0f, cube->rot[2]) << "Evaluation should not modify the animated ID";
|
||||
}
|
||||
|
||||
TEST_F(AnimationEvaluationTest, strip_boundaries__single_strip)
|
||||
{
|
||||
/* Single finite strip, check first, middle, and last frame. */
|
||||
Strip &strip = layer->strip_add(*action, Strip::Type::Keyframe);
|
||||
strip.resize(1.0f, 10.0f);
|
||||
|
||||
/* Set some keys. */
|
||||
StripKeyframeData &strip_data = strip.data<StripKeyframeData>(*action);
|
||||
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
|
||||
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
|
||||
strip_data.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
|
||||
|
||||
/* Evaluate the layer to see how it handles the boundaries + something in between. */
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 48.0f}));
|
||||
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.001f));
|
||||
}
|
||||
|
||||
TEST_F(AnimationEvaluationTest, strip_boundaries__nonoverlapping)
|
||||
{
|
||||
/* Two finite strips that are strictly distinct. */
|
||||
Strip &strip1 = layer->strip_add(*action, Strip::Type::Keyframe);
|
||||
Strip &strip2 = layer->strip_add(*action, Strip::Type::Keyframe);
|
||||
strip1.resize(1.0f, 10.0f);
|
||||
strip2.resize(11.0f, 20.0f);
|
||||
strip2.frame_offset = 10;
|
||||
|
||||
/* Set some keys. */
|
||||
{
|
||||
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
|
||||
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
|
||||
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
|
||||
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
|
||||
}
|
||||
{
|
||||
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
|
||||
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
|
||||
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
|
||||
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
|
||||
}
|
||||
|
||||
/* Check Strip 1. */
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 48.0f}));
|
||||
|
||||
/* Check Strip 2. */
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {11.0f, 47.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {13.0f, 187.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {20.0f, 48.0f}));
|
||||
|
||||
/* Check outside the range of the strips. */
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 0.999f));
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.001f));
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 10.999f));
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 20.001f));
|
||||
}
|
||||
|
||||
TEST_F(AnimationEvaluationTest, strip_boundaries__overlapping_edge)
|
||||
{
|
||||
/* Two finite strips that are overlapping on their edge. */
|
||||
Strip &strip1 = layer->strip_add(*action, Strip::Type::Keyframe);
|
||||
Strip &strip2 = layer->strip_add(*action, Strip::Type::Keyframe);
|
||||
strip1.resize(1.0f, 10.0f);
|
||||
strip2.resize(10.0f, 19.0f);
|
||||
strip2.frame_offset = 9;
|
||||
|
||||
/* Set some keys. */
|
||||
{
|
||||
StripKeyframeData &strip_data1 = strip1.data<StripKeyframeData>(*action);
|
||||
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
|
||||
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
|
||||
strip_data1.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
|
||||
}
|
||||
{
|
||||
StripKeyframeData &strip_data2 = strip2.data<StripKeyframeData>(*action);
|
||||
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {1.0f, 47.0f}, settings);
|
||||
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {5.0f, 327.0f}, settings);
|
||||
strip_data2.keyframe_insert(bmain, *slot, {"location", 0}, {10.0f, 48.0f}, settings);
|
||||
}
|
||||
|
||||
/* Check Strip 1. */
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {1.0f, 47.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {3.0f, 187.0f}));
|
||||
|
||||
/* Check overlapping frame. */
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {10.0f, 47.0f}))
|
||||
<< "On the overlapping frame, only Strip 2 should be evaluated.";
|
||||
|
||||
/* Check Strip 2. */
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {12.0f, 187.0f}));
|
||||
EXPECT_TRUE(test_evaluate_layer("location", 0, {19.0f, 48.0f}));
|
||||
|
||||
/* Check outside the range of the strips. */
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 0.999f));
|
||||
EXPECT_TRUE(test_evaluate_layer_no_result("location", 0, 19.001f));
|
||||
}
|
||||
|
||||
class AccessibleEvaluationResult : public EvaluationResult {
|
||||
public:
|
||||
EvaluationMap &get_map()
|
||||
{
|
||||
return result_;
|
||||
}
|
||||
};
|
||||
|
||||
class AnimationEvaluationResultTest : public bke::BlenderGTestBase {};
|
||||
|
||||
TEST_F(AnimationEvaluationResultTest, prop_identifier_hashing)
|
||||
{
|
||||
AccessibleEvaluationResult result;
|
||||
|
||||
/* Test storing the same result twice, with different memory locations of the RNA paths. This
|
||||
* tests that the mapping uses the actual string, and not just pointer comparison. */
|
||||
const char *rna_path_1 = "pose.bones['Root'].location";
|
||||
const std::string rna_path_2(rna_path_1);
|
||||
ASSERT_NE(rna_path_1, rna_path_2.c_str())
|
||||
<< "This test requires different addresses for the RNA path strings";
|
||||
|
||||
PathResolvedRNA fake_resolved_rna;
|
||||
result.store(rna_path_1, 0, 1.0f, fake_resolved_rna);
|
||||
result.store(rna_path_2, 0, 2.0f, fake_resolved_rna);
|
||||
EXPECT_EQ(1, result.get_map().size())
|
||||
<< "Storing a result for the same property twice should just overwrite the previous value";
|
||||
|
||||
{
|
||||
PropIdentifier key(rna_path_1, 0);
|
||||
AnimatedProperty *anim_prop = result.lookup_ptr(key);
|
||||
EXPECT_EQ(2.0f, anim_prop->value) << "The last-stored result should survive.";
|
||||
}
|
||||
{
|
||||
PropIdentifier key(rna_path_2, 0);
|
||||
AnimatedProperty *anim_prop = result.lookup_ptr(key);
|
||||
EXPECT_EQ(2.0f, anim_prop->value) << "The last-stored result should survive.";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::tests
|
||||
717
blender-5.2.0/source/blender/animrig/intern/fcurve.cc
Normal file
717
blender-5.2.0/source/blender/animrig/intern/fcurve.cc
Normal file
@@ -0,0 +1,717 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include <cfloat>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
#include "ANIM_animdata.hh"
|
||||
#include "ANIM_fcurve.hh"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BLI_math_base.h"
|
||||
#include "BLI_math_vector_types.hh"
|
||||
#include "BLI_string.h"
|
||||
#include "DNA_anim_types.h"
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
KeyframeSettings get_keyframe_settings(const bool from_userprefs)
|
||||
{
|
||||
KeyframeSettings settings = {};
|
||||
settings.keyframe_type = BEZT_KEYTYPE_KEYFRAME;
|
||||
settings.handle = HD_AUTO_ANIM;
|
||||
settings.interpolation = BEZT_IPO_BEZ;
|
||||
|
||||
if (from_userprefs) {
|
||||
settings.interpolation = eBezTriple_Interpolation(U.ipo_new);
|
||||
settings.handle = eBezTriple_Handle(U.keyhandles_new);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
const FCurve *fcurve_find(Span<const FCurve *> fcurves, const FCurveDescriptor &fcurve_descriptor)
|
||||
{
|
||||
for (const FCurve *fcurve : fcurves) {
|
||||
/* Check indices first, much cheaper than a string comparison. */
|
||||
if (fcurve->array_index == fcurve_descriptor.array_index && fcurve->rna_path &&
|
||||
StringRef(fcurve->rna_path) == fcurve_descriptor.rna_path)
|
||||
{
|
||||
return fcurve;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
FCurve *fcurve_find(Span<FCurve *> fcurves, const FCurveDescriptor &fcurve_descriptor)
|
||||
{
|
||||
const FCurve *fcurve = fcurve_find(fcurves.cast<const FCurve *>(), fcurve_descriptor);
|
||||
return const_cast<FCurve *>(fcurve);
|
||||
}
|
||||
|
||||
FCurve *create_fcurve_for_channel(const FCurveDescriptor &fcurve_descriptor)
|
||||
{
|
||||
FCurve *fcu = BKE_fcurve_create();
|
||||
fcu->rna_path = BLI_strdupn(fcurve_descriptor.rna_path.data(),
|
||||
fcurve_descriptor.rna_path.size());
|
||||
fcu->array_index = fcurve_descriptor.array_index;
|
||||
fcu->flag = (FCURVE_VISIBLE | FCURVE_SELECTED);
|
||||
fcu->auto_smoothing = U.auto_smoothing_new;
|
||||
|
||||
if (fcurve_descriptor.prop_type.has_value()) {
|
||||
fcu->flag |= fcurve_flags_for_property_type(*fcurve_descriptor.prop_type);
|
||||
}
|
||||
|
||||
/* Set the fcurve's color mode if needed/able. */
|
||||
if ((U.keying_flag & KEYING_FLAG_XYZ2RGB) != 0 && fcurve_descriptor.prop_subtype.has_value()) {
|
||||
switch (*fcurve_descriptor.prop_subtype) {
|
||||
case PROP_TRANSLATION:
|
||||
case PROP_XYZ:
|
||||
case PROP_EULER:
|
||||
case PROP_COLOR:
|
||||
case PROP_COORDS:
|
||||
fcu->color_mode = FCURVE_COLOR_AUTO_RGB;
|
||||
break;
|
||||
|
||||
case PROP_QUATERNION:
|
||||
fcu->color_mode = FCURVE_COLOR_AUTO_YRGB;
|
||||
break;
|
||||
|
||||
default:
|
||||
/* Leave the color mode as default. */
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return fcu;
|
||||
}
|
||||
|
||||
eFCurve_Flags fcurve_flags_for_property_type(const PropertyType prop_type)
|
||||
{
|
||||
switch (prop_type) {
|
||||
case PROP_FLOAT:
|
||||
return eFCurve_Flags{};
|
||||
case PROP_INT:
|
||||
/* Do integer (only 'whole' numbers) interpolation between all points. */
|
||||
return FCURVE_INT_VALUES;
|
||||
default:
|
||||
/* Do 'discrete' (i.e. enum, boolean values which cannot take any intermediate
|
||||
* values at all) interpolation between all points.
|
||||
* - however, we must also ensure that evaluated values are only integers still.
|
||||
*/
|
||||
return FCURVE_DISCRETE_VALUES | FCURVE_INT_VALUES;
|
||||
}
|
||||
}
|
||||
|
||||
bool fcurve_delete_keyframe_at_time(FCurve *fcurve, const float time)
|
||||
{
|
||||
if (!fcurve || BKE_fcurve_is_protected(*fcurve)) {
|
||||
return false;
|
||||
}
|
||||
bool found;
|
||||
|
||||
const int index = BKE_fcurve_bezt_binarysearch_index(
|
||||
fcurve->bezt, time, fcurve->totvert, &found);
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
|
||||
BKE_fcurve_delete_key(fcurve, index);
|
||||
BKE_fcurve_handles_recalc(*fcurve);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool delete_keyframe_fcurve_legacy(AnimData *adt, FCurve *fcu, float cfra)
|
||||
{
|
||||
if (!fcurve_delete_keyframe_at_time(fcu, cfra)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Empty curves get automatically deleted. */
|
||||
if (BKE_fcurve_is_empty(fcu)) {
|
||||
animdata_fcurve_delete(adt, fcu);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* ************************************************** */
|
||||
/* KEYFRAME INSERTION */
|
||||
|
||||
/* -------------- BezTriple Insertion -------------------- */
|
||||
|
||||
/* Change the Y position of a keyframe to match the input, adjusting handles. */
|
||||
static void replace_bezt_keyframe_ypos(BezTriple *dst, const BezTriple *bezt)
|
||||
{
|
||||
/* Just change the values when replacing, so as to not overwrite handles. */
|
||||
float dy = bezt->vec[1][1] - dst->vec[1][1];
|
||||
|
||||
/* Just apply delta value change to the handle values. */
|
||||
dst->vec[0][1] += dy;
|
||||
dst->vec[1][1] += dy;
|
||||
dst->vec[2][1] += dy;
|
||||
|
||||
dst->f1 = bezt->f1;
|
||||
dst->f2 = bezt->f2;
|
||||
dst->f3 = bezt->f3;
|
||||
|
||||
/* TODO: perform some other operations? */
|
||||
}
|
||||
|
||||
int insert_bezt_fcurve(FCurve *fcu, const BezTriple *bezt, eInsertKeyFlags flag)
|
||||
{
|
||||
int i = 0;
|
||||
|
||||
/* Are there already keyframes? */
|
||||
if (fcu->bezt) {
|
||||
bool replace;
|
||||
i = BKE_fcurve_bezt_binarysearch_index(fcu->bezt, bezt->vec[1][0], fcu->totvert, &replace);
|
||||
|
||||
/* Replace an existing keyframe? */
|
||||
if (replace) {
|
||||
/* `i` may in rare cases exceed array bounds. */
|
||||
if ((i >= 0) && (i < fcu->totvert)) {
|
||||
if (flag & INSERTKEY_OVERWRITE_FULL) {
|
||||
fcu->bezt[i] = *bezt;
|
||||
}
|
||||
else {
|
||||
replace_bezt_keyframe_ypos(&fcu->bezt[i], bezt);
|
||||
}
|
||||
|
||||
if (flag & INSERTKEY_CYCLE_AWARE) {
|
||||
/* If replacing an end point of a cyclic curve without offset,
|
||||
* modify the other end too. */
|
||||
if (ELEM(i, 0, fcu->totvert - 1) && BKE_fcurve_get_cycle_type(*fcu) == FCU_CYCLE_PERFECT)
|
||||
{
|
||||
replace_bezt_keyframe_ypos(&fcu->bezt[i == 0 ? fcu->totvert - 1 : 0], bezt);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Keyframing modes allow not replacing the keyframe. */
|
||||
else if ((flag & INSERTKEY_REPLACE) == 0) {
|
||||
/* Insert new - if we're not restricted to replacing keyframes only. */
|
||||
BezTriple *newb = MEM_new_array_zeroed<BezTriple>(fcu->totvert + 1, "beztriple");
|
||||
|
||||
/* Add the beztriples that should occur before the beztriple to be pasted
|
||||
* (originally in fcu). */
|
||||
if (i > 0) {
|
||||
memcpy(newb, fcu->bezt, i * sizeof(BezTriple));
|
||||
}
|
||||
|
||||
/* Add beztriple to paste at index i. */
|
||||
*(newb + i) = *bezt;
|
||||
|
||||
/* Add the beztriples that occur after the beztriple to be pasted (originally in fcu). */
|
||||
if (i < fcu->totvert) {
|
||||
memcpy(newb + i + 1, fcu->bezt + i, (fcu->totvert - i) * sizeof(BezTriple));
|
||||
}
|
||||
|
||||
/* Replace (+ free) old with new, only if necessary to do so. */
|
||||
MEM_delete(fcu->bezt);
|
||||
fcu->bezt = newb;
|
||||
|
||||
fcu->totvert++;
|
||||
}
|
||||
else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
/* No keyframes yet, but can only add if...
|
||||
* 1) keyframing modes say that keyframes can only be replaced, so adding new ones won't know
|
||||
* 2) there are no samples on the curve
|
||||
* NOTE: maybe we may want to allow this later when doing samples -> bezt conversions,
|
||||
* but for now, having both is asking for trouble
|
||||
*/
|
||||
else if ((flag & INSERTKEY_REPLACE) == 0 && (fcu->fpt == nullptr)) {
|
||||
/* Create new keyframes array. */
|
||||
fcu->bezt = MEM_new_zeroed<BezTriple>("beztriple");
|
||||
*(fcu->bezt) = *bezt;
|
||||
fcu->totvert = 1;
|
||||
}
|
||||
/* Cannot add anything. */
|
||||
else {
|
||||
/* Return error code -1 to prevent any misunderstandings. */
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* We need to return the index, so that some tools which do post-processing can
|
||||
* detect where we added the BezTriple in the array.
|
||||
*/
|
||||
return i;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the FCurve to allow insertion of `bezt` without modifying the curve shape.
|
||||
*
|
||||
* Checks whether it is necessary to apply Bezier subdivision due to involvement of non-auto
|
||||
* handles. If necessary, changes `bezt` handles from Auto to Aligned.
|
||||
*
|
||||
* \param bezt: key being inserted
|
||||
* \param prev: keyframe before that key
|
||||
* \param next: keyframe after that key
|
||||
*/
|
||||
static void subdivide_nonauto_handles(const FCurve *fcu,
|
||||
BezTriple *bezt,
|
||||
BezTriple *prev,
|
||||
BezTriple *next)
|
||||
{
|
||||
if (prev->ipo != BEZT_IPO_BEZ || bezt->ipo != BEZT_IPO_BEZ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Don't change Vector handles, or completely auto regions. */
|
||||
const bool bezt_auto = BEZT_IS_AUTOH(bezt) || (bezt->h1 == HD_VECT && bezt->h2 == HD_VECT);
|
||||
const bool prev_auto = BEZT_IS_AUTOH(prev) || (prev->h2 == HD_VECT);
|
||||
const bool next_auto = BEZT_IS_AUTOH(next) || (next->h1 == HD_VECT);
|
||||
if (bezt_auto && prev_auto && next_auto) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Subdivide the curve. */
|
||||
float delta;
|
||||
if (!BKE_fcurve_bezt_subdivide_handles(bezt, prev, next, &delta)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Decide when to force auto to manual. */
|
||||
if (!BEZT_IS_AUTOH(bezt)) {
|
||||
return;
|
||||
}
|
||||
if ((prev_auto || next_auto) && fcu->auto_smoothing == FCURVE_SMOOTH_CONT_ACCEL) {
|
||||
const float hx = bezt->vec[1][0] - bezt->vec[0][0];
|
||||
const float dx = bezt->vec[1][0] - prev->vec[1][0];
|
||||
|
||||
/* This mode always uses 1/3 of key distance for handle x size. */
|
||||
const bool auto_works_well = fabsf(hx - dx / 3.0f) < 0.001f;
|
||||
if (auto_works_well) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Turn off auto mode. */
|
||||
bezt->h1 = bezt->h2 = HD_ALIGN;
|
||||
}
|
||||
|
||||
void initialize_bezt(BezTriple *beztr,
|
||||
const float2 position,
|
||||
const KeyframeSettings &settings,
|
||||
const eFCurve_Flags fcu_flags)
|
||||
{
|
||||
/* Set all three points, for nicer start position.
|
||||
* NOTE: +/- 1 on vec.x for left and right handles is so that 'free' handles work ok...
|
||||
*/
|
||||
beztr->vec[0][0] = position.x - 1.0f;
|
||||
beztr->vec[0][1] = position.y;
|
||||
beztr->vec[1][0] = position.x;
|
||||
beztr->vec[1][1] = position.y;
|
||||
beztr->vec[2][0] = position.x + 1.0f;
|
||||
beztr->vec[2][1] = position.y;
|
||||
beztr->f1 = beztr->f2 = beztr->f3 = BEZT_FLAG_SELECT;
|
||||
|
||||
beztr->h1 = beztr->h2 = settings.handle;
|
||||
beztr->ipo = settings.interpolation;
|
||||
|
||||
/* Interpolation type used is constrained by the type of values the curve can take. */
|
||||
if (fcu_flags & FCURVE_DISCRETE_VALUES) {
|
||||
beztr->ipo = BEZT_IPO_CONST;
|
||||
}
|
||||
else if ((beztr->ipo == BEZT_IPO_BEZ) && (fcu_flags & FCURVE_INT_VALUES)) {
|
||||
beztr->ipo = BEZT_IPO_LIN;
|
||||
}
|
||||
|
||||
/* Set keyframe type value (supplied),
|
||||
* which should come from the scene settings in most cases. */
|
||||
BEZKEYTYPE_LVALUE(beztr) = settings.keyframe_type;
|
||||
|
||||
/* Set default values for "easing" interpolation mode settings.
|
||||
* NOTE: Even if these modes aren't currently used, if users switch
|
||||
* to these later, we want these to work in a sane way out of
|
||||
* the box.
|
||||
*/
|
||||
|
||||
/* "back" easing - This value used to be used when overshoot=0, but that
|
||||
* introduced discontinuities in how the param worked. */
|
||||
beztr->back = 1.70158f;
|
||||
|
||||
/* "elastic" easing - Values here were hand-optimized for a default duration of
|
||||
* ~10 frames (typical motion-graph motion length). */
|
||||
beztr->amplitude = 0.8f;
|
||||
beztr->period = 4.1f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return whether the given fcurve already evaluates to the same value as the
|
||||
* proposed keyframe at the keyframe's time.
|
||||
*
|
||||
* This is a helper function for determining whether to insert a keyframe or not
|
||||
* when "only insert needed" is enabled.
|
||||
*
|
||||
* NOTE: this does *not* determine whether inserting the keyframe would change
|
||||
* the fcurve at points other than the keyframe itself. For example, even if
|
||||
* inserting the key wouldn't change the fcurve's value at the time of the
|
||||
* keyframe, the resulting changes to bezier interpolation could change the
|
||||
* fcurve on either side of it. This function intentionally does not account for
|
||||
* that, since that's not how the "only insert needed" feature is supposed to
|
||||
* work.
|
||||
*/
|
||||
static bool new_key_needed(const FCurve &fcu, const float frame, const float value)
|
||||
{
|
||||
if (fcu.totvert == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool replace;
|
||||
const int bezt_index = BKE_fcurve_bezt_binarysearch_index(
|
||||
fcu.bezt, frame, fcu.totvert, &replace);
|
||||
|
||||
if (replace) {
|
||||
/* If there is already a key, we only need to modify it if the proposed value is different. */
|
||||
return fcu.bezt[bezt_index].vec[1][1] != value;
|
||||
}
|
||||
|
||||
const int diff_ulp = 32;
|
||||
const float fcu_eval = evaluate_fcurve(&fcu, frame);
|
||||
/* No need to insert a key if the same value is already the value of the FCurve at that point. */
|
||||
if (compare_ff_relative(fcu_eval, value, FLT_EPSILON, diff_ulp)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the point where a key is about to be inserted to be inside the main cycle range.
|
||||
* Returns the type of the cycle if it is enabled and valid.
|
||||
*/
|
||||
static float2 remap_cyclic_keyframe_location(const FCurve &fcu,
|
||||
const eFCU_Cycle_Type type,
|
||||
float2 position)
|
||||
{
|
||||
if (fcu.totvert < 2 || !fcu.bezt) {
|
||||
return position;
|
||||
}
|
||||
|
||||
if (type == FCU_CYCLE_NONE) {
|
||||
return position;
|
||||
}
|
||||
|
||||
BezTriple *first = &fcu.bezt[0], *last = &fcu.bezt[fcu.totvert - 1];
|
||||
const float start = first->vec[1][0], end = last->vec[1][0];
|
||||
|
||||
if (start >= end) {
|
||||
return position;
|
||||
}
|
||||
|
||||
if (position.x < start || position.x > end) {
|
||||
const float period = end - start;
|
||||
const float step = floorf((position.x - start) / period);
|
||||
position.x -= step * period;
|
||||
|
||||
if (type == FCU_CYCLE_OFFSET) {
|
||||
/* Nasty check to handle the case when the modes are different better. */
|
||||
FMod_Cycles *data = static_cast<FMod_Cycles *>(
|
||||
static_cast<FModifier *>(fcu.modifiers.first)->data);
|
||||
short mode = (step >= 0) ? data->after_mode : data->before_mode;
|
||||
|
||||
if (mode == FCM_EXTRAPOLATE_CYCLIC_OFFSET) {
|
||||
position.y -= step * (last->vec[1][1] - first->vec[1][1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return position;
|
||||
}
|
||||
|
||||
SingleKeyingResult insert_vert_fcurve(FCurve *fcu,
|
||||
const float2 position,
|
||||
const KeyframeSettings &settings,
|
||||
eInsertKeyFlags flag)
|
||||
{
|
||||
BLI_assert(fcu != nullptr);
|
||||
|
||||
float2 remapped_position = position;
|
||||
/* Adjust coordinates for cycle aware insertion. */
|
||||
if (flag & INSERTKEY_CYCLE_AWARE) {
|
||||
eFCU_Cycle_Type type = BKE_fcurve_get_cycle_type(*fcu);
|
||||
remapped_position = remap_cyclic_keyframe_location(*fcu, type, position);
|
||||
if (type != FCU_CYCLE_PERFECT) {
|
||||
/* Inhibit action from insert_bezt_fcurve unless it's a perfect cycle. */
|
||||
flag &= ~INSERTKEY_CYCLE_AWARE;
|
||||
}
|
||||
}
|
||||
|
||||
if ((flag & INSERTKEY_NEEDED) && !new_key_needed(*fcu, remapped_position.x, remapped_position.y))
|
||||
{
|
||||
return SingleKeyingResult::NO_KEY_NEEDED;
|
||||
}
|
||||
|
||||
BezTriple beztr = {{{0}}};
|
||||
initialize_bezt(&beztr, remapped_position, settings, eFCurve_Flags(fcu->flag));
|
||||
|
||||
uint oldTot = fcu->totvert;
|
||||
int a;
|
||||
|
||||
/* Add temp beztriple to keyframes. */
|
||||
a = insert_bezt_fcurve(fcu, &beztr, flag);
|
||||
BKE_fcurve_active_keyframe_set(fcu, &fcu->bezt[a]);
|
||||
|
||||
/* Key insertion failed. */
|
||||
if (a < 0) {
|
||||
/* TODO: we need more info from `insert_bezt_fcurve()` called above to
|
||||
* return a more specific failure. */
|
||||
return SingleKeyingResult::UNKNOWN_FAILURE;
|
||||
}
|
||||
|
||||
/* Set handle-type and interpolation. */
|
||||
if ((fcu->totvert > 2) && (flag & INSERTKEY_REPLACE) == 0) {
|
||||
BezTriple *bezt = (fcu->bezt + a);
|
||||
|
||||
/* Set interpolation from previous (if available),
|
||||
* but only if we didn't just replace some keyframe:
|
||||
* - Replacement is indicated by no-change in number of verts.
|
||||
* - When replacing, the user may have specified some interpolation that should be kept.
|
||||
*/
|
||||
if (fcu->totvert > oldTot) {
|
||||
if (a > 0) {
|
||||
bezt->ipo = (bezt - 1)->ipo;
|
||||
}
|
||||
else if (a < fcu->totvert - 1) {
|
||||
bezt->ipo = (bezt + 1)->ipo;
|
||||
}
|
||||
|
||||
if (0 < a && a < (fcu->totvert - 1) && (flag & INSERTKEY_OVERWRITE_FULL) == 0) {
|
||||
subdivide_nonauto_handles(fcu, bezt, bezt - 1, bezt + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Don't recalculate handles if fast is set.
|
||||
* - this is a hack to make importers faster
|
||||
* - we may calculate twice (due to auto-handle needing to be calculated twice)
|
||||
*/
|
||||
if ((flag & INSERTKEY_FAST) == 0) {
|
||||
BKE_fcurve_handles_recalc(*fcu);
|
||||
}
|
||||
|
||||
/* Return the index at which the keyframe was added. */
|
||||
return SingleKeyingResult::SUCCESS;
|
||||
}
|
||||
|
||||
void sample_fcurve_segment(const FCurve *fcu,
|
||||
const float start_frame,
|
||||
const float sample_rate,
|
||||
float *samples,
|
||||
const int sample_count)
|
||||
{
|
||||
for (int i = 0; i < sample_count; i++) {
|
||||
const float evaluation_time = start_frame + (float(i) / sample_rate);
|
||||
samples[i] = evaluate_fcurve(fcu, evaluation_time);
|
||||
}
|
||||
}
|
||||
|
||||
static void remove_fcurve_key_range(FCurve *fcu,
|
||||
const int2 range,
|
||||
const BakeCurveRemove removal_mode)
|
||||
{
|
||||
switch (removal_mode) {
|
||||
|
||||
case BakeCurveRemove::ALL: {
|
||||
BKE_fcurve_delete_keys_all(*fcu);
|
||||
break;
|
||||
}
|
||||
|
||||
case BakeCurveRemove::OUT_RANGE: {
|
||||
bool replace;
|
||||
|
||||
int before_index = BKE_fcurve_bezt_binarysearch_index(
|
||||
fcu->bezt, range[0], fcu->totvert, &replace);
|
||||
|
||||
if (before_index > 0) {
|
||||
BKE_fcurve_delete_keys(*fcu, {0, uint(before_index)});
|
||||
}
|
||||
|
||||
int after_index = BKE_fcurve_bezt_binarysearch_index(
|
||||
fcu->bezt, range[1], fcu->totvert, &replace);
|
||||
/* #OUT_RANGE is treated as exclusive on both ends. */
|
||||
if (replace) {
|
||||
after_index++;
|
||||
}
|
||||
if (after_index < fcu->totvert) {
|
||||
BKE_fcurve_delete_keys(*fcu, {uint(after_index), fcu->totvert});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case BakeCurveRemove::IN_RANGE: {
|
||||
bool replace;
|
||||
const int range_start_index = BKE_fcurve_bezt_binarysearch_index(
|
||||
fcu->bezt, range[0], fcu->totvert, &replace);
|
||||
int range_end_index = BKE_fcurve_bezt_binarysearch_index(
|
||||
fcu->bezt, range[1], fcu->totvert, &replace);
|
||||
if (replace) {
|
||||
range_end_index++;
|
||||
}
|
||||
|
||||
if (range_end_index > range_start_index) {
|
||||
BKE_fcurve_delete_keys(*fcu, {uint(range_start_index), uint(range_end_index)});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void bake_fcurve(FCurve *fcu,
|
||||
const int2 range,
|
||||
const float step,
|
||||
const BakeCurveRemove remove_existing)
|
||||
{
|
||||
BLI_assert(step > 0);
|
||||
const int sample_count = (range[1] - range[0]) / step + 1;
|
||||
float *samples = MEM_new_array_zeroed<float>(sample_count, "Channel Bake Samples");
|
||||
const float sample_rate = 1.0f / step;
|
||||
sample_fcurve_segment(fcu, range[0], sample_rate, samples, sample_count);
|
||||
|
||||
if (remove_existing != BakeCurveRemove::NONE) {
|
||||
remove_fcurve_key_range(fcu, range, remove_existing);
|
||||
}
|
||||
|
||||
BezTriple *baked_keys = MEM_new_array_zeroed<BezTriple>(sample_count, "beztriple");
|
||||
|
||||
const KeyframeSettings settings = get_keyframe_settings(true);
|
||||
|
||||
for (int i = 0; i < sample_count; i++) {
|
||||
BezTriple *key = &baked_keys[i];
|
||||
float2 key_position = {range[0] + i * step, samples[i]};
|
||||
initialize_bezt(key, key_position, settings, eFCurve_Flags(fcu->flag));
|
||||
}
|
||||
|
||||
int merged_size;
|
||||
BezTriple *merged_bezt = BKE_bezier_array_merge(
|
||||
baked_keys, sample_count, fcu->bezt, fcu->totvert, &merged_size);
|
||||
|
||||
if (fcu->bezt != nullptr) {
|
||||
/* Can happen if we removed all keys beforehand. */
|
||||
MEM_delete(fcu->bezt);
|
||||
}
|
||||
MEM_delete(baked_keys);
|
||||
fcu->bezt = merged_bezt;
|
||||
fcu->totvert = merged_size;
|
||||
|
||||
MEM_delete(samples);
|
||||
BKE_fcurve_handles_recalc(*fcu);
|
||||
}
|
||||
|
||||
struct TempFrameValCache {
|
||||
float frame, val;
|
||||
};
|
||||
|
||||
void bake_fcurve_segments(FCurve *fcu)
|
||||
{
|
||||
const BezTriple *bezt, *start = nullptr, *end = nullptr;
|
||||
TempFrameValCache *value_cache, *fp;
|
||||
int sfra, range;
|
||||
int i, n;
|
||||
|
||||
if (fcu->bezt == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
KeyframeSettings settings = get_keyframe_settings(true);
|
||||
settings.keyframe_type = BEZT_KEYTYPE_BREAKDOWN;
|
||||
|
||||
/* Find selected keyframes... once pair has been found, add keyframes. */
|
||||
for (i = 0, bezt = fcu->bezt; i < fcu->totvert; i++, bezt++) {
|
||||
/* check if selected, and which end this is */
|
||||
if (BEZT_ISSEL_ANY(bezt)) {
|
||||
if (start) {
|
||||
/* If next bezt is also selected, don't start sampling yet,
|
||||
* but instead wait for that one to reconsider, to avoid
|
||||
* changing the curve when sampling consecutive segments
|
||||
* (#53229)
|
||||
*/
|
||||
if (i < fcu->totvert - 1) {
|
||||
BezTriple *next = &fcu->bezt[i + 1];
|
||||
if (BEZT_ISSEL_ANY(next)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
end = bezt;
|
||||
|
||||
/* Cache values then add keyframes using these values, as adding
|
||||
* keyframes while sampling will affect the outcome...
|
||||
* - Only start sampling+adding from index=1, so that we don't overwrite original keyframe.
|
||||
*/
|
||||
range = int(ceil(end->vec[1][0] - start->vec[1][0]));
|
||||
sfra = int(floor(start->vec[1][0]));
|
||||
|
||||
if (range) {
|
||||
value_cache = MEM_new_array_zeroed<TempFrameValCache>(range, "IcuFrameValCache");
|
||||
|
||||
/* Sample values. */
|
||||
for (n = 1, fp = value_cache; n < range && fp; n++, fp++) {
|
||||
fp->frame = float(sfra + n);
|
||||
fp->val = evaluate_fcurve(fcu, fp->frame);
|
||||
}
|
||||
|
||||
/* Add keyframes with these, tagging as 'breakdowns'. */
|
||||
for (n = 1, fp = value_cache; n < range && fp; n++, fp++) {
|
||||
animrig::insert_vert_fcurve(fcu, {fp->frame, fp->val}, settings, INSERTKEY_NOFLAGS);
|
||||
}
|
||||
|
||||
MEM_delete(value_cache);
|
||||
|
||||
/* As we added keyframes, we need to compensate so that bezt is at the right place. */
|
||||
bezt = fcu->bezt + i + range - 1;
|
||||
i += (range - 1);
|
||||
}
|
||||
|
||||
/* The current selection island has ended, so start again from scratch. */
|
||||
start = nullptr;
|
||||
end = nullptr;
|
||||
}
|
||||
else {
|
||||
/* Just set start keyframe. */
|
||||
start = bezt;
|
||||
end = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
BKE_fcurve_handles_recalc(*fcu);
|
||||
}
|
||||
|
||||
bool fcurve_frame_has_keyframe(const FCurve *fcu, const float frame)
|
||||
{
|
||||
if (ELEM(nullptr, fcu, fcu->bezt)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((fcu->flag & FCURVE_MUTED) == 0) {
|
||||
bool replace;
|
||||
const int i = BKE_fcurve_bezt_binarysearch_index(fcu->bezt, frame, fcu->totvert, &replace);
|
||||
|
||||
/* #BKE_fcurve_bezt_binarysearch_index will set replace to be 0 or 1
|
||||
* - obviously, 1 represents a match
|
||||
*/
|
||||
if (replace) {
|
||||
/* `i` may in rare cases exceed array bounds. */
|
||||
if ((i >= 0) && (i < fcu->totvert)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
915
blender-5.2.0/source/blender/animrig/intern/keyframing.cc
Normal file
915
blender-5.2.0/source/blender/animrig/intern/keyframing.cc
Normal file
@@ -0,0 +1,915 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_iterators.hh"
|
||||
#include "ANIM_animdata.hh"
|
||||
#include "ANIM_fcurve.hh"
|
||||
#include "ANIM_keyframing.hh"
|
||||
#include "ANIM_rna.hh"
|
||||
#include "ANIM_visualkey.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_nla.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
#include "BLI_math_base.h"
|
||||
#include "BLI_task.hh"
|
||||
#include "BLI_utildefines.h"
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
#include "DEG_depsgraph_query.hh"
|
||||
#include "DNA_anim_types.h"
|
||||
#include "MEM_guardedalloc.h"
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_path.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "WM_types.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
void generate_single_keying_result_report(const SingleKeyingResult result, ReportList *reports)
|
||||
{
|
||||
switch (result) {
|
||||
case SingleKeyingResult::SUCCESS:
|
||||
BKE_reportf(reports, RPT_INFO, "Successfully inserted a key.");
|
||||
break;
|
||||
case SingleKeyingResult::UNKNOWN_FAILURE:
|
||||
BKE_reportf(reports, RPT_ERROR, "Keyframe insertion failed for an unknown reason.");
|
||||
break;
|
||||
case SingleKeyingResult::CANNOT_CREATE_FCURVE:
|
||||
BKE_reportf(reports, RPT_ERROR, "Failed to create the F-Curve.");
|
||||
break;
|
||||
case SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE:
|
||||
BKE_reportf(reports, RPT_ERROR, "The F-Curve is not keyable. It may be locked or sampled.");
|
||||
break;
|
||||
case SingleKeyingResult::NO_KEY_NEEDED:
|
||||
BKE_reportf(
|
||||
reports, RPT_ERROR, "Due to the setting 'Only Insert Needed' no keyframe was inserted.");
|
||||
break;
|
||||
case SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK:
|
||||
BKE_reportf(reports, RPT_ERROR, "Due to the NLA stack setup, no key was inserted.");
|
||||
break;
|
||||
case SingleKeyingResult::ID_NOT_EDITABLE:
|
||||
BKE_reportf(
|
||||
reports, RPT_ERROR, "Inserting key has been skipped because the ID cannot be edited.");
|
||||
break;
|
||||
case SingleKeyingResult::ID_NOT_ANIMATABLE:
|
||||
BKE_reportf(
|
||||
reports, RPT_ERROR, "Inserting key has been skipped because the ID cannot be keyed.");
|
||||
break;
|
||||
case SingleKeyingResult::NO_VALID_LAYER:
|
||||
BKE_reportf(reports, RPT_ERROR, "No valid layer. Cannot insert key.");
|
||||
break;
|
||||
case SingleKeyingResult::NO_VALID_STRIP:
|
||||
BKE_reportf(reports, RPT_ERROR, "No valid strip. Cannot insert key.");
|
||||
break;
|
||||
case SingleKeyingResult::NO_VALID_SLOT:
|
||||
BKE_reportf(reports, RPT_ERROR, "No valid slot. Cannot insert key.");
|
||||
break;
|
||||
case SingleKeyingResult::CANNOT_RESOLVE_PATH:
|
||||
BKE_reportf(reports, RPT_ERROR, "Invalid RNA path. Cannot insert key.");
|
||||
break;
|
||||
case SingleKeyingResult::_KEYING_RESULT_MAX:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
CombinedKeyingResult::CombinedKeyingResult()
|
||||
{
|
||||
result_counter.fill(0);
|
||||
}
|
||||
|
||||
void CombinedKeyingResult::add(const SingleKeyingResult result, const int count)
|
||||
{
|
||||
result_counter[int(result)] += count;
|
||||
}
|
||||
|
||||
void CombinedKeyingResult::merge(const CombinedKeyingResult &other)
|
||||
{
|
||||
for (int i = 0; i < result_counter.size(); i++) {
|
||||
result_counter[i] += other.result_counter[i];
|
||||
}
|
||||
}
|
||||
|
||||
int CombinedKeyingResult::get_count(const SingleKeyingResult result) const
|
||||
{
|
||||
return result_counter[int(result)];
|
||||
}
|
||||
|
||||
bool CombinedKeyingResult::has_errors() const
|
||||
{
|
||||
/* For loop starts at 1 to skip the SUCCESS flag. Assumes that SUCCESS is 0 and the rest of the
|
||||
* enum are sequential values. */
|
||||
static_assert(int(SingleKeyingResult::SUCCESS) == 0);
|
||||
for (int i = 1; i < result_counter.size(); i++) {
|
||||
if (result_counter[i] > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void CombinedKeyingResult::generate_reports(ReportList *reports, const eReportType report_level)
|
||||
{
|
||||
if (!this->has_errors() && this->get_count(SingleKeyingResult::SUCCESS) == 0) {
|
||||
BKE_reportf(
|
||||
reports, RPT_WARNING, "No keys have been inserted and no errors have been reported.");
|
||||
return;
|
||||
}
|
||||
|
||||
Vector<std::string> errors;
|
||||
if (this->get_count(SingleKeyingResult::UNKNOWN_FAILURE) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::UNKNOWN_FAILURE);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("There were {:d} keying failures for unknown reasons.")), error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::CANNOT_CREATE_FCURVE);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Could not create {:d} F-Curve(s). This can happen when only "
|
||||
"inserting to available F-Curves.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE);
|
||||
errors.append(
|
||||
fmt::format(fmt::runtime(RPT_(
|
||||
"{:d} F-Curve(s) are not keyframeable. They might be locked or sampled.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::NO_KEY_NEEDED) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::NO_KEY_NEEDED);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_(
|
||||
"Due to the setting 'Only Insert Needed', {:d} keyframe(s) have not been inserted.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Due to the NLA stack setup, {:d} keyframe(s) have not been inserted.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::ID_NOT_EDITABLE) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::ID_NOT_EDITABLE);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
|
||||
"they are not editable.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::ID_NOT_ANIMATABLE) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::ID_NOT_ANIMATABLE);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
|
||||
"they cannot be animated.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::CANNOT_RESOLVE_PATH) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::CANNOT_RESOLVE_PATH);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
|
||||
"the RNA path wasn't valid for them.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::NO_VALID_LAYER) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::NO_VALID_LAYER);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
|
||||
"there were no layers that could accept the keys.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::NO_VALID_STRIP) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::NO_VALID_STRIP);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
|
||||
"there were no strips that could accept the keys.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (this->get_count(SingleKeyingResult::NO_VALID_SLOT) > 0) {
|
||||
const int error_count = this->get_count(SingleKeyingResult::NO_VALID_SLOT);
|
||||
errors.append(fmt::format(
|
||||
fmt::runtime(RPT_("Inserting keys on {:d} data-block(s) has been skipped because "
|
||||
"of missing action slots.")),
|
||||
error_count));
|
||||
}
|
||||
|
||||
if (errors.is_empty()) {
|
||||
BKE_report(reports, RPT_WARNING, "Encountered unhandled error during keyframing");
|
||||
return;
|
||||
}
|
||||
|
||||
if (errors.size() == 1) {
|
||||
BKE_report(reports, report_level, errors[0].c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
std::string error_message = RPT_("Inserting keyframes failed:");
|
||||
for (const std::string &error : errors) {
|
||||
error_message.append(fmt::format("\n- {}", error));
|
||||
}
|
||||
BKE_report(reports, report_level, error_message.c_str());
|
||||
}
|
||||
|
||||
std::optional<StringRefNull> default_channel_group_for_path(const PointerRNA *animated_struct,
|
||||
const StringRef prop_rna_path)
|
||||
{
|
||||
if (animated_struct->type == RNA_PoseBone) {
|
||||
bPoseChannel *pose_channel = static_cast<bPoseChannel *>(animated_struct->data);
|
||||
return pose_channel->name;
|
||||
}
|
||||
|
||||
if (animated_struct->type == RNA_Object) {
|
||||
if (prop_rna_path.find("location") != StringRef::not_found ||
|
||||
prop_rna_path.find("rotation") != StringRef::not_found ||
|
||||
prop_rna_path.find("scale") != StringRef::not_found)
|
||||
{
|
||||
/* NOTE: Keep this label in sync with the "ID" case in
|
||||
* _keyingsets_utils.py :: get_transform_generators_base_info()
|
||||
*/
|
||||
return "Object Transforms";
|
||||
}
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
void update_autoflags_fcurve_direct(FCurve *fcu, const PropertyType prop_type)
|
||||
{
|
||||
/* First clear out all the flags that should be updated by this function, before setting just the
|
||||
* ones suitable for this property type. */
|
||||
fcu->flag &= ~(FCURVE_INT_VALUES | FCURVE_DISCRETE_VALUES);
|
||||
fcu->flag |= fcurve_flags_for_property_type(prop_type);
|
||||
}
|
||||
|
||||
bool is_keying_flag(const Scene *scene, const eKeying_Flag flag)
|
||||
{
|
||||
if (scene) {
|
||||
return (scene->toolsettings->keying_flag & flag) || (U.keying_flag & flag);
|
||||
}
|
||||
return U.keying_flag & flag;
|
||||
}
|
||||
|
||||
eInsertKeyFlags get_keyframing_flags(Scene *scene)
|
||||
{
|
||||
eInsertKeyFlags flag = INSERTKEY_NOFLAGS;
|
||||
|
||||
/* Visual keying. */
|
||||
if (is_keying_flag(scene, KEYING_FLAG_VISUALKEY)) {
|
||||
flag |= INSERTKEY_MATRIX;
|
||||
}
|
||||
|
||||
/* Cycle-aware keyframe insertion - preserve cycle period and flow. */
|
||||
if (is_keying_flag(scene, KEYING_FLAG_CYCLEAWARE)) {
|
||||
flag |= INSERTKEY_CYCLE_AWARE;
|
||||
}
|
||||
|
||||
if (is_keying_flag(scene, MANUALKEY_FLAG_INSERTNEEDED)) {
|
||||
flag |= INSERTKEY_NEEDED;
|
||||
}
|
||||
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the Action assigned to `adt` (if any) has any keyframes at the
|
||||
* given frame. Since we're only concerned whether a keyframe exists, we can
|
||||
* simply loop until a match is found.
|
||||
*
|
||||
* For layered actions, this only checks for keyframes in the assigned slot.
|
||||
*/
|
||||
static bool assigned_action_has_keyframe_at(AnimData &adt, const float frame)
|
||||
{
|
||||
if (adt.action == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (adt.action->flag & ACT_MUTED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Span<FCurve *> fcurves = animrig::fcurves_for_assigned_action(&adt);
|
||||
/* 1024 is a common value for memory bandwidth limited tasks. The number isn't critical: 512
|
||||
* works fine here, but 128 and 4096 seem to work equally well in testing. */
|
||||
return threading::parallel_reduce<bool>(
|
||||
fcurves.index_range(),
|
||||
512,
|
||||
false,
|
||||
[&](const IndexRange range, const bool is_found) {
|
||||
if (is_found) {
|
||||
return true;
|
||||
}
|
||||
for (FCurve *fcu : fcurves.slice(range)) {
|
||||
if (fcurve_frame_has_keyframe(fcu, frame)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
std::logical_or<bool>());
|
||||
}
|
||||
|
||||
/* Checks whether an Object has a keyframe for a given frame. */
|
||||
static bool object_frame_has_keyframe(Object *ob, const float frame)
|
||||
{
|
||||
if (ob == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check its own animation data - specifically, the action it contains. */
|
||||
if ((ob->adt) && (ob->adt->action)) {
|
||||
/* #41525 - When the active action is a NLA strip being edited,
|
||||
* we need to correct the frame number to "look inside" the
|
||||
* remapped action
|
||||
*/
|
||||
const float ob_frame = BKE_nla_tweakedit_remap(ob->adt, frame, NLATIME_CONVERT_UNMAP);
|
||||
|
||||
if (assigned_action_has_keyframe_at(*ob->adt, ob_frame)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/* nothing found */
|
||||
return false;
|
||||
}
|
||||
|
||||
bool id_frame_has_keyframe(ID *id, float frame)
|
||||
{
|
||||
if (id == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Perform special checks for 'macro' types. */
|
||||
switch (GS(id->name)) {
|
||||
case ID_OB:
|
||||
return object_frame_has_keyframe(id_cast<Object *>(id), frame);
|
||||
|
||||
default: {
|
||||
AnimData *adt = BKE_animdata_from_id(id);
|
||||
|
||||
/* only check keyframes in active action */
|
||||
if (adt) {
|
||||
return assigned_action_has_keyframe_at(*adt, frame);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool key_insertion_may_create_fcurve(const eInsertKeyFlags insert_key_flags)
|
||||
{
|
||||
return (insert_key_flags & (INSERTKEY_REPLACE | INSERTKEY_AVAILABLE)) == 0;
|
||||
}
|
||||
|
||||
Vector<float> get_property_values(PointerRNA *ptr, PropertyRNA *prop, const bool visual_key)
|
||||
{
|
||||
Vector<float> values;
|
||||
|
||||
if (visual_key && visualkey_can_use(ptr, prop)) {
|
||||
/* Visual-keying is only available for object data-blocks and pose-channels,
|
||||
* as it works by key-framing using a value extracted from the final matrix
|
||||
* instead of using the kt system to extract a value. */
|
||||
values = visualkey_get_values(ptr, prop);
|
||||
}
|
||||
else {
|
||||
values = get_rna_values(ptr, prop);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
static float nla_time_remap(float time,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
PointerRNA *id_ptr,
|
||||
AnimData *adt,
|
||||
bAction *act,
|
||||
ListBaseT<NlaKeyframingContext> *nla_cache,
|
||||
NlaKeyframingContext **r_nla_context)
|
||||
{
|
||||
if (adt && adt->action == act) {
|
||||
*r_nla_context = BKE_animsys_get_nla_keyframing_context(
|
||||
nla_cache, id_ptr, adt, anim_eval_context);
|
||||
|
||||
const float remapped_frame = BKE_nla_tweakedit_remap(adt, time, NLATIME_CONVERT_UNMAP);
|
||||
return remapped_frame;
|
||||
}
|
||||
|
||||
*r_nla_context = nullptr;
|
||||
return time;
|
||||
}
|
||||
|
||||
SingleKeyingResult insert_keyframe_direct(PointerRNA &ptr,
|
||||
PropertyRNA &prop,
|
||||
FCurve &fcu,
|
||||
const float fcurve_frame,
|
||||
const eBezTriple_KeyframeType keytype,
|
||||
const eInsertKeyFlags flag)
|
||||
{
|
||||
if ((ptr.owner_id == nullptr) && (ptr.data == nullptr)) {
|
||||
BLI_assert_unreachable();
|
||||
return SingleKeyingResult::UNKNOWN_FAILURE;
|
||||
}
|
||||
|
||||
if (!BKE_fcurve_is_keyframable(fcu)) {
|
||||
return SingleKeyingResult::FCURVE_NOT_KEYFRAMEABLE;
|
||||
}
|
||||
|
||||
/* Update F-Curve flags to ensure proper behavior for property type. */
|
||||
update_autoflags_fcurve_direct(&fcu, RNA_property_type(&prop));
|
||||
|
||||
const bool visual_keyframing = flag & INSERTKEY_MATRIX;
|
||||
Vector<float> values = get_property_values(&ptr, &prop, visual_keyframing);
|
||||
|
||||
const int index = fcu.array_index;
|
||||
if (index < 0 || index >= values.size()) {
|
||||
/* Can only happen if the FCurve and PropertyRNA do not match which
|
||||
* should never be the case. */
|
||||
BLI_assert_unreachable();
|
||||
return SingleKeyingResult::UNKNOWN_FAILURE;
|
||||
}
|
||||
|
||||
KeyframeSettings settings = get_keyframe_settings((flag & INSERTKEY_NO_USERPREF) == 0);
|
||||
settings.keyframe_type = keytype;
|
||||
|
||||
return insert_vert_fcurve(&fcu, {fcurve_frame, values[index]}, settings, flag);
|
||||
}
|
||||
|
||||
/* ************************************************** */
|
||||
/* KEYFRAME DELETION */
|
||||
|
||||
/* Main Keyframing API call:
|
||||
* Use this when validation of necessary animation data isn't necessary as it
|
||||
* already exists. It will delete a keyframe at the current frame.
|
||||
*
|
||||
* The flag argument is used for special settings that alter the behavior of
|
||||
* the keyframe deletion. These include the quick refresh options.
|
||||
*/
|
||||
|
||||
static void deg_tag_after_keyframe_delete(Main *bmain, ID *id, AnimData *adt)
|
||||
{
|
||||
if (adt->action == nullptr) {
|
||||
/* In the case last f-curve was removed need to inform dependency graph
|
||||
* about relations update, since it needs to get rid of animation operation
|
||||
* for this data-block. */
|
||||
DEG_id_tag_update_ex(bmain, id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
DEG_relations_tag_update(bmain);
|
||||
}
|
||||
else {
|
||||
DEG_id_tag_update_ex(bmain, &adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
}
|
||||
}
|
||||
|
||||
int delete_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_path, float cfra)
|
||||
{
|
||||
AnimData *adt = BKE_animdata_from_id(id);
|
||||
|
||||
if (ELEM(nullptr, id, adt)) {
|
||||
BKE_report(reports, RPT_ERROR, "No ID block and/or AnimData to delete keyframe from");
|
||||
return 0;
|
||||
}
|
||||
|
||||
PointerRNA ptr;
|
||||
PropertyRNA *prop;
|
||||
PointerRNA id_ptr = RNA_id_pointer_create(id);
|
||||
if (RNA_path_resolve_property(&id_ptr, rna_path.path.c_str(), &ptr, &prop) == false) {
|
||||
BKE_reportf(
|
||||
reports,
|
||||
RPT_ERROR,
|
||||
"Could not delete keyframe, as RNA path is invalid for the given ID (ID = %s, path = %s)",
|
||||
id->name,
|
||||
rna_path.path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!adt->action) {
|
||||
BKE_reportf(reports, RPT_ERROR, "No action to delete keyframes from for ID = %s", id->name);
|
||||
return 0;
|
||||
}
|
||||
bAction *act = adt->action;
|
||||
cfra = BKE_nla_tweakedit_remap(adt, cfra, NLATIME_CONVERT_UNMAP);
|
||||
int array_index = rna_path.index.value_or(0);
|
||||
int array_index_max = array_index + 1;
|
||||
|
||||
if (!rna_path.index.has_value()) {
|
||||
array_index_max = RNA_property_array_length(&ptr, prop);
|
||||
/* For single properties, increase max_index so that the property itself gets included,
|
||||
* but don't do this for standard arrays since that can cause corruption issues
|
||||
* (extra unused curves).
|
||||
*/
|
||||
if (array_index_max == array_index) {
|
||||
array_index_max++;
|
||||
}
|
||||
}
|
||||
|
||||
Action &action = act->wrap();
|
||||
Vector<FCurve *> modified_fcurves;
|
||||
/* Just being defensive in the face of the NLA shenanigans above. This
|
||||
* probably isn't necessary, but it doesn't hurt. */
|
||||
BLI_assert(adt->action == act && action.slot_for_handle(adt->slot_handle) != nullptr);
|
||||
|
||||
Span<FCurve *> fcurves = fcurves_for_action_slot(action, adt->slot_handle);
|
||||
/* This loop's clause is copied from the pre-existing code for legacy
|
||||
* actions below, to ensure behavioral consistency between the two code
|
||||
* paths. In the future when legacy actions are removed, we can restructure
|
||||
* it to be clearer. */
|
||||
for (; array_index < array_index_max; array_index++) {
|
||||
FCurve *fcurve = fcurve_find(fcurves, {rna_path.path, array_index});
|
||||
if (fcurve == nullptr) {
|
||||
continue;
|
||||
}
|
||||
if (fcurve_delete_keyframe_at_time(fcurve, cfra)) {
|
||||
modified_fcurves.append(fcurve);
|
||||
}
|
||||
}
|
||||
|
||||
if (!modified_fcurves.is_empty()) {
|
||||
for (FCurve *fcurve : modified_fcurves) {
|
||||
if (BKE_fcurve_is_empty(fcurve)) {
|
||||
animdata_fcurve_delete(adt, fcurve);
|
||||
}
|
||||
}
|
||||
deg_tag_after_keyframe_delete(bmain, id, adt);
|
||||
}
|
||||
|
||||
return modified_fcurves.size();
|
||||
}
|
||||
|
||||
/* ************************************************** */
|
||||
/* KEYFRAME CLEAR */
|
||||
|
||||
int clear_keyframe(Main *bmain, ReportList *reports, ID *id, const RNAPath &rna_path)
|
||||
{
|
||||
AnimData *adt = BKE_animdata_from_id(id);
|
||||
|
||||
if (ELEM(nullptr, id, adt)) {
|
||||
BKE_report(reports, RPT_ERROR, "No ID block and/or AnimData to delete keyframe from");
|
||||
return 0;
|
||||
}
|
||||
|
||||
PointerRNA ptr;
|
||||
PropertyRNA *prop;
|
||||
PointerRNA id_ptr = RNA_id_pointer_create(id);
|
||||
if (RNA_path_resolve_property(&id_ptr, rna_path.path.c_str(), &ptr, &prop) == false) {
|
||||
BKE_reportf(
|
||||
reports,
|
||||
RPT_ERROR,
|
||||
"Could not clear keyframe, as RNA path is invalid for the given ID (ID = %s, path = %s)",
|
||||
id->name,
|
||||
rna_path.path.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!adt->action) {
|
||||
BKE_reportf(reports, RPT_ERROR, "No action to delete keyframes from for ID = %s", id->name);
|
||||
return 0;
|
||||
}
|
||||
bAction *act = adt->action;
|
||||
|
||||
Action &action = act->wrap();
|
||||
int key_count = 0;
|
||||
|
||||
if (adt->slot_handle) {
|
||||
Vector<FCurve *> fcurves;
|
||||
foreach_fcurve_in_action_slot_editable(action, adt->slot_handle, [&](FCurve &fcurve) {
|
||||
if (rna_path.index.has_value() && rna_path.index.value() != fcurve.array_index) {
|
||||
return;
|
||||
}
|
||||
if (rna_path.path != fcurve.rna_path) {
|
||||
return;
|
||||
}
|
||||
fcurves.append(&fcurve);
|
||||
});
|
||||
|
||||
for (FCurve *fcu : fcurves) {
|
||||
if (action_fcurve_remove(action, *fcu)) {
|
||||
key_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (key_count) {
|
||||
deg_tag_after_keyframe_delete(bmain, id, adt);
|
||||
}
|
||||
|
||||
return key_count;
|
||||
}
|
||||
|
||||
struct KeyInsertData {
|
||||
float2 position;
|
||||
int array_index;
|
||||
};
|
||||
|
||||
static SingleKeyingResult insert_key_layer(Main *bmain,
|
||||
Action &action,
|
||||
Layer &layer,
|
||||
const Slot &slot,
|
||||
const std::string &rna_path,
|
||||
PropertyRNA *prop,
|
||||
const std::optional<StringRefNull> channel_group,
|
||||
const KeyInsertData &key_data,
|
||||
const KeyframeSettings &key_settings,
|
||||
const eInsertKeyFlags insert_key_flags)
|
||||
{
|
||||
assert_baklava_phase_1_invariants(layer);
|
||||
BLI_assert(layer.strips().size() == 1);
|
||||
|
||||
const bool do_cyclic = (insert_key_flags & INSERTKEY_CYCLE_AWARE) && action.is_cyclic();
|
||||
|
||||
const PropertyType prop_type = RNA_property_type(prop);
|
||||
const PropertySubType prop_subtype = RNA_property_subtype(prop);
|
||||
|
||||
Strip *strip = layer.strip(0);
|
||||
return strip->data<StripKeyframeData>(action).keyframe_insert(
|
||||
bmain,
|
||||
slot,
|
||||
{rna_path, key_data.array_index, prop_type, prop_subtype, channel_group},
|
||||
key_data.position,
|
||||
key_settings,
|
||||
insert_key_flags,
|
||||
do_cyclic ? std::optional(action.get_frame_range()) : std::nullopt);
|
||||
}
|
||||
|
||||
static std::pair<Layer *, Slot *> prep_action_layer_for_keying(Action &action, ID &animated_id)
|
||||
{
|
||||
BLI_assert_msg(
|
||||
ELEM(get_action(animated_id), &action, nullptr),
|
||||
"The animated ID should not be using another Action than the one passed to this function");
|
||||
|
||||
Slot *slot = assign_action_ensure_slot_for_keying(action, animated_id);
|
||||
BLI_assert_msg(
|
||||
slot,
|
||||
"The conditions that would cause this Slot assignment to fail (such as the ID not being "
|
||||
"animatible) should have been caught and handled by higher-level functions.");
|
||||
|
||||
action.layer_keystrip_ensure();
|
||||
|
||||
/* TODO: we currently assume this will always successfully find a layer.
|
||||
* However, that may not be true in the future when we implement features like
|
||||
* layer locking: if layers already exist, but they are all locked, then the
|
||||
* default layer won't be added by the line above, but there also won't be any
|
||||
* layers we can insert keys into. */
|
||||
Layer *layer = action.get_layer_for_keyframing();
|
||||
BLI_assert(layer != nullptr);
|
||||
|
||||
return std::make_pair(layer, slot);
|
||||
}
|
||||
|
||||
static CombinedKeyingResult insert_key_layered_action(
|
||||
Main *bmain,
|
||||
Action &action,
|
||||
Layer &layer,
|
||||
const Slot &slot,
|
||||
PropertyRNA *prop,
|
||||
const std::optional<StringRefNull> channel_group,
|
||||
const std::string &rna_path,
|
||||
const float frame,
|
||||
const Span<float> values,
|
||||
const eInsertKeyFlags insert_key_flags,
|
||||
const KeyframeSettings &key_settings,
|
||||
const BitSpan keying_mask)
|
||||
{
|
||||
BLI_assert(bmain != nullptr);
|
||||
|
||||
int property_array_index = 0;
|
||||
CombinedKeyingResult combined_result;
|
||||
for (float value : values) {
|
||||
if (!keying_mask[property_array_index]) {
|
||||
combined_result.add(SingleKeyingResult::UNABLE_TO_INSERT_TO_NLA_STACK);
|
||||
property_array_index++;
|
||||
continue;
|
||||
}
|
||||
const KeyInsertData key_data = {{frame, value}, property_array_index};
|
||||
const SingleKeyingResult result = insert_key_layer(bmain,
|
||||
action,
|
||||
layer,
|
||||
slot,
|
||||
rna_path,
|
||||
prop,
|
||||
channel_group,
|
||||
key_data,
|
||||
key_settings,
|
||||
insert_key_flags);
|
||||
|
||||
combined_result.add(result);
|
||||
property_array_index++;
|
||||
}
|
||||
return combined_result;
|
||||
}
|
||||
|
||||
CombinedKeyingResult insert_keyframes(Main *bmain,
|
||||
PointerRNA *struct_pointer,
|
||||
const std::optional<StringRefNull> channel_group,
|
||||
const Span<RNAPath> rna_paths,
|
||||
const std::optional<float> scene_frame,
|
||||
const AnimationEvalContext &anim_eval_context,
|
||||
const eBezTriple_KeyframeType key_type,
|
||||
const eInsertKeyFlags insert_key_flags)
|
||||
|
||||
{
|
||||
ID *id = struct_pointer->owner_id;
|
||||
PointerRNA id_pointer = RNA_id_pointer_create(id);
|
||||
CombinedKeyingResult combined_result;
|
||||
|
||||
/* Init animdata if none available yet. */
|
||||
AnimData *adt = BKE_animdata_ensure_id(id);
|
||||
if (adt == nullptr) {
|
||||
combined_result.add(SingleKeyingResult::ID_NOT_ANIMATABLE);
|
||||
return combined_result;
|
||||
}
|
||||
|
||||
if ((adt->action == nullptr) && (insert_key_flags & INSERTKEY_AVAILABLE)) {
|
||||
combined_result.add(SingleKeyingResult::CANNOT_CREATE_FCURVE, rna_paths.size());
|
||||
return combined_result;
|
||||
}
|
||||
|
||||
if (const bAction *action = adt->action) {
|
||||
if (ID_IS_LINKED(action) || ID_IS_OVERRIDE_LIBRARY(action)) {
|
||||
combined_result.add(SingleKeyingResult::ID_NOT_EDITABLE, rna_paths.size());
|
||||
return combined_result;
|
||||
}
|
||||
}
|
||||
|
||||
bAction *dna_action = id_action_ensure(bmain, id);
|
||||
BLI_assert(dna_action != nullptr);
|
||||
Action &action = dna_action->wrap();
|
||||
|
||||
KeyframeSettings key_settings = get_keyframe_settings(
|
||||
(insert_key_flags & INSERTKEY_NO_USERPREF) == 0);
|
||||
key_settings.keyframe_type = key_type;
|
||||
|
||||
/* NOTE: keyframing functions can deal with the nla_context being a nullptr. */
|
||||
ListBaseT<NlaKeyframingContext> nla_cache = {nullptr, nullptr};
|
||||
NlaKeyframingContext *nla_context = nullptr;
|
||||
const float nla_frame = nla_time_remap(scene_frame.value_or(anim_eval_context.eval_time),
|
||||
&anim_eval_context,
|
||||
&id_pointer,
|
||||
adt,
|
||||
dna_action,
|
||||
&nla_cache,
|
||||
&nla_context);
|
||||
const bool visual_keyframing = insert_key_flags & INSERTKEY_MATRIX;
|
||||
|
||||
auto [layer, slot] = prep_action_layer_for_keying(action, *struct_pointer->owner_id);
|
||||
for (const RNAPath &rna_path : rna_paths) {
|
||||
PointerRNA ptr;
|
||||
PropertyRNA *prop = nullptr;
|
||||
const bool path_resolved = RNA_path_resolve_property(
|
||||
struct_pointer, rna_path.path.c_str(), &ptr, &prop);
|
||||
if (!path_resolved) {
|
||||
combined_result.add(SingleKeyingResult::CANNOT_RESOLVE_PATH);
|
||||
continue;
|
||||
}
|
||||
|
||||
Vector<float> rna_values = get_property_values(&ptr, prop, visual_keyframing);
|
||||
BitVector<> rna_values_mask(rna_values.size(), false);
|
||||
bool force_all;
|
||||
|
||||
/* NOTE: this function call is complex with interesting/non-obvious effects.
|
||||
* Please see its documentation for details. */
|
||||
BKE_animsys_nla_remap_keyframe_values(nla_context,
|
||||
&ptr,
|
||||
prop,
|
||||
rna_values.as_mutable_span(),
|
||||
rna_path.index.value_or(-1),
|
||||
&anim_eval_context,
|
||||
&force_all,
|
||||
rna_values_mask);
|
||||
|
||||
std::optional<std::string> rna_path_id_to_prop = RNA_path_from_ID_to_property(&ptr, prop);
|
||||
if (!rna_path_id_to_prop.has_value()) {
|
||||
/* In the case of nested RNA properties the path cannot be reconstructed in all cases. There
|
||||
* may be a system in place in the future, see #122427. */
|
||||
if (struct_pointer->data != id) {
|
||||
continue;
|
||||
}
|
||||
/* However if the struct pointer happens to be an ID pointer we can use the path that was
|
||||
* passed in. This fixes issues like #132195. */
|
||||
rna_path_id_to_prop = rna_path.path;
|
||||
}
|
||||
|
||||
/* Handle the `force_all` condition mentioned above, ensuring the
|
||||
* "all-or-nothing" behavior if needed.
|
||||
*
|
||||
* TODO: this currently doesn't account for the "Only Insert Available"
|
||||
* flag, which also needs to be accounted for to actually ensure
|
||||
* all-or-nothing behavior. This is because the function this part of the
|
||||
* code originally came from (see #122053) also didn't account for it.
|
||||
* Presumably that was an oversight, and should be addressed. But for now
|
||||
* we're faithfully reproducing the original behavior.
|
||||
*/
|
||||
eInsertKeyFlags insert_key_flags_adjusted = insert_key_flags;
|
||||
if (force_all && (insert_key_flags & (INSERTKEY_REPLACE | INSERTKEY_AVAILABLE))) {
|
||||
/* Determine if at least one element would succeed getting keyed. */
|
||||
bool at_least_one_would_succeed = false;
|
||||
for (int i = 0; i < rna_values.size(); i++) {
|
||||
const FCurve *fcu = fcurve_find_in_action(dna_action, {*rna_path_id_to_prop, i});
|
||||
if (!fcu) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* We found an fcurve, and "Only Replace" is not on, so a key insertion
|
||||
* would succeed according to the two flags we're accounting for. */
|
||||
if (!(insert_key_flags & INSERTKEY_REPLACE)) {
|
||||
at_least_one_would_succeed = true;
|
||||
break;
|
||||
}
|
||||
|
||||
/* "Only Replace" *is* on, so a key insertion would succeed only if we
|
||||
* actually replace an existing keyframe. */
|
||||
bool replace;
|
||||
BKE_fcurve_bezt_binarysearch_index(fcu->bezt, nla_frame, fcu->totvert, &replace);
|
||||
if (replace) {
|
||||
at_least_one_would_succeed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* If at least one would succeed, then we disable all keying flags that
|
||||
* would prevent the other elements from getting keyed as well. */
|
||||
if (at_least_one_would_succeed) {
|
||||
insert_key_flags_adjusted &= ~(INSERTKEY_REPLACE | INSERTKEY_AVAILABLE);
|
||||
}
|
||||
}
|
||||
|
||||
CombinedKeyingResult result;
|
||||
|
||||
const std::optional<StringRefNull> this_rna_path_channel_group =
|
||||
channel_group.has_value() ? *channel_group :
|
||||
default_channel_group_for_path(&ptr, *rna_path_id_to_prop);
|
||||
|
||||
result = insert_key_layered_action(bmain,
|
||||
action,
|
||||
*layer,
|
||||
*slot,
|
||||
prop,
|
||||
this_rna_path_channel_group,
|
||||
*rna_path_id_to_prop,
|
||||
nla_frame,
|
||||
rna_values,
|
||||
insert_key_flags,
|
||||
key_settings,
|
||||
rna_values_mask);
|
||||
|
||||
combined_result.merge(result);
|
||||
}
|
||||
|
||||
BKE_animsys_free_nla_keyframing_context_cache(&nla_cache);
|
||||
|
||||
if (combined_result.get_count(SingleKeyingResult::SUCCESS) > 0) {
|
||||
/* NOTE: this is NOT using ID_RECALC_ANIMATION on purpose, because that would be quite annoying
|
||||
* in the following case:
|
||||
*
|
||||
* - Key Cube's loc/rot/scale.
|
||||
* - Go to another frame.
|
||||
* - Translate, rotate, and scale the cube.
|
||||
* - Hover over the loc/rot/scale properties and one by one press 'I' to
|
||||
* insert a key there.
|
||||
*
|
||||
* If ID_RECALC_ANIMATION were used, keying the location would immediately cause a flush of the
|
||||
* animation data, popping the rotation and scale back to their animated values. */
|
||||
DEG_id_tag_update(&dna_action->id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
|
||||
/* TODO: it's not entirely clear why the action we got wouldn't be the same
|
||||
* as the action in AnimData. Further, it's not clear why it would need to
|
||||
* be tagged for a depsgraph update regardless. This code is here because it
|
||||
* was part of the function this one was refactored from, but at some point
|
||||
* this should be investigated and either documented or removed. */
|
||||
if (!ELEM(adt->action, nullptr, dna_action)) {
|
||||
DEG_id_tag_update(&adt->action->id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
}
|
||||
}
|
||||
|
||||
return combined_result;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
353
blender-5.2.0/source/blender/animrig/intern/keyframing_auto.cc
Normal file
353
blender-5.2.0/source/blender/animrig/intern/keyframing_auto.cc
Normal file
@@ -0,0 +1,353 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_fcurve.hh"
|
||||
#include "BKE_scene.hh"
|
||||
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_path.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "ANIM_keyframing.hh"
|
||||
#include "ANIM_keyingsets.hh"
|
||||
|
||||
#include "WM_api.hh"
|
||||
#include "WM_types.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
static eInsertKeyFlags get_autokey_flags(const Scene *scene)
|
||||
{
|
||||
eInsertKeyFlags flag = INSERTKEY_NOFLAGS;
|
||||
|
||||
/* Visual keying. */
|
||||
if (is_keying_flag(scene, KEYING_FLAG_VISUALKEY)) {
|
||||
flag |= INSERTKEY_MATRIX;
|
||||
}
|
||||
|
||||
/* Only needed. */
|
||||
if (is_keying_flag(scene, AUTOKEY_FLAG_INSERTNEEDED)) {
|
||||
flag |= INSERTKEY_NEEDED;
|
||||
}
|
||||
|
||||
/* Only insert available. */
|
||||
if (is_keying_flag(scene, AUTOKEY_FLAG_INSERTAVAILABLE)) {
|
||||
flag |= INSERTKEY_AVAILABLE;
|
||||
}
|
||||
|
||||
/* Keyframing mode - only replace existing keyframes. */
|
||||
if (is_autokey_mode(scene, AUTOKEY_MODE_EDITKEYS)) {
|
||||
flag |= INSERTKEY_REPLACE;
|
||||
}
|
||||
|
||||
/* Cycle-aware keyframe insertion - preserve cycle period and flow. */
|
||||
if (is_keying_flag(scene, KEYING_FLAG_CYCLEAWARE)) {
|
||||
flag |= INSERTKEY_CYCLE_AWARE;
|
||||
}
|
||||
|
||||
return flag;
|
||||
}
|
||||
|
||||
bool is_autokey_on(const Scene *scene)
|
||||
{
|
||||
if (scene) {
|
||||
return scene->toolsettings->autokey_mode & AUTOKEY_ON;
|
||||
}
|
||||
return U.autokey_mode & AUTOKEY_ON;
|
||||
}
|
||||
|
||||
bool is_autokey_mode(const Scene *scene, const eAutokey_Mode mode)
|
||||
{
|
||||
if (scene) {
|
||||
return scene->toolsettings->autokey_mode == mode;
|
||||
}
|
||||
return U.autokey_mode == mode;
|
||||
}
|
||||
|
||||
bool autokeyframe_cfra_can_key(const Scene *scene, ID *id)
|
||||
{
|
||||
/* Only filter if auto-key mode requires this. */
|
||||
if (!is_autokey_on(scene)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (is_autokey_mode(scene, AUTOKEY_MODE_EDITKEYS)) {
|
||||
/* Replace Mode:
|
||||
* For whole block, only key if there's a keyframe on that frame already
|
||||
* This is a valid assumption when we're blocking + tweaking
|
||||
*/
|
||||
const float cfra = BKE_scene_frame_get(scene);
|
||||
return id_frame_has_keyframe(id, cfra);
|
||||
}
|
||||
|
||||
/* Normal Mode (or treat as being normal mode):
|
||||
*
|
||||
* Just in case the flags aren't set properly (i.e. only on/off is set, without a mode)
|
||||
* let's set the "normal" flag too, so that it will all be sane everywhere...
|
||||
*/
|
||||
scene->toolsettings->autokey_mode = AUTOKEY_MODE_NORMAL;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void autokeyframe_object(bContext *C, const Scene *scene, Object *ob, Span<RNAPath> rna_paths)
|
||||
{
|
||||
BLI_assert(ob != nullptr);
|
||||
BLI_assert(scene != nullptr);
|
||||
BLI_assert(C != nullptr);
|
||||
|
||||
ID *id = &ob->id;
|
||||
if (!autokeyframe_cfra_can_key(scene, id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReportList *reports = CTX_wm_reports(C);
|
||||
KeyingSet *active_ks = scene_get_active_keyingset(scene);
|
||||
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
|
||||
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(
|
||||
depsgraph, BKE_scene_frame_get(scene));
|
||||
|
||||
/* Get flags used for inserting keyframes. */
|
||||
const eInsertKeyFlags flag = get_autokey_flags(scene);
|
||||
|
||||
/* Add data-source override for the object. */
|
||||
Vector<PointerRNA> sources;
|
||||
relative_keyingset_add_source(sources, id);
|
||||
|
||||
if (is_keying_flag(scene, AUTOKEY_FLAG_ONLYKEYINGSET) && (active_ks)) {
|
||||
/* Only insert into active keyingset
|
||||
* NOTE: we assume here that the active Keying Set
|
||||
* does not need to have its iterator overridden.
|
||||
*/
|
||||
apply_keyingset(C, &sources, active_ks, ModifyKeyMode::INSERT, anim_eval_context.eval_time);
|
||||
return;
|
||||
}
|
||||
|
||||
const float scene_frame = BKE_scene_frame_get(scene);
|
||||
Main *bmain = CTX_data_main(C);
|
||||
|
||||
CombinedKeyingResult combined_result;
|
||||
for (PointerRNA ptr : sources) {
|
||||
const CombinedKeyingResult result = insert_keyframes(
|
||||
bmain,
|
||||
&ptr,
|
||||
std::nullopt,
|
||||
rna_paths,
|
||||
scene_frame,
|
||||
anim_eval_context,
|
||||
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
|
||||
flag);
|
||||
combined_result.merge(result);
|
||||
}
|
||||
|
||||
if (combined_result.get_count(SingleKeyingResult::SUCCESS) == 0) {
|
||||
combined_result.generate_reports(reports);
|
||||
}
|
||||
}
|
||||
|
||||
bool autokeyframe_object(bContext *C, Scene *scene, Object *ob, KeyingSet *ks)
|
||||
{
|
||||
if (!autokeyframe_cfra_can_key(scene, &ob->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Now insert the key-frame(s) using the Keying Set:
|
||||
* 1) Add data-source override for the Object.
|
||||
* 2) Insert key-frames.
|
||||
* 3) Free the extra info.
|
||||
*/
|
||||
Vector<PointerRNA> sources;
|
||||
relative_keyingset_add_source(sources, &ob->id);
|
||||
apply_keyingset(C, &sources, ks, ModifyKeyMode::INSERT, BKE_scene_frame_get(scene));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool autokeyframe_pchan(bContext *C, Scene *scene, Object *ob, bPoseChannel *pchan, KeyingSet *ks)
|
||||
{
|
||||
if (!autokeyframe_cfra_can_key(scene, &ob->id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Now insert the keyframe(s) using the Keying Set:
|
||||
* 1) Add data-source override for the pose-channel.
|
||||
* 2) Insert key-frames.
|
||||
* 3) Free the extra info.
|
||||
*/
|
||||
Vector<PointerRNA> sources;
|
||||
relative_keyingset_add_source(sources, &ob->id, RNA_PoseBone, pchan);
|
||||
apply_keyingset(C, &sources, ks, ModifyKeyMode::INSERT, BKE_scene_frame_get(scene));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void autokeyframe_pose_channel(bContext *C,
|
||||
Scene *scene,
|
||||
Object *ob,
|
||||
bPoseChannel *pose_channel,
|
||||
Span<RNAPath> rna_paths,
|
||||
short targetless_ik)
|
||||
{
|
||||
BLI_assert(C != nullptr);
|
||||
BLI_assert(scene != nullptr);
|
||||
BLI_assert(ob != nullptr);
|
||||
BLI_assert(pose_channel != nullptr);
|
||||
|
||||
Main *bmain = CTX_data_main(C);
|
||||
ID *id = &ob->id;
|
||||
|
||||
if (!animrig::autokeyframe_cfra_can_key(scene, id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReportList *reports = CTX_wm_reports(C);
|
||||
KeyingSet *active_ks = scene_get_active_keyingset(scene);
|
||||
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
|
||||
const float scene_frame = BKE_scene_frame_get(scene);
|
||||
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
|
||||
scene_frame);
|
||||
|
||||
/* flag is initialized from UserPref keyframing settings
|
||||
* - special exception for targetless IK - INSERTKEY_MATRIX keyframes should get
|
||||
* visual keyframes even if flag not set, as it's not that useful otherwise
|
||||
* (for quick animation recording)
|
||||
*/
|
||||
eInsertKeyFlags flag = get_autokey_flags(scene);
|
||||
|
||||
if (targetless_ik) {
|
||||
flag |= INSERTKEY_MATRIX;
|
||||
}
|
||||
|
||||
Vector<PointerRNA> sources;
|
||||
/* Add data-source override for the camera object. */
|
||||
relative_keyingset_add_source(sources, id, RNA_PoseBone, pose_channel);
|
||||
|
||||
/* only insert into active keyingset? */
|
||||
if (is_keying_flag(scene, AUTOKEY_FLAG_ONLYKEYINGSET) && (active_ks)) {
|
||||
/* Run the active Keying Set on the current data-source. */
|
||||
apply_keyingset(C, &sources, active_ks, ModifyKeyMode::INSERT, anim_eval_context.eval_time);
|
||||
return;
|
||||
}
|
||||
|
||||
CombinedKeyingResult combined_result;
|
||||
for (PointerRNA &ptr : sources) {
|
||||
const CombinedKeyingResult result = insert_keyframes(
|
||||
bmain,
|
||||
&ptr,
|
||||
std::nullopt,
|
||||
rna_paths,
|
||||
scene_frame,
|
||||
anim_eval_context,
|
||||
eBezTriple_KeyframeType(scene->toolsettings->keyframe_type),
|
||||
flag);
|
||||
combined_result.merge(result);
|
||||
}
|
||||
|
||||
if (combined_result.get_count(SingleKeyingResult::SUCCESS) == 0) {
|
||||
combined_result.generate_reports(reports);
|
||||
}
|
||||
}
|
||||
|
||||
bool autokeyframe_property(bContext *C,
|
||||
Scene *scene,
|
||||
PointerRNA *ptr,
|
||||
PropertyRNA *prop,
|
||||
const int rnaindex,
|
||||
const float cfra,
|
||||
const bool only_if_property_keyed)
|
||||
{
|
||||
|
||||
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
|
||||
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
|
||||
cfra);
|
||||
bAction *action;
|
||||
bool driven;
|
||||
bool special;
|
||||
|
||||
/* For entire array buttons we check the first component, it's not perfect
|
||||
* but works well enough in typical cases. */
|
||||
const int rnaindex_check = (rnaindex == -1) ? 0 : rnaindex;
|
||||
FCurve *fcu = BKE_fcurve_find_by_rna_context_ui(
|
||||
C, ptr, prop, rnaindex_check, nullptr, &action, &driven, &special);
|
||||
|
||||
/* Only early out when we actually want an existing F-curve already
|
||||
* (e.g. auto-keyframing from buttons). */
|
||||
if (fcu == nullptr && (driven || special || only_if_property_keyed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (driven) {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool changed = false;
|
||||
if (special) {
|
||||
/* NLA Strip property. */
|
||||
if (is_autokey_on(scene)) {
|
||||
ReportList *reports = CTX_wm_reports(C);
|
||||
ToolSettings *ts = scene->toolsettings;
|
||||
|
||||
const SingleKeyingResult result = insert_keyframe_direct(
|
||||
*ptr,
|
||||
*prop,
|
||||
*fcu,
|
||||
anim_eval_context.eval_time,
|
||||
eBezTriple_KeyframeType(ts->keyframe_type),
|
||||
eInsertKeyFlags(0));
|
||||
changed = result == SingleKeyingResult::SUCCESS;
|
||||
if (result != SingleKeyingResult::SUCCESS) {
|
||||
generate_single_keying_result_report(result, reports);
|
||||
}
|
||||
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr);
|
||||
}
|
||||
}
|
||||
else {
|
||||
ID *id = ptr->owner_id;
|
||||
Main *bmain = CTX_data_main(C);
|
||||
|
||||
/* TODO: this should probably respect the keyingset only option for anim */
|
||||
if (autokeyframe_cfra_can_key(scene, id)) {
|
||||
ToolSettings *ts = scene->toolsettings;
|
||||
const eInsertKeyFlags flag = get_autokey_flags(scene);
|
||||
|
||||
if (only_if_property_keyed) {
|
||||
/* NOTE: We use rnaindex instead of fcu->array_index,
|
||||
* because a button may control all items of an array at once.
|
||||
* E.g., color wheels (see #42567). */
|
||||
BLI_assert((fcu->array_index == rnaindex) || (rnaindex == -1));
|
||||
}
|
||||
|
||||
const std::optional<std::string> group = (fcu && fcu->grp) ? std::optional(fcu->grp->name) :
|
||||
std::nullopt;
|
||||
const std::string path = fcu ? fcu->rna_path :
|
||||
RNA_path_from_ID_to_property(ptr, prop).value_or("");
|
||||
/* NOTE: `rnaindex == -1` is a magic number, meaning either "operate on
|
||||
* all elements" or "not an array property". */
|
||||
const std::optional<int> array_index = rnaindex < 0 ? std::nullopt : std::optional(rnaindex);
|
||||
|
||||
PointerRNA id_pointer = RNA_id_pointer_create(ptr->owner_id);
|
||||
CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&id_pointer,
|
||||
group,
|
||||
{{path, {}, array_index}},
|
||||
std::nullopt,
|
||||
anim_eval_context,
|
||||
eBezTriple_KeyframeType(ts->keyframe_type),
|
||||
flag);
|
||||
changed = result.get_count(SingleKeyingResult::SUCCESS) != 0;
|
||||
WM_event_add_notifier(C, NC_ANIMATION | ND_KEYFRAME | NA_EDITED, nullptr);
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
973
blender-5.2.0/source/blender/animrig/intern/keyframing_test.cc
Normal file
973
blender-5.2.0/source/blender/animrig/intern/keyframing_test.cc
Normal file
@@ -0,0 +1,973 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_keyframing.hh"
|
||||
#include "ANIM_nla.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_animsys.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_material.hh"
|
||||
#include "BKE_mesh.hh"
|
||||
#include "BKE_nla.hh"
|
||||
#include "BKE_object.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_material_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::tests {
|
||||
class KeyframingTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
Main *bmain;
|
||||
|
||||
/* For standard single-action testing. */
|
||||
Object *object;
|
||||
PointerRNA object_rna_pointer;
|
||||
|
||||
/* For pose bone single-action testing. */
|
||||
Object *armature_object;
|
||||
bArmature *armature;
|
||||
PointerRNA armature_object_rna_pointer;
|
||||
|
||||
/* For NLA testing. */
|
||||
Object *object_with_nla;
|
||||
PointerRNA object_with_nla_rna_pointer;
|
||||
bAction *nla_action;
|
||||
|
||||
/* For action reuse testing. */
|
||||
Object *cube;
|
||||
PointerRNA cube_rna_pointer;
|
||||
Mesh *cube_mesh;
|
||||
PointerRNA cube_mesh_rna_pointer;
|
||||
Material *material;
|
||||
PointerRNA material_rna_pointer;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
bmain = BKE_main_new();
|
||||
|
||||
object = BKE_object_add_only_object(bmain, OB_EMPTY, "Empty");
|
||||
object_rna_pointer = RNA_id_pointer_create(&object->id);
|
||||
|
||||
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);
|
||||
armature_object_rna_pointer = RNA_id_pointer_create(&armature_object->id);
|
||||
|
||||
cube = BKE_object_add_only_object(bmain, OB_MESH, "cube");
|
||||
cube_rna_pointer = RNA_id_pointer_create(&cube->id);
|
||||
cube_mesh = BKE_mesh_add(bmain, "cube_mesh");
|
||||
cube_mesh_rna_pointer = RNA_id_pointer_create(&cube_mesh->id);
|
||||
/* Removing the implicit id user. Using BKE_mesh_assign_object increments the user count which
|
||||
* would leave it at 2 otherwise. */
|
||||
id_us_min(&cube_mesh->id);
|
||||
BKE_mesh_assign_object(bmain, cube, cube_mesh);
|
||||
material = BKE_material_add(bmain, "material");
|
||||
material_rna_pointer = RNA_id_pointer_create(&material->id);
|
||||
|
||||
id_us_min(&material->id);
|
||||
BKE_object_material_assign(bmain, cube, material, 0, BKE_MAT_ASSIGN_OBDATA);
|
||||
|
||||
object_with_nla = BKE_object_add_only_object(bmain, OB_EMPTY, "EmptyWithNLA");
|
||||
object_with_nla_rna_pointer = RNA_id_pointer_create(&object_with_nla->id);
|
||||
nla_action = BKE_id_new<bAction>(bmain, "NLAAction");
|
||||
/* Set up an NLA system with a single NLA track with a single offset-in-time
|
||||
* NLA strip, and make that strip active and in tweak mode. */
|
||||
AnimData *adt = BKE_animdata_ensure_id(&object_with_nla->id);
|
||||
NlaTrack *track = BKE_nlatrack_new_head(&adt->nla_tracks, false);
|
||||
ASSERT_NE(track, nullptr);
|
||||
NlaStrip *strip = BKE_nlastrip_new(nla_action, object_with_nla->id);
|
||||
BKE_nlatrack_add_strip(track, strip, false);
|
||||
ASSERT_NE(strip, nullptr);
|
||||
ASSERT_TRUE(animrig::nla::assign_action(*strip, nla_action->wrap(), object_with_nla->id));
|
||||
track->flag |= NLATRACK_ACTIVE;
|
||||
strip->flag |= NLASTRIP_FLAG_ACTIVE;
|
||||
strip->start = -10.0;
|
||||
strip->end = 990.0;
|
||||
strip->actstart = 0.0;
|
||||
strip->actend = 1000.0;
|
||||
strip->scale = 1.0;
|
||||
strip->blendmode = NLASTRIP_MODE_COMBINE;
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
|
||||
Channelbag *get_channelbag_in_first_layer(Object &object)
|
||||
{
|
||||
Action &action = object.adt->action->wrap();
|
||||
if (action.layer_array_num == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
Layer *layer = action.layer(0);
|
||||
if (layer->strip_array_num == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
Strip *strip = layer->strip(0);
|
||||
BLI_assert(strip->type() == Strip::Type::Keyframe);
|
||||
StripKeyframeData &strip_data = strip->data<animrig::StripKeyframeData>(action);
|
||||
return strip_data.channelbag_for_slot(object.adt->slot_handle);
|
||||
}
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------
|
||||
* Tests for `insert_keyframes()` with layered actions.
|
||||
*/
|
||||
|
||||
/* Keying a non-array property. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__non_array_property)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
/* First time should create:
|
||||
* - AnimData
|
||||
* - Action
|
||||
* - Slot
|
||||
* - Layer
|
||||
* - Infinite KeyframeStrip
|
||||
* - FCurve with a single key
|
||||
*/
|
||||
object->empty_drawsize = 42.0;
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"empty_display_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
|
||||
/* The action has a slot, it's named properly, and it's correctly assigned
|
||||
* to the object. */
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
Slot *slot = action.slot(0);
|
||||
EXPECT_STREQ(object->id.name, slot->identifier);
|
||||
EXPECT_STREQ(object->adt->last_slot_identifier, slot->identifier);
|
||||
EXPECT_EQ(object->adt->slot_handle, slot->handle);
|
||||
|
||||
/* We have the default layer and strip. */
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
EXPECT_TRUE(strlen(action.layer(0)->name) > 0);
|
||||
Strip *strip = action.layer(0)->strip(0);
|
||||
ASSERT_TRUE(strip->is_infinite());
|
||||
ASSERT_EQ(Strip::Type::Keyframe, strip->type());
|
||||
StripKeyframeData *strip_data = &strip->data<StripKeyframeData>(action);
|
||||
/* We have a channel bag for the slot. */
|
||||
Channelbag *channelbag = strip_data->channelbag_for_slot(*slot);
|
||||
ASSERT_NE(nullptr, channelbag);
|
||||
|
||||
/* The fcurves in the channel bag are what we expect. */
|
||||
EXPECT_EQ(1, channelbag->fcurves().size());
|
||||
const FCurve *fcurve = channelbag->fcurve_find({"empty_display_size", 0});
|
||||
ASSERT_NE(nullptr, fcurve);
|
||||
ASSERT_NE(nullptr, fcurve->bezt);
|
||||
EXPECT_EQ(1, fcurve->totvert);
|
||||
EXPECT_EQ(1.0, fcurve->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(42.0, fcurve->bezt[0].vec[1][1]);
|
||||
|
||||
/* Second time inserting with a different value on the same frame should
|
||||
* simply replace the key. */
|
||||
object->empty_drawsize = 86.0;
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"empty_display_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(1, fcurve->totvert);
|
||||
EXPECT_EQ(1.0, fcurve->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(86.0, fcurve->bezt[0].vec[1][1]);
|
||||
|
||||
/* Third time inserting on a different time should add a second key. */
|
||||
object->empty_drawsize = 7.0;
|
||||
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"empty_display_size"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_3.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(2, fcurve->totvert);
|
||||
EXPECT_EQ(1.0, fcurve->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(86.0, fcurve->bezt[0].vec[1][1]);
|
||||
EXPECT_EQ(10.0, fcurve->bezt[1].vec[1][0]);
|
||||
EXPECT_EQ(7.0, fcurve->bezt[1].vec[1][1]);
|
||||
}
|
||||
|
||||
TEST_F(KeyframingTest, insert_keyframes__action_reuse)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
CombinedKeyingResult result_ob;
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&armature_object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(armature_object->adt != nullptr);
|
||||
ASSERT_TRUE(armature_object->adt->action != nullptr);
|
||||
|
||||
PointerRNA armature_rna_pointer = RNA_id_pointer_create(&armature->id);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&armature_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"display_type"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(armature->adt != nullptr);
|
||||
ASSERT_TRUE(armature->adt->action != nullptr);
|
||||
|
||||
/* Action is expected to be reused between object and data. */
|
||||
ASSERT_EQ(armature->adt->action, armature_object->adt->action);
|
||||
|
||||
Action &action = armature->adt->action->wrap();
|
||||
/* Should have two slots now. */
|
||||
ASSERT_EQ(action.slot_array_num, 2);
|
||||
for (Slot *slot : action.slots()) {
|
||||
ASSERT_TRUE(slot->idtype == ID_AR || slot->idtype == ID_OB);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(KeyframingTest, insert_keyframes__action_reuse_material)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
CombinedKeyingResult result_ob;
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&material_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"pass_index"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(material->adt != nullptr);
|
||||
ASSERT_TRUE(material->adt->action != nullptr);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(cube->adt != nullptr);
|
||||
ASSERT_TRUE(cube->adt->action != nullptr);
|
||||
|
||||
/* Actions are not shared between object and material. */
|
||||
ASSERT_NE(cube->adt->action, material->adt->action);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_mesh_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"remesh_voxel_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(cube_mesh->adt != nullptr);
|
||||
ASSERT_TRUE(cube_mesh->adt->action != nullptr);
|
||||
|
||||
/* Reuse between Object and object data. */
|
||||
ASSERT_EQ(cube_mesh->adt->action, cube->adt->action);
|
||||
/* Still no reuse from mesh to material. */
|
||||
ASSERT_NE(cube_mesh->adt->action, material->adt->action);
|
||||
|
||||
Action &action = cube->adt->action->wrap();
|
||||
/* Should have two slots now. */
|
||||
ASSERT_EQ(action.slot_array_num, 2);
|
||||
|
||||
/* Material action should have only 1 slot. */
|
||||
ASSERT_EQ(material->adt->action->wrap().slot_array_num, 1);
|
||||
|
||||
for (Slot *slot : action.slots()) {
|
||||
ASSERT_TRUE(slot->idtype == ID_ME || slot->idtype == ID_OB);
|
||||
ASSERT_NE(slot->idtype, ID_MA);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(KeyframingTest, insert_keyframes__action_reuse_multiuser)
|
||||
{
|
||||
Object *another_object = BKE_object_add_only_object(bmain, OB_MESH, "another_object");
|
||||
PointerRNA another_object_rna_pointer = RNA_id_pointer_create(&another_object->id);
|
||||
BKE_mesh_assign_object(bmain, another_object, cube_mesh);
|
||||
|
||||
ASSERT_EQ(ID_REFCOUNTING_USERS(&cube_mesh->id), 2);
|
||||
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
CombinedKeyingResult result_ob;
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(cube->adt != nullptr);
|
||||
ASSERT_TRUE(cube->adt->action != nullptr);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&cube_mesh_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"remesh_voxel_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 1);
|
||||
ASSERT_TRUE(cube_mesh->adt != nullptr);
|
||||
ASSERT_TRUE(cube_mesh->adt->action != nullptr);
|
||||
|
||||
/* When an ID is used more than once, the action should not be reused. */
|
||||
ASSERT_NE(cube->adt->action, cube_mesh->adt->action);
|
||||
|
||||
result_ob = insert_keyframes(bmain,
|
||||
&another_object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
ASSERT_EQ(result_ob.get_count(SingleKeyingResult::SUCCESS), 3);
|
||||
ASSERT_TRUE(another_object->adt != nullptr);
|
||||
ASSERT_TRUE(another_object->adt->action != nullptr);
|
||||
|
||||
/* Given that those two objects are connected by a mesh (which due to this has two users) the
|
||||
* action shouldn't be reused between them. */
|
||||
ASSERT_NE(cube->adt->action, another_object->adt->action);
|
||||
}
|
||||
|
||||
/* Keying a single element of an array property. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__single_element)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
const CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler", std::nullopt, 0}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
EXPECT_EQ(1, channelbag->fcurves().size());
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
|
||||
}
|
||||
|
||||
/* Keying all elements of an array property. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__all_elements)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
const CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
EXPECT_EQ(3, result.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
EXPECT_EQ(3, channelbag->fcurves().size());
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 1}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 2}));
|
||||
}
|
||||
|
||||
/* Keying a pose bone from its own RNA pointer. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__pose_bone_rna_pointer)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
bPoseChannel *pchan = BKE_pose_channel_find_name(armature_object->pose, "Bone");
|
||||
PointerRNA pose_bone_rna_pointer = RNA_pointer_create_discrete(
|
||||
&armature_object->id, RNA_PoseBone, pchan);
|
||||
|
||||
const CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&pose_bone_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler", std::nullopt, 0}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, armature_object->adt);
|
||||
ASSERT_NE(nullptr, armature_object->adt->action);
|
||||
Action &action = armature_object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
EXPECT_EQ(1, channelbag->fcurves().size());
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"pose.bones[\"Bone\"].rotation_euler", 0}));
|
||||
}
|
||||
|
||||
/* Keying a pose bone from its owning ID's RNA pointer. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__pose_bone_owner_id_pointer)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
const CombinedKeyingResult result = insert_keyframes(
|
||||
bmain,
|
||||
&armature_object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"pose.bones[\"Bone\"].rotation_euler", std::nullopt, 0}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, armature_object->adt);
|
||||
ASSERT_NE(nullptr, armature_object->adt->action);
|
||||
Action &action = armature_object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
EXPECT_EQ(1, channelbag->fcurves().size());
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"pose.bones[\"Bone\"].rotation_euler", 0}));
|
||||
}
|
||||
|
||||
/* Keying multiple elements of multiple properties at once. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__multiple_properties)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
const CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{
|
||||
{"empty_display_size"},
|
||||
{"location"},
|
||||
{"rotation_euler", std::nullopt, 0},
|
||||
{"rotation_euler", std::nullopt, 2},
|
||||
},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
EXPECT_EQ(6, result.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
EXPECT_EQ(6, channelbag->fcurves().size());
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"empty_display_size", 0}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"location", 0}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"location", 1}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"location", 2}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 2}));
|
||||
}
|
||||
|
||||
/* Keying more than one ID on the same action. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__multiple_ids)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
/* First object should crate the action and get a slot and channel bag. */
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"empty_display_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
|
||||
/* The action has a slot and it's assigned to the first object. */
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
Slot *slot_1 = action.slot_for_handle(object->adt->slot_handle);
|
||||
ASSERT_NE(nullptr, slot_1);
|
||||
EXPECT_STREQ(object->id.name, slot_1->identifier);
|
||||
EXPECT_STREQ(object->adt->last_slot_identifier, slot_1->identifier);
|
||||
|
||||
/* Get the keyframe strip. */
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
|
||||
/* We have a single channel bag, and it's for the first object's slot. */
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag_1 = strip_data->channelbag_for_slot(*slot_1);
|
||||
ASSERT_NE(nullptr, channelbag_1);
|
||||
|
||||
/* Assign the action to the second object, with no slot. */
|
||||
ASSERT_TRUE(assign_action(&action, armature_object->id));
|
||||
ASSERT_EQ(assign_action_slot(nullptr, armature_object->id), ActionSlotAssignmentResult::OK);
|
||||
|
||||
/* Keying the second object should go into the same action, creating a new
|
||||
* slot and channel bag. */
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
|
||||
&armature_object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"empty_display_size"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
|
||||
ASSERT_EQ(2, action.slots().size());
|
||||
Slot *slot_2 = action.slot_for_handle(armature_object->adt->slot_handle);
|
||||
ASSERT_NE(nullptr, slot_2);
|
||||
EXPECT_STREQ(armature_object->id.name, slot_2->identifier);
|
||||
EXPECT_STREQ(armature_object->adt->last_slot_identifier, slot_2->identifier);
|
||||
|
||||
ASSERT_EQ(2, strip_data->channelbags().size());
|
||||
Channelbag *channelbag_2 = strip_data->channelbag_for_slot(*slot_2);
|
||||
ASSERT_NE(nullptr, channelbag_2);
|
||||
}
|
||||
|
||||
/* Keying with the "Only Insert Available" flag. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__only_available)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
/* First attempt should fail, because there are no fcurves yet. */
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_AVAILABLE);
|
||||
|
||||
EXPECT_EQ(0, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
|
||||
/* It's unclear why an AnimData should be created if keying fails
|
||||
* here. It may even be undesirable. This check is just here to ensure no
|
||||
* *unintentional* changes in behavior. */
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
/* No action is created when using the flag INSERTKEY_AVAILABLE on an
|
||||
* object without an action. */
|
||||
ASSERT_EQ(nullptr, object->adt->action);
|
||||
|
||||
/* Insert a key on two of the elements without using the flag so that there
|
||||
* will be two fcurves. */
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{
|
||||
{"rotation_euler", std::nullopt, 0},
|
||||
{"rotation_euler", std::nullopt, 2},
|
||||
},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
/* If an action is created, it should be the default action with one
|
||||
* layer and an infinite keyframe strip. */
|
||||
Action &action = object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
EXPECT_EQ(object->adt->slot_handle, action.slot(0)->handle);
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
|
||||
EXPECT_EQ(2, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
/* Second attempt should succeed with two keys, because two of the elements
|
||||
* now have fcurves. */
|
||||
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_AVAILABLE);
|
||||
|
||||
EXPECT_EQ(2, result_3.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(2, channelbag->fcurves().size());
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 0}));
|
||||
EXPECT_NE(nullptr, channelbag->fcurve_find({"rotation_euler", 2}));
|
||||
}
|
||||
|
||||
/* Keying with the "Only Replace" flag. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__only_replace)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
/* First attempt should fail, because there are no fcurves yet. */
|
||||
object->rot[0] = 42.0;
|
||||
object->rot[1] = 42.0;
|
||||
object->rot[2] = 42.0;
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_REPLACE);
|
||||
EXPECT_EQ(0, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
|
||||
/* Insert a key for two of the elements so that there will be two fcurves with
|
||||
* one key each. */
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{
|
||||
{"rotation_euler", std::nullopt, 0},
|
||||
{"rotation_euler", std::nullopt, 2},
|
||||
},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(2, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
ASSERT_EQ(2, channelbag->fcurves().size());
|
||||
const FCurve *fcurve_x = channelbag->fcurve_find({"rotation_euler", 0});
|
||||
const FCurve *fcurve_z = channelbag->fcurve_find({"rotation_euler", 2});
|
||||
EXPECT_EQ(1, fcurve_x->totvert);
|
||||
EXPECT_EQ(1, fcurve_z->totvert);
|
||||
EXPECT_EQ(1.0, fcurve_x->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(42.0, fcurve_x->bezt[0].vec[1][1]);
|
||||
EXPECT_EQ(1.0, fcurve_z->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(42.0, fcurve_z->bezt[0].vec[1][1]);
|
||||
|
||||
/* Second attempt should also fail, because we insert on a different frame
|
||||
* than the two keys we just created. */
|
||||
object->rot[0] = 86.0;
|
||||
object->rot[1] = 86.0;
|
||||
object->rot[2] = 86.0;
|
||||
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
5.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_REPLACE);
|
||||
EXPECT_EQ(0, result_3.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(2, channelbag->fcurves().size());
|
||||
EXPECT_EQ(1, fcurve_x->totvert);
|
||||
EXPECT_EQ(1, fcurve_z->totvert);
|
||||
EXPECT_EQ(1.0, fcurve_x->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(42.0, fcurve_x->bezt[0].vec[1][1]);
|
||||
EXPECT_EQ(1.0, fcurve_z->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(42.0, fcurve_z->bezt[0].vec[1][1]);
|
||||
|
||||
/* The third attempt, keying on the original frame, should succeed and replace
|
||||
* the existing key on each fcurve. */
|
||||
const CombinedKeyingResult result_4 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_REPLACE);
|
||||
EXPECT_EQ(2, result_4.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(2, channelbag->fcurves().size());
|
||||
EXPECT_EQ(1, fcurve_x->totvert);
|
||||
EXPECT_EQ(1, fcurve_z->totvert);
|
||||
EXPECT_EQ(1.0, fcurve_x->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(86.0, fcurve_x->bezt[0].vec[1][1]);
|
||||
EXPECT_EQ(1.0, fcurve_z->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(86.0, fcurve_z->bezt[0].vec[1][1]);
|
||||
}
|
||||
|
||||
/* Keying with the "Only Insert Needed" flag. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__only_needed)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
/* First attempt should succeed, because there are no fcurves yet. */
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NEEDED);
|
||||
EXPECT_EQ(3, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
ASSERT_NE(nullptr, object->adt);
|
||||
ASSERT_NE(nullptr, object->adt->action);
|
||||
Action &action = object->adt->action->wrap();
|
||||
ASSERT_EQ(1, action.slots().size());
|
||||
ASSERT_EQ(1, action.layers().size());
|
||||
ASSERT_EQ(1, action.layer(0)->strips().size());
|
||||
StripKeyframeData *strip_data = &action.layer(0)->strip(0)->data<StripKeyframeData>(action);
|
||||
ASSERT_EQ(1, strip_data->channelbags().size());
|
||||
Channelbag *channelbag = strip_data->channelbag(0);
|
||||
|
||||
ASSERT_EQ(3, channelbag->fcurves().size());
|
||||
const FCurve *fcurve_x = channelbag->fcurve_find({"rotation_euler", 0});
|
||||
const FCurve *fcurve_y = channelbag->fcurve_find({"rotation_euler", 1});
|
||||
const FCurve *fcurve_z = channelbag->fcurve_find({"rotation_euler", 2});
|
||||
EXPECT_EQ(1, fcurve_x->totvert);
|
||||
EXPECT_EQ(1, fcurve_y->totvert);
|
||||
EXPECT_EQ(1, fcurve_z->totvert);
|
||||
|
||||
/* Second attempt should fail, because there is now an fcurve for the
|
||||
* property, but its value matches the current property value. */
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NEEDED);
|
||||
EXPECT_EQ(0, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(3, channelbag->fcurves().size());
|
||||
EXPECT_EQ(1, fcurve_x->totvert);
|
||||
EXPECT_EQ(1, fcurve_y->totvert);
|
||||
EXPECT_EQ(1, fcurve_z->totvert);
|
||||
|
||||
/* Third attempt should succeed on two elements, because we change the value
|
||||
* of those elements to differ from the existing fcurves. */
|
||||
object->rot[0] = 123.0;
|
||||
object->rot[2] = 123.0;
|
||||
const CombinedKeyingResult result_3 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_euler"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NEEDED);
|
||||
|
||||
EXPECT_EQ(2, result_3.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(3, channelbag->fcurves().size());
|
||||
EXPECT_EQ(2, fcurve_x->totvert);
|
||||
EXPECT_EQ(1, fcurve_y->totvert);
|
||||
EXPECT_EQ(2, fcurve_z->totvert);
|
||||
}
|
||||
|
||||
/* Passing the frame number explicitly vs not. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__optional_frame)
|
||||
{
|
||||
/* If the frame number is not explicitly passed, the eval frame from the
|
||||
* animation evaluation context should be used. */
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 5.0};
|
||||
|
||||
object->rotmode = ROT_MODE_XYZ;
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_mode"}},
|
||||
std::nullopt,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
Channelbag *channelbag = get_channelbag_in_first_layer(*object);
|
||||
FCurve *fcurve = channelbag->fcurve_find({"rotation_mode", 0});
|
||||
EXPECT_EQ(5.0, fcurve->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(float(ROT_MODE_XYZ), fcurve->bezt[0].vec[1][1]);
|
||||
|
||||
/* If the frame number *is* explicitly passed, it should be used. */
|
||||
object->rotmode = ROT_MODE_QUAT;
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"rotation_mode"}},
|
||||
10.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(1, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
EXPECT_EQ(5.0, fcurve->bezt[0].vec[1][0]);
|
||||
EXPECT_EQ(float(ROT_MODE_XYZ), fcurve->bezt[0].vec[1][1]);
|
||||
EXPECT_EQ(10.0, fcurve->bezt[1].vec[1][0]);
|
||||
EXPECT_EQ(float(ROT_MODE_QUAT), fcurve->bezt[1].vec[1][1]);
|
||||
}
|
||||
|
||||
/* Passing the channel group explicitly vs not. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__optional_channel_group)
|
||||
{
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
/* If the channel group is not explicitly passed, the default should be used. */
|
||||
const CombinedKeyingResult result_1 = insert_keyframes(
|
||||
bmain,
|
||||
&object_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location", std::nullopt, 0}, {"visible_shadow"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(2, result_1.get_count(SingleKeyingResult::SUCCESS));
|
||||
|
||||
Channelbag *channelbag = get_channelbag_in_first_layer(*object);
|
||||
/* Location X should get the default transform group. */
|
||||
FCurve *fcurve_location_x = channelbag->fcurve_find({"location", 0});
|
||||
ASSERT_NE(nullptr, fcurve_location_x->grp);
|
||||
EXPECT_EQ(0, strcmp("Object Transforms", fcurve_location_x->grp->name));
|
||||
|
||||
/* Shadow visibility should get no group. */
|
||||
FCurve *fcurve_visible_shadow = channelbag->fcurve_find({"visible_shadow", 0});
|
||||
ASSERT_EQ(nullptr, fcurve_visible_shadow->grp);
|
||||
|
||||
/* If the channel group *is* explicitly passed, it should override the default. */
|
||||
const CombinedKeyingResult result_2 = insert_keyframes(
|
||||
bmain,
|
||||
&object_rna_pointer,
|
||||
"Foo",
|
||||
{{"location", std::nullopt, 1}, {"hide_render"}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
EXPECT_EQ(2, result_2.get_count(SingleKeyingResult::SUCCESS));
|
||||
|
||||
/* Both location Y and render visibility should get the "Foo" group. */
|
||||
FCurve *fcurve_location_y = channelbag->fcurve_find({"location", 1});
|
||||
ASSERT_NE(nullptr, fcurve_location_y->grp);
|
||||
EXPECT_EQ(0, strcmp("Foo", fcurve_location_y->grp->name));
|
||||
FCurve *fcurve_hide_render = channelbag->fcurve_find({"hide_render", 0});
|
||||
ASSERT_NE(nullptr, fcurve_hide_render->grp);
|
||||
EXPECT_EQ(0, strcmp("Foo", fcurve_hide_render->grp->name));
|
||||
}
|
||||
|
||||
/* Inserting a key into an NLA strip that has a time offset should remap the
|
||||
* key's time to the local time of the strip. */
|
||||
TEST_F(KeyframingTest, insert_keyframes__nla_time_remapping)
|
||||
{
|
||||
BKE_nla_tweakmode_enter({object_with_nla->id, *object_with_nla->adt});
|
||||
AnimationEvalContext anim_eval_context = {nullptr, 1.0};
|
||||
|
||||
const CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&object_with_nla_rna_pointer,
|
||||
std::nullopt,
|
||||
{{"location", std::nullopt, 0}},
|
||||
1.0,
|
||||
anim_eval_context,
|
||||
BEZT_KEYTYPE_KEYFRAME,
|
||||
INSERTKEY_NOFLAGS);
|
||||
|
||||
EXPECT_EQ(1, result.get_count(SingleKeyingResult::SUCCESS));
|
||||
Action &act = nla_action->wrap();
|
||||
Layer *layer = act.layer(0);
|
||||
Strip *strip = layer->strip(0);
|
||||
BLI_assert(strip->type() == Strip::Type::Keyframe);
|
||||
StripKeyframeData &strip_data = strip->data<animrig::StripKeyframeData>(act);
|
||||
Channelbag *channelbag = strip_data.channelbag_for_slot(nla_action->wrap().slots()[0]->handle);
|
||||
EXPECT_EQ(1, channelbag->fcurve_array_num);
|
||||
FCurve *fcurve = channelbag->fcurve_find({"location", 0});
|
||||
ASSERT_NE(nullptr, fcurve);
|
||||
ASSERT_NE(nullptr, fcurve->bezt);
|
||||
EXPECT_EQ(1, fcurve->totvert);
|
||||
EXPECT_EQ(11.0, fcurve->bezt[0].vec[1][0]);
|
||||
BKE_nla_tweakmode_exit({object_with_nla->id, *object_with_nla->adt});
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::tests
|
||||
469
blender-5.2.0/source/blender/animrig/intern/keyingsets.cc
Normal file
469
blender-5.2.0/source/blender/animrig/intern/keyingsets.cc
Normal file
@@ -0,0 +1,469 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_keyframing.hh"
|
||||
#include "ANIM_keyingsets.hh"
|
||||
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_context.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_scene_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
|
||||
#include "WM_api.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
/* Keying Set Type Info declarations. */
|
||||
static ListBaseT<KeyingSetInfo> keyingset_type_infos = {nullptr, nullptr};
|
||||
ListBaseT<KeyingSet> builtin_keyingsets = {nullptr, nullptr};
|
||||
|
||||
namespace animrig {
|
||||
|
||||
void keyingset_info_register(KeyingSetInfo *keyingset_info)
|
||||
{
|
||||
/* Create a new KeyingSet
|
||||
* - inherit name and keyframing settings from the typeinfo
|
||||
*/
|
||||
KeyingSet *keyingset = BKE_keyingset_add(&builtin_keyingsets,
|
||||
keyingset_info->idname,
|
||||
keyingset_info->name,
|
||||
eKS_Settings{},
|
||||
keyingset_info->keyingflag);
|
||||
|
||||
/* Link this KeyingSet with its typeinfo. */
|
||||
memcpy(&keyingset->typeinfo, keyingset_info->idname, sizeof(keyingset->typeinfo));
|
||||
|
||||
/* Copy description. */
|
||||
STRNCPY(keyingset->description, keyingset_info->description);
|
||||
|
||||
/* Add type-info to the list. */
|
||||
BLI_addtail(&keyingset_type_infos, keyingset_info);
|
||||
}
|
||||
|
||||
void keyingset_info_unregister(Main *bmain, KeyingSetInfo *keyingset_info)
|
||||
{
|
||||
/* Find relevant builtin KeyingSets which use this, and remove them. */
|
||||
/* TODO: this isn't done now, since unregister is really only used at the moment when we
|
||||
* reload the scripts, which kind of defeats the purpose of "builtin"? */
|
||||
for (KeyingSet &keyingset : builtin_keyingsets.items_mutable()) {
|
||||
/* Remove if matching typeinfo name. */
|
||||
if (!STREQ(keyingset.typeinfo, keyingset_info->idname)) {
|
||||
continue;
|
||||
}
|
||||
Scene *scene;
|
||||
BKE_keyingset_free_paths(&keyingset);
|
||||
BLI_remlink(&builtin_keyingsets, &keyingset);
|
||||
|
||||
for (scene = static_cast<Scene *>(bmain->scenes.first); scene;
|
||||
scene = static_cast<Scene *>(scene->id.next))
|
||||
{
|
||||
BLI_remlink_safe(&scene->keyingsets, &keyingset);
|
||||
}
|
||||
|
||||
MEM_delete(&keyingset);
|
||||
}
|
||||
|
||||
BLI_freelinkN(&keyingset_type_infos, keyingset_info);
|
||||
}
|
||||
|
||||
void keyingset_infos_exit()
|
||||
{
|
||||
/* Free type infos. */
|
||||
for (KeyingSetInfo &keyingset_info : keyingset_type_infos.items_mutable()) {
|
||||
/* Free extra RNA data, and remove from list. */
|
||||
if (keyingset_info.rna_ext.free) {
|
||||
keyingset_info.rna_ext.free(keyingset_info.rna_ext.data);
|
||||
}
|
||||
BLI_freelinkN(&keyingset_type_infos, &keyingset_info);
|
||||
}
|
||||
|
||||
BKE_keyingsets_free(&builtin_keyingsets);
|
||||
}
|
||||
|
||||
bool keyingset_find_id(KeyingSet *keyingset, ID *id)
|
||||
{
|
||||
if (ELEM(nullptr, keyingset, id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return BLI_findptr(&keyingset->paths, id, offsetof(KS_Path, id)) != nullptr;
|
||||
}
|
||||
|
||||
KeyingSetInfo *keyingset_info_find_name(const char name[])
|
||||
{
|
||||
if ((name == nullptr) || (name[0] == 0)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Search by comparing names. */
|
||||
return static_cast<KeyingSetInfo *>(
|
||||
BLI_findstring(&keyingset_type_infos, name, offsetof(KeyingSetInfo, idname)));
|
||||
}
|
||||
|
||||
KeyingSet *builtin_keyingset_get_named(const char name[])
|
||||
{
|
||||
if (name[0] == 0) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Loop over KeyingSets checking names. */
|
||||
for (KeyingSet &keyingset : builtin_keyingsets) {
|
||||
if (STREQ(name, keyingset.idname)) {
|
||||
return &keyingset;
|
||||
}
|
||||
}
|
||||
|
||||
/* Complain about missing keying sets on debug builds. */
|
||||
#ifndef NDEBUG
|
||||
printf("%s: '%s' not found\n", __func__, name);
|
||||
#endif
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
KeyingSet *get_keyingset_for_autokeying(const Scene *scene, const char *transformKSName)
|
||||
{
|
||||
/* Get KeyingSet to use
|
||||
* - use the active KeyingSet if defined (and user wants to use it for all autokeying),
|
||||
* or otherwise key transforms only
|
||||
*/
|
||||
if (is_keying_flag(scene, AUTOKEY_FLAG_ONLYKEYINGSET) && (scene->active_keyingset)) {
|
||||
return scene_get_active_keyingset(scene);
|
||||
}
|
||||
|
||||
if (is_keying_flag(scene, AUTOKEY_FLAG_INSERTAVAILABLE)) {
|
||||
return builtin_keyingset_get_named(ANIM_KS_AVAILABLE_ID);
|
||||
}
|
||||
|
||||
return builtin_keyingset_get_named(transformKSName);
|
||||
}
|
||||
|
||||
KeyingSet *scene_get_active_keyingset(const Scene *scene)
|
||||
{
|
||||
/* If no scene, we've got no hope of finding the Keying Set. */
|
||||
if (scene == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
/* Currently, there are several possibilities here:
|
||||
* - 0: no active keying set
|
||||
* - > 0: one of the user-defined Keying Sets, but indices start from 0 (hence the -1)
|
||||
* - < 0: a builtin keying set
|
||||
*/
|
||||
if (scene->active_keyingset > 0) {
|
||||
return static_cast<KeyingSet *>(BLI_findlink(&scene->keyingsets, scene->active_keyingset - 1));
|
||||
}
|
||||
return static_cast<KeyingSet *>(
|
||||
BLI_findlink(&builtin_keyingsets, (-scene->active_keyingset) - 1));
|
||||
}
|
||||
|
||||
void relative_keyingset_add_source(Vector<PointerRNA> &sources,
|
||||
ID *id,
|
||||
StructRNA *srna,
|
||||
void *data)
|
||||
{
|
||||
if (ELEM(nullptr, srna, data, id)) {
|
||||
return;
|
||||
}
|
||||
sources.append(RNA_pointer_create_discrete(id, srna, data));
|
||||
}
|
||||
|
||||
void relative_keyingset_add_source(Vector<PointerRNA> &sources, ID *id)
|
||||
{
|
||||
if (id == nullptr) {
|
||||
return;
|
||||
}
|
||||
sources.append(RNA_id_pointer_create(id));
|
||||
}
|
||||
|
||||
/* Special 'Overrides' Iterator for Relative KeyingSets ------ */
|
||||
|
||||
/* Iterator used for overriding the behavior of iterators defined for
|
||||
* relative Keying Sets, with the main usage of this being operators
|
||||
* requiring Auto Keyframing. Internal Use Only!
|
||||
*/
|
||||
static void RKS_ITER_overrides_list(KeyingSetInfo *keyingset_info,
|
||||
bContext *C,
|
||||
KeyingSet *keyingset,
|
||||
Vector<PointerRNA> &sources)
|
||||
{
|
||||
for (PointerRNA ptr : sources) {
|
||||
/* Run generate callback on this data. */
|
||||
keyingset_info->generate(keyingset_info, C, keyingset, &ptr);
|
||||
}
|
||||
}
|
||||
|
||||
ModifyKeyReturn validate_keyingset(bContext *C, Vector<PointerRNA> *sources, KeyingSet *keyingset)
|
||||
{
|
||||
if (keyingset == nullptr) {
|
||||
return ModifyKeyReturn::SUCCESS;
|
||||
}
|
||||
|
||||
/* If relative Keying Sets, poll and build up the paths. */
|
||||
if (keyingset->flag & KEYINGSET_ABSOLUTE) {
|
||||
return ModifyKeyReturn::SUCCESS;
|
||||
}
|
||||
|
||||
KeyingSetInfo *keyingset_info = keyingset_info_find_name(keyingset->typeinfo);
|
||||
|
||||
/* Clear all existing paths
|
||||
* NOTE: BKE_keyingset_free_paths() frees all of the paths for the KeyingSet, but not the set
|
||||
* itself.
|
||||
*/
|
||||
BKE_keyingset_free_paths(keyingset);
|
||||
|
||||
/* Get the associated 'type info' for this KeyingSet. */
|
||||
if (keyingset_info == nullptr) {
|
||||
return ModifyKeyReturn::MISSING_TYPEINFO;
|
||||
}
|
||||
/* TODO: check for missing callbacks! */
|
||||
|
||||
/* Check if it can be used in the current context. */
|
||||
if (!keyingset_info->poll(keyingset_info, C)) {
|
||||
/* Poll callback tells us that KeyingSet is useless in current context. */
|
||||
/* FIXME: the poll callback needs to give us more info why. */
|
||||
return ModifyKeyReturn::INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
/* If a list of data sources are provided, run a special iterator over them,
|
||||
* otherwise, just continue per normal.
|
||||
*/
|
||||
if (sources != nullptr) {
|
||||
RKS_ITER_overrides_list(keyingset_info, C, keyingset, *sources);
|
||||
}
|
||||
else {
|
||||
keyingset_info->iter(keyingset_info, C, keyingset);
|
||||
}
|
||||
|
||||
/* If we don't have any paths now, then this still qualifies as invalid context. */
|
||||
/* FIXME: we need some error conditions (to be retrieved from the iterator why this failed!)
|
||||
*/
|
||||
if (keyingset->paths.is_empty()) {
|
||||
return ModifyKeyReturn::INVALID_CONTEXT;
|
||||
}
|
||||
|
||||
return ModifyKeyReturn::SUCCESS;
|
||||
}
|
||||
|
||||
/* Determine which keying flags apply based on the override flags. */
|
||||
static eInsertKeyFlags keyingset_apply_keying_flags(const eInsertKeyFlags base_flags,
|
||||
const eInsertKeyFlags overrides,
|
||||
const eInsertKeyFlags own_flags)
|
||||
{
|
||||
/* Pass through all flags by default (i.e. even not explicitly listed ones). */
|
||||
eInsertKeyFlags result = base_flags;
|
||||
|
||||
/* The logic for whether a keying flag applies is as follows:
|
||||
* - If the flag in question is set in "overrides", that means that the
|
||||
* status of that flag in "own_flags" is used
|
||||
* - If however the flag isn't set, then its value in "base_flags" is used
|
||||
* instead (i.e. no override)
|
||||
*/
|
||||
#define APPLY_KEYINGFLAG_OVERRIDE(kflag) \
|
||||
if (overrides & kflag) { \
|
||||
result &= ~kflag; \
|
||||
result |= (own_flags & kflag); \
|
||||
}
|
||||
|
||||
/* Apply the flags one by one...
|
||||
* (See rna_def_common_keying_flags() for the supported flags)
|
||||
*/
|
||||
APPLY_KEYINGFLAG_OVERRIDE(INSERTKEY_NEEDED)
|
||||
APPLY_KEYINGFLAG_OVERRIDE(INSERTKEY_MATRIX)
|
||||
|
||||
#undef APPLY_KEYINGFLAG_OVERRIDE
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static int insert_key_to_keying_set_path(bContext *C,
|
||||
KS_Path *keyingset_path,
|
||||
KeyingSet *keyingset,
|
||||
const eInsertKeyFlags insert_key_flags,
|
||||
const ModifyKeyMode mode,
|
||||
const float frame)
|
||||
{
|
||||
if (!keyingset_path->rna_path) {
|
||||
/* In case the path is incomplete/not filled in by the user. */
|
||||
return 0;
|
||||
}
|
||||
/* Since keying settings can be defined on the paths too,
|
||||
* apply the settings for this path first. */
|
||||
const eInsertKeyFlags path_insert_key_flags = keyingset_apply_keying_flags(
|
||||
insert_key_flags,
|
||||
eInsertKeyFlags(keyingset_path->keyingoverride),
|
||||
eInsertKeyFlags(keyingset_path->keyingflag));
|
||||
|
||||
const char *groupname = nullptr;
|
||||
/* Get pointer to name of group to add channels to. */
|
||||
if (keyingset_path->groupmode == KSP_GROUP_NONE) {
|
||||
groupname = nullptr;
|
||||
}
|
||||
else if (keyingset_path->groupmode == KSP_GROUP_KSNAME) {
|
||||
groupname = keyingset->name;
|
||||
}
|
||||
else {
|
||||
groupname = keyingset_path->group;
|
||||
}
|
||||
|
||||
/* Init - array_length should be greater than array_index so that
|
||||
* normal non-array entries get keyframed correctly.
|
||||
*/
|
||||
int array_index = keyingset_path->array_index;
|
||||
int array_length = array_index;
|
||||
|
||||
/* Get length of array if whole array option is enabled. */
|
||||
if (keyingset_path->flag & KSP_FLAG_WHOLE_ARRAY) {
|
||||
PointerRNA ptr;
|
||||
PropertyRNA *prop;
|
||||
|
||||
PointerRNA id_ptr = RNA_id_pointer_create(keyingset_path->id);
|
||||
if (RNA_path_resolve_property(&id_ptr, keyingset_path->rna_path, &ptr, &prop)) {
|
||||
array_length = RNA_property_array_length(&ptr, prop);
|
||||
/* Start from start of array, instead of the previously specified index - #48020 */
|
||||
array_index = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* We should do at least one step. */
|
||||
if (array_length == array_index) {
|
||||
array_length++;
|
||||
}
|
||||
|
||||
Main *bmain = CTX_data_main(C);
|
||||
ReportList *reports = CTX_wm_reports(C);
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
const eBezTriple_KeyframeType keytype = eBezTriple_KeyframeType(
|
||||
scene->toolsettings->keyframe_type);
|
||||
/* For each possible index, perform operation
|
||||
* - Assume that array-length is greater than index. */
|
||||
Depsgraph *depsgraph = CTX_data_depsgraph_pointer(C);
|
||||
const AnimationEvalContext anim_eval_context = BKE_animsys_eval_context_construct(depsgraph,
|
||||
frame);
|
||||
int keyed_channels = 0;
|
||||
|
||||
CombinedKeyingResult combined_result;
|
||||
for (; array_index < array_length; array_index++) {
|
||||
if (mode == ModifyKeyMode::INSERT) {
|
||||
const std::optional<StringRefNull> group = groupname ? std::optional(groupname) :
|
||||
std::nullopt;
|
||||
const std::optional<int> index = array_index >= 0 ? std::optional(array_index) :
|
||||
std::nullopt;
|
||||
PointerRNA id_rna_pointer = RNA_id_pointer_create(keyingset_path->id);
|
||||
CombinedKeyingResult result = insert_keyframes(bmain,
|
||||
&id_rna_pointer,
|
||||
group,
|
||||
{{keyingset_path->rna_path, {}, index}},
|
||||
std::nullopt,
|
||||
anim_eval_context,
|
||||
keytype,
|
||||
path_insert_key_flags);
|
||||
keyed_channels += result.get_count(SingleKeyingResult::SUCCESS);
|
||||
combined_result.merge(result);
|
||||
}
|
||||
else if (mode == ModifyKeyMode::DELETE_KEY) {
|
||||
RNAPath rna_path = {keyingset_path->rna_path, std::nullopt, array_index};
|
||||
if (array_index < 0) {
|
||||
rna_path.index = std::nullopt;
|
||||
}
|
||||
keyed_channels += delete_keyframe(bmain, reports, keyingset_path->id, rna_path, frame);
|
||||
}
|
||||
}
|
||||
|
||||
if (combined_result.get_count(SingleKeyingResult::SUCCESS) == 0) {
|
||||
combined_result.generate_reports(reports);
|
||||
}
|
||||
|
||||
switch (GS(keyingset_path->id->name)) {
|
||||
case ID_OB: /* Object (or Object-Related) Keyframes */
|
||||
{
|
||||
Object *ob = reinterpret_cast<Object *>(keyingset_path->id);
|
||||
|
||||
/* XXX: only object transforms? */
|
||||
DEG_id_tag_update(&ob->id, ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
DEG_id_tag_update(keyingset_path->id, ID_RECALC_ANIMATION_NO_FLUSH);
|
||||
break;
|
||||
}
|
||||
|
||||
WM_main_add_notifier(NC_ANIMATION | ND_KEYFRAME | NA_ADDED, nullptr);
|
||||
|
||||
return keyed_channels;
|
||||
}
|
||||
|
||||
int apply_keyingset(bContext *C,
|
||||
Vector<PointerRNA> *sources,
|
||||
KeyingSet *keyingset,
|
||||
const ModifyKeyMode mode,
|
||||
const float cfra)
|
||||
{
|
||||
if (keyingset == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Scene *scene = CTX_data_scene(C);
|
||||
const eInsertKeyFlags base_kflags = get_keyframing_flags(scene);
|
||||
eInsertKeyFlags kflag = INSERTKEY_NOFLAGS;
|
||||
if (mode == ModifyKeyMode::INSERT) {
|
||||
/* Use context settings as base. */
|
||||
kflag = keyingset_apply_keying_flags(base_kflags,
|
||||
eInsertKeyFlags(keyingset->keyingoverride),
|
||||
eInsertKeyFlags(keyingset->keyingflag));
|
||||
}
|
||||
else if (mode == ModifyKeyMode::DELETE_KEY) {
|
||||
kflag = INSERTKEY_NOFLAGS;
|
||||
}
|
||||
|
||||
/* If relative Keying Sets, poll and build up the paths. */
|
||||
{
|
||||
const ModifyKeyReturn error = validate_keyingset(C, sources, keyingset);
|
||||
if (error != ModifyKeyReturn::SUCCESS) {
|
||||
BLI_assert(int(error) < 0);
|
||||
return int(error);
|
||||
}
|
||||
}
|
||||
|
||||
ReportList *reports = CTX_wm_reports(C);
|
||||
int keyed_channels = 0;
|
||||
|
||||
/* Apply the paths as specified in the KeyingSet now. */
|
||||
for (KS_Path &keyingset_path : keyingset->paths) {
|
||||
/* Skip path if no ID pointer is specified. */
|
||||
if (keyingset_path.id == nullptr) {
|
||||
BKE_reportf(reports,
|
||||
RPT_WARNING,
|
||||
"Skipping path in keying set, as it has no ID (KS = '%s', path = '%s[%d]')",
|
||||
keyingset->name,
|
||||
keyingset_path.rna_path,
|
||||
keyingset_path.array_index);
|
||||
continue;
|
||||
}
|
||||
|
||||
keyed_channels += insert_key_to_keying_set_path(
|
||||
C, &keyingset_path, keyingset, kflag, mode, cfra);
|
||||
}
|
||||
|
||||
/* Return the number of channels successfully affected. */
|
||||
BLI_assert(keyed_channels >= 0);
|
||||
return keyed_channels;
|
||||
}
|
||||
|
||||
} // namespace animrig
|
||||
} // namespace blender
|
||||
251
blender-5.2.0/source/blender/animrig/intern/nla.cc
Normal file
251
blender-5.2.0/source/blender/animrig/intern/nla.cc
Normal file
@@ -0,0 +1,251 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "BLI_bit_vector.hh"
|
||||
#include "BLI_dynstr.h"
|
||||
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_fcurve.hh"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_types.hh"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "ANIM_keyframing.hh"
|
||||
#include "ANIM_nla.hh"
|
||||
|
||||
namespace blender::animrig::nla {
|
||||
|
||||
bool assign_action(NlaStrip &strip, Action &action, ID &animated_id)
|
||||
{
|
||||
if (!generic_assign_action(
|
||||
animated_id, &action, strip.act, strip.action_slot_handle, strip.last_slot_identifier))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* For the NLA, the auto slot selection gets one more fallback option (compared to the generic
|
||||
* code). This is to support the following scenario:
|
||||
*
|
||||
* - Python script creates an Action, and adds some F-Curves via the legacy API.
|
||||
* - This creates a slot 'XXSlot'.
|
||||
* - The script creates multiple NLA strips for that Action.
|
||||
* - The desired result is that these strips get the same Slot assigned as well.
|
||||
*
|
||||
* The generic code doesn't work for this. The first strip assignment would see the slot
|
||||
* `XXSlot`, and because it has never been used, just use it. This would change its name to, for
|
||||
* example, `OBSlot`. The second strip assignment would not see a 'virgin' slot, and thus not
|
||||
* auto-select `OBSlot`. This behavior makes sense when assigning Actions in the Action editor
|
||||
* (it shouldn't automatically pick the first slot of matching ID type), but for the NLA I
|
||||
* (Sybren) feel that it could be a bit more 'enthusiastic' in auto-picking a slot.
|
||||
*/
|
||||
if (strip.action_slot_handle == Slot::unassigned && action.slots().size() == 1) {
|
||||
Slot *first_slot = action.slot(0);
|
||||
if (first_slot->is_suitable_for(animated_id)) {
|
||||
const ActionSlotAssignmentResult result = assign_action_slot(strip, first_slot, animated_id);
|
||||
BLI_assert_msg(result == ActionSlotAssignmentResult::OK,
|
||||
"Assigning a slot that we know is suitable should work");
|
||||
UNUSED_VARS_NDEBUG(result);
|
||||
}
|
||||
}
|
||||
|
||||
/* Regardless of slot auto-selection, the Action assignment worked just fine. */
|
||||
return true;
|
||||
}
|
||||
|
||||
void unassign_action(NlaStrip &strip, ID &animated_id)
|
||||
{
|
||||
const bool ok = generic_assign_action(
|
||||
animated_id, nullptr, strip.act, strip.action_slot_handle, strip.last_slot_identifier);
|
||||
BLI_assert_msg(ok, "Un-assigning an Action from an NLA strip should always work.");
|
||||
UNUSED_VARS_NDEBUG(ok);
|
||||
}
|
||||
|
||||
ActionSlotAssignmentResult assign_action_slot(NlaStrip &strip,
|
||||
Slot *slot_to_assign,
|
||||
ID &animated_id)
|
||||
{
|
||||
BLI_assert(strip.act);
|
||||
|
||||
return generic_assign_action_slot(slot_to_assign,
|
||||
animated_id,
|
||||
strip.act,
|
||||
strip.action_slot_handle,
|
||||
strip.last_slot_identifier);
|
||||
}
|
||||
|
||||
ActionSlotAssignmentResult assign_action_slot_handle(NlaStrip &strip,
|
||||
const slot_handle_t slot_handle,
|
||||
ID &animated_id)
|
||||
{
|
||||
BLI_assert(strip.act);
|
||||
|
||||
Action &action = strip.act->wrap();
|
||||
Slot *slot_to_assign = action.slot_for_handle(slot_handle);
|
||||
|
||||
return assign_action_slot(strip, slot_to_assign, animated_id);
|
||||
}
|
||||
|
||||
/* Check indices that were intended to be remapped and report any failed remaps. */
|
||||
static void get_keyframe_values_create_reports(ReportList *reports,
|
||||
const PointerRNA &ptr,
|
||||
const PropertyRNA *prop,
|
||||
const int index,
|
||||
const int count,
|
||||
const bool force_all,
|
||||
const BitSpan successful_remaps)
|
||||
{
|
||||
|
||||
DynStr *ds_failed_indices = BLI_dynstr_new();
|
||||
|
||||
int total_failed = 0;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const bool cur_index_evaluated = ELEM(index, i, -1) || force_all;
|
||||
if (!cur_index_evaluated) {
|
||||
/* `values[i]` was never intended to be remapped. */
|
||||
continue;
|
||||
}
|
||||
|
||||
if (successful_remaps[i]) {
|
||||
/* `values[i]` successfully remapped. */
|
||||
continue;
|
||||
}
|
||||
|
||||
total_failed++;
|
||||
/* Report that `values[i]` were intended to be remapped but failed remapping process. */
|
||||
BLI_dynstr_appendf(ds_failed_indices, "%d, ", i);
|
||||
}
|
||||
|
||||
if (total_failed == 0) {
|
||||
BLI_dynstr_free(ds_failed_indices);
|
||||
return;
|
||||
}
|
||||
|
||||
char *str_failed_indices = BLI_dynstr_get_cstring(ds_failed_indices);
|
||||
BLI_dynstr_free(ds_failed_indices);
|
||||
|
||||
BKE_reportf(reports,
|
||||
RPT_WARNING,
|
||||
"Could not insert %i keyframe(s) due to zero NLA influence, base value, or value "
|
||||
"remapping failed: %s.%s for indices [%s]",
|
||||
total_failed,
|
||||
ptr.owner_id->name,
|
||||
RNA_property_ui_name(prop),
|
||||
str_failed_indices);
|
||||
|
||||
MEM_delete(str_failed_indices);
|
||||
}
|
||||
|
||||
static BitVector<> nla_map_keyframe_values_and_generate_reports(
|
||||
const MutableSpan<float> values,
|
||||
const int index,
|
||||
PointerRNA &ptr,
|
||||
PropertyRNA &prop,
|
||||
NlaKeyframingContext *nla_context,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
ReportList *reports,
|
||||
bool *force_all)
|
||||
{
|
||||
BitVector<> successful_remaps(values.size(), false);
|
||||
BKE_animsys_nla_remap_keyframe_values(
|
||||
nla_context, &ptr, &prop, values, index, anim_eval_context, force_all, successful_remaps);
|
||||
get_keyframe_values_create_reports(
|
||||
reports, ptr, &prop, index, values.size(), false, successful_remaps);
|
||||
return successful_remaps;
|
||||
}
|
||||
|
||||
bool insert_keyframe_direct(ReportList *reports,
|
||||
PointerRNA ptr,
|
||||
PropertyRNA *prop,
|
||||
FCurve *fcu,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
eBezTriple_KeyframeType keytype,
|
||||
NlaKeyframingContext *nla_context,
|
||||
eInsertKeyFlags flag)
|
||||
{
|
||||
if (fcu == nullptr) {
|
||||
BKE_report(reports, RPT_ERROR, "No F-Curve to add keyframes to");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!BKE_fcurve_is_keyframable(*fcu)) {
|
||||
BKE_report(reports, RPT_ERROR, "FCurve is not keyable. Cannot insert keyframes");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((ptr.owner_id == nullptr) && (ptr.data == nullptr)) {
|
||||
BKE_report(
|
||||
reports, RPT_ERROR, "No RNA pointer available to retrieve values for keyframing from");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (prop == nullptr) {
|
||||
PointerRNA tmp_ptr;
|
||||
|
||||
if (RNA_path_resolve_property(&ptr, fcu->rna_path, &tmp_ptr, &prop) == false) {
|
||||
const char *idname = (ptr.owner_id) ? ptr.owner_id->name : RPT_("<No ID pointer>");
|
||||
|
||||
BKE_reportf(reports,
|
||||
RPT_ERROR,
|
||||
"Could not insert keyframe, as RNA path is invalid for the given ID (ID = %s, "
|
||||
"path = %s)",
|
||||
idname,
|
||||
fcu->rna_path);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Property found, so overwrite 'ptr' to make later code easier. */
|
||||
ptr = tmp_ptr;
|
||||
}
|
||||
|
||||
/* Update F-Curve flags to ensure proper behavior for property type. */
|
||||
update_autoflags_fcurve_direct(fcu, RNA_property_type(prop));
|
||||
|
||||
const int index = fcu->array_index;
|
||||
const bool visual_keyframing = flag & INSERTKEY_MATRIX;
|
||||
Vector<float> values = get_property_values(&ptr, prop, visual_keyframing);
|
||||
|
||||
BitVector<> successful_remaps = nla_map_keyframe_values_and_generate_reports(
|
||||
values.as_mutable_span(),
|
||||
index,
|
||||
ptr,
|
||||
*prop,
|
||||
nla_context,
|
||||
anim_eval_context,
|
||||
reports,
|
||||
nullptr);
|
||||
|
||||
float current_value = 0.0f;
|
||||
if (index >= 0 && index < values.size()) {
|
||||
current_value = values[index];
|
||||
}
|
||||
|
||||
/* This happens if NLA rejects this insertion. */
|
||||
if (!successful_remaps[index]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeyframeSettings settings = get_keyframe_settings((flag & INSERTKEY_NO_USERPREF) == 0);
|
||||
settings.keyframe_type = keytype;
|
||||
|
||||
const SingleKeyingResult result = insert_vert_fcurve(
|
||||
fcu, {anim_eval_context->eval_time, current_value}, settings, flag);
|
||||
|
||||
if (result != SingleKeyingResult::SUCCESS) {
|
||||
BKE_reportf(reports,
|
||||
RPT_ERROR,
|
||||
"Failed to insert keys on F-Curve with path '%s[%d]', ensure that it is not "
|
||||
"locked or sampled, and try removing F-Modifiers",
|
||||
fcu->rna_path,
|
||||
fcu->array_index);
|
||||
}
|
||||
return result == SingleKeyingResult::SUCCESS;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::nla
|
||||
160
blender-5.2.0/source/blender/animrig/intern/nla_test.cc
Normal file
160
blender-5.2.0/source/blender/animrig/intern/nla_test.cc
Normal file
@@ -0,0 +1,160 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_nla.hh"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_gtest_base.hh"
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_nla.hh"
|
||||
#include "BKE_object.hh"
|
||||
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::nla::tests {
|
||||
|
||||
class NLASlottedActionTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
Main *bmain;
|
||||
Action *action;
|
||||
Object *cube;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
bmain = BKE_main_new();
|
||||
action = BKE_id_new<Action>(bmain, "ACÄnimåtië");
|
||||
action->id.us = 0; /* Nothing references this yet. */
|
||||
cube = BKE_object_add_only_object(bmain, OB_EMPTY, "Küüübus");
|
||||
cube->id.us = 0; /* Nothing references this yet. */
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(NLASlottedActionTest, assign_slot_to_nla_strip)
|
||||
{
|
||||
ASSERT_EQ(action->id.us, 0);
|
||||
|
||||
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
|
||||
NlaTrack *track = BKE_nlatrack_new_tail(&adt->nla_tracks, false);
|
||||
|
||||
/* Create a strip. This automatically assigns the Action, but for now with the old flow. */
|
||||
NlaStrip *strip = BKE_nlastrip_new(action, cube->id);
|
||||
BKE_nlatrack_add_strip(track, strip, false);
|
||||
|
||||
EXPECT_EQ(strip->action_slot_handle, Slot::unassigned);
|
||||
EXPECT_STREQ(strip->last_slot_identifier, "");
|
||||
|
||||
/* Unassign the Action that was automatically assigned via BKE_nlastrip_new(). */
|
||||
nla::unassign_action(*strip, cube->id);
|
||||
EXPECT_EQ(strip->act, nullptr);
|
||||
EXPECT_EQ(action->id.us, 0);
|
||||
|
||||
/* Assign an Action with a never-assigned slot. This should be picked automatically. */
|
||||
Slot &virgin_slot = action->slot_add();
|
||||
|
||||
/* Assign the Action. */
|
||||
EXPECT_TRUE(nla::assign_action(*strip, *action, cube->id));
|
||||
EXPECT_EQ(strip->action_slot_handle, virgin_slot.handle);
|
||||
EXPECT_STREQ(strip->last_slot_identifier, virgin_slot.identifier);
|
||||
EXPECT_EQ(action->id.us, 1);
|
||||
EXPECT_EQ(strip->act, action);
|
||||
EXPECT_EQ(virgin_slot.idtype, GS(cube->id.name));
|
||||
|
||||
/* Unassign the Action. */
|
||||
nla::unassign_action(*strip, cube->id);
|
||||
EXPECT_EQ(strip->act, nullptr);
|
||||
EXPECT_EQ(action->id.us, 0);
|
||||
|
||||
/* Create a slot for this ID, and make the NLA strip forget what slot it was assigned to before.
|
||||
* Assigning the Action should now auto-pick the slot with the ID name. */
|
||||
Slot &slot = action->slot_add_for_id(cube->id);
|
||||
strip->last_slot_identifier[0] = '\0';
|
||||
EXPECT_TRUE(nla::assign_action(*strip, *action, cube->id));
|
||||
EXPECT_EQ(strip->action_slot_handle, slot.handle);
|
||||
EXPECT_STREQ(strip->last_slot_identifier, slot.identifier);
|
||||
EXPECT_EQ(action->id.us, 1);
|
||||
EXPECT_EQ(strip->act, action);
|
||||
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
|
||||
|
||||
/* Unassign the slot, but keep the Action assigned. */
|
||||
EXPECT_EQ(nla::assign_action_slot(*strip, nullptr, cube->id), ActionSlotAssignmentResult::OK);
|
||||
EXPECT_EQ(strip->action_slot_handle, Slot::unassigned);
|
||||
EXPECT_STREQ(strip->last_slot_identifier, slot.identifier);
|
||||
EXPECT_EQ(action->id.us, 1);
|
||||
EXPECT_EQ(strip->act, action);
|
||||
EXPECT_FALSE(slot.runtime_users().contains(&cube->id));
|
||||
|
||||
/* Unassign the Action, then reassign it. It should pick the same slot again. */
|
||||
nla::unassign_action(*strip, cube->id);
|
||||
EXPECT_TRUE(nla::assign_action(*strip, *action, cube->id));
|
||||
EXPECT_EQ(strip->action_slot_handle, slot.handle);
|
||||
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
|
||||
}
|
||||
|
||||
TEST_F(NLASlottedActionTest, assign_slot_to_multiple_strips)
|
||||
{
|
||||
AnimData *adt = BKE_animdata_ensure_id(&cube->id);
|
||||
NlaTrack *track = BKE_nlatrack_new_tail(&adt->nla_tracks, false);
|
||||
|
||||
/* Create two strips. This automatically assigns the Action, but for now with
|
||||
* the old flow (so no slots). */
|
||||
NlaStrip *strip1 = BKE_nlastrip_new(action, cube->id);
|
||||
strip1->start = 1;
|
||||
strip1->end = 4;
|
||||
NlaStrip *strip2 = BKE_nlastrip_new(action, cube->id);
|
||||
strip1->start = 47;
|
||||
strip1->end = 327;
|
||||
ASSERT_TRUE(BKE_nlatrack_add_strip(track, strip1, false));
|
||||
ASSERT_TRUE(BKE_nlatrack_add_strip(track, strip2, false));
|
||||
ASSERT_EQ(1, adt->nla_tracks.count());
|
||||
ASSERT_EQ(2, track->strips.count());
|
||||
|
||||
nla::unassign_action(*strip1, cube->id);
|
||||
nla::unassign_action(*strip2, cube->id);
|
||||
|
||||
/* Create a virgin slot, it should be auto-picked. */
|
||||
Slot &slot = action->slot_add();
|
||||
EXPECT_TRUE(nla::assign_action(*strip1, *action, cube->id));
|
||||
EXPECT_EQ(strip1->action_slot_handle, slot.handle);
|
||||
EXPECT_STREQ(strip1->last_slot_identifier, slot.identifier);
|
||||
EXPECT_EQ(slot.idtype, ID_OB);
|
||||
|
||||
/* Assign another slot 'manually'. */
|
||||
Slot &other_slot = action->slot_add();
|
||||
EXPECT_EQ(nla::assign_action_slot(*strip1, &other_slot, cube->id),
|
||||
ActionSlotAssignmentResult::OK);
|
||||
EXPECT_EQ(strip1->action_slot_handle, other_slot.handle);
|
||||
|
||||
/* Assign the Action + slot to the second strip. */
|
||||
EXPECT_TRUE(nla::assign_action(*strip2, *action, cube->id));
|
||||
EXPECT_EQ(nla::assign_action_slot(*strip2, &slot, cube->id), ActionSlotAssignmentResult::OK);
|
||||
|
||||
/* The cube should be registered as user of the slot. */
|
||||
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
|
||||
|
||||
nla::unassign_action(*strip1, cube->id);
|
||||
|
||||
/* The cube should still be registered as user of the slot, as there is a 2nd
|
||||
* strip that references it. */
|
||||
EXPECT_TRUE(slot.runtime_users().contains(&cube->id));
|
||||
|
||||
/* Remove the last use of this slot. */
|
||||
nla::unassign_action(*strip2, cube->id);
|
||||
EXPECT_FALSE(slot.runtime_users().contains(&cube->id));
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::nla::tests
|
||||
170
blender-5.2.0/source/blender/animrig/intern/pose.cc
Normal file
170
blender-5.2.0/source/blender/animrig/intern/pose.cc
Normal file
@@ -0,0 +1,170 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include "ANIM_pose.hh"
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_armature.hh"
|
||||
#include "BLI_listbase.h"
|
||||
#include "DNA_anim_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "RNA_access.hh"
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
namespace {
|
||||
|
||||
using ActionApplier =
|
||||
FunctionRef<void(PointerRNA *, bAction *, slot_handle_t, const AnimationEvalContext *)>;
|
||||
|
||||
void pose_apply_restore_fcurves(const Span<FCurve *> fcurves)
|
||||
{
|
||||
for (FCurve *fcu : fcurves) {
|
||||
fcu->flag &= ~FCURVE_DISABLED;
|
||||
}
|
||||
}
|
||||
|
||||
/* Returns a vector of all FCurves on which the fcurve flag was modified. */
|
||||
Vector<FCurve *> pose_apply_disable_fcurves_for_unselected_bones(
|
||||
bAction *action, const slot_handle_t slot_handle, const bke::BoneNameSet &selected_bone_names)
|
||||
{
|
||||
Vector<FCurve *> modified_fcurves;
|
||||
auto disable_unselected_fcurve = [&](FCurve *fcu, const char *bone_name) {
|
||||
const bool is_bone_selected = selected_bone_names.contains(bone_name);
|
||||
if (!is_bone_selected) {
|
||||
if (!(fcu->flag & FCURVE_DISABLED)) {
|
||||
/* FCurve is not yet disabled, we need to reset that later. */
|
||||
modified_fcurves.append(fcu);
|
||||
}
|
||||
fcu->flag |= FCURVE_DISABLED;
|
||||
}
|
||||
};
|
||||
bke::BKE_action_find_fcurves_with_bones(action, slot_handle, disable_unselected_fcurve);
|
||||
return modified_fcurves;
|
||||
}
|
||||
|
||||
void pose_apply(Object *ob,
|
||||
bAction *action,
|
||||
const slot_handle_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
ActionApplier applier)
|
||||
{
|
||||
bPose *pose = ob->pose;
|
||||
if (pose == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action->wrap().slot_array_num == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bke::BoneNameSet selected_bone_names = bke::BKE_pose_channel_find_selected_names(ob);
|
||||
|
||||
/* Mute all FCurves that are not associated with selected bones. This separates the concept of
|
||||
* bone selection from the FCurve evaluation code. */
|
||||
Vector<FCurve *> modified_fcurves = pose_apply_disable_fcurves_for_unselected_bones(
|
||||
action, slot_handle, selected_bone_names);
|
||||
|
||||
/* Apply the Action. */
|
||||
PointerRNA pose_owner_ptr = RNA_id_pointer_create(&ob->id);
|
||||
|
||||
applier(&pose_owner_ptr, action, slot_handle, anim_eval_context);
|
||||
|
||||
pose_apply_restore_fcurves(modified_fcurves);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void pose_apply_action_all_bones(Object *ob,
|
||||
bAction *action,
|
||||
const int32_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context)
|
||||
{
|
||||
PointerRNA pose_owner_ptr = RNA_id_pointer_create(&ob->id);
|
||||
animsys_evaluate_action(&pose_owner_ptr, action, slot_handle, anim_eval_context, false);
|
||||
}
|
||||
|
||||
void pose_apply_action_blend(Object *ob,
|
||||
bAction *action,
|
||||
const int32_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
const float blend_factor)
|
||||
{
|
||||
auto evaluate_and_blend = [blend_factor](PointerRNA *ptr,
|
||||
bAction *act,
|
||||
const int32_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context) {
|
||||
animsys_blend_in_action(ptr, act, slot_handle, anim_eval_context, blend_factor);
|
||||
};
|
||||
|
||||
pose_apply(ob, action, slot_handle, anim_eval_context, evaluate_and_blend);
|
||||
}
|
||||
|
||||
void pose_apply_action_blend_all_bones(Object *ob,
|
||||
bAction *action,
|
||||
slot_handle_t slot_handle,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
const float blend_factor)
|
||||
{
|
||||
PointerRNA pose_owner_ptr = RNA_id_pointer_create(&ob->id);
|
||||
animsys_blend_in_action(&pose_owner_ptr, action, slot_handle, anim_eval_context, blend_factor);
|
||||
}
|
||||
|
||||
bool any_bone_selected(const Span<const Object *> objects)
|
||||
{
|
||||
for (const Object *obj : objects) {
|
||||
if (!obj->pose) {
|
||||
continue;
|
||||
}
|
||||
for (bPoseChannel &pose_bone : obj->pose->chanbase) {
|
||||
if (pose_bone.flag & POSE_SELECTED) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void pose_apply_action(const Span<Object *> objects,
|
||||
Action &pose_action,
|
||||
const AnimationEvalContext *anim_eval_context,
|
||||
const float blend_factor)
|
||||
{
|
||||
if (any_bone_selected(objects)) {
|
||||
for (Object *object : objects) {
|
||||
Slot &slot = get_best_pose_slot_for_id(object->id, pose_action);
|
||||
pose_apply_action_blend(object, &pose_action, slot.handle, anim_eval_context, blend_factor);
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* In the case of nothing selected, act as if all is selected. This is a convenience feature
|
||||
* for the artists so they don't have to be specific in their selection all the time. */
|
||||
for (Object *object : objects) {
|
||||
Slot &slot = get_best_pose_slot_for_id(object->id, pose_action);
|
||||
pose_apply_action_blend_all_bones(
|
||||
object, &pose_action, slot.handle, anim_eval_context, blend_factor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Slot &get_best_pose_slot_for_id(const ID &id, Action &pose_data)
|
||||
{
|
||||
BLI_assert_msg(pose_data.slot_array_num > 0,
|
||||
"Actions without slots have no data. This should have been caught earlier.");
|
||||
|
||||
Slot *slot = generic_slot_for_autoassign(id, pose_data, "");
|
||||
if (slot == nullptr) {
|
||||
slot = pose_data.slot(0);
|
||||
}
|
||||
|
||||
return *slot;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig
|
||||
562
blender-5.2.0/source/blender/animrig/intern/pose_test.cc
Normal file
562
blender-5.2.0/source/blender/animrig/intern/pose_test.cc
Normal file
@@ -0,0 +1,562 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "BKE_pose.hh"
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_math_matrix.h"
|
||||
#include "BLI_math_rotation.h"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "BKE_action.hh"
|
||||
#include "BKE_anim_data.hh"
|
||||
#include "BKE_animsys.h"
|
||||
#include "BKE_armature.hh"
|
||||
#include "BKE_gtest_base.hh"
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_object.hh"
|
||||
|
||||
#include "DEG_depsgraph.hh"
|
||||
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_pose.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
constexpr char msg_unexpected_modification[] =
|
||||
"Properties not stored in the pose are expected to not be modified.";
|
||||
|
||||
namespace animrig::tests {
|
||||
|
||||
class PoseTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
Main *bmain;
|
||||
Action *pose_action;
|
||||
Object *obj_empty;
|
||||
Object *obj_armature_a;
|
||||
Object *obj_armature_b;
|
||||
StripKeyframeData *keyframe_data;
|
||||
const animrig::KeyframeSettings key_settings = {BEZT_KEYTYPE_KEYFRAME, HD_AUTO, BEZT_IPO_BEZ};
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
bmain = BKE_main_new();
|
||||
pose_action = BKE_id_new<Action>(bmain, "pose_data");
|
||||
Layer &layer = pose_action->layer_add("first_layer");
|
||||
Strip &strip = layer.strip_add(*pose_action, Strip::Type::Keyframe);
|
||||
keyframe_data = &strip.data<StripKeyframeData>(*pose_action);
|
||||
|
||||
obj_empty = BKE_object_add_only_object(bmain, OB_EMPTY, "obj_empty");
|
||||
obj_armature_a = BKE_object_add_only_object(bmain, OB_ARMATURE, "obj_armature_a");
|
||||
obj_armature_b = BKE_object_add_only_object(bmain, OB_ARMATURE, "obj_armature_b");
|
||||
|
||||
bArmature *armature = BKE_armature_add(bmain, "ArmatureA");
|
||||
obj_armature_a->data = id_cast<ID *>(armature);
|
||||
|
||||
Bone *bone = MEM_new<Bone>("BONE");
|
||||
STRNCPY(bone->name, "BoneA");
|
||||
BLI_addtail(&armature->bonebase, bone);
|
||||
|
||||
bone = MEM_new<Bone>("BONE");
|
||||
STRNCPY(bone->name, "BoneB");
|
||||
BLI_addtail(&armature->bonebase, bone);
|
||||
|
||||
BKE_pose_ensure(bmain, obj_armature_a, armature, false);
|
||||
|
||||
armature = BKE_armature_add(bmain, "ArmatureB");
|
||||
obj_armature_b->data = id_cast<ID *>(armature);
|
||||
|
||||
bone = MEM_new<Bone>("BONE");
|
||||
STRNCPY(bone->name, "BoneA");
|
||||
BLI_addtail(&armature->bonebase, bone);
|
||||
|
||||
bone = MEM_new<Bone>("BONE");
|
||||
STRNCPY(bone->name, "BoneB");
|
||||
BLI_addtail(&armature->bonebase, bone);
|
||||
|
||||
BKE_pose_ensure(bmain, obj_armature_b, armature, false);
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PoseTest, get_best_slot)
|
||||
{
|
||||
Slot &first_slot = pose_action->slot_add();
|
||||
Slot &second_slot = pose_action->slot_add_for_id(obj_empty->id);
|
||||
|
||||
EXPECT_EQ(&get_best_pose_slot_for_id(obj_empty->id, *pose_action), &second_slot);
|
||||
EXPECT_EQ(&get_best_pose_slot_for_id(obj_armature_a->id, *pose_action), &first_slot);
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_object)
|
||||
{
|
||||
/* Since pose bones live on the object, the code is already set up to handle objects
|
||||
* transforms, even though the name suggests it only applies to bones. */
|
||||
Slot &first_slot = pose_action->slot_add();
|
||||
EXPECT_EQ(obj_empty->loc[0], 0.0f);
|
||||
keyframe_data->keyframe_insert(bmain, first_slot, {"location", 0}, {1, 10}, key_settings);
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
animrig::pose_apply_action_all_bones(obj_empty, pose_action, first_slot.handle, &eval_context);
|
||||
EXPECT_EQ(obj_empty->loc[0], 10.0f);
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_all_bones_single_slot)
|
||||
{
|
||||
Slot &first_slot = pose_action->slot_add();
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, first_slot, {"pose.bones[\"BoneA\"].location", 0}, {1, 10}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, first_slot, {"pose.bones[\"BoneB\"].location", 1}, {1, 5}, key_settings);
|
||||
|
||||
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
bPoseChannel *bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
|
||||
|
||||
bone_a->loc[1] = 1.0;
|
||||
bone_a->loc[2] = 2.0;
|
||||
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
animrig::pose_apply_action_all_bones(
|
||||
obj_armature_a, pose_action, first_slot.handle, &eval_context);
|
||||
EXPECT_EQ(bone_a->loc[0], 10.0);
|
||||
EXPECT_EQ(bone_b->loc[1], 5.0);
|
||||
|
||||
EXPECT_EQ(bone_a->loc[1], 1.0) << msg_unexpected_modification;
|
||||
EXPECT_EQ(bone_a->loc[2], 2.0);
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_all_bones_multiple_slots)
|
||||
{
|
||||
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
|
||||
Slot &slot_b = pose_action->slot_add_for_id(obj_armature_b->id);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].location", 0}, {1, 5}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneB\"].location", 0}, {1, 5}, key_settings);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_b, {"pose.bones[\"BoneA\"].location", 1}, {1, 10}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_b, {"pose.bones[\"BoneB\"].location", 1}, {1, 10}, key_settings);
|
||||
|
||||
bPoseChannel *arm_a_bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
bPoseChannel *arm_a_bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
|
||||
|
||||
bPoseChannel *arm_b_bone_a = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneA");
|
||||
bPoseChannel *arm_b_bone_b = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneB");
|
||||
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
animrig::pose_apply_action_all_bones(obj_armature_a, pose_action, slot_a.handle, &eval_context);
|
||||
|
||||
EXPECT_EQ(arm_a_bone_a->loc[0], 5.0);
|
||||
EXPECT_EQ(arm_a_bone_a->loc[1], 0.0) << msg_unexpected_modification;
|
||||
EXPECT_EQ(arm_a_bone_a->loc[2], 0.0) << msg_unexpected_modification;
|
||||
|
||||
EXPECT_EQ(arm_a_bone_b->loc[0], 5.0);
|
||||
|
||||
EXPECT_EQ(arm_b_bone_a->loc[1], 0.0) << "Other armature should not be affected yet.";
|
||||
|
||||
animrig::pose_apply_action_all_bones(obj_armature_b, pose_action, slot_b.handle, &eval_context);
|
||||
|
||||
EXPECT_EQ(arm_b_bone_b->loc[0], 0.0) << msg_unexpected_modification;
|
||||
EXPECT_EQ(arm_b_bone_b->loc[1], 10.0);
|
||||
EXPECT_EQ(arm_b_bone_b->loc[2], 0.0) << msg_unexpected_modification;
|
||||
|
||||
EXPECT_EQ(arm_a_bone_a->loc[0], 5.0) << "Other armature should not be affected.";
|
||||
|
||||
/* Any slot can be applied, even if it hasn't been added for the ID. */
|
||||
animrig::pose_apply_action_all_bones(obj_armature_a, pose_action, slot_b.handle, &eval_context);
|
||||
|
||||
EXPECT_EQ(arm_b_bone_b->loc[1], arm_b_bone_a->loc[1])
|
||||
<< "Applying the same pose should result in the same values.";
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_blend_single_slot)
|
||||
{
|
||||
Slot &first_slot = pose_action->slot_add();
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, first_slot, {"pose.bones[\"BoneA\"].location", 0}, {1, 10}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, first_slot, {"pose.bones[\"BoneB\"].location", 1}, {1, 5}, key_settings);
|
||||
|
||||
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
bPoseChannel *bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
|
||||
|
||||
bone_a->loc[0] = 0.0;
|
||||
bone_b->loc[1] = 0.0;
|
||||
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
animrig::pose_apply_action_blend_all_bones(
|
||||
obj_armature_a, pose_action, first_slot.handle, &eval_context, 1.0);
|
||||
|
||||
EXPECT_NEAR(bone_a->loc[0], 10.0, 0.001);
|
||||
EXPECT_NEAR(bone_b->loc[1], 5.0, 0.001);
|
||||
|
||||
bone_a->loc[0] = 0.0;
|
||||
bone_b->loc[1] = 0.0;
|
||||
|
||||
animrig::pose_apply_action_blend_all_bones(
|
||||
obj_armature_a, pose_action, first_slot.handle, &eval_context, 0.5);
|
||||
|
||||
EXPECT_NEAR(bone_a->loc[0], 5.0, 0.001);
|
||||
EXPECT_NEAR(bone_b->loc[1], 2.5, 0.001);
|
||||
|
||||
bone_a->loc[0] = 0.0;
|
||||
bone_b->loc[1] = 0.0;
|
||||
|
||||
bone_a->flag |= POSE_SELECTED;
|
||||
bone_b->flag &= ~POSE_SELECTED;
|
||||
|
||||
/* This should only affect the selected bone. */
|
||||
animrig::pose_apply_action_blend(
|
||||
obj_armature_a, pose_action, first_slot.handle, &eval_context, 0.5);
|
||||
|
||||
EXPECT_NEAR(bone_a->loc[0], 5.0, 0.001);
|
||||
EXPECT_NEAR(bone_b->loc[1], 0.0, 0.001);
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_multiple_objects)
|
||||
{
|
||||
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
|
||||
Slot &slot_b = pose_action->slot_add_for_id(obj_armature_b->id);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].location", 0}, {1, 5}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneB\"].location", 0}, {1, 5}, key_settings);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_b, {"pose.bones[\"BoneA\"].location", 1}, {1, 10}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_b, {"pose.bones[\"BoneB\"].location", 1}, {1, 10}, key_settings);
|
||||
|
||||
bPoseChannel *arm_a_bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
bPoseChannel *arm_a_bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
|
||||
|
||||
bPoseChannel *arm_b_bone_a = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneA");
|
||||
bPoseChannel *arm_b_bone_b = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneB");
|
||||
|
||||
Vector<bPoseChannel *> all_bones = {arm_a_bone_a, arm_a_bone_b, arm_b_bone_a, arm_b_bone_b};
|
||||
|
||||
for (bPoseChannel *pose_bone : all_bones) {
|
||||
pose_bone->flag &= ~POSE_SELECTED;
|
||||
pose_bone->loc[0] = 0.0;
|
||||
pose_bone->loc[1] = 0.0;
|
||||
}
|
||||
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
|
||||
|
||||
/* No bones are selected, this should affect all bones. */
|
||||
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_a_bone_b->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_a->loc[1], 10, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_b->loc[1], 10, 0.001);
|
||||
|
||||
for (bPoseChannel *pose_bone : all_bones) {
|
||||
pose_bone->loc[0] = 0.0;
|
||||
pose_bone->loc[1] = 0.0;
|
||||
}
|
||||
|
||||
arm_a_bone_a->flag |= POSE_SELECTED;
|
||||
|
||||
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
|
||||
|
||||
/* Only the one selected bone should be affected. */
|
||||
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_a_bone_b->loc[0], 0, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_a->loc[1], 0, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
|
||||
|
||||
for (bPoseChannel *pose_bone : all_bones) {
|
||||
pose_bone->loc[0] = 0.0;
|
||||
pose_bone->loc[1] = 0.0;
|
||||
}
|
||||
|
||||
arm_a_bone_a->flag |= POSE_SELECTED;
|
||||
arm_b_bone_a->flag |= POSE_SELECTED;
|
||||
|
||||
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
|
||||
|
||||
/* Only the two selected bones from different armatures should be affected. */
|
||||
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_a_bone_b->loc[0], 0, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_a->loc[1], 10, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
|
||||
|
||||
for (bPoseChannel *pose_bone : all_bones) {
|
||||
pose_bone->loc[0] = 0.0;
|
||||
pose_bone->loc[1] = 0.0;
|
||||
}
|
||||
|
||||
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 0.5);
|
||||
|
||||
/* Blending half way. */
|
||||
EXPECT_NEAR(arm_a_bone_a->loc[0], 2.5, 0.001);
|
||||
EXPECT_NEAR(arm_a_bone_b->loc[0], 0, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_a->loc[1], 5, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
|
||||
|
||||
for (bPoseChannel *pose_bone : all_bones) {
|
||||
pose_bone->loc[0] = 0.0;
|
||||
pose_bone->loc[1] = 0.0;
|
||||
}
|
||||
|
||||
arm_a_bone_a->flag |= POSE_SELECTED;
|
||||
arm_a_bone_b->flag |= POSE_SELECTED;
|
||||
arm_b_bone_a->flag |= POSE_SELECTED;
|
||||
|
||||
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
|
||||
|
||||
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_a_bone_b->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_a->loc[1], 10, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_b->loc[1], 0, 0.001);
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_multiple_objects_single_slot)
|
||||
{
|
||||
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].location", 0}, {1, 5}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneB\"].location", 0}, {1, 5}, key_settings);
|
||||
|
||||
bPoseChannel *arm_a_bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
bPoseChannel *arm_a_bone_b = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneB");
|
||||
|
||||
bPoseChannel *arm_b_bone_a = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneA");
|
||||
bPoseChannel *arm_b_bone_b = BKE_pose_channel_find_name(obj_armature_b->pose, "BoneB");
|
||||
|
||||
Vector<bPoseChannel *> all_bones = {arm_a_bone_a, arm_a_bone_b, arm_b_bone_a, arm_b_bone_b};
|
||||
|
||||
for (bPoseChannel *pose_bone : all_bones) {
|
||||
pose_bone->flag &= ~POSE_SELECTED;
|
||||
pose_bone->loc[0] = 0.0;
|
||||
pose_bone->loc[1] = 0.0;
|
||||
}
|
||||
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
animrig::pose_apply_action({obj_armature_a, obj_armature_b}, *pose_action, &eval_context, 1.0);
|
||||
|
||||
/* No bones are selected, this should affect all bones. Armature B has no slot, it should fall
|
||||
* back to slot 0. */
|
||||
EXPECT_NEAR(arm_a_bone_a->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_a_bone_b->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_a->loc[0], 5, 0.001);
|
||||
EXPECT_NEAR(arm_b_bone_b->loc[0], 5, 0.001);
|
||||
}
|
||||
|
||||
static void reset_pose_bone_rotations(bPoseChannel &pose_bone)
|
||||
{
|
||||
pose_bone.eul[0] = 0;
|
||||
pose_bone.eul[1] = 0;
|
||||
pose_bone.eul[2] = 0;
|
||||
|
||||
pose_bone.quat[0] = 1;
|
||||
pose_bone.quat[1] = 0;
|
||||
pose_bone.quat[2] = 0;
|
||||
pose_bone.quat[3] = 0;
|
||||
|
||||
pose_bone.rotAngle = 0;
|
||||
pose_bone.rotAxis[0] = 0;
|
||||
pose_bone.rotAxis[1] = 0;
|
||||
pose_bone.rotAxis[2] = 0;
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_differing_rotation_mode_from_euler)
|
||||
{
|
||||
/* When the pose has a different rotation mode than the data it is being applied to, the system
|
||||
* should convert the rotation. */
|
||||
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_euler", 0}, {1, 3.14}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_euler", 1}, {1, 1}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_euler", 2}, {1, 0}, key_settings);
|
||||
|
||||
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
const bke::PChanBone pchanbone_a{bone_a, bone_a->bone_get(*obj_armature_a)};
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
|
||||
/* First check that applying works if the rotation mode matches. */
|
||||
bone_a->rotmode = ROT_MODE_XYZ;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
EXPECT_NEAR(bone_a->eul[0], 3.14, 0.001);
|
||||
EXPECT_NEAR(bone_a->eul[1], 1, 0.001);
|
||||
EXPECT_NEAR(bone_a->eul[2], 0, 0.001);
|
||||
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
float expected_matrix[4][4];
|
||||
copy_m4_m4(expected_matrix, bone_a->chan_mat);
|
||||
|
||||
/* Check that other rotation modes work the same as applying euler directly. */
|
||||
bone_a->rotmode = ROT_MODE_QUAT;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_AXISANGLE;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
/* Not doing blend testing here since the rotation matrix will not align. Component wise
|
||||
* interpolation of euler angles and matrix interpolation is expected to yield different
|
||||
* results. */
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_differing_rotation_mode_from_quaternion)
|
||||
{
|
||||
/* When the pose has a different rotation mode than the data it is being applied to, the system
|
||||
* should convert the rotation. */
|
||||
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
|
||||
|
||||
float quaternion[4] = {0.877, 0.11, -0.483, -0.164};
|
||||
/* We have to have a normalized quaternion otherwise the resulting matrix will be off between
|
||||
* different rotation modes. */
|
||||
normalize_qt(quaternion);
|
||||
keyframe_data->keyframe_insert(bmain,
|
||||
slot_a,
|
||||
{"pose.bones[\"BoneA\"].rotation_quaternion", 0},
|
||||
{1, quaternion[0]},
|
||||
key_settings);
|
||||
keyframe_data->keyframe_insert(bmain,
|
||||
slot_a,
|
||||
{"pose.bones[\"BoneA\"].rotation_quaternion", 1},
|
||||
{1, quaternion[1]},
|
||||
key_settings);
|
||||
keyframe_data->keyframe_insert(bmain,
|
||||
slot_a,
|
||||
{"pose.bones[\"BoneA\"].rotation_quaternion", 2},
|
||||
{1, quaternion[2]},
|
||||
key_settings);
|
||||
keyframe_data->keyframe_insert(bmain,
|
||||
slot_a,
|
||||
{"pose.bones[\"BoneA\"].rotation_quaternion", 3},
|
||||
{1, quaternion[3]},
|
||||
key_settings);
|
||||
|
||||
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
const bke::PChanBone pchanbone_a{bone_a, bone_a->bone_get(*obj_armature_a)};
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
|
||||
/* First check that applying works if the rotation mode matches. */
|
||||
bone_a->rotmode = ROT_MODE_QUAT;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
EXPECT_NEAR(bone_a->quat[0], quaternion[0], 0.001);
|
||||
EXPECT_NEAR(bone_a->quat[1], quaternion[1], 0.001);
|
||||
EXPECT_NEAR(bone_a->quat[2], quaternion[2], 0.001);
|
||||
EXPECT_NEAR(bone_a->quat[3], quaternion[3], 0.001);
|
||||
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
float expected_matrix[4][4];
|
||||
copy_m4_m4(expected_matrix, bone_a->chan_mat);
|
||||
|
||||
/* Check that other rotation modes work the same as applying quaternion directly. */
|
||||
bone_a->rotmode = ROT_MODE_XYZ;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_AXISANGLE;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
reset_pose_bone_rotations(*bone_a);
|
||||
|
||||
/* Also test with blend factor other than 1. */
|
||||
bone_a->rotmode = ROT_MODE_QUAT;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
copy_m4_m4(expected_matrix, bone_a->chan_mat);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_AXISANGLE;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_XYZ;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
}
|
||||
|
||||
TEST_F(PoseTest, apply_action_differing_rotation_mode_from_axisangle)
|
||||
{
|
||||
/* When the pose has a different rotation mode than the data it is being applied to, the system
|
||||
* should convert the rotation. */
|
||||
Slot &slot_a = pose_action->slot_add_for_id(obj_armature_a->id);
|
||||
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 0}, {1, 0.66}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 1}, {1, -0.3}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 2}, {1, 0.86}, key_settings);
|
||||
keyframe_data->keyframe_insert(
|
||||
bmain, slot_a, {"pose.bones[\"BoneA\"].rotation_axis_angle", 3}, {1, -0.42}, key_settings);
|
||||
|
||||
bPoseChannel *bone_a = BKE_pose_channel_find_name(obj_armature_a->pose, "BoneA");
|
||||
const bke::PChanBone pchanbone_a{bone_a, bone_a->bone_get(*obj_armature_a)};
|
||||
AnimationEvalContext eval_context = {nullptr, 1.0f};
|
||||
|
||||
/* First check that applying works if the rotation mode matches. */
|
||||
bone_a->rotmode = ROT_MODE_AXISANGLE;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
EXPECT_NEAR(bone_a->rotAngle, 0.66, 0.001);
|
||||
EXPECT_NEAR(bone_a->rotAxis[0], -0.3, 0.001);
|
||||
EXPECT_NEAR(bone_a->rotAxis[1], 0.86, 0.001);
|
||||
EXPECT_NEAR(bone_a->rotAxis[2], -0.42, 0.001);
|
||||
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
float expected_matrix[4][4];
|
||||
copy_m4_m4(expected_matrix, bone_a->chan_mat);
|
||||
|
||||
/* Check that other rotation modes work the same as applying quaternion directly. */
|
||||
bone_a->rotmode = ROT_MODE_XYZ;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_QUAT;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 1.0);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
reset_pose_bone_rotations(*bone_a);
|
||||
|
||||
/* Also test with blend factor other than 1. */
|
||||
bone_a->rotmode = ROT_MODE_AXISANGLE;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
copy_m4_m4(expected_matrix, bone_a->chan_mat);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_QUAT;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
|
||||
bone_a->rotmode = ROT_MODE_XYZ;
|
||||
animrig::pose_apply_action({obj_armature_a}, *pose_action, &eval_context, 0.7);
|
||||
BKE_pchan_calc_mat(pchanbone_a);
|
||||
EXPECT_M4_NEAR(expected_matrix, bone_a->chan_mat, 0.001);
|
||||
}
|
||||
|
||||
} // namespace animrig::tests
|
||||
} // namespace blender
|
||||
406
blender-5.2.0/source/blender/animrig/intern/versioning.cc
Normal file
406
blender-5.2.0/source/blender/animrig/intern/versioning.cc
Normal file
@@ -0,0 +1,406 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
/* This is versioning code, so it's allowed to touch on deprecated DNA fields. */
|
||||
|
||||
#define DNA_DEPRECATED_ALLOW
|
||||
|
||||
#include "ANIM_action.hh"
|
||||
#include "ANIM_action_iterators.hh"
|
||||
#include "ANIM_action_legacy.hh"
|
||||
#include "ANIM_versioning.hh"
|
||||
|
||||
#include "DNA_action_types.h"
|
||||
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_nla.hh"
|
||||
#include "BKE_node.hh"
|
||||
#include "BKE_report.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
#include "BLI_string_utf8.h"
|
||||
|
||||
#include "BLT_translation.hh"
|
||||
|
||||
#include "BLO_readfile.hh"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
namespace blender::animrig::versioning {
|
||||
|
||||
bool action_is_layered(const bAction &dna_action)
|
||||
{
|
||||
/* NOTE: due to how forward-compatibility is handled when writing Actions to
|
||||
* blend files, it is important that this function does NOT check
|
||||
* `Action.idroot` as part of its determination of whether this is a layered
|
||||
* action or not.
|
||||
*
|
||||
* See: `action_blend_write()` and `action_blend_read_data()`
|
||||
*/
|
||||
|
||||
const animrig::Action &action = dna_action.wrap();
|
||||
|
||||
const bool has_layered_data = action.layer_array_num > 0 || action.slot_array_num > 0;
|
||||
const bool has_animato_data = !(action.curves.is_empty() && action.groups.is_empty());
|
||||
|
||||
return has_layered_data || !has_animato_data;
|
||||
}
|
||||
|
||||
void convert_legacy_animato_actions(Main &bmain)
|
||||
{
|
||||
for (bAction &dna_action : bmain.actions) {
|
||||
animrig::Action &action = dna_action.wrap();
|
||||
|
||||
if (action_is_layered(action) && !action.is_empty()) {
|
||||
/* This is just a safety net. Blender files that trigger this versioning code are not
|
||||
* expected to have any layered/slotted Actions.
|
||||
*
|
||||
* Empty Actions, even though they are valid "layered" Actions, should still get through
|
||||
* versioning, though, to ensure they have the default "Legacy Slot" and a zero idroot. */
|
||||
continue;
|
||||
}
|
||||
|
||||
convert_legacy_animato_action(action);
|
||||
}
|
||||
}
|
||||
|
||||
void convert_legacy_animato_action(bAction &dna_action)
|
||||
{
|
||||
Action &action = dna_action.wrap();
|
||||
/* Check that this is a legacy action.
|
||||
* Cannot use `!action_is_layered` because that would be false on empty actions. */
|
||||
BLI_assert(action.layer_array_num == 0 && action.slot_array_num == 0);
|
||||
|
||||
/* Store this ahead of time, because adding the slot sets the action's idroot
|
||||
* to 0. We also set the action's idroot to 0 manually, just to be defensive
|
||||
* so we don't depend on esoteric behavior in `slot_add()`. */
|
||||
const int16_t idtype = action.idroot;
|
||||
action.idroot = 0;
|
||||
|
||||
/* Initialize the Action's last_slot_handle field to its default value, before
|
||||
* we create a new slot. */
|
||||
action.last_slot_handle = DNA_DEFAULT_ACTION_LAST_SLOT_HANDLE;
|
||||
|
||||
Slot &slot = action.slot_add();
|
||||
slot.idtype = idtype;
|
||||
|
||||
const std::string slot_identifier{slot.idtype_string() +
|
||||
DATA_(legacy::DEFAULT_LEGACY_SLOT_NAME)};
|
||||
action.slot_identifier_define(slot, slot_identifier);
|
||||
|
||||
Layer &layer = action.layer_add(DATA_(legacy::DEFAULT_LEGACY_LAYER_NAME));
|
||||
animrig::Strip &strip = layer.strip_add(action, animrig::Strip::Type::Keyframe);
|
||||
Channelbag &bag = strip.data<StripKeyframeData>(action).channelbag_for_slot_ensure(slot);
|
||||
const int fcu_count = action.curves.count();
|
||||
const int group_count = action.groups.count();
|
||||
bag.fcurve_array = MEM_new_array_zeroed<FCurve *>(fcu_count, "Action versioning - fcurves");
|
||||
bag.fcurve_array_num = fcu_count;
|
||||
bag.group_array = MEM_new_array_zeroed<bActionGroup *>(group_count,
|
||||
"Action versioning - groups");
|
||||
bag.group_array_num = group_count;
|
||||
|
||||
int fcurve_index = 0;
|
||||
for (const auto [group_index, group] : action.groups.enumerate()) {
|
||||
bag.group_array[group_index] = &group;
|
||||
|
||||
group.channelbag = &bag;
|
||||
group.fcurve_range_start = fcurve_index;
|
||||
|
||||
for (FCurve &fcu : group.channels) {
|
||||
if (fcu.grp != &group) {
|
||||
break;
|
||||
}
|
||||
bag.fcurve_array[fcurve_index++] = &fcu;
|
||||
}
|
||||
|
||||
group.fcurve_range_length = fcurve_index - group.fcurve_range_start;
|
||||
}
|
||||
|
||||
for (FCurve &fcu : action.curves) {
|
||||
/* Any fcurves with groups have already been added to the fcurve array. */
|
||||
if (fcu.grp) {
|
||||
continue;
|
||||
}
|
||||
bag.fcurve_array[fcurve_index++] = &fcu;
|
||||
}
|
||||
|
||||
BLI_assert(fcurve_index == fcu_count);
|
||||
|
||||
action.curves = {nullptr, nullptr};
|
||||
action.groups = {nullptr, nullptr};
|
||||
}
|
||||
|
||||
void tag_action_user_for_slotted_actions_conversion(ID &animated_id)
|
||||
{
|
||||
animated_id.runtime->readfile_data->tags.action_assignment_needs_slot = true;
|
||||
}
|
||||
|
||||
void tag_action_users_for_slotted_actions_conversion(Main &bmain)
|
||||
{
|
||||
/* This function is only called when the blend-file is old enough to NOT use
|
||||
* slotted Actions, so we can safely tag anything that uses an Action. */
|
||||
|
||||
auto flag_adt = [](ID &animated_id,
|
||||
bAction *& /*action_ptr_ref*/,
|
||||
slot_handle_t & /*slot_handle_ref*/,
|
||||
char * /*last_slot_identifier*/) -> bool {
|
||||
tag_action_user_for_slotted_actions_conversion(animated_id);
|
||||
|
||||
/* Once tagged, the foreach loop can stop, because more tagging of the same
|
||||
* ID doesn't do anything. */
|
||||
return false;
|
||||
};
|
||||
|
||||
ID *id;
|
||||
FOREACH_MAIN_ID_BEGIN (&bmain, id) {
|
||||
foreach_action_slot_use_with_references(*id, flag_adt);
|
||||
|
||||
/* Process embedded IDs, as these are not listed in bmain, but still can
|
||||
* have their own Action+Slot. Unfortunately there is no generic looper
|
||||
* for embedded IDs. At this moment the only animatable embedded ID is a
|
||||
* node tree. */
|
||||
bNodeTree *node_tree = bke::node_tree_from_id(id);
|
||||
if (node_tree) {
|
||||
foreach_action_slot_use_with_references(node_tree->id, flag_adt);
|
||||
}
|
||||
}
|
||||
FOREACH_MAIN_ID_END;
|
||||
}
|
||||
|
||||
void convert_legacy_action_assignments(Main &bmain, ReportList *reports)
|
||||
{
|
||||
auto version_slot_assignment = [&](ID &animated_id,
|
||||
bAction *dna_action,
|
||||
PointerRNA &action_slot_owner_ptr,
|
||||
PropertyRNA &action_slot_prop,
|
||||
char *last_used_slot_identifier) {
|
||||
BLI_assert(dna_action); /* Ensured by the foreach loop. */
|
||||
Action &action = dna_action->wrap();
|
||||
|
||||
if (action.slot_array_num == 0) {
|
||||
/* There's a few reasons why this Action doesn't have a slot. It could simply be a slotted
|
||||
* Action without slots, or a legacy-but-not-yet-versioned Action, or it could be it is a
|
||||
* _really_ old (pre-2.50) Action. The latter are upgraded in do_versions_after_setup(), but
|
||||
* this function can be called earlier than that. So better gracefully skip those. */
|
||||
return true;
|
||||
}
|
||||
|
||||
/* If there is already a slot assigned, there's nothing to do here. */
|
||||
PointerRNA current_slot_ptr = RNA_property_pointer_get(&action_slot_owner_ptr,
|
||||
&action_slot_prop);
|
||||
if (current_slot_ptr.data) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Reset the "last used slot identifier" to the default "Legacy Slot". That way
|
||||
* generic_slot_for_autoassign() will pick up on legacy slots automatically.
|
||||
*
|
||||
* Note that this function should only run on legacy users of Actions, i.e. they are not
|
||||
* expected to have any last-used slot at all. The field in DNA can still be set, though,
|
||||
* because the 4.3 code already has the data model for slotted Actions. */
|
||||
|
||||
/* Ensure that the identifier has the correct ID type prefix. */
|
||||
*reinterpret_cast<short *>(last_used_slot_identifier) = GS(animated_id.name);
|
||||
|
||||
static_assert(Slot::identifier_length_max > 2); /* Because of the -2 below. */
|
||||
BLI_strncpy_utf8(last_used_slot_identifier + 2,
|
||||
DATA_(legacy::DEFAULT_LEGACY_SLOT_NAME),
|
||||
Slot::identifier_length_max - 2);
|
||||
|
||||
Slot *slot_to_assign = generic_slot_for_autoassign(
|
||||
animated_id, action, last_used_slot_identifier);
|
||||
if (!slot_to_assign) {
|
||||
/* This means that there is no slot that can be found by name, not even the "Legacy Slot"
|
||||
* name. Keep the ID unanimated, as this means that the referenced Action has changed
|
||||
* significantly since this file was opened. */
|
||||
BKE_reportf(reports,
|
||||
RPT_WARNING,
|
||||
"\"%s\" is using Action \"%s\", which does not have a slot with identifier "
|
||||
"\"%s\" or \"%s\". Manually assign the right action slot to \"%s\".\n",
|
||||
animated_id.name,
|
||||
action.id.name + 2,
|
||||
last_used_slot_identifier,
|
||||
animated_id.name,
|
||||
animated_id.name + 2);
|
||||
return true;
|
||||
}
|
||||
|
||||
PointerRNA slot_to_assign_ptr = RNA_pointer_create_discrete(
|
||||
&action.id, RNA_ActionSlot, slot_to_assign);
|
||||
RNA_property_pointer_set(
|
||||
&action_slot_owner_ptr, &action_slot_prop, slot_to_assign_ptr, reports);
|
||||
RNA_property_update_main(&bmain, nullptr, &action_slot_owner_ptr, &action_slot_prop);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Note that the code below does not remove the `action_assignment_needs_slot` tag. One ID can
|
||||
* use multiple Actions (via NLA, Action constraints, etc.); if one of those Action is a legacy
|
||||
* one from a linked datablock, this ID may needs to be re-visited after the library file was
|
||||
* versioned. Rather than trying to figure out if re-visiting is necessary, this function is safe
|
||||
* to call multiple times, and all that's lost is a little bit of CPU time. */
|
||||
|
||||
ID *id;
|
||||
FOREACH_MAIN_ID_BEGIN (&bmain, id) {
|
||||
/* Process the ID itself. */
|
||||
if (BLO_readfile_id_runtime_tags(*id).action_assignment_needs_slot) {
|
||||
foreach_action_slot_use_with_rna(*id, version_slot_assignment);
|
||||
}
|
||||
|
||||
/* Process embedded IDs, as these are not listed in bmain, but still can
|
||||
* have their own Action+Slot. Unfortunately there is no generic looper
|
||||
* for embedded IDs. At this moment the only animatable embedded ID is a
|
||||
* node tree. */
|
||||
bNodeTree *node_tree = bke::node_tree_from_id(id);
|
||||
if (node_tree && BLO_readfile_id_runtime_tags(node_tree->id).action_assignment_needs_slot) {
|
||||
foreach_action_slot_use_with_rna(node_tree->id, version_slot_assignment);
|
||||
}
|
||||
}
|
||||
FOREACH_MAIN_ID_END;
|
||||
}
|
||||
|
||||
void action_groups_reconstruct(bAction *act)
|
||||
{
|
||||
if (!act) {
|
||||
return;
|
||||
}
|
||||
/* Check that this is a legacy action.
|
||||
* Cannot use `!action_is_layered` because that would be false on empty actions. */
|
||||
BLI_assert(act->layer_array_num == 0 && act->slot_array_num == 0);
|
||||
/* Clear out all group channels. Channels that are actually in use are
|
||||
* reconstructed below; this step is necessary to clear out unused groups. */
|
||||
for (bActionGroup &group : act->groups) {
|
||||
group.channels.clear_no_delete();
|
||||
}
|
||||
/* Sort the channels into the group lists, destroying the act->curves list. */
|
||||
ListBaseT<FCurve> ungrouped = {nullptr, nullptr};
|
||||
for (FCurve &fcurve : act->curves.items_mutable()) {
|
||||
if (fcurve.grp) {
|
||||
BLI_assert(BLI_findindex(&act->groups, fcurve.grp) >= 0);
|
||||
BLI_addtail(&fcurve.grp->channels, &fcurve);
|
||||
}
|
||||
else {
|
||||
BLI_addtail(&ungrouped, &fcurve);
|
||||
}
|
||||
}
|
||||
/* Recombine into the main list. */
|
||||
act->curves.clear_no_delete();
|
||||
for (bActionGroup &group : act->groups) {
|
||||
/* Copy the list header to preserve the pointers in the group. */
|
||||
ListBase tmp = group.channels;
|
||||
BLI_movelisttolist(&act->curves, &tmp);
|
||||
}
|
||||
BLI_movelisttolist(&act->curves, &ungrouped);
|
||||
}
|
||||
|
||||
using IDFCurveCallback = FunctionRef<bool(ID *, FCurve *)>;
|
||||
|
||||
/**
|
||||
* Iterates over FCurves until the callback returns false or all FCurves were visited.
|
||||
*
|
||||
* \returns true if all FCurves were visited.
|
||||
*/
|
||||
static bool fcurves_listbase_apply_cb(ID *id,
|
||||
ListBaseT<FCurve> *fcurves,
|
||||
const IDFCurveCallback func)
|
||||
{
|
||||
for (FCurve &fcu : *fcurves) {
|
||||
if (!func(id, &fcu)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Helper for adt_apply_all_fcurves_cb() - Recursively go through each NLA strip */
|
||||
static bool nlastrips_apply_all_curves_cb(ID *id,
|
||||
ListBaseT<NlaStrip> *strips,
|
||||
const IDFCurveCallback func)
|
||||
{
|
||||
for (NlaStrip &strip : *strips) {
|
||||
if (strip.act) {
|
||||
if (!fcurves_listbase_apply_cb(id, &strip.act->curves, func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (!nlastrips_apply_all_curves_cb(id, &strip.strips, func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool adt_apply_all_fcurves_cb(ID *id, AnimData *adt, const IDFCurveCallback func)
|
||||
{
|
||||
if (adt->action) {
|
||||
if (!fcurves_listbase_apply_cb(id, &adt->action->curves, func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (adt->tmpact) {
|
||||
if (!fcurves_listbase_apply_cb(id, &adt->tmpact->curves, func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Drivers, stored as a list of F-Curves. */
|
||||
if (!fcurves_listbase_apply_cb(id, &adt->drivers, func)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* NLA Data - Animation Data for Strips */
|
||||
for (NlaTrack &nlt : adt->nla_tracks) {
|
||||
if (!nlastrips_apply_all_curves_cb(id, &nlt.strips, func)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void fcurves_id_cb(ID *id, const FunctionRef<void(ID *, FCurve *)> func)
|
||||
{
|
||||
AnimData *adt = BKE_animdata_from_id(id);
|
||||
if (adt != nullptr) {
|
||||
/* Use a little wrapper function to always return 'true' and thus keep the loop looping. */
|
||||
const auto wrapper = [&func](ID *id, FCurve *fcurve) {
|
||||
func(id, fcurve);
|
||||
return true;
|
||||
};
|
||||
adt_apply_all_fcurves_cb(id, adt, wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
void fcurves_main_cb(Main *bmain, const FunctionRef<void(ID *, FCurve *)> func)
|
||||
{
|
||||
/* Use a little wrapper function to always return 'true' and thus keep the loop looping. */
|
||||
const auto wrapper = [&func](ID *id, FCurve *fcurve) {
|
||||
func(id, fcurve);
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Use the AnimData-based function so that we don't have to reimplement all that stuff */
|
||||
BKE_animdata_main_cb(bmain,
|
||||
[&](ID *id, AnimData *adt) { adt_apply_all_fcurves_cb(id, adt, wrapper); });
|
||||
}
|
||||
|
||||
Vector<FCurve *> fcurves_for_legacy_action(bAction *action)
|
||||
{
|
||||
if (!action) {
|
||||
return {};
|
||||
}
|
||||
Vector<FCurve *> fcurves;
|
||||
for (FCurve &fcu : action->curves) {
|
||||
fcurves.append(&fcu);
|
||||
}
|
||||
return fcurves;
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::versioning
|
||||
@@ -0,0 +1,86 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/* The tests in this file need to be able to test deprecated data as well. */
|
||||
#define DNA_DEPRECATED_ALLOW
|
||||
|
||||
#include "ANIM_versioning.hh"
|
||||
|
||||
#include "DNA_action_types.h"
|
||||
|
||||
#include "BKE_gtest_base.hh"
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::animrig::versioning::tests {
|
||||
|
||||
class AnimrigVersioninTest : public bke::BlenderGTestBase {};
|
||||
|
||||
TEST_F(AnimrigVersioninTest, action_is_layered)
|
||||
{
|
||||
/* This unit test doesn't put valid data in the action under test. Since action_is_layered()
|
||||
* only looks at the length of lists, and not their contents, that should be fine. */
|
||||
|
||||
{ /* Animato Action only fcurves / Blender version [2.5, 4.4) */
|
||||
bAction action = {};
|
||||
Link /* FCurve */ fake_fcurve = {};
|
||||
|
||||
BLI_addtail(&action.curves, &fake_fcurve);
|
||||
EXPECT_FALSE(action_is_layered(action))
|
||||
<< "Animato Actions should NOT be considered 'layered'";
|
||||
}
|
||||
|
||||
{ /* Animato Action with fcurves + groups / Blender version [2.5, 4.4) */
|
||||
bAction action = {};
|
||||
Link /* FCurve */ fake_fcurve = {};
|
||||
Link /* bActionGroup */ fake_group = {};
|
||||
|
||||
BLI_addtail(&action.curves, &fake_fcurve);
|
||||
BLI_addtail(&action.groups, &fake_group);
|
||||
EXPECT_FALSE(action_is_layered(action))
|
||||
<< "Animato Actions should NOT be considered 'layered'";
|
||||
}
|
||||
|
||||
{ /* Animato Action with only groups / Blender version [2.5, 4.4) */
|
||||
bAction action = {};
|
||||
Link /* bActionGroup */ fake_group = {};
|
||||
|
||||
BLI_addtail(&action.groups, &fake_group);
|
||||
EXPECT_FALSE(action_is_layered(action))
|
||||
<< "Animato Actions should NOT be considered 'layered'";
|
||||
}
|
||||
|
||||
{ /* Layered Action with only layers / Blender version 4.4 and newer. */
|
||||
bAction action = {};
|
||||
action.layer_array_num = 1;
|
||||
|
||||
EXPECT_TRUE(action_is_layered(action)) << "Layered Actions should be considered 'layered'";
|
||||
}
|
||||
|
||||
{ /* Layered Action with only slots / Blender version 4.4 and newer. */
|
||||
bAction action = {};
|
||||
action.slot_array_num = 1;
|
||||
|
||||
EXPECT_TRUE(action_is_layered(action)) << "Layered Actions should be considered 'layered'";
|
||||
}
|
||||
|
||||
{ /* Layered Action as it exists on disk, with forward-compatible info in there. */
|
||||
bAction action = {};
|
||||
Link /* FCurve */ fake_fcurve = {};
|
||||
action.layer_array_num = 1;
|
||||
|
||||
BLI_addtail(&action.curves, &fake_fcurve);
|
||||
EXPECT_TRUE(action_is_layered(action))
|
||||
<< "Layered Actions with forward-compat data should be considered 'layered'";
|
||||
}
|
||||
|
||||
{ /* Completely zeroed out Action. */
|
||||
bAction action = {};
|
||||
EXPECT_TRUE(action_is_layered(action)) << "Zero'ed-out Actions should be considered 'layered'";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::animrig::versioning::tests
|
||||
272
blender-5.2.0/source/blender/animrig/intern/visualkey.cc
Normal file
272
blender-5.2.0/source/blender/animrig/intern/visualkey.cc
Normal file
@@ -0,0 +1,272 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup animrig
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
#include "ANIM_rna.hh"
|
||||
#include "ANIM_visualkey.hh"
|
||||
|
||||
#include "BKE_armature.hh"
|
||||
|
||||
#include "BLI_math_matrix.h"
|
||||
#include "BLI_math_rotation.h"
|
||||
|
||||
#include "DNA_constraint_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
#include "DNA_rigidbody_types.h"
|
||||
|
||||
#include "RNA_access.hh"
|
||||
#include "RNA_prototypes.hh"
|
||||
|
||||
namespace blender::animrig {
|
||||
|
||||
/* Internal status codes for visualkey_can_use. */
|
||||
enum {
|
||||
VISUALKEY_NONE = 0,
|
||||
VISUALKEY_LOC,
|
||||
VISUALKEY_ROT,
|
||||
VISUALKEY_SCA,
|
||||
};
|
||||
|
||||
bool visualkey_can_use(PointerRNA *ptr, PropertyRNA *prop)
|
||||
{
|
||||
bConstraint *con = nullptr;
|
||||
bool has_rigidbody = false;
|
||||
bool has_parent = false;
|
||||
|
||||
if (ELEM(nullptr, ptr, ptr->data, prop)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Get first constraint and determine type of keyframe constraints to check for
|
||||
* - constraints can be on either Objects or PoseChannels, so we only check if the
|
||||
* ptr->type is RNA_Object or RNA_PoseBone, which are the RNA wrapping-info for
|
||||
* those structs, allowing us to identify the owner of the data
|
||||
*/
|
||||
if (ptr->type == RNA_Object) {
|
||||
Object *ob = static_cast<Object *>(ptr->data);
|
||||
RigidBodyOb *rbo = ob->rigidbody_object;
|
||||
|
||||
con = static_cast<bConstraint *>(ob->constraints.first);
|
||||
has_parent = (ob->parent != nullptr);
|
||||
|
||||
/* Active rigidbody objects only, as only those are affected by sim. */
|
||||
has_rigidbody = ((rbo) && (rbo->type == RBO_TYPE_ACTIVE));
|
||||
}
|
||||
else if (ptr->type == RNA_PoseBone) {
|
||||
bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr->data);
|
||||
|
||||
if (pchan->constflag & (PCHAN_HAS_IK | PCHAN_INFLUENCED_BY_IK)) {
|
||||
/* Spline IK cannot generally be keyed visually, because (at least with the default
|
||||
* constraint settings) it requires non-uniform scaling that causes shearing in child bones,
|
||||
* which cannot be represented by the bone's loc/rot/scale properties. */
|
||||
return true;
|
||||
}
|
||||
|
||||
con = static_cast<bConstraint *>(pchan->constraints.first);
|
||||
has_parent = (pchan->parent != nullptr);
|
||||
}
|
||||
else {
|
||||
BLI_assert_msg(false,
|
||||
"visualkey_can_use called for data-block that is not an Object or PoseBone.");
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Parent or rigidbody are always matching, no need to check further. */
|
||||
if (has_parent || has_rigidbody) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Only do visual keying on transforms. */
|
||||
const char *identifier = RNA_property_identifier(prop);
|
||||
if (identifier == nullptr) {
|
||||
printf("%s failed: nullptr identifier\n", __func__);
|
||||
return false;
|
||||
}
|
||||
|
||||
short searchtype = VISUALKEY_NONE;
|
||||
if (strstr(identifier, "location")) {
|
||||
searchtype = VISUALKEY_LOC;
|
||||
}
|
||||
else if (strstr(identifier, "rotation")) {
|
||||
searchtype = VISUALKEY_ROT;
|
||||
}
|
||||
else if (strstr(identifier, "scale")) {
|
||||
searchtype = VISUALKEY_SCA;
|
||||
}
|
||||
else {
|
||||
printf("%s failed: identifier - '%s'\n", __func__, identifier);
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Check constraints. */
|
||||
for (; con; con = con->next) {
|
||||
/* only consider constraint if it is not disabled, and has influence */
|
||||
if (con->flag & CONSTRAINT_DISABLE) {
|
||||
continue;
|
||||
}
|
||||
if (con->enforce == 0.0f) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Some constraints may alter these transforms. */
|
||||
switch (con->type) {
|
||||
/* Multi-transform constraints. */
|
||||
case CONSTRAINT_TYPE_CHILDOF:
|
||||
case CONSTRAINT_TYPE_ARMATURE:
|
||||
return true;
|
||||
case CONSTRAINT_TYPE_TRANSFORM:
|
||||
case CONSTRAINT_TYPE_TRANSLIKE:
|
||||
return true;
|
||||
case CONSTRAINT_TYPE_FOLLOWPATH:
|
||||
return true;
|
||||
case CONSTRAINT_TYPE_KINEMATIC:
|
||||
return true;
|
||||
|
||||
/* Single-transform constraints. */
|
||||
case CONSTRAINT_TYPE_TRACKTO:
|
||||
if (searchtype == VISUALKEY_ROT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_DAMPTRACK:
|
||||
if (searchtype == VISUALKEY_ROT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_ROTLIMIT:
|
||||
if (searchtype == VISUALKEY_ROT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_LOCLIMIT:
|
||||
if (searchtype == VISUALKEY_LOC) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_SIZELIMIT:
|
||||
if (searchtype == VISUALKEY_SCA) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_DISTLIMIT:
|
||||
if (searchtype == VISUALKEY_LOC) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_ROTLIKE:
|
||||
if (searchtype == VISUALKEY_ROT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_LOCLIKE:
|
||||
if (searchtype == VISUALKEY_LOC) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_SIZELIKE:
|
||||
if (searchtype == VISUALKEY_SCA) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_LOCKTRACK:
|
||||
if (searchtype == VISUALKEY_ROT) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case CONSTRAINT_TYPE_MINMAX:
|
||||
if (searchtype == VISUALKEY_LOC) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Vector<float> visualkey_get_values(PointerRNA *ptr, PropertyRNA *prop)
|
||||
{
|
||||
Vector<float> values;
|
||||
const char *identifier = RNA_property_identifier(prop);
|
||||
float tmat[4][4];
|
||||
int rotmode;
|
||||
|
||||
/* Handle for Objects or PoseChannels only
|
||||
* - only Location, Rotation or Scale keyframes are supported currently
|
||||
* - constraints can be on either Objects or PoseChannels, so we only check if the
|
||||
* ptr->type is RNA_Object or RNA_PoseBone, which are the RNA wrapping-info for
|
||||
* those structs, allowing us to identify the owner of the data
|
||||
* - assume that array_index will be sane
|
||||
*/
|
||||
if (ptr->type == RNA_Object) {
|
||||
Object *ob = static_cast<Object *>(ptr->data);
|
||||
/* Loc code is specific... */
|
||||
if (strstr(identifier, "location")) {
|
||||
values.extend({ob->object_to_world().location(), 3});
|
||||
return values;
|
||||
}
|
||||
|
||||
copy_m4_m4(tmat, ob->object_to_world().ptr());
|
||||
rotmode = ob->rotmode;
|
||||
}
|
||||
else if (ptr->type == RNA_PoseBone) {
|
||||
Object *ob = id_cast<Object *>(ptr->owner_id);
|
||||
bPoseChannel *pchan = static_cast<bPoseChannel *>(ptr->data);
|
||||
Bone *bone = pchan->bone_get(*ob);
|
||||
|
||||
BKE_armature_mat_pose_to_bone({pchan, bone}, pchan->pose_mat, tmat);
|
||||
rotmode = pchan->rotmode;
|
||||
|
||||
/* Loc code is specific... */
|
||||
if (strstr(identifier, "location")) {
|
||||
/* Only use for non-connected bones. */
|
||||
if ((bone->parent == nullptr) || !(bone->flag & BONE_CONNECTED)) {
|
||||
values.extend({tmat[3], 3});
|
||||
return values;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
return get_rna_values(ptr, prop);
|
||||
}
|
||||
|
||||
/* Rot/Scale code are common! */
|
||||
if (strstr(identifier, "rotation_euler")) {
|
||||
values.resize(3);
|
||||
mat4_to_eulO(values.data(), rotmode, tmat);
|
||||
return values;
|
||||
}
|
||||
|
||||
if (strstr(identifier, "rotation_quaternion")) {
|
||||
values.resize(4);
|
||||
mat4_to_quat(values.data(), tmat);
|
||||
return values;
|
||||
}
|
||||
|
||||
if (strstr(identifier, "rotation_axis_angle")) {
|
||||
/* w = 0, x,y,z = 1,2,3 */
|
||||
values.resize(4);
|
||||
mat4_to_axis_angle(values.data() + 1, values.data() + 0, tmat);
|
||||
return values;
|
||||
}
|
||||
|
||||
if (strstr(identifier, "scale")) {
|
||||
values.resize(3);
|
||||
mat4_to_size(values.data(), tmat);
|
||||
return values;
|
||||
}
|
||||
|
||||
/* As the function hasn't returned yet, read value from system in the default way. */
|
||||
return get_rna_values(ptr, prop);
|
||||
}
|
||||
} // namespace blender::animrig
|
||||
399
blender-5.2.0/source/blender/asset_system/AS_asset_catalog.hh
Normal file
399
blender-5.2.0/source/blender/asset_system/AS_asset_catalog.hh
Normal file
@@ -0,0 +1,399 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <set>
|
||||
#include <string>
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_map.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_uuid.h"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "AS_asset_catalog_path.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class AssetCatalog;
|
||||
class AssetCatalogCollection;
|
||||
class AssetCatalogDefinitionFile;
|
||||
class AssetCatalogFilter;
|
||||
class AssetCatalogTree;
|
||||
|
||||
using CatalogID = UUID;
|
||||
using CatalogPathComponent = std::string;
|
||||
/* Would be nice to be able to use `std::filesystem::path` for this, but it's currently not
|
||||
* available on the minimum macOS target version. */
|
||||
using CatalogFilePath = std::string;
|
||||
using OwningAssetCatalogMap = Map<CatalogID, std::unique_ptr<AssetCatalog>>;
|
||||
|
||||
/* Manages the asset catalogs of a single asset library (i.e. of catalogs defined in a single
|
||||
* directory hierarchy). */
|
||||
class AssetCatalogService {
|
||||
std::unique_ptr<AssetCatalogCollection> catalog_collection_;
|
||||
|
||||
/**
|
||||
* Cached catalog tree storage. Lazy-created by #AssetCatalogService::catalog_tree().
|
||||
*/
|
||||
std::shared_ptr<AssetCatalogTree> catalog_tree_;
|
||||
std::recursive_mutex catalog_tree_mutex_;
|
||||
|
||||
Vector<std::unique_ptr<AssetCatalogCollection>> undo_snapshots_;
|
||||
Vector<std::unique_ptr<AssetCatalogCollection>> redo_snapshots_;
|
||||
|
||||
CatalogFilePath asset_library_root_;
|
||||
bool is_read_only_ = false;
|
||||
|
||||
friend class AssetLibraryService;
|
||||
friend class AssetLibrary;
|
||||
|
||||
public:
|
||||
static const CatalogFilePath DEFAULT_CATALOG_FILENAME;
|
||||
|
||||
struct read_only_tag {};
|
||||
|
||||
explicit AssetCatalogService(const CatalogFilePath &asset_library_root = {},
|
||||
std::optional<read_only_tag> read_only_tag = std::nullopt);
|
||||
explicit AssetCatalogService(read_only_tag);
|
||||
~AssetCatalogService();
|
||||
|
||||
/**
|
||||
* Set tag indicating that some catalog modifications are unsaved, which could
|
||||
* get lost on exit. This tag is not set by internal catalog code, the catalog
|
||||
* service user is responsible for it. It is cleared by #write_to_disk().
|
||||
*
|
||||
* This "dirty" state is tracked per catalog, so that it's possible to gracefully load changes
|
||||
* from disk. Any catalog with unsaved changes will not be overwritten by on-disk changes. */
|
||||
void tag_has_unsaved_changes(AssetCatalog *edited_catalog = nullptr);
|
||||
bool has_unsaved_changes() const;
|
||||
|
||||
/**
|
||||
* Check if this is a read-only service meaning the user shouldn't be able to do edits. This is
|
||||
* not enforced by internal catalog code, the catalog service user is responsible for it. For
|
||||
* example the UI should disallow edits.
|
||||
*/
|
||||
bool is_read_only() const;
|
||||
|
||||
/** Load asset catalog definitions from the files found in the asset library. */
|
||||
void load_from_disk();
|
||||
/** Load asset catalog definitions from the given file or directory. */
|
||||
void load_from_disk(const CatalogFilePath &file_or_directory_path);
|
||||
|
||||
/**
|
||||
* Duplicate the catalogs from \a other_service into this one. Does not rebuild the tree, this
|
||||
* needs to be done by the caller (call #rebuild_tree()!).
|
||||
*
|
||||
* \note If a catalog from \a other already exists in this collection (identified by catalog ID),
|
||||
* it will be skipped and \a on_duplicate_items will be called.
|
||||
*/
|
||||
void add_from_existing(const AssetCatalogService &other_service,
|
||||
FunctionRef<void(const AssetCatalog &existing,
|
||||
const AssetCatalog &to_be_ignored)> on_duplicate_items);
|
||||
|
||||
/**
|
||||
* Write the catalog definitions to disk.
|
||||
*
|
||||
* The location where the catalogs are saved is variable, and depends on the location of the
|
||||
* blend file. The first matching rule wins:
|
||||
*
|
||||
* - Already loaded a CDF from disk?
|
||||
* -> Always write to that file.
|
||||
* - The directory containing the blend file has a blender_assets.cats.txt file?
|
||||
* -> Merge with & write to that file.
|
||||
* - The directory containing the blend file is part of an asset library, as per
|
||||
* the user's preferences?
|
||||
* -> Merge with & write to ${ASSET_LIBRARY_ROOT}/blender_assets.cats.txt
|
||||
* - Create a new file blender_assets.cats.txt next to the blend file.
|
||||
*
|
||||
* Return true on success, which either means there were no in-memory categories to save,
|
||||
* or the save was successful. */
|
||||
bool write_to_disk(const CatalogFilePath &blend_file_path);
|
||||
|
||||
/**
|
||||
* Ensure that the next call to #on_blend_save_post() will choose a new location for the CDF
|
||||
* suitable for the location of the blend file (regardless of where the current catalogs come
|
||||
* from), and that catalogs will be merged with already-existing ones in that location.
|
||||
*
|
||||
* Use this for a "Save as..." that has to write the catalogs to the new blend file location,
|
||||
* instead of updating the previously read CDF. */
|
||||
void prepare_to_merge_on_write();
|
||||
|
||||
/**
|
||||
* Merge on-disk changes into the in-memory asset catalogs.
|
||||
* This should be called before writing the asset catalogs to disk.
|
||||
*
|
||||
* - New on-disk catalogs are loaded into memory.
|
||||
* - Already-known on-disk catalogs are ignored (so will be overwritten with our in-memory
|
||||
* data). This includes in-memory marked-as-deleted catalogs.
|
||||
*/
|
||||
void reload_catalogs();
|
||||
|
||||
/** Return catalog with the given ID. Return nullptr if not found. */
|
||||
AssetCatalog *find_catalog(CatalogID catalog_id) const;
|
||||
|
||||
/**
|
||||
* Return first catalog with the given path. Return nullptr if not found. This is not an
|
||||
* efficient call as it's just a linear search over the catalogs.
|
||||
*
|
||||
* If there are multiple catalogs with the same path, return the first-loaded one. If there is
|
||||
* none marked as "first loaded", return the one with the lowest UUID. */
|
||||
AssetCatalog *find_catalog_by_path(const AssetCatalogPath &path) const;
|
||||
|
||||
/**
|
||||
* Return true only if this catalog is known.
|
||||
* This treats deleted catalogs as "unknown". */
|
||||
bool is_catalog_known(CatalogID catalog_id) const;
|
||||
|
||||
/**
|
||||
* Create a filter object that can be used to determine whether an asset belongs to the given
|
||||
* catalog, or any of the catalogs in the sub-tree rooted at the given catalog.
|
||||
*
|
||||
* \see #AssetCatalogFilter
|
||||
*/
|
||||
AssetCatalogFilter create_catalog_filter(CatalogID active_catalog_id) const;
|
||||
|
||||
/**
|
||||
* Create a catalog with some sensible auto-generated catalog ID.
|
||||
* The catalog will be saved to the default catalog file.
|
||||
*
|
||||
* NOTE: this does NOT mark the catalog service itself as 'has changes'. The caller is
|
||||
* responsible for that.
|
||||
*
|
||||
* \see #tag_has_unsaved_changes()
|
||||
*/
|
||||
AssetCatalog *create_catalog(const AssetCatalogPath &catalog_path);
|
||||
|
||||
/**
|
||||
* Delete all catalogs with the given path, and their children.
|
||||
*/
|
||||
void prune_catalogs_by_path(const AssetCatalogPath &path);
|
||||
|
||||
/**
|
||||
* Delete all catalogs with the same path as the identified catalog, and their children.
|
||||
* This call is the same as calling `prune_catalogs_by_path(find_catalog(catalog_id)->path)`.
|
||||
*/
|
||||
void prune_catalogs_by_id(CatalogID catalog_id);
|
||||
|
||||
/**
|
||||
* Update the catalog path, also updating the catalog path of all sub-catalogs.
|
||||
*/
|
||||
void update_catalog_path(CatalogID catalog_id, const AssetCatalogPath &new_catalog_path);
|
||||
|
||||
/**
|
||||
* May be called from multiple threads.
|
||||
*/
|
||||
std::shared_ptr<const AssetCatalogTree> catalog_tree();
|
||||
|
||||
/** Return true only if there are no catalogs known. */
|
||||
bool is_empty() const;
|
||||
|
||||
/**
|
||||
* Store the current catalogs in the undo stack.
|
||||
* This snapshots everything in the #AssetCatalogCollection. */
|
||||
void undo_push();
|
||||
/**
|
||||
* Restore the last-saved undo snapshot, pushing the current state onto the redo stack.
|
||||
* The caller is responsible for first checking that undoing is possible.
|
||||
*/
|
||||
void undo();
|
||||
bool is_undo_possbile() const;
|
||||
/**
|
||||
* Restore the last-saved redo snapshot, pushing the current state onto the undo stack.
|
||||
* The caller is responsible for first checking that undoing is possible. */
|
||||
void redo();
|
||||
bool is_redo_possbile() const;
|
||||
|
||||
protected:
|
||||
void load_directory_recursive(const CatalogFilePath &directory_path);
|
||||
void load_single_file(const CatalogFilePath &catalog_definition_file_path);
|
||||
|
||||
/** Implementation of #write_to_disk() that doesn't clear the "has unsaved changes" tag. */
|
||||
bool write_to_disk_ex(const CatalogFilePath &blend_file_path);
|
||||
void untag_has_unsaved_changes();
|
||||
bool is_catalog_known_with_unsaved_changes(CatalogID catalog_id) const;
|
||||
|
||||
/**
|
||||
* Delete catalogs, only keeping them when they are either listed in
|
||||
* \a catalogs_to_keep or have unsaved changes.
|
||||
*
|
||||
* \note Deleted catalogs are hard-deleted, i.e. they just vanish instead of
|
||||
* remembering them as "deleted".
|
||||
*/
|
||||
void purge_catalogs_not_listed(const Set<CatalogID> &catalogs_to_keep);
|
||||
|
||||
/**
|
||||
* Delete a catalog, without deleting any of its children and without rebuilding the catalog
|
||||
* tree. The deletion in "Soft", in the sense that the catalog pointer is moved from `catalogs_`
|
||||
* to `deleted_catalogs_`; the AssetCatalog instance itself is kept in memory. As a result, it
|
||||
* will be removed from a CDF when saved to disk.
|
||||
*
|
||||
* This is a lower-level function than #prune_catalogs_by_path.
|
||||
*
|
||||
* NOTE: this does NOT mark the catalog service itself as 'has changes'. The caller is
|
||||
* responsible for that.
|
||||
*
|
||||
* \see #tag_has_unsaved_changes()
|
||||
*/
|
||||
void delete_catalog_by_id_soft(CatalogID catalog_id);
|
||||
|
||||
/**
|
||||
* Hard delete a catalog. This simply removes the catalog from existence. The deletion will not
|
||||
* be remembered, and reloading the CDF will bring it back. */
|
||||
void delete_catalog_by_id_hard(CatalogID catalog_id);
|
||||
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> parse_catalog_file(
|
||||
const CatalogFilePath &catalog_definition_file_path);
|
||||
|
||||
/**
|
||||
* Construct an in-memory catalog definition file (CDF) from the currently known catalogs.
|
||||
* This object can then be processed further before saving to disk. */
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> construct_cdf_in_memory(
|
||||
const CatalogFilePath &file_path) const;
|
||||
|
||||
/**
|
||||
* Find a suitable path to write a CDF to.
|
||||
*
|
||||
* This depends on the location of the blend file, and on whether a CDF already exists next to it
|
||||
* or whether the blend file is saved inside an asset library.
|
||||
*/
|
||||
static CatalogFilePath find_suitable_cdf_path_for_writing(
|
||||
const CatalogFilePath &blend_file_path);
|
||||
|
||||
std::unique_ptr<AssetCatalogTree> read_into_tree() const;
|
||||
/**
|
||||
* Ensure a #catalog_tree() will update the tree. Must be called whenever the contained user
|
||||
* visible catalogs change.
|
||||
* May be called from multiple threads.
|
||||
*/
|
||||
void invalidate_catalog_tree();
|
||||
|
||||
/**
|
||||
* For every catalog, ensure that its parent path also has a known catalog.
|
||||
*/
|
||||
void create_missing_catalogs();
|
||||
|
||||
/**
|
||||
* For every catalog, mark it as "dirty".
|
||||
*/
|
||||
void tag_all_catalogs_as_unsaved_changes();
|
||||
|
||||
/* For access by subclasses, as those will not be marked as friend by #AssetCatalogCollection. */
|
||||
const AssetCatalogDefinitionFile *get_catalog_definition_file() const;
|
||||
const OwningAssetCatalogMap &get_catalogs() const;
|
||||
const OwningAssetCatalogMap &get_deleted_catalogs() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* Asset Catalog definition, containing a symbolic ID and a path that points to a node in the
|
||||
* catalog hierarchy.
|
||||
*
|
||||
* \warning The asset system may reload catalogs, invalidating pointers. Thus it's not recommended
|
||||
* to store pointers to asset catalogs. Store the #CatalogID instead and do a lookup when
|
||||
* needed.
|
||||
*/
|
||||
class AssetCatalog {
|
||||
public:
|
||||
const CatalogID catalog_id;
|
||||
AssetCatalogPath path;
|
||||
/**
|
||||
* Simple, human-readable name for the asset catalog. This is stored on assets alongside the
|
||||
* catalog ID; the catalog ID is a UUID that is not human-readable,
|
||||
* so to avoid complete data-loss when the catalog definition file gets lost,
|
||||
* we also store a human-readable simple name for the catalog.
|
||||
*
|
||||
* It should fit in sizeof(AssetMetaData::catalog_simple_name) bytes. */
|
||||
std::string simple_name;
|
||||
|
||||
struct Flags {
|
||||
/* Treat this catalog as deleted. Keeping deleted catalogs around is necessary to support
|
||||
* merging of on-disk changes with in-memory changes. */
|
||||
bool is_deleted = false;
|
||||
|
||||
/* Sort this catalog first when there are multiple catalogs with the same catalog path. This
|
||||
* ensures that in a situation where missing catalogs were auto-created, and then
|
||||
* load-and-merged with a file that also has these catalogs, the first one in that file is
|
||||
* always sorted first, regardless of the sort order of its UUID. */
|
||||
bool is_first_loaded = false;
|
||||
|
||||
/* Merging on-disk changes into memory will not overwrite this catalog.
|
||||
* For example, when a catalog was renamed (i.e. changed path) in this Blender session,
|
||||
* reloading the catalog definition file should not overwrite that change.
|
||||
*
|
||||
* Note that this flag is ignored when is_deleted=true; deleted catalogs that are still in
|
||||
* memory are considered "unsaved" by definition. */
|
||||
bool has_unsaved_changes = false;
|
||||
} flags;
|
||||
|
||||
AssetCatalog() = delete;
|
||||
AssetCatalog(CatalogID catalog_id, const AssetCatalogPath &path, const std::string &simple_name);
|
||||
|
||||
/**
|
||||
* Create a new Catalog with the given path, auto-generating a sensible catalog simple-name.
|
||||
*
|
||||
* NOTE: the given path will be cleaned up (trailing spaces removed, etc.), so the returned
|
||||
* `AssetCatalog`'s path differ from the given one.
|
||||
*/
|
||||
static std::unique_ptr<AssetCatalog> from_path(const AssetCatalogPath &path);
|
||||
|
||||
/** Make a new simple name for the catalog, based on its path. */
|
||||
void simple_name_refresh();
|
||||
|
||||
protected:
|
||||
/** Generate a sensible catalog ID for the given path. */
|
||||
static std::string sensible_simple_name_for_path(const AssetCatalogPath &path);
|
||||
};
|
||||
|
||||
/** Comparator for asset catalogs, ordering by (path, first_seen, UUID). */
|
||||
struct AssetCatalogLessThan {
|
||||
bool operator()(const AssetCatalog *lhs, const AssetCatalog *rhs) const
|
||||
{
|
||||
if (lhs->path != rhs->path) {
|
||||
return lhs->path < rhs->path;
|
||||
}
|
||||
|
||||
if (lhs->flags.is_first_loaded != rhs->flags.is_first_loaded) {
|
||||
return lhs->flags.is_first_loaded;
|
||||
}
|
||||
|
||||
return lhs->catalog_id < rhs->catalog_id;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set that stores catalogs ordered by (path, UUID).
|
||||
* Being a set, duplicates are removed. The catalog's simple name is ignored in this. */
|
||||
using AssetCatalogOrderedSet = std::set<const AssetCatalog *, AssetCatalogLessThan>;
|
||||
using MutableAssetCatalogOrderedSet = std::set<AssetCatalog *, AssetCatalogLessThan>;
|
||||
|
||||
/**
|
||||
* Filter that can determine whether an asset should be visible or not, based on its catalog ID.
|
||||
*
|
||||
* \see AssetCatalogService::create_catalog_filter()
|
||||
*/
|
||||
class AssetCatalogFilter {
|
||||
const Set<CatalogID> matching_catalog_ids_;
|
||||
const Set<CatalogID> known_catalog_ids_;
|
||||
|
||||
friend AssetCatalogService;
|
||||
|
||||
public:
|
||||
bool contains(CatalogID asset_catalog_id) const;
|
||||
|
||||
/* So that all unknown catalogs can be shown under "Unassigned". */
|
||||
bool is_known(CatalogID asset_catalog_id) const;
|
||||
|
||||
protected:
|
||||
explicit AssetCatalogFilter(Set<CatalogID> &&matching_catalog_ids,
|
||||
Set<CatalogID> &&known_catalog_ids);
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,131 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* Location of an Asset Catalog in the catalog tree, denoted by slash-separated path components.
|
||||
*
|
||||
* Each path component is a string that is not allowed to have slashes or colons. The latter is to
|
||||
* make things easy to save in the colon-delimited Catalog Definition File format.
|
||||
*
|
||||
* The path of a catalog determines where in the catalog hierarchy the catalog is shown. Examples
|
||||
* are "Characters/Ellie/Poses/Hand" or "Kit_bash/City/Skyscrapers". The path looks like a
|
||||
* file-system path, with a few differences:
|
||||
*
|
||||
* - Only slashes are used as path component separators.
|
||||
* - All paths are absolute, so there is no need for a leading slash.
|
||||
*
|
||||
* See https://developer.blender.org/docs/features/asset_system/backend/asset_catalogs/
|
||||
*
|
||||
* Paths are stored as byte sequences, and assumed to be UTF8.
|
||||
*/
|
||||
class AssetCatalogPath {
|
||||
friend std::ostream &operator<<(std::ostream &stream, const AssetCatalogPath &path_to_append);
|
||||
|
||||
/**
|
||||
* The path itself, such as "Agents/Secret/327".
|
||||
*/
|
||||
std::string path_;
|
||||
|
||||
public:
|
||||
static const char SEPARATOR;
|
||||
|
||||
AssetCatalogPath() = default;
|
||||
AssetCatalogPath(StringRef path);
|
||||
AssetCatalogPath(std::string path);
|
||||
AssetCatalogPath(const char *path);
|
||||
AssetCatalogPath(const AssetCatalogPath &other_path) = default;
|
||||
AssetCatalogPath(AssetCatalogPath &&other_path) noexcept;
|
||||
~AssetCatalogPath() = default;
|
||||
|
||||
uint64_t hash() const;
|
||||
uint64_t length() const; /* Length of the path in bytes. */
|
||||
|
||||
/** C-string representation of the path. */
|
||||
const char *c_str() const;
|
||||
const std::string &str() const;
|
||||
|
||||
/* The last path component, used as label in the tree view. */
|
||||
StringRefNull name() const;
|
||||
|
||||
/* In-class operators, because of the implicit `AssetCatalogPath(StringRef)` constructor.
|
||||
* Otherwise `string == string` could cast both sides to `AssetCatalogPath`. */
|
||||
bool operator==(const AssetCatalogPath &other_path) const;
|
||||
bool operator!=(const AssetCatalogPath &other_path) const;
|
||||
bool operator<(const AssetCatalogPath &other_path) const;
|
||||
AssetCatalogPath &operator=(const AssetCatalogPath &other_path) = default;
|
||||
AssetCatalogPath &operator=(AssetCatalogPath &&other_path) = default;
|
||||
|
||||
/** Concatenate two paths, returning the new path. */
|
||||
AssetCatalogPath operator/(const AssetCatalogPath &path_to_append) const;
|
||||
|
||||
/* False when the path is empty, true otherwise. */
|
||||
operator bool() const;
|
||||
|
||||
/** Creates and ensures that the path is cleaned up. */
|
||||
static AssetCatalogPath from_user_input(const char *path);
|
||||
|
||||
/**
|
||||
* Clean up the path. This ensures:
|
||||
* - Every path component is stripped of its leading/trailing spaces.
|
||||
* - Empty components (caused by double slashes or leading/trailing slashes) are removed.
|
||||
* - Invalid characters are replaced with valid ones.
|
||||
*/
|
||||
[[nodiscard]] AssetCatalogPath cleanup() const;
|
||||
|
||||
/**
|
||||
* \return true only if the given path is a parent of this catalog's path.
|
||||
* When this catalog's path is equal to the given path, return true as well.
|
||||
* In other words, this defines a weak subset.
|
||||
*
|
||||
* True: "some/path/there" is contained in "some/path" and "some".
|
||||
* False: "path/there" is not contained in "some/path/there".
|
||||
*
|
||||
* Note that non-cleaned-up paths (so for example starting or ending with a
|
||||
* slash) are not supported, and result in undefined behavior.
|
||||
*/
|
||||
bool is_contained_in(const AssetCatalogPath &other_path) const;
|
||||
|
||||
/**
|
||||
* \return the parent path, or an empty path if there is no parent.
|
||||
*/
|
||||
AssetCatalogPath parent() const;
|
||||
|
||||
/**
|
||||
* Change the initial part of the path from `from_path` to `to_path`.
|
||||
* If this path does not start with `from_path`, return an empty path as result.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* AssetCatalogPath path("some/path/to/some/catalog");
|
||||
* path.rebase("some/path", "new/base") -> "new/base/to/some/catalog"
|
||||
*/
|
||||
AssetCatalogPath rebase(const AssetCatalogPath &from_path,
|
||||
const AssetCatalogPath &to_path) const;
|
||||
|
||||
/** Call the callback function for each path component, in left-to-right order. */
|
||||
using ComponentIteratorFn = FunctionRef<void(StringRef component_name, bool is_last_component)>;
|
||||
void iterate_components(ComponentIteratorFn callback) const;
|
||||
|
||||
protected:
|
||||
/** Strip leading/trailing spaces and replace disallowed characters. */
|
||||
static std::string cleanup_component(StringRef component_name);
|
||||
};
|
||||
|
||||
/** Output the path as string. */
|
||||
std::ostream &operator<<(std::ostream &stream, const AssetCatalogPath &path_to_append);
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,109 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*
|
||||
* A representation of the catalog paths as tree structure. Each component of the catalog tree is
|
||||
* represented by an #AssetCatalogTreeItem. The last path component of an item is used as its name,
|
||||
* which may also be shown to the user.
|
||||
* An item can not have multiple children with the same name. That means the name uniquely
|
||||
* identifies an item within its parent.
|
||||
*
|
||||
* There is no single root tree element, the #AssetCatalogTree instance itself represents the root.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* Representation of a catalog path in the #AssetCatalogTree.
|
||||
*/
|
||||
class AssetCatalogTreeItem {
|
||||
public:
|
||||
/** Container for child items. Uses a #std::map to keep items ordered by their name (i.e. their
|
||||
* last catalog component). */
|
||||
using ChildMap = std::map<std::string, AssetCatalogTreeItem>;
|
||||
using ItemIterFn = FunctionRef<void(const AssetCatalogTreeItem &)>;
|
||||
|
||||
private:
|
||||
/** Child tree items, ordered by their names. */
|
||||
ChildMap children_;
|
||||
/** The user visible name of this component. */
|
||||
CatalogPathComponent name_;
|
||||
CatalogID catalog_id_;
|
||||
/** Copy of #AssetCatalog::simple_name. */
|
||||
std::string simple_name_;
|
||||
/** Copy of #AssetCatalog::flags.has_unsaved_changes. */
|
||||
bool has_unsaved_changes_ = false;
|
||||
|
||||
/** Pointer back to the parent item. Used to reconstruct the hierarchy from an item (e.g. to
|
||||
* build a path). */
|
||||
const AssetCatalogTreeItem *parent_ = nullptr;
|
||||
|
||||
friend class AssetCatalogTree;
|
||||
|
||||
public:
|
||||
AssetCatalogTreeItem(StringRef name,
|
||||
CatalogID catalog_id,
|
||||
StringRef simple_name,
|
||||
const AssetCatalogTreeItem *parent = nullptr);
|
||||
|
||||
CatalogID get_catalog_id() const;
|
||||
StringRefNull get_simple_name() const;
|
||||
StringRefNull get_name() const;
|
||||
bool has_unsaved_changes() const;
|
||||
/** Return the full catalog path, defined as the name of this catalog prefixed by the full
|
||||
* catalog path of its parent and a separator. */
|
||||
AssetCatalogPath catalog_path() const;
|
||||
int count_parents() const;
|
||||
bool has_children() const;
|
||||
|
||||
/** Iterate over children calling \a callback for each of them, but do not recurse into their
|
||||
* children. */
|
||||
void foreach_child(ItemIterFn callback) const;
|
||||
void foreach_item(ItemIterFn callback) const;
|
||||
|
||||
private:
|
||||
static void foreach_item_recursive(const ChildMap &children_, ItemIterFn callback);
|
||||
};
|
||||
|
||||
class AssetCatalogTree {
|
||||
using ChildMap = AssetCatalogTreeItem::ChildMap;
|
||||
using ItemIterFn = AssetCatalogTreeItem::ItemIterFn;
|
||||
|
||||
/** Child tree items, ordered by their names. */
|
||||
ChildMap root_items_;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Ensure an item representing \a catalog is in the tree, adding it if necessary.
|
||||
*
|
||||
* \param skip_prefix: If set and the catalog path starts with this prefix path, the prefix path
|
||||
* will be stripped, and the catalog will be inserted into the tree as if it started after
|
||||
* this prefix. For example if the path of \a catalog is "Lorem ipsum/dolor/sit", and \a
|
||||
* skip_prefix is set to "Lorem ipsum/dolor", then the catalog will be inserted as if the path
|
||||
* was "sit". Catalogs whose path do not start with the prefix will be unaffected.
|
||||
*/
|
||||
void insert_item(const AssetCatalog &catalog,
|
||||
std::optional<StringRef> skip_prefix = std::nullopt);
|
||||
|
||||
void foreach_item(ItemIterFn callback) const;
|
||||
/** Iterate over root items calling \a callback for each of them, but do not recurse into their
|
||||
* children. */
|
||||
void foreach_root_item(ItemIterFn callback) const;
|
||||
|
||||
bool is_empty() const;
|
||||
|
||||
const AssetCatalogTreeItem *find_item(const AssetCatalogPath &path) const;
|
||||
const AssetCatalogTreeItem *find_root_item(const AssetCatalogPath &path) const;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,30 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* Status of the asset's file(s) on disk, compared to the remote asset listing.
|
||||
*/
|
||||
enum class RemoteAssetFileStatus {
|
||||
/** Just so you can recognize a zero-initialized field of this type. */
|
||||
UNSET = 0,
|
||||
/** The asset's main file does not exist on disk. */
|
||||
NOT_ON_DISK = 1,
|
||||
/** All the asset's files exist on disk, and match the listing's hashes. */
|
||||
MATCH = 2,
|
||||
/** At least one of the asset's files exists on disk, but doesn't match the listing's hash. */
|
||||
NO_MATCH = 3,
|
||||
/* In the future there will likely be another option here: INCOMPLETE. It will indicate that the
|
||||
* asset's main file, which contains the asset datablock, exists, but the asset's other files do
|
||||
* not. As such, this will only be added when Blender supports multi-file assets. */
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
401
blender-5.2.0/source/blender/asset_system/AS_asset_library.hh
Normal file
401
blender-5.2.0/source/blender/asset_system/AS_asset_library.hh
Normal file
@@ -0,0 +1,401 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_representation.hh" /* For URLWithHash. */
|
||||
|
||||
#include "DNA_asset_types.h"
|
||||
|
||||
#include "BLI_mutex.hh"
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "BKE_callbacks.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct Main;
|
||||
|
||||
namespace bke::id {
|
||||
class IDRemapper;
|
||||
}
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
class AssetRepresentation;
|
||||
|
||||
/**
|
||||
* AssetLibrary provides access to an asset library's data.
|
||||
*
|
||||
* The asset library contains catalogs and storage for asset representations. It could be extended
|
||||
* to also include asset indexes and more.
|
||||
*/
|
||||
class AssetLibrary {
|
||||
eAssetLibraryType library_type_;
|
||||
/** See #is_read_only(). */
|
||||
bool is_read_only_ = true;
|
||||
/**
|
||||
* The name this asset library will be displayed as in the UI. Will also be used as a weak way
|
||||
* to identify an asset library (e.g. by #AssetWeakReference).
|
||||
*/
|
||||
std::string name_;
|
||||
/** If this is an asset library on disk, the top-level directory path. Normalized using
|
||||
* #normalize_directory_path(). Shared pointer so assets can safely point to it, and don't have
|
||||
* to hold a copy (which is the size of `std::string` + the allocated buffer, if no short string
|
||||
* optimization is used). With thousands of assets this might make a reasonable difference. */
|
||||
std::shared_ptr<std::string> root_path_;
|
||||
|
||||
/**
|
||||
* AssetStorage for assets (better said their representations) that are considered to be part of
|
||||
* this library. Assets are not automatically loaded into this when loading an asset library.
|
||||
* Assets have to be loaded externally and added to this storage via
|
||||
* #add_external_on_disk_asset() or #add_local_id_asset(). So this really is arbitrary storage as
|
||||
* far as #AssetLibrary is concerned (allowing the API user to manage partial library storage and
|
||||
* partial loading, so only relevant parts of a library are kept in memory).
|
||||
*
|
||||
* For now, multiple parts of Blender just keep adding their own assets to this storage. E.g.
|
||||
* multiple asset browsers might load multiple representations for the same asset into this.
|
||||
* Currently there is just no way to properly identify assets, or keep track of which assets are
|
||||
* already in memory and which not. Neither do we keep track of how many parts of Blender are
|
||||
* using an asset or an asset library, which is needed to know when assets can be freed.
|
||||
*/
|
||||
struct AssetStorage {
|
||||
/* Uses shared pointers so the UI can acquire weak pointers. It can then ensure pointers are
|
||||
* not dangling before accessing. */
|
||||
|
||||
Set<std::shared_ptr<AssetRepresentation>> external_assets;
|
||||
Mutex external_assets_mutex;
|
||||
/* Store local ID assets separately for efficient lookups.
|
||||
* TODO(Julian): A [ID *, asset] or even [ID.session_uid, asset] map would be preferable for
|
||||
* faster lookups. Not possible until each asset is only represented once in the storage. */
|
||||
Set<std::shared_ptr<AssetRepresentation>> local_id_assets;
|
||||
Mutex local_id_assets_mutex;
|
||||
};
|
||||
AssetStorage asset_storage_;
|
||||
|
||||
protected:
|
||||
/* Changing this pointer should be protected using #catalog_service_mutex_. Note that changes
|
||||
* within the catalog service may still happen without the mutex being locked. They should be
|
||||
* protected separately.
|
||||
*
|
||||
* This is a #shared_ptr (rather than #unique_ptr) so that readers can keep the service alive
|
||||
* while using it, even if another thread replaces #catalog_service_ in the meantime (which frees
|
||||
* the previously referenced service). See #catalog_service_ptr(). */
|
||||
std::shared_ptr<AssetCatalogService> catalog_service_;
|
||||
mutable std::recursive_mutex catalog_service_mutex_;
|
||||
|
||||
/** Assets owned by this library may be imported with a different method than set in
|
||||
* #import_method_ above, it's just a default. */
|
||||
bool may_override_import_method_ = false;
|
||||
|
||||
bCallbackFuncStore on_save_callback_store_{};
|
||||
|
||||
public:
|
||||
/* Controlled by #ed::asset::catalogs_set_save_catalogs_when_file_is_saved,
|
||||
* for managing the "Save Catalog Changes" in the quit-confirmation dialog box. */
|
||||
static bool save_catalogs_when_file_is_saved;
|
||||
|
||||
friend class AssetLibraryService;
|
||||
friend class AssetRepresentation;
|
||||
|
||||
/**
|
||||
* \param is_read_only: If true, the user should not be able to edit assets or asset catalogs
|
||||
* from this library. See #is_read_only().
|
||||
* \param name: The name this asset library will be displayed in the UI as. Will also be used as
|
||||
* a weak way to identify an asset library (e.g. by #AssetWeakReference). Make sure
|
||||
* this is set for any custom (not builtin) asset library. That is,
|
||||
* #ASSET_LIBRARY_CUSTOM ones.
|
||||
* \param root_path: If this is an asset library on disk, the top-level directory path.
|
||||
*/
|
||||
AssetLibrary(eAssetLibraryType library_type,
|
||||
bool is_read_only,
|
||||
StringRef name = "",
|
||||
StringRef root_path = "");
|
||||
virtual ~AssetLibrary();
|
||||
|
||||
/**
|
||||
* Execute \a fn for every asset library that is loaded and enabled. The asset library is passed
|
||||
* to the \a fn call.
|
||||
*
|
||||
* \note Libraries may note be freed during the iteration.
|
||||
*
|
||||
* \param include_all_library: When true, \a fn will also be executed for the "All" asset
|
||||
* library. This is just a combination of the other ones, so usually iterating over it is
|
||||
* redundant.
|
||||
*/
|
||||
static void foreach_loaded(FunctionRef<void(AssetLibrary &)> fn, bool include_all_library);
|
||||
|
||||
/**
|
||||
* (Re-)download the remote listing for this library.
|
||||
*
|
||||
* This only has an effect for asset libraries that are themselves a remote library, or contain
|
||||
* one (such as the "Essentials" library if it includes online essentials, or the "All" if there
|
||||
* are any remote libraries included).
|
||||
*
|
||||
* The "Allow Online Access" option will be enforced internally, but probably some check to give
|
||||
* a user message should be done at a higher levl.
|
||||
*/
|
||||
virtual void force_remote_listing_download() const;
|
||||
|
||||
/**
|
||||
* Get the #AssetLibraryReference referencing this library. This can fail for custom libraries,
|
||||
* which have too look up their #bUserAssetLibrary. It will not return a value for values that
|
||||
* were loaded directly through a path.
|
||||
*/
|
||||
virtual std::optional<AssetLibraryReference> library_reference() const = 0;
|
||||
|
||||
/**
|
||||
* Get the import method that should be used for assets in this library.
|
||||
*
|
||||
* \return The import method or no value if the library doesn't support importing. For example
|
||||
* because the library is the "Current File" library or the library was removed from the
|
||||
* Preferences.
|
||||
*/
|
||||
virtual std::optional<eAssetImportMethod> import_method() const = 0;
|
||||
|
||||
virtual bool use_relative_paths() const;
|
||||
|
||||
/**
|
||||
* Return the URL of the remote asset library, or #std::nullopt if this is not a remote library.
|
||||
*
|
||||
* Note: don't use this as a way to distinguish remote vs. local libraries. Either query the
|
||||
* asset itself, or use #is_or_contains_remote_libraries(). The Essentials and All libraries may
|
||||
* contain a mixture of remote and local assets.
|
||||
*/
|
||||
virtual std::optional<StringRefNull> remote_url() const;
|
||||
|
||||
AssetCatalogService &catalog_service() const;
|
||||
|
||||
/**
|
||||
* Get shared ownership of the catalog service. Unlike #catalog_service(), this keeps the service
|
||||
* alive for as long as the returned pointer is held, even if another thread replaces the
|
||||
* library's catalog service in the meantime (e.g. a background catalog reload job). Use this
|
||||
* instead of #catalog_service() when accessing the service from a thread that may run
|
||||
* concurrently with such a replacement (e.g. the drawing/main thread while an asset read job is
|
||||
* running). */
|
||||
std::shared_ptr<AssetCatalogService> catalog_service_ptr() const;
|
||||
|
||||
/**
|
||||
* Create a representation of an asset to be considered part of this library. Once the
|
||||
* representation is not needed anymore, it must be freed using #remove_asset(), or there will be
|
||||
* leaking that's only cleared when the library storage is destructed (typically on exit or
|
||||
* loading a different file).
|
||||
*
|
||||
* \param relative_asset_path: The path of the asset relative to the asset library root. With
|
||||
* this the asset must be uniquely identifiable within the asset
|
||||
* library.
|
||||
* \return A weak pointer to the new asset representation. The caller needs to keep some
|
||||
* reference stored to be able to call #remove_asset(). This would be dangling once the
|
||||
* asset library is destructed, so a weak pointer should be used to reference it.
|
||||
*/
|
||||
std::weak_ptr<AssetRepresentation> add_external_on_disk_asset(
|
||||
StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata);
|
||||
/** See #AssetLibrary::add_external_on_disk_asset(). Use this for assets that are not available
|
||||
* on disk, and part of an online asset library. */
|
||||
std::weak_ptr<AssetRepresentation> add_external_online_asset(
|
||||
StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata,
|
||||
OnlineAssetInfo online_info);
|
||||
/** See #AssetLibrary::add_external_on_disk_asset(). */
|
||||
std::weak_ptr<AssetRepresentation> add_local_id_asset(ID &id);
|
||||
/**
|
||||
* Remove an asset from the library that was added using #add_external_on_disk_asset() or
|
||||
* #add_local_id_asset(). Can usually be expected to be constant time complexity (worst case may
|
||||
* differ).
|
||||
* \note This is safe to call if \a asset is freed (dangling reference), will not perform any
|
||||
* change then.
|
||||
* \return True on success, false if the asset couldn't be found inside the library (also the
|
||||
* case when the reference is dangling).
|
||||
*/
|
||||
bool remove_asset(AssetRepresentation &asset);
|
||||
|
||||
/**
|
||||
* Remap ID pointers for local ID assets, see #BKE_lib_remap.hh. When an ID pointer would be
|
||||
* mapped to null (typically when an ID gets removed), the asset is removed, because we don't
|
||||
* support such empty/null assets.
|
||||
*/
|
||||
void remap_ids_and_remove_invalid(const bke::id::IDRemapper &mappings);
|
||||
|
||||
/**
|
||||
* Update `catalog_simple_name` by looking up the asset's catalog by its ID.
|
||||
*
|
||||
* No-op if the catalog cannot be found. This could be the kind of "the
|
||||
* catalog definition file is corrupt/lost" scenario that the simple name is
|
||||
* meant to help recover from.
|
||||
*/
|
||||
void refresh_catalog_simplename(AssetMetaData *asset_data);
|
||||
|
||||
void load_or_reload_catalogs();
|
||||
|
||||
void on_blend_save_handler_register();
|
||||
void on_blend_save_handler_unregister();
|
||||
|
||||
void on_blend_save_post(Main *bmain, PointerRNA **pointers, int num_pointers);
|
||||
|
||||
std::string resolve_asset_weak_reference_to_full_path(const AssetWeakReference &asset_reference);
|
||||
|
||||
eAssetLibraryType library_type() const;
|
||||
StringRefNull name() const;
|
||||
StringRefNull root_path() const;
|
||||
/**
|
||||
* Check if this is a read-only library, meaning the user shouldn't be able to do edits to
|
||||
* assets and asset catalogs from this library.
|
||||
*
|
||||
* \note This isn't enforced by the asset system - the UI or other editing code has to respect
|
||||
* this flag. Also see #AssetCatalogService::is_read_only().
|
||||
*
|
||||
* Of course it's possible to modify the .blend files containing the assets manually; and
|
||||
* similarly, to open a .blend file in the library directory to edit asset catalogs. This
|
||||
* function only speaks for editing directly *via this library*.
|
||||
*/
|
||||
bool is_read_only() const;
|
||||
|
||||
protected:
|
||||
/** Load catalogs that have changed on disk. */
|
||||
virtual void refresh_catalogs();
|
||||
};
|
||||
|
||||
/** Get all asset library references which are enabled and for which the directory exists. */
|
||||
Vector<AssetLibraryReference> all_valid_asset_library_refs();
|
||||
|
||||
AssetLibraryReference all_library_reference();
|
||||
AssetLibraryReference essentials_library_reference();
|
||||
AssetLibraryReference current_file_library_reference();
|
||||
AssetLibraryReference online_essentials_library_reference();
|
||||
|
||||
void all_library_tag_catalogs_dirty();
|
||||
void all_library_reload_catalogs_if_dirty();
|
||||
|
||||
/**
|
||||
* Return whether this is a remote asset library, or contains remote assets.
|
||||
*
|
||||
* The All and Essentials libraries can have a mixture of local & remote assets.
|
||||
*/
|
||||
bool is_or_contains_remote_libraries(const AssetLibraryReference &reference);
|
||||
|
||||
bool contains_assets_from_remote_url(const AssetLibrary &library, StringRef remote_url);
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
/**
|
||||
* Load the data for an asset library, but not the asset representations themselves (loading these
|
||||
* is currently not done in the asset system).
|
||||
*
|
||||
* For the "All" asset library (#ASSET_LIBRARY_ALL), every other known asset library will be
|
||||
* loaded as well. So a call to #AssetLibrary::foreach_loaded() can be expected to iterate over all
|
||||
* libraries.
|
||||
*
|
||||
* \warning Catalogs are reloaded, invalidating catalog pointers. Do not store catalog pointers,
|
||||
* store CatalogIDs instead and lookup the catalog where needed.
|
||||
*/
|
||||
asset_system::AssetLibrary *AS_asset_library_load(const Main *bmain,
|
||||
const AssetLibraryReference &library_reference);
|
||||
|
||||
std::string AS_asset_library_root_path_from_library_ref(
|
||||
const AssetLibraryReference &library_reference);
|
||||
|
||||
/**
|
||||
* Try to find an appropriate location for an asset library root from a file or directory path.
|
||||
* Does not check if \a input_path exists.
|
||||
*
|
||||
* The design is made to find an appropriate asset library path from a .blend file path, but
|
||||
* technically works with any file or directory as \a input_path.
|
||||
* Design is:
|
||||
* * If \a input_path lies within a known asset library path (i.e. an asset library registered in
|
||||
* the Preferences), return the asset library path.
|
||||
* * Otherwise, if \a input_path has a parent path, return the parent path (e.g. to use the
|
||||
* directory a .blend file is in as asset library root).
|
||||
* * If \a input_path is empty or doesn't have a parent path (e.g. because a .blend wasn't saved
|
||||
* yet), there is no suitable path. The caller has to decide how to handle this case.
|
||||
*
|
||||
* \return The returned asset library path with a trailing slash,
|
||||
* or an empty string if no suitable path is found.
|
||||
*/
|
||||
std::string AS_asset_library_find_suitable_root_path_from_path(StringRefNull input_path);
|
||||
|
||||
/**
|
||||
* Uses the current location on disk of the file represented by \a bmain as input to
|
||||
* #AS_asset_library_find_suitable_root_path_from_path(). Refer to it for a design
|
||||
* description.
|
||||
*
|
||||
* \return True if the function could find a valid, that is, a non-empty path to return in \a
|
||||
* r_library_path. If \a bmain wasn't saved into a file yet, the return value will be
|
||||
* false.
|
||||
*/
|
||||
std::string AS_asset_library_find_suitable_root_path_from_main(const Main *bmain);
|
||||
|
||||
/**
|
||||
* Force clearing of all asset library data. After calling this, new asset libraries can be loaded
|
||||
* just as usual using #AS_asset_library_load(), no init or other setup is needed.
|
||||
*
|
||||
* Does not need to be called on exit, this is handled internally.
|
||||
*/
|
||||
void AS_asset_libraries_exit();
|
||||
|
||||
/**
|
||||
* Return the #AssetLibrary rooted at the given directory path.
|
||||
*
|
||||
* Will return the same pointer for repeated calls, until another blend file is loaded.
|
||||
*
|
||||
* To get the in-memory-only "current file" asset library, pass an empty path.
|
||||
*/
|
||||
asset_system::AssetLibrary *AS_asset_library_load_from_directory(const char *name,
|
||||
const char *library_dirpath);
|
||||
|
||||
/** Return whether any loaded AssetLibrary has unsaved changes to its catalogs. */
|
||||
bool AS_asset_library_has_any_unsaved_catalogs();
|
||||
|
||||
/**
|
||||
* An asset library can include local IDs (IDs in the current file). Their pointers need to be
|
||||
* remapped on change (or assets removed as IDs gets removed).
|
||||
*/
|
||||
void AS_asset_library_remap_ids(const bke::id::IDRemapper &mappings);
|
||||
|
||||
/**
|
||||
* Attempt to resolve a full path to an asset based on the currently available (not necessary
|
||||
* loaded) asset libraries, and split it into it's directory, ID group and ID name components. The
|
||||
* path is not guaranteed to exist on disk. On failure to resolve the reference, return arguments
|
||||
* will point to null.
|
||||
*
|
||||
* \note Only works for asset libraries on disk and the "Current File" one (others can't be
|
||||
* resolved).
|
||||
*
|
||||
* \param r_path_buffer: Buffer to hold the result in on success. Will be the full path with null
|
||||
* terminators instead of slashes separating the directory, group and name
|
||||
* components. Must be at least #FILE_MAX_LIBEXTRA long.
|
||||
* \param r_dir: Returns the .blend file path with native slashes on success. Optional (passing
|
||||
* null is allowed). For the "Current File" library this will be empty.
|
||||
* \param r_group: Returns the ID group such as "Object", "Material" or "Brush". Optional (passing
|
||||
* null is allowed).
|
||||
* \param r_name: Returns the ID name on success. Optional (passing null is allowed).
|
||||
*/
|
||||
void AS_asset_full_path_explode_from_weak_ref(const AssetWeakReference *asset_reference,
|
||||
char r_path_buffer[/*FILE_MAX_LIBEXTRA*/ 1282],
|
||||
char **r_dir,
|
||||
char **r_group,
|
||||
char **r_name);
|
||||
|
||||
/**
|
||||
* Updates the default import method for asset libraries based on
|
||||
* #U.experimental.no_data_block_packing.
|
||||
*/
|
||||
void AS_asset_library_import_method_ensure_valid(Main &bmain);
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,266 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*
|
||||
* \brief Main runtime representation of an asset.
|
||||
*
|
||||
* Abstraction to reference an asset, with necessary data for display & interaction.
|
||||
* https://developer.blender.org/docs/features/asset_system/backend/#asset-representation
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <variant>
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_utility_mixins.hh"
|
||||
|
||||
#include "DNA_ID_enums.h"
|
||||
#include "DNA_asset_types.h"
|
||||
|
||||
#include "AS_asset_file_status.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct AssetMetaData;
|
||||
struct bContext;
|
||||
struct ID;
|
||||
struct PreviewImage;
|
||||
struct ReportList;
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
class AssetLibrary;
|
||||
struct OnlineAssetInfo;
|
||||
struct OnlineAssetFile;
|
||||
struct URLWithHash;
|
||||
|
||||
class AssetRepresentation : NonCopyable, NonMovable {
|
||||
/** Pointer back to the asset library that owns this asset representation. */
|
||||
AssetLibrary &owner_asset_library_;
|
||||
/**
|
||||
* Uniquely identifies the asset within the asset library. Currently this is always a path (path
|
||||
* within the asset library).
|
||||
*/
|
||||
/* Mutable to allow lazy updating on name changes in #library_relative_identifier(). */
|
||||
mutable std::string relative_identifier_;
|
||||
|
||||
struct ExternalAsset {
|
||||
std::string name;
|
||||
int id_type = 0;
|
||||
std::unique_ptr<AssetMetaData> metadata_ = nullptr;
|
||||
PreviewImage *preview_ = nullptr;
|
||||
|
||||
/**
|
||||
* Status of this asset's file(s) compared to the remote listing.
|
||||
* Only meaningful for assets from a remote library that have been checked against the listing.
|
||||
* For online-only assets (#online_info_ is set), the status is stored there instead.
|
||||
*
|
||||
* \see #AssetRepresentation::remote_file_status()
|
||||
* \see #AssetRepresentation::remote_file_status_set()
|
||||
*/
|
||||
RemoteAssetFileStatus remote_file_status_ = RemoteAssetFileStatus::UNSET;
|
||||
|
||||
/**
|
||||
* Set if this is an online asset only.
|
||||
*
|
||||
* Note that this can also be set on online assets when their files have been downloaded
|
||||
* locally. To distinguish between 'pure online' (so no file) and other cases, use the
|
||||
* file_status_ field above.
|
||||
*
|
||||
* \see #AssetRepresentation::is_online_only()
|
||||
*/
|
||||
std::unique_ptr<OnlineAssetInfo> online_info_;
|
||||
};
|
||||
std::variant<ExternalAsset, ID *> asset_;
|
||||
|
||||
friend class AssetLibrary;
|
||||
|
||||
public:
|
||||
/**
|
||||
* Constructs an asset representation for an external ID stored on disk. The asset will not be
|
||||
* editable.
|
||||
*
|
||||
* For online assets, use the version with #online_info below.
|
||||
*/
|
||||
AssetRepresentation(StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata,
|
||||
AssetLibrary &owner_asset_library);
|
||||
/**
|
||||
* Constructs an asset representation for an external ID stored online (requiring download).
|
||||
*/
|
||||
AssetRepresentation(StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata,
|
||||
AssetLibrary &owner_asset_library,
|
||||
OnlineAssetInfo online_info);
|
||||
/**
|
||||
* Constructs an asset representation for an ID stored in the current file. This makes the asset
|
||||
* local and fully editable.
|
||||
*/
|
||||
AssetRepresentation(ID &id, AssetLibrary &owner_asset_library);
|
||||
~AssetRepresentation();
|
||||
|
||||
/**
|
||||
* Create a weak reference for this asset that can be written to files, but can break under a
|
||||
* number of conditions.
|
||||
* A weak reference can only be created if an asset representation is owned by an asset library.
|
||||
*/
|
||||
AssetWeakReference make_weak_reference() const;
|
||||
|
||||
/**
|
||||
* Makes sure the asset ready to load a preview, if necessary.
|
||||
*
|
||||
* For local IDs it calls #BKE_previewimg_id_get(). For others, this sets loading information
|
||||
* to the preview but doesn't actually load it. To load it, attach its
|
||||
* #PreviewImageRuntime::icon_id to a UI button (UI loads it asynchronously then) or call
|
||||
* #BKE_previewimg_ensure() (not asynchronous).
|
||||
*
|
||||
* For online assets this triggers downloading of the preview.
|
||||
*/
|
||||
void ensure_previewable(const bContext &C, ReportList *reports = nullptr);
|
||||
/**
|
||||
* Get the preview of this asset.
|
||||
*
|
||||
* This will only return a preview for local ID assets or after #ensure_previewable() was
|
||||
* called.
|
||||
*/
|
||||
PreviewImage *get_preview() const;
|
||||
|
||||
StringRefNull get_name() const;
|
||||
ID_Type get_id_type() const;
|
||||
AssetMetaData &get_metadata() const;
|
||||
|
||||
StringRefNull library_relative_identifier() const;
|
||||
std::string full_path() const;
|
||||
|
||||
/**
|
||||
* Return the absolute path of the blend file that contains this asset.
|
||||
*
|
||||
* Note that this performs a file-system check to see whether the blend file actually exists.
|
||||
* If it does not, an empty string is returned. This generally shouldn't be an issue, but can
|
||||
* happen, for example when the blend file is deleted and the asset browser not refreshed.
|
||||
*
|
||||
* This check is a necessity because data-blocks may have .blend and slashes in their name, and
|
||||
* directory names may also end in `.blend`, resulting in an identifier like
|
||||
* `directory.blend/Objects/filename.blend/Actions/hand/wave.blend/Actions/hi.blend`.
|
||||
* Here the file is `directory.blend/Objects/filename.blend` and the asset is an Action named
|
||||
* `hand/wave.blend/Actions/hi.blend`.
|
||||
*/
|
||||
std::string full_library_path() const;
|
||||
|
||||
/**
|
||||
* For online assets (see #is_online_only()), the files that make up this asset.
|
||||
*
|
||||
* Will return an empty span if this is not an online asset.
|
||||
*/
|
||||
Span<OnlineAssetFile> online_asset_files() const;
|
||||
/**
|
||||
* Return the sum of sizes of all files associated with this asset, according to the listing.
|
||||
*/
|
||||
std::optional<int64_t> online_asset_files_combined_size_in_bytes() const;
|
||||
/**
|
||||
* For online assets (see #is_online_only()), the URL the asset's preview should be requested
|
||||
* from.
|
||||
*
|
||||
* Will return an empty value if this is not an online asset.
|
||||
*/
|
||||
std::optional<StringRefNull> online_asset_preview_url() const;
|
||||
/**
|
||||
* For online assets (see #is_online_only()), the hash of the asset's preview.
|
||||
*
|
||||
* Will return an empty value if this is not an online asset.
|
||||
*/
|
||||
std::optional<StringRefNull> online_asset_preview_hash() const;
|
||||
|
||||
/**
|
||||
* Turn the online asset into a normal asset. This removes the online data, and the "is online"
|
||||
* marking, turning it into a regular on-disk asset.
|
||||
*
|
||||
* No-op if this is not an online asset.
|
||||
*/
|
||||
void online_asset_mark_downloaded();
|
||||
|
||||
/**
|
||||
* Get the import method to use for this asset. A different one may be used if
|
||||
* #may_override_import_method() returns true, otherwise, the returned value must be used. If
|
||||
* there is no import method predefined for this asset no value is returned.
|
||||
*/
|
||||
std::optional<eAssetImportMethod> get_import_method() const;
|
||||
/**
|
||||
* Returns if this asset may be imported with an import method other than the one returned by
|
||||
* #get_import_method(). Also returns true if there is no predefined import method
|
||||
* (when #get_import_method() returns no value).
|
||||
*/
|
||||
bool may_override_import_method() const;
|
||||
bool get_use_relative_path() const;
|
||||
/**
|
||||
* If this asset is stored inside this current file (#is_local_id() is true), this returns the
|
||||
* ID's pointer, otherwise null.
|
||||
*/
|
||||
ID *local_id() const;
|
||||
/** Returns if this asset is stored inside this current file, and as such fully editable. */
|
||||
bool is_local_id() const;
|
||||
/**
|
||||
* The asset is purely stored online, there is no local file on disk for this.
|
||||
*
|
||||
* Regardless of what this function returns, there may be 'online info' (information from a
|
||||
* remote asset listing) available, even when the file is on disk and this function returns
|
||||
* `false`.
|
||||
*
|
||||
* \see #remote_file_status()
|
||||
*/
|
||||
bool is_online_only() const;
|
||||
/**
|
||||
* Returns whether the asset is stored in a probably-editable .asset.blend file.
|
||||
*
|
||||
* NOTE: This is suitable for poll functions (which should not open other files). The actual
|
||||
* operator should still check that `G_FILE_ASSET_EDIT_FILE` / `Main::is_asset_edit_file` is set
|
||||
* on the `.asset.blend` file (no utility function for this exists yet).
|
||||
*
|
||||
* NOTE: this function does cause _some_ disk I/O, as it checks one (or more) paths for
|
||||
* existence. See #AssetRepresentation::full_library_path() for more info.
|
||||
*
|
||||
* If the asset is already imported, this check can be done via
|
||||
* `bke::asset_edit_id_is_editable(asset_id)` and `bke::asset_edit_id_is_writable(asset_id)`.
|
||||
*/
|
||||
bool is_potentially_editable_asset_blend() const;
|
||||
|
||||
/**
|
||||
* Status of this asset's on-disk file(s) compared to the remote listing.
|
||||
* Returns #AssetFileStatus::UNSET if the asset has not been checked against a listing.
|
||||
* For on-disk assets this reflects the status stamped after listing comparison.
|
||||
* For online-only assets this reflects the status from #OnlineAssetInfo.
|
||||
*/
|
||||
RemoteAssetFileStatus remote_file_status() const;
|
||||
/** Set the file status for on-disk assets. No-op for online-only assets. */
|
||||
void remote_file_status_set(RemoteAssetFileStatus status);
|
||||
/**
|
||||
* Store the remote listings online info on an on-disk asset so it can be re-downloaded.
|
||||
* Replaces any previously set online info.
|
||||
*/
|
||||
void online_info_set(OnlineAssetInfo info);
|
||||
|
||||
/**
|
||||
* Return whether this asset requires (re-)downloading before it can be used.
|
||||
*
|
||||
* True for online-only assets (#is_online_only()) and for on-disk assets whose files no longer
|
||||
* match the remote listing (e.g. #AssetFileStatus::NO_MATCH).
|
||||
*/
|
||||
bool needs_download() const;
|
||||
|
||||
AssetLibrary &owner_asset_library() const;
|
||||
};
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,56 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace blender {
|
||||
struct bContext;
|
||||
}
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* C++ wrapper around the DiskFileHashService class implemented in Python.
|
||||
*
|
||||
* Run the following to see which hash algorithms are supported:
|
||||
*
|
||||
* `blender -b --python-expr "import hashlib; print(hashlib.algorithms_available)"`
|
||||
*/
|
||||
class DiskFileHashService {
|
||||
private:
|
||||
std::string storage_path_;
|
||||
|
||||
public:
|
||||
explicit DiskFileHashService(StringRef storage_path);
|
||||
~DiskFileHashService();
|
||||
|
||||
/** Return the hash of a file on disk. */
|
||||
std::string get_hash(StringRef filepath, StringRef hash_algorithm);
|
||||
|
||||
/** Check the file on disk, to see if it matches the given properties. */
|
||||
bool file_matches(StringRef filepath,
|
||||
StringRef hash_algorithm,
|
||||
StringRef hexhash,
|
||||
int64_t size_in_bytes);
|
||||
|
||||
private:
|
||||
/** Release the Python instance associated with this DFHS. */
|
||||
void release_python();
|
||||
};
|
||||
|
||||
/**
|
||||
* Obtain a DiskFileHashService, which stores its cache at the given location.
|
||||
*/
|
||||
std::unique_ptr<DiskFileHashService> disk_file_hash_service_get(StringRef storage_path);
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,39 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
namespace blender {
|
||||
class UUID;
|
||||
}
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
StringRefNull essentials_directory_path();
|
||||
StringRefNull online_essentials_cache_directory_path();
|
||||
StringRefNull online_essentials_url();
|
||||
|
||||
/**
|
||||
* Check if the given URL matches the online essentials URL, with or without the optional
|
||||
* `_asset-library-meta.json` ending. If the `.json` file name ending isn't present, the trailing
|
||||
* slash is necessary for the URLs to match.
|
||||
*/
|
||||
bool is_online_essentials_url(StringRef url);
|
||||
|
||||
/**
|
||||
* Check if the given absolute directory path is the online essentials cache path. If the path ends
|
||||
* in a trailing slash, that's stripped before comparing.
|
||||
*/
|
||||
bool is_online_essentials_dirpath(StringRef dirpath);
|
||||
|
||||
/** Returns false for catalogs that are based on disabled experimental features. */
|
||||
bool skip_experimental_asset_catalog(const UUID &catalog_id);
|
||||
|
||||
} // namespace blender::asset_system
|
||||
264
blender-5.2.0/source/blender/asset_system/AS_remote_library.hh
Normal file
264
blender-5.2.0/source/blender/asset_system/AS_remote_library.hh
Normal file
@@ -0,0 +1,264 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <optional>
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "AS_asset_file_status.hh"
|
||||
|
||||
namespace blender {
|
||||
struct bContext;
|
||||
struct bUserAssetLibrary;
|
||||
struct Main;
|
||||
struct ReportList;
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
struct RemoteLibraryDefinitionRef {
|
||||
StringRefNull remote_url;
|
||||
StringRefNull cache_dirpath;
|
||||
|
||||
RemoteLibraryDefinitionRef(const bUserAssetLibrary &library_definition);
|
||||
RemoteLibraryDefinitionRef(StringRefNull remote_url, StringRefNull cache_dirpath)
|
||||
: remote_url(remote_url), cache_dirpath(cache_dirpath)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
constexpr StringRefNull REMOTE_LIBRARY_TOP_META_FILE_NAME = "_asset-library-meta.json";
|
||||
constexpr StringRefNull REMOTE_LIBRARY_TOP_META_FILE_NAME_LEADING_SLASH =
|
||||
"/_asset-library-meta.json";
|
||||
|
||||
/**
|
||||
* Get the absolute file path to the `_asset-library-meta.json` of the given library's cache
|
||||
* directory.
|
||||
*/
|
||||
std::string remote_library_top_meta_file_path(const RemoteLibraryDefinitionRef &library);
|
||||
|
||||
bool remote_library_url_ends_with_top_meta_file_name(const StringRef url);
|
||||
|
||||
/**
|
||||
* Iterates all libraries registered in the Preferences and calls the given function with the URL
|
||||
* of the library.
|
||||
*
|
||||
* \note Does not include the online essentials library.
|
||||
*/
|
||||
void foreach_registered_user_remote_library(FunctionRef<void(bUserAssetLibrary &)> fn);
|
||||
|
||||
/**
|
||||
* Combination of a URL of a remote resource, and its hash.
|
||||
*/
|
||||
struct URLWithHash {
|
||||
std::string url;
|
||||
/** String in the form `{HASH_TYPE}:{HASH_VALUE}`. */
|
||||
std::string hash;
|
||||
};
|
||||
|
||||
/** Information of a single file of an online asset. */
|
||||
struct OnlineAssetFile {
|
||||
/**
|
||||
* The path within the asset library this file should be downloaded to.
|
||||
* Relative to the library root.
|
||||
*/
|
||||
std::string path;
|
||||
int64_t size_in_bytes;
|
||||
/** The URL the asset should be downloaded from. */
|
||||
URLWithHash url;
|
||||
};
|
||||
|
||||
/**
|
||||
* Information specific to online assets.
|
||||
*
|
||||
* This is constructed from the remote asset listing and contains all data needed to download and
|
||||
* verify related fragments. #AssetRepresentation stores this for online assets.
|
||||
*/
|
||||
struct OnlineAssetInfo {
|
||||
/**
|
||||
* The files for this asset.
|
||||
* The first one contains the asset data-blocks, and subsequent files are dependencies.
|
||||
*/
|
||||
Vector<OnlineAssetFile> files;
|
||||
std::optional<URLWithHash> preview_url;
|
||||
|
||||
/**
|
||||
* Return the asset's main file, i.e. the file containing the asset data-block.
|
||||
*
|
||||
* This can only return an empty string in error cases, i.e. when the `files` vector (see above)
|
||||
* is empty. This should never happen; file-less assets should be rejected when loading the
|
||||
* listing.
|
||||
*
|
||||
* NOTE: Blender currently only has preliminary support for multi-file assets (it downloads them
|
||||
* correctly, but there's little in place to check for conflicting versions, or to handle things
|
||||
* like copying non-blend files to the project directory). Even though the 'files' list will
|
||||
* likely only have one element (at least that is the case at the time of writing), this function
|
||||
* should not be used as a shortcut when trying to obtain "the asset's files".
|
||||
*/
|
||||
StringRefNull asset_file() const;
|
||||
};
|
||||
|
||||
class AssetRepresentation;
|
||||
|
||||
float remote_library_total_asset_downloads_progress();
|
||||
/** Return true if there is any asset file (any file in an assets file set) being downloaded. */
|
||||
bool remote_library_has_unfinished_asset_downloads();
|
||||
|
||||
/**
|
||||
* Ensures the remote library cache directory exists, and calls the Python downloader. Doesn't do
|
||||
* anything if a download with the library's URL is already ongoing.
|
||||
*/
|
||||
void remote_library_request_download(const RemoteLibraryDefinitionRef &library_definition);
|
||||
void remote_library_cancel_all_listing_downloads(const bContext &C);
|
||||
|
||||
void remote_library_request_asset_download(const bContext &C,
|
||||
const AssetRepresentation &asset,
|
||||
ReportList *reports);
|
||||
void remote_library_request_preview_download(const bContext &C,
|
||||
const AssetRepresentation &asset,
|
||||
const StringRef dst_filepath,
|
||||
ReportList *reports);
|
||||
|
||||
void remote_library_cancel_all_asset_downloads(bContext &C);
|
||||
|
||||
/**
|
||||
* Get the absolute path to an online library's cache directory using \a library_dirname as library
|
||||
* identifier.
|
||||
*
|
||||
* The path is the general cache directory (e.g. `$HOME/.cache/blender/remote-assets/`) plus the
|
||||
* \a library_dirname as subdirectory.
|
||||
*
|
||||
* The resulting path will be shortened to #FILE_MAXDIR if necessary.
|
||||
*/
|
||||
std::string remote_library_cache_directory_path(StringRefNull library_dirname);
|
||||
/**
|
||||
* Determine the absolute path of the asset library's on-disk cache directory for downloaded files,
|
||||
* based on the library's URL.
|
||||
*
|
||||
* The path is the general cache directory (e.g. `$HOME/.cache/blender/remote-assets/`) plus a
|
||||
* shortened MD5 hash of the remote URL to identify the library.
|
||||
*
|
||||
* This is based on the remote URL of the library, and not the library name, as the name can be
|
||||
* user-chosen, so the URL is a more stable identifier. And if there happen to be multiple
|
||||
* libraries in the preferences, with the same URL, they'll share the same cache.
|
||||
*
|
||||
* The resulting path will be shortened to #FILE_MAXDIR if necessary.
|
||||
*/
|
||||
std::string remote_library_cache_directory_path_from_url(StringRef remote_url);
|
||||
|
||||
/**
|
||||
* Get the absolute file path the preview for \a asset is expected at once downloaded.
|
||||
*
|
||||
* The path is built like this:
|
||||
* - Online library cache directory (e.g.
|
||||
* `$HOME/.cache/blender/remote-assets/1a2b3c-my.assets.com/`)
|
||||
* - `_thumbs/large/`
|
||||
* - The first two characters of the MD5 hash of the full asset path
|
||||
* (#AssetRepresentation.full_path()).
|
||||
* - The next 30 characters of the MD5 hash.
|
||||
* - If the download URL of the preview has an extension (some string after a period), up to 6
|
||||
* characters of that extension. (Previews load fine regardless of the extension. But the
|
||||
* extension is still a useful indicator, and some file browsers can display previews that way.)
|
||||
*
|
||||
* The reason hashes are used within `_thumbs/large/` instead of the relative path of the asset (or
|
||||
* another relative path derived from the preview URL) is to keep paths short enough to not violate
|
||||
* path length limitations.
|
||||
*/
|
||||
std::string remote_library_asset_preview_path(const AssetRepresentation &asset);
|
||||
|
||||
/**
|
||||
* Status information about an externally loaded asset library listing, stored globally.
|
||||
*
|
||||
* Remote asset library downloading is handled in Python. This API allows storing status
|
||||
* information globally per URL. Asset UIs can then query the status and reflect it accordingly.
|
||||
*
|
||||
* Another important use is coordinating the Python side downloading with the C++ side loading.
|
||||
* The C++ asset library loading might have to wait for Python to be done downloading and
|
||||
* validating individual asset listing pages, and load in these new pages as they become ready.
|
||||
*
|
||||
* All functions must be called on the same thread.
|
||||
*/
|
||||
class RemoteLibraryLoadingStatus {
|
||||
public:
|
||||
enum Status {
|
||||
Loading,
|
||||
Finished,
|
||||
Failure,
|
||||
Cancelled,
|
||||
};
|
||||
using TimePoint = std::chrono::time_point<std::chrono::steady_clock>;
|
||||
using FileSystemTimePoint = std::filesystem::file_time_type;
|
||||
|
||||
private:
|
||||
float timeout_ = 0.0f;
|
||||
FileSystemTimePoint loading_start_time_point_ = {};
|
||||
TimePoint last_updated_time_point_ = {};
|
||||
/* See #RemoteLibraryLoadingStatus::handle_timeout(). */
|
||||
TimePoint last_timeout_handled_time_point_ = {};
|
||||
TimePoint last_new_pages_time_point_ = {};
|
||||
|
||||
std::optional<Status> status_ = std::nullopt;
|
||||
std::optional<StringRefNull> failure_message_ = std::nullopt;
|
||||
bool metafiles_in_place_ = false;
|
||||
|
||||
public:
|
||||
static void begin_loading(StringRef url, float timeout);
|
||||
/** Let the state know that the loading is still ongoing, resetting the timeout. */
|
||||
static void ping_still_loading(StringRef url);
|
||||
static void ping_new_pages(StringRef url);
|
||||
static void ping_new_preview(const bContext &C, StringRef preview_full_filepath);
|
||||
static void ping_asset_file_progress(StringRef absolute_file_url, int64_t size_in_bytes);
|
||||
/** Should be called when an asset file download has completed successfully. */
|
||||
static void ping_asset_file_download_succeeded(const bContext &C,
|
||||
StringRef library_url,
|
||||
StringRef absolute_file_url,
|
||||
StringRef local_file_abspath);
|
||||
/** Should be called when an asset file download has failed. Partial progress for the file is
|
||||
* reset to zero, since a future retry has to start from scratch. */
|
||||
static void ping_asset_file_download_failed(const bContext &C,
|
||||
StringRef library_url,
|
||||
StringRef absolute_file_url,
|
||||
StringRef local_file_abspath);
|
||||
/** Inform the asset system that there are no more pending asset file downloads for any asset
|
||||
* library. */
|
||||
static void ping_download_queue_done(const bContext &C);
|
||||
static void ping_metafiles_in_place(StringRef url);
|
||||
static void set_finished(StringRef url);
|
||||
static void set_cancelled(const StringRef url);
|
||||
static void set_failure(StringRef url, std::optional<StringRefNull> failure_message);
|
||||
|
||||
static std::optional<StringRefNull> failure_message(StringRef url);
|
||||
static std::optional<RemoteLibraryLoadingStatus::Status> status(StringRef url);
|
||||
static std::optional<bool> metafiles_in_place(StringRef url);
|
||||
static std::optional<FileSystemTimePoint> loading_start_time(const StringRef url);
|
||||
static std::optional<TimePoint> last_new_pages_time(StringRef url);
|
||||
|
||||
/**
|
||||
* Checks if the status storage timed out, because it hasn't received status updates for the
|
||||
* given timeout duration. Changes the status to failure in that case.
|
||||
*
|
||||
* Note that this function doesn't do more than check if the timeout is reached, and changing
|
||||
* state to failure if so. It's meant to be called in regular, short intervals to make the whole
|
||||
* timeout handling work. Current remote asset library loading takes care of this.
|
||||
*
|
||||
* \return True if the loading status switched to #Status::Failure due to timing out.
|
||||
*/
|
||||
static bool handle_timeout(StringRef url);
|
||||
|
||||
private:
|
||||
/** Update the last update time point, effectively resetting the time-out timer. */
|
||||
void reset_timeout();
|
||||
};
|
||||
|
||||
} // namespace asset_system
|
||||
} // namespace blender
|
||||
107
blender-5.2.0/source/blender/asset_system/CMakeLists.txt
Normal file
107
blender-5.2.0/source/blender/asset_system/CMakeLists.txt
Normal file
@@ -0,0 +1,107 @@
|
||||
# SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
#
|
||||
# SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
set(INC
|
||||
PUBLIC .
|
||||
intern
|
||||
intern/library_types
|
||||
../makesrna
|
||||
../editors/include
|
||||
|
||||
# RNA_prototypes.hh
|
||||
${CMAKE_BINARY_DIR}/source/blender/makesrna
|
||||
)
|
||||
|
||||
set(INC_SYS
|
||||
)
|
||||
|
||||
set(SRC
|
||||
intern/asset_catalog.cc
|
||||
intern/asset_catalog_collection.cc
|
||||
intern/asset_catalog_definition_file.cc
|
||||
intern/asset_catalog_path.cc
|
||||
intern/asset_catalog_tree.cc
|
||||
intern/asset_library.cc
|
||||
intern/asset_library_service.cc
|
||||
intern/asset_representation.cc
|
||||
intern/disk_file_hash_service.cc
|
||||
intern/library_types/all_library.cc
|
||||
intern/library_types/common.cc
|
||||
intern/library_types/essentials_library.cc
|
||||
intern/library_types/on_disk_library.cc
|
||||
intern/library_types/preferences_on_disk_library.cc
|
||||
intern/library_types/remote_library.cc
|
||||
intern/library_types/runtime_library.cc
|
||||
intern/utils.cc
|
||||
|
||||
AS_asset_catalog.hh
|
||||
AS_asset_catalog_path.hh
|
||||
AS_asset_catalog_tree.hh
|
||||
AS_asset_library.hh
|
||||
AS_asset_representation.hh
|
||||
AS_disk_file_hash_service.hh
|
||||
AS_essentials_library.hh
|
||||
AS_remote_library.hh
|
||||
intern/asset_catalog_collection.hh
|
||||
intern/asset_catalog_definition_file.hh
|
||||
intern/asset_library_service.hh
|
||||
intern/library_types/all_library.hh
|
||||
intern/library_types/common.hh
|
||||
intern/library_types/essentials_library.hh
|
||||
intern/library_types/on_disk_library.hh
|
||||
intern/library_types/preferences_on_disk_library.hh
|
||||
intern/library_types/remote_library.hh
|
||||
intern/library_types/runtime_library.hh
|
||||
intern/utils.hh
|
||||
)
|
||||
|
||||
set(LIB
|
||||
PRIVATE bf::blenkernel
|
||||
PRIVATE bf::blenlib
|
||||
PRIVATE bf::blentranslation
|
||||
PRIVATE bf::dna
|
||||
PRIVATE bf::imbuf
|
||||
PRIVATE bf::intern::clog
|
||||
PRIVATE bf::intern::guardedalloc
|
||||
PRIVATE bf::windowmanager
|
||||
)
|
||||
|
||||
if(WITH_PYTHON)
|
||||
list(APPEND INC
|
||||
../python
|
||||
)
|
||||
add_definitions(-DWITH_PYTHON)
|
||||
endif()
|
||||
|
||||
blender_add_lib(bf_asset_system "${SRC}" "${INC}" "${INC_SYS}" "${LIB}")
|
||||
add_library(bf::asset_system ALIAS bf_asset_system)
|
||||
|
||||
if(WITH_GTESTS)
|
||||
set(TEST_INC
|
||||
../editors/asset
|
||||
)
|
||||
set(TEST_SRC
|
||||
tests/asset_catalog_path_test.cc
|
||||
tests/asset_catalog_test.cc
|
||||
tests/asset_catalog_tree_test.cc
|
||||
tests/asset_library_service_test.cc
|
||||
tests/asset_library_test.cc
|
||||
tests/asset_representation_test.cc
|
||||
tests/essentials_library_test.cc
|
||||
tests/remote_library_test.cc
|
||||
)
|
||||
set(TEST_COMMON_SRC
|
||||
tests/asset_library_test_common.hh
|
||||
)
|
||||
set(TEST_LIB
|
||||
bf_asset_system
|
||||
PRIVATE bf_editor_asset
|
||||
)
|
||||
blender_add_test_suite_lib(asset_system
|
||||
"${TEST_SRC}" "${INC};${TEST_INC}" "${INC_SYS}" "${LIB};${TEST_LIB}" "${TEST_COMMON_SRC}"
|
||||
)
|
||||
endif()
|
||||
|
||||
# RNA_prototypes.hh dna_type_offsets.h
|
||||
add_dependencies(bf_asset_system bf_rna)
|
||||
@@ -0,0 +1,761 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <set>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
#include "AS_asset_library.hh"
|
||||
#include "AS_essentials_library.hh"
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
/* For S_ISREG() and S_ISDIR() on Windows. */
|
||||
#ifdef WIN32
|
||||
# include "BLI_winstuff.h"
|
||||
#endif
|
||||
|
||||
#include "asset_library_service.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"asset.catalog"};
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
const CatalogFilePath AssetCatalogService::DEFAULT_CATALOG_FILENAME = "blender_assets.cats.txt";
|
||||
|
||||
AssetCatalogService::AssetCatalogService(const CatalogFilePath &asset_library_root,
|
||||
std::optional<read_only_tag> read_only_tag)
|
||||
: catalog_collection_(std::make_unique<AssetCatalogCollection>()),
|
||||
asset_library_root_(asset_library_root),
|
||||
is_read_only_(read_only_tag ? true : false)
|
||||
{
|
||||
}
|
||||
|
||||
AssetCatalogService::AssetCatalogService(read_only_tag /*unused*/) : AssetCatalogService()
|
||||
{
|
||||
const_cast<bool &>(is_read_only_) = true;
|
||||
}
|
||||
|
||||
AssetCatalogService::~AssetCatalogService() = default;
|
||||
|
||||
void AssetCatalogService::tag_has_unsaved_changes(AssetCatalog *edited_catalog)
|
||||
{
|
||||
BLI_assert(!is_read_only_);
|
||||
|
||||
if (edited_catalog) {
|
||||
edited_catalog->flags.has_unsaved_changes = true;
|
||||
}
|
||||
BLI_assert(catalog_collection_);
|
||||
catalog_collection_->has_unsaved_changes_ = true;
|
||||
}
|
||||
|
||||
void AssetCatalogService::untag_has_unsaved_changes()
|
||||
{
|
||||
BLI_assert(catalog_collection_);
|
||||
catalog_collection_->has_unsaved_changes_ = false;
|
||||
|
||||
/* TODO(Sybren): refactor; this is more like "post-write cleanup" than "remove a tag" code. */
|
||||
|
||||
/* Forget about any deleted catalogs. */
|
||||
if (catalog_collection_->catalog_definition_file_) {
|
||||
for (CatalogID catalog_id : catalog_collection_->deleted_catalogs_.keys()) {
|
||||
catalog_collection_->catalog_definition_file_->forget(catalog_id);
|
||||
}
|
||||
}
|
||||
catalog_collection_->deleted_catalogs_.clear();
|
||||
|
||||
/* Mark all remaining catalogs as "without unsaved changes". */
|
||||
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
|
||||
catalog_uptr->flags.has_unsaved_changes = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetCatalogService::has_unsaved_changes() const
|
||||
{
|
||||
BLI_assert(catalog_collection_);
|
||||
return catalog_collection_->has_unsaved_changes_;
|
||||
}
|
||||
|
||||
bool AssetCatalogService::is_read_only() const
|
||||
{
|
||||
return is_read_only_;
|
||||
}
|
||||
|
||||
void AssetCatalogService::tag_all_catalogs_as_unsaved_changes()
|
||||
{
|
||||
for (auto &catalog : catalog_collection_->catalogs_.values()) {
|
||||
catalog->flags.has_unsaved_changes = true;
|
||||
}
|
||||
catalog_collection_->has_unsaved_changes_ = true;
|
||||
}
|
||||
|
||||
bool AssetCatalogService::is_empty() const
|
||||
{
|
||||
BLI_assert(catalog_collection_);
|
||||
return catalog_collection_->catalogs_.is_empty();
|
||||
}
|
||||
|
||||
const OwningAssetCatalogMap &AssetCatalogService::get_catalogs() const
|
||||
{
|
||||
return catalog_collection_->catalogs_;
|
||||
}
|
||||
const OwningAssetCatalogMap &AssetCatalogService::get_deleted_catalogs() const
|
||||
{
|
||||
return catalog_collection_->deleted_catalogs_;
|
||||
}
|
||||
|
||||
const AssetCatalogDefinitionFile *AssetCatalogService::get_catalog_definition_file() const
|
||||
{
|
||||
return catalog_collection_->catalog_definition_file_.get();
|
||||
}
|
||||
|
||||
AssetCatalog *AssetCatalogService::find_catalog(CatalogID catalog_id) const
|
||||
{
|
||||
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
|
||||
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
|
||||
if (catalog_uptr_ptr == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
return catalog_uptr_ptr->get();
|
||||
}
|
||||
|
||||
AssetCatalog *AssetCatalogService::find_catalog_by_path(const AssetCatalogPath &path) const
|
||||
{
|
||||
/* Use an AssetCatalogOrderedSet to find the 'best' catalog for this path. This will be the first
|
||||
* one loaded from disk, or if that does not exist the one with the lowest UUID. This ensures
|
||||
* stable, predictable results. */
|
||||
MutableAssetCatalogOrderedSet ordered_catalogs;
|
||||
|
||||
for (const auto &catalog : catalog_collection_->catalogs_.values()) {
|
||||
if (catalog->path == path) {
|
||||
ordered_catalogs.insert(catalog.get());
|
||||
}
|
||||
}
|
||||
|
||||
if (ordered_catalogs.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
MutableAssetCatalogOrderedSet::iterator best_choice_it = ordered_catalogs.begin();
|
||||
return *best_choice_it;
|
||||
}
|
||||
|
||||
bool AssetCatalogService::is_catalog_known(CatalogID catalog_id) const
|
||||
{
|
||||
BLI_assert(catalog_collection_);
|
||||
return catalog_collection_->catalogs_.contains(catalog_id);
|
||||
}
|
||||
|
||||
AssetCatalogFilter AssetCatalogService::create_catalog_filter(
|
||||
const CatalogID active_catalog_id) const
|
||||
{
|
||||
Set<CatalogID> matching_catalog_ids;
|
||||
Set<CatalogID> known_catalog_ids;
|
||||
matching_catalog_ids.add(active_catalog_id);
|
||||
|
||||
const AssetCatalog *active_catalog = this->find_catalog(active_catalog_id);
|
||||
|
||||
/* This cannot just iterate over tree items to get all the required data, because tree items only
|
||||
* represent single UUIDs. It could be used to get the main UUIDs of the children, though, and
|
||||
* then only do an exact match on the path (instead of the more complex `is_contained_in()`
|
||||
* call). Without an extra indexed-by-path acceleration structure, this is still going to require
|
||||
* a linear search, though. */
|
||||
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
|
||||
if (active_catalog && catalog_uptr->path.is_contained_in(active_catalog->path)) {
|
||||
matching_catalog_ids.add(catalog_uptr->catalog_id);
|
||||
}
|
||||
known_catalog_ids.add(catalog_uptr->catalog_id);
|
||||
}
|
||||
|
||||
return AssetCatalogFilter(std::move(matching_catalog_ids), std::move(known_catalog_ids));
|
||||
}
|
||||
|
||||
void AssetCatalogService::delete_catalog_by_id_soft(const CatalogID catalog_id)
|
||||
{
|
||||
std::unique_ptr<AssetCatalog> *catalog_uptr_ptr = catalog_collection_->catalogs_.lookup_ptr(
|
||||
catalog_id);
|
||||
if (catalog_uptr_ptr == nullptr) {
|
||||
/* Catalog cannot be found, which is fine. */
|
||||
return;
|
||||
}
|
||||
|
||||
/* Mark the catalog as deleted. */
|
||||
AssetCatalog *catalog = catalog_uptr_ptr->get();
|
||||
catalog->flags.is_deleted = true;
|
||||
|
||||
/* Move ownership from catalog_collection_->catalogs_ to catalog_collection_->deleted_catalogs_.
|
||||
*/
|
||||
catalog_collection_->deleted_catalogs_.add(catalog_id, std::move(*catalog_uptr_ptr));
|
||||
|
||||
/* The catalog can now be removed from the map without freeing the actual AssetCatalog. */
|
||||
catalog_collection_->catalogs_.remove(catalog_id);
|
||||
}
|
||||
|
||||
void AssetCatalogService::delete_catalog_by_id_hard(CatalogID catalog_id)
|
||||
{
|
||||
catalog_collection_->catalogs_.remove(catalog_id);
|
||||
catalog_collection_->deleted_catalogs_.remove(catalog_id);
|
||||
|
||||
/* TODO(@sybren): adjust this when supporting multiple CDFs. */
|
||||
catalog_collection_->catalog_definition_file_->forget(catalog_id);
|
||||
}
|
||||
|
||||
void AssetCatalogService::prune_catalogs_by_path(const AssetCatalogPath &path)
|
||||
{
|
||||
/* Build a collection of catalog IDs to delete. */
|
||||
Set<CatalogID> catalogs_to_delete;
|
||||
for (const auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
|
||||
const AssetCatalog *cat = catalog_uptr.get();
|
||||
if (cat->path.is_contained_in(path)) {
|
||||
catalogs_to_delete.add(cat->catalog_id);
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete the catalogs. */
|
||||
for (const CatalogID cat_id : catalogs_to_delete) {
|
||||
this->delete_catalog_by_id_soft(cat_id);
|
||||
}
|
||||
|
||||
this->invalidate_catalog_tree();
|
||||
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
|
||||
}
|
||||
|
||||
void AssetCatalogService::prune_catalogs_by_id(const CatalogID catalog_id)
|
||||
{
|
||||
const AssetCatalog *catalog = find_catalog(catalog_id);
|
||||
BLI_assert_msg(catalog, "trying to prune asset catalogs by the path of a non-existent catalog");
|
||||
if (!catalog) {
|
||||
return;
|
||||
}
|
||||
this->prune_catalogs_by_path(catalog->path);
|
||||
}
|
||||
|
||||
void AssetCatalogService::update_catalog_path(const CatalogID catalog_id,
|
||||
const AssetCatalogPath &new_catalog_path)
|
||||
{
|
||||
AssetCatalog *renamed_cat = this->find_catalog(catalog_id);
|
||||
const AssetCatalogPath old_cat_path = renamed_cat->path;
|
||||
|
||||
for (auto &catalog_uptr : catalog_collection_->catalogs_.values()) {
|
||||
AssetCatalog *cat = catalog_uptr.get();
|
||||
|
||||
const AssetCatalogPath new_path = cat->path.rebase(old_cat_path, new_catalog_path);
|
||||
if (!new_path) {
|
||||
continue;
|
||||
}
|
||||
cat->path = new_path;
|
||||
cat->simple_name_refresh();
|
||||
this->tag_has_unsaved_changes(cat);
|
||||
|
||||
/* TODO(Sybren): go over all assets that are assigned to this catalog, defined in the current
|
||||
* blend file, and update the catalog simple name stored there. */
|
||||
}
|
||||
|
||||
this->create_missing_catalogs();
|
||||
this->invalidate_catalog_tree();
|
||||
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
|
||||
}
|
||||
|
||||
AssetCatalog *AssetCatalogService::create_catalog(const AssetCatalogPath &catalog_path)
|
||||
{
|
||||
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path(catalog_path);
|
||||
catalog->flags.has_unsaved_changes = true;
|
||||
|
||||
/* So we can std::move(catalog) and still use the non-owning pointer: */
|
||||
AssetCatalog *const catalog_ptr = catalog.get();
|
||||
|
||||
/* TODO(@sybren): move the `AssetCatalog::from_path()` function to another place, that can reuse
|
||||
* catalogs when a catalog with the given path is already known, and avoid duplicate catalog IDs.
|
||||
*/
|
||||
BLI_assert_msg(!catalog_collection_->catalogs_.contains(catalog->catalog_id),
|
||||
"duplicate catalog ID not supported");
|
||||
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
|
||||
|
||||
if (catalog_collection_->catalog_definition_file_) {
|
||||
/* Ensure the new catalog gets written to disk at some point. If there is no CDF in memory yet,
|
||||
* it's enough to have the catalog known to the service as it'll be saved to a new file. */
|
||||
catalog_collection_->catalog_definition_file_->add_new(catalog_ptr);
|
||||
}
|
||||
|
||||
this->invalidate_catalog_tree();
|
||||
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
|
||||
|
||||
return catalog_ptr;
|
||||
}
|
||||
|
||||
static std::string asset_definition_default_file_path_from_dir(StringRef asset_library_root)
|
||||
{
|
||||
char file_path[PATH_MAX];
|
||||
BLI_path_join(file_path,
|
||||
sizeof(file_path),
|
||||
asset_library_root.data(),
|
||||
AssetCatalogService::DEFAULT_CATALOG_FILENAME.data());
|
||||
return file_path;
|
||||
}
|
||||
|
||||
void AssetCatalogService::load_from_disk()
|
||||
{
|
||||
this->load_from_disk(asset_library_root_);
|
||||
}
|
||||
|
||||
void AssetCatalogService::load_from_disk(const CatalogFilePath &file_or_directory_path)
|
||||
{
|
||||
BLI_stat_t status;
|
||||
if (BLI_stat(file_or_directory_path.data(), &status) == -1) {
|
||||
/* It's fine if the catalogs file doesn't exist, it just means there are no catalogs. */
|
||||
CLOG_DEBUG(&LOG, "path not found: %s", file_or_directory_path.data());
|
||||
return;
|
||||
}
|
||||
|
||||
if (S_ISREG(status.st_mode)) {
|
||||
this->load_single_file(file_or_directory_path);
|
||||
}
|
||||
else if (S_ISDIR(status.st_mode)) {
|
||||
this->load_directory_recursive(file_or_directory_path);
|
||||
}
|
||||
else {
|
||||
/* TODO(@sybren): throw an appropriate exception. */
|
||||
}
|
||||
|
||||
/* TODO: Should there be a sanitize step? E.g. to remove catalogs with identical paths? */
|
||||
|
||||
this->create_missing_catalogs();
|
||||
this->invalidate_catalog_tree();
|
||||
}
|
||||
|
||||
void AssetCatalogService::add_from_existing(
|
||||
const AssetCatalogService &other_service,
|
||||
AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items)
|
||||
{
|
||||
catalog_collection_->add_catalogs_from_existing(*other_service.catalog_collection_,
|
||||
on_duplicate_items);
|
||||
}
|
||||
|
||||
void AssetCatalogService::load_directory_recursive(const CatalogFilePath &directory_path)
|
||||
{
|
||||
/* TODO(@sybren): implement proper multi-file support. For now, just load
|
||||
* the default file if it is there. */
|
||||
CatalogFilePath file_path = asset_definition_default_file_path_from_dir(directory_path);
|
||||
|
||||
if (!BLI_exists(file_path.data())) {
|
||||
/* No file to be loaded is perfectly fine. */
|
||||
CLOG_DEBUG(&LOG, "path not found: %s", file_path.data());
|
||||
return;
|
||||
}
|
||||
|
||||
this->load_single_file(file_path);
|
||||
}
|
||||
|
||||
void AssetCatalogService::load_single_file(const CatalogFilePath &catalog_definition_file_path)
|
||||
{
|
||||
/* TODO(@sybren): check that #catalog_definition_file_path is contained in #asset_library_root_,
|
||||
* otherwise some assumptions may fail. */
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> cdf = parse_catalog_file(
|
||||
catalog_definition_file_path);
|
||||
|
||||
BLI_assert_msg(!catalog_collection_->catalog_definition_file_,
|
||||
"Only loading of a single catalog definition file is supported.");
|
||||
catalog_collection_->catalog_definition_file_ = std::move(cdf);
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::parse_catalog_file(
|
||||
const CatalogFilePath &catalog_definition_file_path)
|
||||
{
|
||||
auto cdf = std::make_unique<AssetCatalogDefinitionFile>(catalog_definition_file_path);
|
||||
|
||||
/* TODO(Sybren): this might have to move to a higher level when supporting multiple CDFs. */
|
||||
Set<AssetCatalogPath> seen_paths;
|
||||
|
||||
auto catalog_parsed_callback = [this, catalog_definition_file_path, &seen_paths](
|
||||
std::unique_ptr<AssetCatalog> catalog) {
|
||||
if (skip_experimental_asset_catalog(catalog->catalog_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (catalog_collection_->catalogs_.contains(catalog->catalog_id)) {
|
||||
/* TODO(@sybren): apparently another CDF was already loaded. This is not supported yet. */
|
||||
std::cerr << catalog_definition_file_path << ": multiple definitions of catalog "
|
||||
<< catalog->catalog_id << " in multiple files, ignoring this one." << std::endl;
|
||||
/* Don't store 'catalog'; unique_ptr will free its memory. */
|
||||
return false;
|
||||
}
|
||||
|
||||
catalog->flags.is_first_loaded = seen_paths.add(catalog->path);
|
||||
|
||||
/* The AssetCatalog pointer is now owned by the AssetCatalogService. */
|
||||
catalog_collection_->catalogs_.add_new(catalog->catalog_id, std::move(catalog));
|
||||
return true;
|
||||
};
|
||||
|
||||
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
|
||||
|
||||
return cdf;
|
||||
}
|
||||
|
||||
void AssetCatalogService::reload_catalogs()
|
||||
{
|
||||
/* TODO(Sybren): expand to support multiple CDFs. */
|
||||
AssetCatalogDefinitionFile *const cdf = catalog_collection_->catalog_definition_file_.get();
|
||||
if (!cdf || cdf->file_path.empty() || !BLI_is_file(cdf->file_path.c_str())) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Keeps track of the catalog IDs that are seen in the CDF, so that we also know what was deleted
|
||||
* from the file on disk. */
|
||||
Set<CatalogID> cats_in_file;
|
||||
|
||||
auto catalog_parsed_callback = [this, &cats_in_file](std::unique_ptr<AssetCatalog> catalog) {
|
||||
if (skip_experimental_asset_catalog(catalog->catalog_id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const CatalogID catalog_id = catalog->catalog_id;
|
||||
cats_in_file.add(catalog_id);
|
||||
|
||||
const bool should_skip = this->is_catalog_known_with_unsaved_changes(catalog_id);
|
||||
if (should_skip) {
|
||||
/* Do not overwrite unsaved local changes. */
|
||||
return false;
|
||||
}
|
||||
|
||||
/* This is either a new catalog, or we can just replace the in-memory one with the newly loaded
|
||||
* one. */
|
||||
catalog_collection_->catalogs_.add_overwrite(catalog_id, std::move(catalog));
|
||||
return true;
|
||||
};
|
||||
|
||||
cdf->parse_catalog_file(cdf->file_path, catalog_parsed_callback);
|
||||
this->purge_catalogs_not_listed(cats_in_file);
|
||||
this->create_missing_catalogs();
|
||||
this->invalidate_catalog_tree();
|
||||
}
|
||||
|
||||
void AssetCatalogService::purge_catalogs_not_listed(const Set<CatalogID> &catalogs_to_keep)
|
||||
{
|
||||
Set<CatalogID> cats_to_remove;
|
||||
for (CatalogID cat_id : this->catalog_collection_->catalogs_.keys()) {
|
||||
if (catalogs_to_keep.contains(cat_id)) {
|
||||
continue;
|
||||
}
|
||||
if (this->is_catalog_known_with_unsaved_changes(cat_id)) {
|
||||
continue;
|
||||
}
|
||||
/* This catalog is not on disk, but also not modified, so get rid of it. */
|
||||
cats_to_remove.add(cat_id);
|
||||
}
|
||||
|
||||
for (CatalogID cat_id : cats_to_remove) {
|
||||
this->delete_catalog_by_id_hard(cat_id);
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetCatalogService::is_catalog_known_with_unsaved_changes(const CatalogID catalog_id) const
|
||||
{
|
||||
if (catalog_collection_->deleted_catalogs_.contains(catalog_id)) {
|
||||
/* Deleted catalogs are always considered modified, by definition. */
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::unique_ptr<AssetCatalog> *catalog_uptr_ptr =
|
||||
catalog_collection_->catalogs_.lookup_ptr(catalog_id);
|
||||
if (!catalog_uptr_ptr) {
|
||||
/* Catalog is unknown. */
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool has_unsaved_changes = (*catalog_uptr_ptr)->flags.has_unsaved_changes;
|
||||
return has_unsaved_changes;
|
||||
}
|
||||
|
||||
bool AssetCatalogService::write_to_disk(const CatalogFilePath &blend_file_path)
|
||||
{
|
||||
/* The caller should probably check this somewhat earlier and properly disable whatever operation
|
||||
* triggers the writing. */
|
||||
BLI_assert(!is_read_only_);
|
||||
if (is_read_only_) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this->write_to_disk_ex(blend_file_path)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->untag_has_unsaved_changes();
|
||||
this->invalidate_catalog_tree();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetCatalogService::write_to_disk_ex(const CatalogFilePath &blend_file_path)
|
||||
{
|
||||
/* TODO(Sybren): expand to support multiple CDFs. */
|
||||
|
||||
/* - Already loaded a CDF from disk? -> Only write to that file when there were actual changes.
|
||||
* This prevents touching the file, which can cause issues when multiple Blender instances are
|
||||
* accessing the same file (like on shared storage, Sync-thing, etc.). See #111576.
|
||||
*/
|
||||
if (catalog_collection_->catalog_definition_file_) {
|
||||
/* Always sync with what's on disk. */
|
||||
this->reload_catalogs();
|
||||
|
||||
if (!this->has_unsaved_changes() &&
|
||||
catalog_collection_->catalog_definition_file_->exists_on_disk())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return catalog_collection_->catalog_definition_file_->write_to_disk();
|
||||
}
|
||||
|
||||
if (catalog_collection_->is_empty()) {
|
||||
/* Avoid saving anything, when there is nothing to save. */
|
||||
return true; /* Writing nothing when there is nothing to write is still a success. */
|
||||
}
|
||||
|
||||
const CatalogFilePath cdf_path_to_write = find_suitable_cdf_path_for_writing(blend_file_path);
|
||||
catalog_collection_->catalog_definition_file_ = construct_cdf_in_memory(cdf_path_to_write);
|
||||
this->reload_catalogs();
|
||||
return catalog_collection_->catalog_definition_file_->write_to_disk();
|
||||
}
|
||||
|
||||
void AssetCatalogService::prepare_to_merge_on_write()
|
||||
{
|
||||
/* TODO(Sybren): expand to support multiple CDFs. */
|
||||
|
||||
if (!catalog_collection_->catalog_definition_file_) {
|
||||
/* There is no CDF connected, so it's a no-op. */
|
||||
return;
|
||||
}
|
||||
|
||||
/* Remove any association with the CDF, so that a new location will be chosen
|
||||
* when the blend file is saved. */
|
||||
catalog_collection_->catalog_definition_file_.reset();
|
||||
|
||||
/* Mark all in-memory catalogs as "dirty", to force them to be kept around on
|
||||
* the next "load-merge-write" cycle. */
|
||||
this->tag_all_catalogs_as_unsaved_changes();
|
||||
}
|
||||
|
||||
CatalogFilePath AssetCatalogService::find_suitable_cdf_path_for_writing(
|
||||
const CatalogFilePath &blend_file_path)
|
||||
{
|
||||
BLI_assert_msg(!blend_file_path.empty(),
|
||||
"A non-empty .blend file path is required to be able to determine where the "
|
||||
"catalog definition file should be put");
|
||||
|
||||
/* Ask the asset library API for an appropriate location. */
|
||||
const std::string suitable_root_path = AS_asset_library_find_suitable_root_path_from_path(
|
||||
blend_file_path);
|
||||
if (!suitable_root_path.empty()) {
|
||||
char asset_lib_cdf_path[PATH_MAX];
|
||||
BLI_path_join(asset_lib_cdf_path,
|
||||
sizeof(asset_lib_cdf_path),
|
||||
suitable_root_path.c_str(),
|
||||
DEFAULT_CATALOG_FILENAME.c_str());
|
||||
return asset_lib_cdf_path;
|
||||
}
|
||||
|
||||
/* Determine the default CDF path in the same directory of the blend file. */
|
||||
char blend_dir_path[PATH_MAX];
|
||||
BLI_path_split_dir_part(blend_file_path.c_str(), blend_dir_path, sizeof(blend_dir_path));
|
||||
const CatalogFilePath cdf_path_next_to_blend = asset_definition_default_file_path_from_dir(
|
||||
blend_dir_path);
|
||||
return cdf_path_next_to_blend;
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogService::construct_cdf_in_memory(
|
||||
const CatalogFilePath &file_path) const
|
||||
{
|
||||
auto cdf = std::make_unique<AssetCatalogDefinitionFile>(file_path);
|
||||
|
||||
for (auto &catalog : catalog_collection_->catalogs_.values()) {
|
||||
cdf->add_new(catalog.get());
|
||||
}
|
||||
|
||||
return cdf;
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalogTree> AssetCatalogService::read_into_tree() const
|
||||
{
|
||||
auto tree = std::make_unique<AssetCatalogTree>();
|
||||
|
||||
/* Go through the catalogs, insert each path component into the tree where needed. */
|
||||
for (auto &catalog : catalog_collection_->catalogs_.values()) {
|
||||
tree->insert_item(*catalog);
|
||||
}
|
||||
|
||||
return tree;
|
||||
}
|
||||
|
||||
void AssetCatalogService::invalidate_catalog_tree()
|
||||
{
|
||||
std::lock_guard lock{catalog_tree_mutex_};
|
||||
this->catalog_tree_ = nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<const AssetCatalogTree> AssetCatalogService::catalog_tree()
|
||||
{
|
||||
std::lock_guard lock{catalog_tree_mutex_};
|
||||
if (!catalog_tree_) {
|
||||
/* Ensure all catalog paths lead to valid catalogs. This is important for the catalog tree to
|
||||
* be usable, e.g. it makes sure every item in the tree maps to an actual catalog. */
|
||||
this->create_missing_catalogs();
|
||||
|
||||
catalog_tree_ = read_into_tree();
|
||||
}
|
||||
return catalog_tree_;
|
||||
}
|
||||
|
||||
void AssetCatalogService::create_missing_catalogs()
|
||||
{
|
||||
/* Construct an ordered set of paths to check, so that parents are ordered before children. */
|
||||
std::set<AssetCatalogPath> paths_to_check;
|
||||
for (auto &catalog : catalog_collection_->catalogs_.values()) {
|
||||
paths_to_check.insert(catalog->path);
|
||||
}
|
||||
|
||||
std::set<AssetCatalogPath> seen_paths;
|
||||
/* The empty parent should never be created, so always be considered "seen". */
|
||||
seen_paths.insert(AssetCatalogPath(""));
|
||||
|
||||
/* Find and create missing direct parents (so ignoring parents-of-parents). */
|
||||
while (!paths_to_check.empty()) {
|
||||
/* Pop the first path of the queue. */
|
||||
const AssetCatalogPath path = *paths_to_check.begin();
|
||||
paths_to_check.erase(paths_to_check.begin());
|
||||
|
||||
if (seen_paths.contains(path)) {
|
||||
/* This path has been seen already, so it can be ignored. */
|
||||
continue;
|
||||
}
|
||||
seen_paths.insert(path);
|
||||
|
||||
const AssetCatalogPath parent_path = path.parent();
|
||||
if (seen_paths.contains(parent_path)) {
|
||||
/* The parent exists, continue to the next path. */
|
||||
continue;
|
||||
}
|
||||
|
||||
/* The parent doesn't exist, so create it and queue it up for checking its parent. */
|
||||
AssetCatalog *parent_catalog = this->create_catalog(parent_path);
|
||||
parent_catalog->flags.has_unsaved_changes = true;
|
||||
|
||||
paths_to_check.insert(parent_path);
|
||||
}
|
||||
|
||||
/* TODO(Sybren): bind the newly created catalogs to a CDF, if we know about it. */
|
||||
}
|
||||
|
||||
bool AssetCatalogService::is_undo_possbile() const
|
||||
{
|
||||
return !undo_snapshots_.is_empty();
|
||||
}
|
||||
|
||||
bool AssetCatalogService::is_redo_possbile() const
|
||||
{
|
||||
return !redo_snapshots_.is_empty();
|
||||
}
|
||||
|
||||
void AssetCatalogService::undo()
|
||||
{
|
||||
BLI_assert_msg(is_undo_possbile(), "Undo stack is empty");
|
||||
|
||||
redo_snapshots_.append(std::move(catalog_collection_));
|
||||
catalog_collection_ = undo_snapshots_.pop_last();
|
||||
this->create_missing_catalogs();
|
||||
this->invalidate_catalog_tree();
|
||||
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
|
||||
}
|
||||
|
||||
void AssetCatalogService::redo()
|
||||
{
|
||||
BLI_assert(!is_read_only_);
|
||||
BLI_assert_msg(is_redo_possbile(), "Redo stack is empty");
|
||||
|
||||
undo_snapshots_.append(std::move(catalog_collection_));
|
||||
catalog_collection_ = redo_snapshots_.pop_last();
|
||||
this->create_missing_catalogs();
|
||||
this->invalidate_catalog_tree();
|
||||
AssetLibraryService::get()->tag_all_library_catalogs_dirty();
|
||||
}
|
||||
|
||||
void AssetCatalogService::undo_push()
|
||||
{
|
||||
BLI_assert(!is_read_only_);
|
||||
std::unique_ptr<AssetCatalogCollection> snapshot = catalog_collection_->deep_copy();
|
||||
undo_snapshots_.append(std::move(snapshot));
|
||||
redo_snapshots_.clear();
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
AssetCatalog::AssetCatalog(const CatalogID catalog_id,
|
||||
const AssetCatalogPath &path,
|
||||
const std::string &simple_name)
|
||||
: catalog_id(catalog_id), path(path), simple_name(simple_name)
|
||||
{
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalog> AssetCatalog::from_path(const AssetCatalogPath &path)
|
||||
{
|
||||
const AssetCatalogPath clean_path = path.cleanup();
|
||||
const CatalogID cat_id = BLI_uuid_generate_random();
|
||||
const std::string simple_name = sensible_simple_name_for_path(clean_path);
|
||||
auto catalog = std::make_unique<AssetCatalog>(cat_id, clean_path, simple_name);
|
||||
return catalog;
|
||||
}
|
||||
|
||||
void AssetCatalog::simple_name_refresh()
|
||||
{
|
||||
this->simple_name = sensible_simple_name_for_path(this->path);
|
||||
}
|
||||
|
||||
std::string AssetCatalog::sensible_simple_name_for_path(const AssetCatalogPath &path)
|
||||
{
|
||||
std::string name = path.str();
|
||||
std::replace(name.begin(), name.end(), AssetCatalogPath::SEPARATOR, '-');
|
||||
if (name.length() < MAX_NAME - 1) {
|
||||
return name;
|
||||
}
|
||||
|
||||
/* Trim off the start of the path, as that's the most generic part and thus contains the least
|
||||
* information. */
|
||||
return "..." + name.substr(name.length() - 60);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
AssetCatalogFilter::AssetCatalogFilter(Set<CatalogID> &&matching_catalog_ids,
|
||||
Set<CatalogID> &&known_catalog_ids)
|
||||
: matching_catalog_ids_(std::move(matching_catalog_ids)),
|
||||
known_catalog_ids_(std::move(known_catalog_ids))
|
||||
{
|
||||
}
|
||||
|
||||
bool AssetCatalogFilter::contains(const CatalogID asset_catalog_id) const
|
||||
{
|
||||
return matching_catalog_ids_.contains(asset_catalog_id);
|
||||
}
|
||||
|
||||
bool AssetCatalogFilter::is_known(const CatalogID asset_catalog_id) const
|
||||
{
|
||||
if (BLI_uuid_is_nil(asset_catalog_id)) {
|
||||
return false;
|
||||
}
|
||||
return known_catalog_ids_.contains(asset_catalog_id);
|
||||
}
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,73 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
#include "asset_catalog_collection.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
bool AssetCatalogCollection::is_empty() const
|
||||
{
|
||||
return catalogs_.is_empty() && deleted_catalogs_.is_empty();
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalogCollection> AssetCatalogCollection::deep_copy() const
|
||||
{
|
||||
auto copy = std::make_unique<AssetCatalogCollection>();
|
||||
|
||||
copy->has_unsaved_changes_ = this->has_unsaved_changes_;
|
||||
copy->catalogs_ = copy_catalog_map(this->catalogs_);
|
||||
copy->deleted_catalogs_ = copy_catalog_map(this->deleted_catalogs_);
|
||||
|
||||
if (catalog_definition_file_) {
|
||||
copy->catalog_definition_file_ = catalog_definition_file_->copy_and_remap(
|
||||
copy->catalogs_, copy->deleted_catalogs_);
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
static void copy_catalog_map_into_existing(
|
||||
const OwningAssetCatalogMap &source,
|
||||
OwningAssetCatalogMap &dest,
|
||||
AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items)
|
||||
{
|
||||
for (const auto &orig_catalog_uptr : source.values()) {
|
||||
if (dest.contains(orig_catalog_uptr->catalog_id)) {
|
||||
if (on_duplicate_items) {
|
||||
on_duplicate_items(*dest.lookup(orig_catalog_uptr->catalog_id), *orig_catalog_uptr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
auto copy_catalog_uptr = std::make_unique<AssetCatalog>(*orig_catalog_uptr);
|
||||
dest.add_new(copy_catalog_uptr->catalog_id, std::move(copy_catalog_uptr));
|
||||
}
|
||||
}
|
||||
|
||||
void AssetCatalogCollection::add_catalogs_from_existing(
|
||||
const AssetCatalogCollection &other,
|
||||
AssetCatalogCollection::OnDuplicateCatalogIdFn on_duplicate_items)
|
||||
{
|
||||
copy_catalog_map_into_existing(other.catalogs_, catalogs_, on_duplicate_items);
|
||||
}
|
||||
|
||||
OwningAssetCatalogMap AssetCatalogCollection::copy_catalog_map(const OwningAssetCatalogMap &orig)
|
||||
{
|
||||
OwningAssetCatalogMap copy;
|
||||
copy_catalog_map_into_existing(
|
||||
orig, copy, /*on_duplicate_items=*/[](const AssetCatalog &, const AssetCatalog &) {
|
||||
/* `copy` was empty before. If this happens it means there was a duplicate in the `orig`
|
||||
* catalog map which should've been caught already. */
|
||||
BLI_assert_unreachable();
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,68 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class AssetLibraryService;
|
||||
|
||||
/**
|
||||
* All catalogs that are owned by a single asset library, and managed by a single instance of
|
||||
* #AssetCatalogService. The undo system for asset catalog edits contains historical copies of this
|
||||
* struct.
|
||||
*/
|
||||
class AssetCatalogCollection {
|
||||
protected:
|
||||
/** All catalogs known, except the known-but-deleted ones. */
|
||||
OwningAssetCatalogMap catalogs_;
|
||||
|
||||
/** Catalogs that have been deleted. They are kept around so that the load-merge-save of catalog
|
||||
* definition files can actually delete them if they already existed on disk (instead of the
|
||||
* merge operation resurrecting them). */
|
||||
OwningAssetCatalogMap deleted_catalogs_;
|
||||
|
||||
/* For now only a single catalog definition file is supported.
|
||||
* The aim is to support an arbitrary number of such files per asset library in the future. */
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> catalog_definition_file_;
|
||||
|
||||
/** Whether any of the catalogs have unsaved changes. */
|
||||
bool has_unsaved_changes_ = false;
|
||||
|
||||
friend AssetCatalogService;
|
||||
friend AssetLibraryService;
|
||||
|
||||
public:
|
||||
AssetCatalogCollection() = default;
|
||||
AssetCatalogCollection(const AssetCatalogCollection &other) = delete;
|
||||
AssetCatalogCollection(AssetCatalogCollection &&other) noexcept = default;
|
||||
|
||||
/** Check if this contains any catalogs or deleted catalogs. Doesn't check if a CDF is present.
|
||||
*/
|
||||
bool is_empty() const;
|
||||
|
||||
std::unique_ptr<AssetCatalogCollection> deep_copy() const;
|
||||
using OnDuplicateCatalogIdFn =
|
||||
FunctionRef<void(const AssetCatalog &existing, const AssetCatalog &to_be_ignored)>;
|
||||
/**
|
||||
* Copy the catalogs from \a other and append them to this collection. Copies no other data
|
||||
* otherwise.
|
||||
*
|
||||
* \note If a catalog from \a other already exists in this collection (identified by catalog ID),
|
||||
* it will be skipped and \a on_duplicate_items will be called.
|
||||
*/
|
||||
void add_catalogs_from_existing(const AssetCatalogCollection &other,
|
||||
OnDuplicateCatalogIdFn on_duplicate_items);
|
||||
|
||||
protected:
|
||||
static OwningAssetCatalogMap copy_catalog_map(const OwningAssetCatalogMap &orig);
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,298 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include "BLI_fileops.hh"
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "asset_catalog_definition_file.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"asset.catalog"};
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
const int AssetCatalogDefinitionFile::SUPPORTED_VERSION = 1;
|
||||
const std::string AssetCatalogDefinitionFile::VERSION_MARKER = "VERSION ";
|
||||
|
||||
const std::string AssetCatalogDefinitionFile::HEADER =
|
||||
"# This is an Asset Catalog Definition file for Blender.\n"
|
||||
"#\n"
|
||||
"# Empty lines and lines starting with `#` will be ignored.\n"
|
||||
"# The first non-ignored line should be the version indicator.\n"
|
||||
"# Other lines are of the format \"UUID:catalog/path/for/assets:simple catalog name\"\n";
|
||||
|
||||
bool AssetCatalogDefinitionFile::contains(const CatalogID catalog_id) const
|
||||
{
|
||||
return catalogs_.contains(catalog_id);
|
||||
}
|
||||
|
||||
void AssetCatalogDefinitionFile::add_new(AssetCatalog *catalog)
|
||||
{
|
||||
catalogs_.add_new(catalog->catalog_id, catalog);
|
||||
}
|
||||
|
||||
void AssetCatalogDefinitionFile::add_overwrite(AssetCatalog *catalog)
|
||||
{
|
||||
catalogs_.add_overwrite(catalog->catalog_id, catalog);
|
||||
}
|
||||
|
||||
void AssetCatalogDefinitionFile::forget(CatalogID catalog_id)
|
||||
{
|
||||
catalogs_.remove(catalog_id);
|
||||
}
|
||||
|
||||
void AssetCatalogDefinitionFile::parse_catalog_file(
|
||||
const CatalogFilePath &catalog_definition_file_path,
|
||||
AssetCatalogParsedFn catalog_loaded_callback)
|
||||
{
|
||||
fstream infile(catalog_definition_file_path, std::ios::in);
|
||||
|
||||
if (!infile.is_open()) {
|
||||
CLOG_ERROR(&LOG, "%s: unable to open file", catalog_definition_file_path.c_str());
|
||||
return;
|
||||
}
|
||||
bool seen_version_number = false;
|
||||
std::string line;
|
||||
while (std::getline(infile, line)) {
|
||||
const StringRef trimmed_line = StringRef(line).trim();
|
||||
if (trimmed_line.is_empty() || trimmed_line[0] == '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!seen_version_number) {
|
||||
/* The very first non-ignored line should be the version declaration. */
|
||||
const bool is_valid_version = this->parse_version_line(trimmed_line);
|
||||
if (!is_valid_version) {
|
||||
std::cerr << catalog_definition_file_path
|
||||
<< ": first line should be version declaration; ignoring file." << std::endl;
|
||||
break;
|
||||
}
|
||||
seen_version_number = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalog> catalog = this->parse_catalog_line(trimmed_line);
|
||||
if (!catalog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
AssetCatalog *non_owning_ptr = catalog.get();
|
||||
const bool keep_catalog = catalog_loaded_callback(std::move(catalog));
|
||||
if (!keep_catalog) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* The AssetDefinitionFile should include this catalog when writing it back to disk. */
|
||||
this->add_overwrite(non_owning_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetCatalogDefinitionFile::parse_version_line(const StringRef line)
|
||||
{
|
||||
if (!line.startswith(VERSION_MARKER)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string version_string = line.substr(VERSION_MARKER.length());
|
||||
const int file_version = std::atoi(version_string.c_str());
|
||||
|
||||
/* No versioning, just a blunt check whether it's the right one. */
|
||||
return file_version == SUPPORTED_VERSION;
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalog> AssetCatalogDefinitionFile::parse_catalog_line(const StringRef line)
|
||||
{
|
||||
const char delim = ':';
|
||||
const int64_t first_delim = line.find_first_of(delim);
|
||||
if (first_delim == StringRef::not_found) {
|
||||
std::cerr << "Invalid catalog line in " << this->file_path << ": " << line << std::endl;
|
||||
return std::unique_ptr<AssetCatalog>(nullptr);
|
||||
}
|
||||
|
||||
/* Parse the catalog ID. */
|
||||
const std::string id_as_string = line.substr(0, first_delim).trim();
|
||||
bUUID catalog_id;
|
||||
const bool uuid_parsed_ok = BLI_uuid_parse_string(&catalog_id, id_as_string.c_str());
|
||||
if (!uuid_parsed_ok) {
|
||||
std::cerr << "Invalid UUID in " << this->file_path << ": " << line << std::endl;
|
||||
return std::unique_ptr<AssetCatalog>(nullptr);
|
||||
}
|
||||
|
||||
/* Parse the path and simple name. */
|
||||
const StringRef path_and_simple_name = line.substr(first_delim + 1);
|
||||
const int64_t second_delim = path_and_simple_name.find_first_of(delim);
|
||||
|
||||
std::string path_in_file;
|
||||
std::string simple_name;
|
||||
if (second_delim == 0) {
|
||||
/* Delimiter as first character means there is no path. These lines are to be ignored. */
|
||||
return std::unique_ptr<AssetCatalog>(nullptr);
|
||||
}
|
||||
|
||||
if (second_delim == StringRef::not_found) {
|
||||
/* No delimiter means no simple name, just treat it as all "path". */
|
||||
path_in_file = path_and_simple_name;
|
||||
simple_name = "";
|
||||
}
|
||||
else {
|
||||
path_in_file = path_and_simple_name.substr(0, second_delim);
|
||||
simple_name = path_and_simple_name.substr(second_delim + 1).trim();
|
||||
}
|
||||
|
||||
AssetCatalogPath catalog_path = path_in_file;
|
||||
return std::make_unique<AssetCatalog>(catalog_id, catalog_path.cleanup(), simple_name);
|
||||
}
|
||||
|
||||
AssetCatalogDefinitionFile::AssetCatalogDefinitionFile(const CatalogFilePath &file_path)
|
||||
: file_path(file_path)
|
||||
{
|
||||
}
|
||||
|
||||
bool AssetCatalogDefinitionFile::write_to_disk() const
|
||||
{
|
||||
BLI_assert_msg(!this->file_path.empty(), "Writing to CDF requires its file path to be known");
|
||||
return this->write_to_disk(this->file_path);
|
||||
}
|
||||
|
||||
bool AssetCatalogDefinitionFile::write_to_disk(const CatalogFilePath &dest_file_path) const
|
||||
{
|
||||
const CatalogFilePath writable_path = dest_file_path + ".writing";
|
||||
const CatalogFilePath backup_path = dest_file_path + "~";
|
||||
|
||||
if (!this->write_to_disk_unsafe(writable_path)) {
|
||||
/* TODO: communicate what went wrong. */
|
||||
return false;
|
||||
}
|
||||
if (BLI_exists(dest_file_path.c_str())) {
|
||||
if (BLI_rename_overwrite(dest_file_path.c_str(), backup_path.c_str())) {
|
||||
/* TODO: communicate what went wrong. */
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (BLI_rename_overwrite(writable_path.c_str(), dest_file_path.c_str())) {
|
||||
/* TODO: communicate what went wrong. */
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AssetCatalogDefinitionFile::exists_on_disk() const
|
||||
{
|
||||
return BLI_exists(this->file_path.c_str());
|
||||
}
|
||||
|
||||
bool AssetCatalogDefinitionFile::write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const
|
||||
{
|
||||
char directory[PATH_MAX];
|
||||
BLI_path_split_dir_part(dest_file_path.c_str(), directory, sizeof(directory));
|
||||
if (!ensure_directory_exists(directory)) {
|
||||
/* TODO(Sybren): pass errors to the UI somehow. */
|
||||
return false;
|
||||
}
|
||||
|
||||
fstream output(dest_file_path, std::ios::out);
|
||||
|
||||
/* TODO(@sybren): remember the line ending style that was originally read, then use that to write
|
||||
* the file again. */
|
||||
|
||||
/* Write the header. */
|
||||
output << HEADER;
|
||||
output << "" << std::endl;
|
||||
output << VERSION_MARKER << SUPPORTED_VERSION << std::endl;
|
||||
output << "" << std::endl;
|
||||
|
||||
/* Write the catalogs, ordered by path (primary) and UUID (secondary). */
|
||||
AssetCatalogOrderedSet catalogs_by_path;
|
||||
for (const AssetCatalog *catalog : catalogs_.values()) {
|
||||
if (catalog->flags.is_deleted) {
|
||||
continue;
|
||||
}
|
||||
catalogs_by_path.insert(catalog);
|
||||
}
|
||||
|
||||
for (const AssetCatalog *catalog : catalogs_by_path) {
|
||||
output << catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name
|
||||
<< std::endl;
|
||||
}
|
||||
output.close();
|
||||
return !output.bad();
|
||||
}
|
||||
|
||||
bool AssetCatalogDefinitionFile::ensure_directory_exists(
|
||||
const CatalogFilePath &directory_path) const
|
||||
{
|
||||
/* TODO(@sybren): design a way to get such errors presented to users (or ensure that they never
|
||||
* occur). */
|
||||
if (directory_path.empty()) {
|
||||
std::cerr
|
||||
<< "AssetCatalogService: no asset library root configured, unable to ensure it exists."
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (BLI_exists(directory_path.data())) {
|
||||
if (!BLI_is_dir(directory_path.data())) {
|
||||
std::cerr << "AssetCatalogService: " << directory_path
|
||||
<< " exists but is not a directory, this is not a supported situation."
|
||||
<< std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Root directory exists, work is done. */
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Ensure the root directory exists. */
|
||||
std::error_code err_code;
|
||||
if (!BLI_dir_create_recursive(directory_path.data())) {
|
||||
std::cerr << "AssetCatalogService: error creating directory " << directory_path << ": "
|
||||
<< err_code << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Root directory has been created, work is done. */
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> AssetCatalogDefinitionFile::copy_and_remap(
|
||||
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const
|
||||
{
|
||||
auto copy = std::make_unique<AssetCatalogDefinitionFile>(*this);
|
||||
copy->catalogs_.clear();
|
||||
|
||||
/* Remap pointers of the copy from the original AssetCatalogCollection to the given one. */
|
||||
for (CatalogID catalog_id : catalogs_.keys()) {
|
||||
/* The catalog can be in the regular or the deleted map. */
|
||||
const std::unique_ptr<AssetCatalog> *remapped_catalog_uptr_ptr = catalogs.lookup_ptr(
|
||||
catalog_id);
|
||||
if (remapped_catalog_uptr_ptr) {
|
||||
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
|
||||
continue;
|
||||
}
|
||||
|
||||
remapped_catalog_uptr_ptr = deleted_catalogs.lookup_ptr(catalog_id);
|
||||
if (remapped_catalog_uptr_ptr) {
|
||||
copy->catalogs_.add_new(catalog_id, remapped_catalog_uptr_ptr->get());
|
||||
continue;
|
||||
}
|
||||
|
||||
BLI_assert_msg(false, "A CDF should only reference known catalogs.");
|
||||
}
|
||||
|
||||
return copy;
|
||||
}
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,91 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*
|
||||
* Classes internal to the asset system for asset catalog management.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* Keeps track of which catalogs are defined in a certain file on disk.
|
||||
* Only contains non-owning pointers to the #AssetCatalog instances, so ensure the lifetime of this
|
||||
* class is shorter than that of the #`AssetCatalog`s themselves.
|
||||
*/
|
||||
class AssetCatalogDefinitionFile {
|
||||
protected:
|
||||
/* Catalogs stored in this file. They are mapped by ID to make it possible to query whether a
|
||||
* catalog is already known, without having to find the corresponding `AssetCatalog*`. */
|
||||
Map<CatalogID, AssetCatalog *> catalogs_;
|
||||
|
||||
public:
|
||||
/* For now this is the only version of the catalog definition files that is supported.
|
||||
* Later versioning code may be added to handle older files. */
|
||||
const static int SUPPORTED_VERSION;
|
||||
/* String that's matched in the catalog definition file to know that the line is the version
|
||||
* declaration. It has to start with a space to ensure it won't match any hypothetical future
|
||||
* field that starts with "VERSION". */
|
||||
const static std::string VERSION_MARKER;
|
||||
const static std::string HEADER;
|
||||
|
||||
const CatalogFilePath file_path;
|
||||
|
||||
AssetCatalogDefinitionFile(const CatalogFilePath &file_path);
|
||||
|
||||
/**
|
||||
* Write the catalog definitions to the same file they were read from.
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk() const;
|
||||
/**
|
||||
* Write the catalog definitions to an arbitrary file path.
|
||||
*
|
||||
* Any existing file is backed up to "filename~". Any previously existing backup is overwritten.
|
||||
*
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk(const CatalogFilePath &dest_file_path) const;
|
||||
|
||||
/**
|
||||
* Returns whether this file exists on disk.
|
||||
*/
|
||||
bool exists_on_disk() const;
|
||||
|
||||
bool contains(CatalogID catalog_id) const;
|
||||
/** Add a catalog, overwriting the one with the same catalog ID. */
|
||||
void add_overwrite(AssetCatalog *catalog);
|
||||
/** Add a new catalog. Undefined behavior if a catalog with the same ID was already added. */
|
||||
void add_new(AssetCatalog *catalog);
|
||||
|
||||
/** Remove the catalog from the collection of catalogs stored in this file. */
|
||||
void forget(CatalogID catalog_id);
|
||||
|
||||
using AssetCatalogParsedFn = FunctionRef<bool(std::unique_ptr<AssetCatalog>)>;
|
||||
void parse_catalog_file(const CatalogFilePath &catalog_definition_file_path,
|
||||
AssetCatalogParsedFn catalog_loaded_callback);
|
||||
|
||||
std::unique_ptr<AssetCatalogDefinitionFile> copy_and_remap(
|
||||
const OwningAssetCatalogMap &catalogs, const OwningAssetCatalogMap &deleted_catalogs) const;
|
||||
|
||||
protected:
|
||||
bool parse_version_line(StringRef line);
|
||||
std::unique_ptr<AssetCatalog> parse_catalog_line(StringRef line);
|
||||
|
||||
/**
|
||||
* Write the catalog definitions to the given file path.
|
||||
* Return true when the file was written correctly, false when there was a problem.
|
||||
*/
|
||||
bool write_to_disk_unsafe(const CatalogFilePath &dest_file_path) const;
|
||||
bool ensure_directory_exists(const CatalogFilePath &directory_path) const;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,228 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "AS_asset_catalog_path.hh"
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include <sstream>
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
const char AssetCatalogPath::SEPARATOR = '/';
|
||||
|
||||
AssetCatalogPath::AssetCatalogPath(std::string path) : path_(std::move(path)) {}
|
||||
|
||||
AssetCatalogPath::AssetCatalogPath(StringRef path) : path_(path) {}
|
||||
|
||||
AssetCatalogPath::AssetCatalogPath(const char *path) : path_(path) {}
|
||||
|
||||
AssetCatalogPath::AssetCatalogPath(AssetCatalogPath &&other_path) noexcept
|
||||
: path_(std::move(other_path.path_))
|
||||
{
|
||||
}
|
||||
|
||||
uint64_t AssetCatalogPath::hash() const
|
||||
{
|
||||
std::hash<std::string> hasher{};
|
||||
return hasher(path_);
|
||||
}
|
||||
|
||||
uint64_t AssetCatalogPath::length() const
|
||||
{
|
||||
return path_.length();
|
||||
}
|
||||
|
||||
const char *AssetCatalogPath::c_str() const
|
||||
{
|
||||
return path_.c_str();
|
||||
}
|
||||
|
||||
const std::string &AssetCatalogPath::str() const
|
||||
{
|
||||
return path_;
|
||||
}
|
||||
|
||||
StringRefNull AssetCatalogPath::name() const
|
||||
{
|
||||
const size_t last_sep_index = path_.rfind(SEPARATOR);
|
||||
if (last_sep_index == std::string::npos) {
|
||||
return StringRefNull(path_);
|
||||
}
|
||||
|
||||
return StringRefNull(path_.c_str() + last_sep_index + 1);
|
||||
}
|
||||
|
||||
bool AssetCatalogPath::operator==(const AssetCatalogPath &other_path) const
|
||||
{
|
||||
return path_ == other_path.path_;
|
||||
}
|
||||
|
||||
bool AssetCatalogPath::operator!=(const AssetCatalogPath &other_path) const
|
||||
{
|
||||
return !(*this == other_path);
|
||||
}
|
||||
|
||||
bool AssetCatalogPath::operator<(const AssetCatalogPath &other_path) const
|
||||
{
|
||||
return path_ < other_path.path_;
|
||||
}
|
||||
|
||||
AssetCatalogPath AssetCatalogPath::operator/(const AssetCatalogPath &path_to_append) const
|
||||
{
|
||||
/* `"" / "path"` or `"path" / ""` should just result in `"path"` */
|
||||
if (!*this) {
|
||||
return path_to_append;
|
||||
}
|
||||
if (!path_to_append) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
std::stringstream new_path;
|
||||
new_path << path_ << SEPARATOR << path_to_append.path_;
|
||||
return AssetCatalogPath(new_path.str());
|
||||
}
|
||||
|
||||
AssetCatalogPath::operator bool() const
|
||||
{
|
||||
return !path_.empty();
|
||||
}
|
||||
|
||||
std::ostream &operator<<(std::ostream &stream, const AssetCatalogPath &path_to_append)
|
||||
{
|
||||
stream << path_to_append.path_;
|
||||
return stream;
|
||||
}
|
||||
|
||||
AssetCatalogPath AssetCatalogPath::from_user_input(const char *path)
|
||||
{
|
||||
return AssetCatalogPath(path).cleanup();
|
||||
}
|
||||
|
||||
AssetCatalogPath AssetCatalogPath::cleanup() const
|
||||
{
|
||||
std::stringstream clean_components;
|
||||
bool first_component_seen = false;
|
||||
|
||||
this->iterate_components([&clean_components, &first_component_seen](StringRef component_name,
|
||||
bool /*is_last_component*/) {
|
||||
const std::string clean_component = cleanup_component(component_name);
|
||||
|
||||
if (clean_component.empty()) {
|
||||
/* These are caused by leading, trailing, or double slashes. */
|
||||
return;
|
||||
}
|
||||
|
||||
/* If a previous path component has been streamed already, we need a path separator. This
|
||||
* cannot use the `is_last_component` boolean, because the last component might be skipped due
|
||||
* to the condition above. */
|
||||
if (first_component_seen) {
|
||||
clean_components << SEPARATOR;
|
||||
}
|
||||
first_component_seen = true;
|
||||
|
||||
clean_components << clean_component;
|
||||
});
|
||||
|
||||
return AssetCatalogPath(clean_components.str());
|
||||
}
|
||||
|
||||
std::string AssetCatalogPath::cleanup_component(StringRef component_name)
|
||||
{
|
||||
std::string cleaned = component_name.trim();
|
||||
/* Replace colons with something else, as those are used in the CDF file as delimiter. */
|
||||
std::replace(cleaned.begin(), cleaned.end(), ':', '-');
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
bool AssetCatalogPath::is_contained_in(const AssetCatalogPath &other_path) const
|
||||
{
|
||||
if (!other_path) {
|
||||
/* The empty path contains all other paths. */
|
||||
return true;
|
||||
}
|
||||
|
||||
if (path_ == other_path.path_) {
|
||||
/* Weak is-in relation: equal paths contain each other. */
|
||||
return true;
|
||||
}
|
||||
|
||||
/* To be a child path of 'other_path', our path must be at least a separator and another
|
||||
* character longer. */
|
||||
if (this->length() < other_path.length() + 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Create StringRef to be able to use .startswith(). */
|
||||
const StringRef this_path(path_);
|
||||
const bool prefix_ok = this_path.startswith(other_path.path_);
|
||||
const char next_char = this_path[other_path.length()];
|
||||
return prefix_ok && next_char == SEPARATOR;
|
||||
}
|
||||
|
||||
AssetCatalogPath AssetCatalogPath::parent() const
|
||||
{
|
||||
if (!*this) {
|
||||
return AssetCatalogPath("");
|
||||
}
|
||||
std::string::size_type last_sep_index = path_.rfind(SEPARATOR);
|
||||
if (last_sep_index == std::string::npos) {
|
||||
return AssetCatalogPath("");
|
||||
}
|
||||
return AssetCatalogPath(path_.substr(0, last_sep_index));
|
||||
}
|
||||
|
||||
void AssetCatalogPath::iterate_components(ComponentIteratorFn callback) const
|
||||
{
|
||||
const char *next_slash_ptr;
|
||||
|
||||
for (const char *path_component = path_.data(); path_component && path_component[0];
|
||||
/* Jump to one after the next slash if there is any. */
|
||||
path_component = next_slash_ptr ? next_slash_ptr + 1 : nullptr)
|
||||
{
|
||||
/* Note that this also treats backslashes as component separators, which
|
||||
* helps in cleaning up backslash-separated paths. */
|
||||
next_slash_ptr = BLI_path_slash_find(path_component);
|
||||
|
||||
const bool is_last_component = next_slash_ptr == nullptr;
|
||||
/* Note that this won't be null terminated. */
|
||||
const StringRef component_name = is_last_component ?
|
||||
path_component :
|
||||
StringRef(path_component,
|
||||
next_slash_ptr - path_component);
|
||||
|
||||
callback(component_name, is_last_component);
|
||||
}
|
||||
}
|
||||
|
||||
AssetCatalogPath AssetCatalogPath::rebase(const AssetCatalogPath &from_path,
|
||||
const AssetCatalogPath &to_path) const
|
||||
{
|
||||
if (!from_path) {
|
||||
if (!to_path) {
|
||||
return AssetCatalogPath("");
|
||||
}
|
||||
return to_path / *this;
|
||||
}
|
||||
|
||||
if (!this->is_contained_in(from_path)) {
|
||||
return AssetCatalogPath("");
|
||||
}
|
||||
|
||||
if (*this == from_path) {
|
||||
/* Early return, because otherwise the length+1 below is going to cause problems. */
|
||||
return to_path;
|
||||
}
|
||||
|
||||
/* When from_path = "test", we need to skip "test/" to get the rest of the path, hence the +1. */
|
||||
const StringRef suffix = StringRef(path_).substr(from_path.length() + 1);
|
||||
const AssetCatalogPath path_suffix(suffix);
|
||||
return to_path / path_suffix;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,182 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
AssetCatalogTreeItem::AssetCatalogTreeItem(StringRef name,
|
||||
CatalogID catalog_id,
|
||||
StringRef simple_name,
|
||||
const AssetCatalogTreeItem *parent)
|
||||
: name_(name), catalog_id_(catalog_id), simple_name_(simple_name), parent_(parent)
|
||||
{
|
||||
}
|
||||
|
||||
CatalogID AssetCatalogTreeItem::get_catalog_id() const
|
||||
{
|
||||
return catalog_id_;
|
||||
}
|
||||
|
||||
StringRefNull AssetCatalogTreeItem::get_name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
StringRefNull AssetCatalogTreeItem::get_simple_name() const
|
||||
{
|
||||
return simple_name_;
|
||||
}
|
||||
bool AssetCatalogTreeItem::has_unsaved_changes() const
|
||||
{
|
||||
return has_unsaved_changes_;
|
||||
}
|
||||
|
||||
AssetCatalogPath AssetCatalogTreeItem::catalog_path() const
|
||||
{
|
||||
AssetCatalogPath current_path = name_;
|
||||
for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
|
||||
current_path = AssetCatalogPath(parent->name_) / current_path;
|
||||
}
|
||||
return current_path;
|
||||
}
|
||||
|
||||
int AssetCatalogTreeItem::count_parents() const
|
||||
{
|
||||
int i = 0;
|
||||
for (const AssetCatalogTreeItem *parent = parent_; parent; parent = parent->parent_) {
|
||||
i++;
|
||||
}
|
||||
return i;
|
||||
}
|
||||
|
||||
bool AssetCatalogTreeItem::has_children() const
|
||||
{
|
||||
return !children_.empty();
|
||||
}
|
||||
|
||||
void AssetCatalogTreeItem::foreach_item_recursive(const AssetCatalogTreeItem::ChildMap &children,
|
||||
const ItemIterFn callback)
|
||||
{
|
||||
for (const auto &[key, item] : children) {
|
||||
callback(item);
|
||||
foreach_item_recursive(item.children_, callback);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetCatalogTreeItem::foreach_child(const ItemIterFn callback) const
|
||||
{
|
||||
for (const auto &[key, item] : children_) {
|
||||
callback(item);
|
||||
}
|
||||
}
|
||||
|
||||
void AssetCatalogTreeItem::foreach_item(const ItemIterFn callback) const
|
||||
{
|
||||
AssetCatalogTreeItem::foreach_item_recursive(children_, callback);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------- */
|
||||
|
||||
void AssetCatalogTree::insert_item(const AssetCatalog &catalog,
|
||||
const std::optional<StringRef> skip_prefix)
|
||||
{
|
||||
const AssetCatalogTreeItem *parent = nullptr;
|
||||
/* The children for the currently iterated component, where the following component should be
|
||||
* added to (if not there yet). */
|
||||
AssetCatalogTreeItem::ChildMap *current_item_children = &root_items_;
|
||||
|
||||
BLI_assert_msg(!ELEM(catalog.path.str()[0], '/', '\\'),
|
||||
"Malformed catalog path; should not start with a separator");
|
||||
|
||||
const CatalogID nil_id{};
|
||||
|
||||
std::optional<StringRef> skip_prefix_tmp = skip_prefix;
|
||||
|
||||
catalog.path.iterate_components([&](StringRef component_name, const bool is_last_component) {
|
||||
if (skip_prefix_tmp && skip_prefix_tmp->startswith(component_name)) {
|
||||
if (skip_prefix_tmp->size() == component_name.size() ||
|
||||
(*skip_prefix)[component_name.size()] == AssetCatalogPath::SEPARATOR)
|
||||
{
|
||||
skip_prefix_tmp = skip_prefix_tmp->drop_prefix(component_name.size() + 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert new tree element - if no matching one is there yet! */
|
||||
auto [key_and_item, was_inserted] = current_item_children->emplace(
|
||||
component_name,
|
||||
AssetCatalogTreeItem(component_name,
|
||||
is_last_component ? catalog.catalog_id : nil_id,
|
||||
is_last_component ? catalog.simple_name : "",
|
||||
parent));
|
||||
AssetCatalogTreeItem &item = key_and_item->second;
|
||||
|
||||
/* If full path of this catalog already exists as parent path of a previously read catalog,
|
||||
* we can ensure this tree item's UUID is set here. */
|
||||
if (is_last_component) {
|
||||
if (BLI_uuid_is_nil(item.catalog_id_) || catalog.flags.is_first_loaded) {
|
||||
item.catalog_id_ = catalog.catalog_id;
|
||||
}
|
||||
item.has_unsaved_changes_ = catalog.flags.has_unsaved_changes;
|
||||
}
|
||||
|
||||
/* Walk further into the path (no matter if a new item was created or not). */
|
||||
parent = &item;
|
||||
current_item_children = &item.children_;
|
||||
});
|
||||
}
|
||||
|
||||
void AssetCatalogTree::foreach_item(AssetCatalogTreeItem::ItemIterFn callback) const
|
||||
{
|
||||
AssetCatalogTreeItem::foreach_item_recursive(root_items_, callback);
|
||||
}
|
||||
|
||||
void AssetCatalogTree::foreach_root_item(const ItemIterFn callback) const
|
||||
{
|
||||
for (const auto &[key, item] : root_items_) {
|
||||
callback(item);
|
||||
}
|
||||
}
|
||||
|
||||
bool AssetCatalogTree::is_empty() const
|
||||
{
|
||||
return root_items_.empty();
|
||||
}
|
||||
|
||||
const AssetCatalogTreeItem *AssetCatalogTree::find_item(const AssetCatalogPath &path) const
|
||||
{
|
||||
const AssetCatalogTreeItem *result = nullptr;
|
||||
this->foreach_item([&](const AssetCatalogTreeItem &item) {
|
||||
if (result) {
|
||||
/* There is no way to stop iteration. */
|
||||
return;
|
||||
}
|
||||
if (item.catalog_path() == path) {
|
||||
result = &item;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
const AssetCatalogTreeItem *AssetCatalogTree::find_root_item(const AssetCatalogPath &path) const
|
||||
{
|
||||
const AssetCatalogTreeItem *result = nullptr;
|
||||
this->foreach_root_item([&](const AssetCatalogTreeItem &item) {
|
||||
if (result) {
|
||||
/* There is no way to stop iteration. */
|
||||
return;
|
||||
}
|
||||
if (item.catalog_path() == path) {
|
||||
result = &item;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,615 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_library.hh"
|
||||
#include "AS_asset_representation.hh"
|
||||
#include "AS_essentials_library.hh"
|
||||
#include "AS_remote_library.hh"
|
||||
|
||||
#include "BKE_lib_remap.hh"
|
||||
#include "BKE_main.hh"
|
||||
#include "BKE_preferences.h"
|
||||
|
||||
#include "BLI_listbase.h" // IWYU pragma: keep
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "DNA_asset_types.h"
|
||||
#include "DNA_space_types.h"
|
||||
#include "DNA_userdef_types.h"
|
||||
#include "DNA_windowmanager_types.h"
|
||||
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_library_service.hh"
|
||||
#include "runtime_library.hh"
|
||||
#include "utils.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
using namespace blender::asset_system;
|
||||
|
||||
bool AssetLibrary::save_catalogs_when_file_is_saved = true;
|
||||
|
||||
void AS_asset_libraries_exit()
|
||||
{
|
||||
/* NOTE: Can probably removed once #WITH_DESTROY_VIA_LOAD_HANDLER gets enabled by default. */
|
||||
|
||||
AssetLibraryService::destroy();
|
||||
}
|
||||
|
||||
AssetLibrary *AS_asset_library_load(const Main *bmain,
|
||||
const AssetLibraryReference &library_reference)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
return service->get_asset_library(bmain, library_reference);
|
||||
}
|
||||
|
||||
AssetLibrary *AS_asset_library_load_from_directory(const char *name, const char *library_dirpath)
|
||||
{
|
||||
/* NOTE: Loading an asset library at this point only means loading the catalogs.
|
||||
* Later on this should invoke reading of asset representations too. */
|
||||
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *lib;
|
||||
if (library_dirpath == nullptr || library_dirpath[0] == '\0') {
|
||||
lib = service->get_asset_library_current_file();
|
||||
}
|
||||
else {
|
||||
lib = service->get_asset_library_on_disk_custom(name, library_dirpath);
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
bool AS_asset_library_has_any_unsaved_catalogs()
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
return service->has_any_unsaved_catalogs();
|
||||
}
|
||||
|
||||
std::string AS_asset_library_root_path_from_library_ref(
|
||||
const AssetLibraryReference &library_reference)
|
||||
{
|
||||
return AssetLibraryService::root_path_from_library_ref(library_reference);
|
||||
}
|
||||
|
||||
std::string AS_asset_library_find_suitable_root_path_from_path(const StringRefNull input_path)
|
||||
{
|
||||
if (bUserAssetLibrary *preferences_lib = BKE_preferences_asset_library_containing_path(
|
||||
&U, input_path.c_str()))
|
||||
{
|
||||
return preferences_lib->dirpath;
|
||||
}
|
||||
|
||||
char buffer[FILE_MAXDIR];
|
||||
BLI_path_split_dir_part(input_path.c_str(), buffer, FILE_MAXDIR);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
std::string AS_asset_library_find_suitable_root_path_from_main(const Main *bmain)
|
||||
{
|
||||
return AS_asset_library_find_suitable_root_path_from_path(bmain->filepath);
|
||||
}
|
||||
|
||||
void AS_asset_library_remap_ids(const bke::id::IDRemapper &mappings)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
service->foreach_loaded_asset_library(
|
||||
[mappings](AssetLibrary &library) { library.remap_ids_and_remove_invalid(mappings); }, true);
|
||||
}
|
||||
|
||||
void AS_asset_full_path_explode_from_weak_ref(const AssetWeakReference *asset_reference,
|
||||
char r_path_buffer[/*FILE_MAX_LIBEXTRA*/ 1282],
|
||||
char **r_dir,
|
||||
char **r_group,
|
||||
char **r_name)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
std::optional<AssetLibraryService::ExplodedPath> exploded =
|
||||
service->resolve_asset_weak_reference_to_exploded_path(*asset_reference);
|
||||
|
||||
if (!exploded) {
|
||||
if (r_dir) {
|
||||
*r_dir = nullptr;
|
||||
}
|
||||
if (r_group) {
|
||||
*r_group = nullptr;
|
||||
}
|
||||
if (r_name) {
|
||||
*r_name = nullptr;
|
||||
}
|
||||
r_path_buffer[0] = '\0';
|
||||
return;
|
||||
}
|
||||
|
||||
BLI_assert(!exploded->group_component.is_empty());
|
||||
BLI_assert(!exploded->name_component.is_empty());
|
||||
|
||||
BLI_strncpy(r_path_buffer, exploded->full_path->c_str(), /*FILE_MAX_LIBEXTRA*/ 1282);
|
||||
|
||||
if (!exploded->dir_component.is_empty()) {
|
||||
r_path_buffer[exploded->dir_component.size()] = '\0';
|
||||
r_path_buffer[exploded->dir_component.size() + 1 + exploded->group_component.size()] = '\0';
|
||||
|
||||
if (r_dir) {
|
||||
*r_dir = r_path_buffer;
|
||||
}
|
||||
if (r_group) {
|
||||
*r_group = r_path_buffer + exploded->dir_component.size() + 1;
|
||||
}
|
||||
if (r_name) {
|
||||
*r_name = r_path_buffer + exploded->dir_component.size() + 1 +
|
||||
exploded->group_component.size() + 1;
|
||||
}
|
||||
}
|
||||
else {
|
||||
r_path_buffer[exploded->group_component.size()] = '\0';
|
||||
|
||||
if (r_dir) {
|
||||
*r_dir = nullptr;
|
||||
}
|
||||
if (r_group) {
|
||||
*r_group = r_path_buffer;
|
||||
}
|
||||
if (r_name) {
|
||||
*r_name = r_path_buffer + exploded->group_component.size() + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void update_import_method_for_user_libraries()
|
||||
{
|
||||
for (bUserAssetLibrary &library : U.asset_libraries) {
|
||||
if (U.experimental.no_data_block_packing) {
|
||||
if (library.import_method == ASSET_IMPORT_PACK) {
|
||||
library.import_method = ASSET_IMPORT_APPEND_REUSE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (library.import_method == ASSET_IMPORT_APPEND_REUSE) {
|
||||
library.import_method = ASSET_IMPORT_PACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void update_import_method_for_asset_browsers(Main &bmain)
|
||||
{
|
||||
for (bScreen &screen : bmain.screens) {
|
||||
for (ScrArea &area : screen.areabase) {
|
||||
for (SpaceLink &sl : area.spacedata) {
|
||||
if (sl.spacetype != SPACE_FILE) {
|
||||
continue;
|
||||
}
|
||||
SpaceFile *sfile = reinterpret_cast<SpaceFile *>(&sl);
|
||||
if (!sfile->asset_params) {
|
||||
continue;
|
||||
}
|
||||
if (U.experimental.no_data_block_packing) {
|
||||
if (sfile->asset_params->import_method == FILE_ASSET_IMPORT_PACK) {
|
||||
sfile->asset_params->import_method = FILE_ASSET_IMPORT_APPEND_REUSE;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (sfile->asset_params->import_method == FILE_ASSET_IMPORT_APPEND_REUSE) {
|
||||
sfile->asset_params->import_method = FILE_ASSET_IMPORT_PACK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AS_asset_library_import_method_ensure_valid(Main &bmain)
|
||||
{
|
||||
update_import_method_for_user_libraries();
|
||||
update_import_method_for_asset_browsers(bmain);
|
||||
}
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
AssetLibrary::AssetLibrary(eAssetLibraryType library_type,
|
||||
const bool is_read_only,
|
||||
StringRef name,
|
||||
StringRef root_path)
|
||||
: library_type_(library_type),
|
||||
is_read_only_(is_read_only),
|
||||
name_(name),
|
||||
root_path_(std::make_shared<std::string>(utils::normalize_directory_path(root_path))),
|
||||
catalog_service_(std::make_unique<AssetCatalogService>(
|
||||
*root_path_,
|
||||
is_read_only ? std::optional{AssetCatalogService::read_only_tag{}} : std::nullopt))
|
||||
{
|
||||
}
|
||||
|
||||
AssetLibrary::~AssetLibrary()
|
||||
{
|
||||
if (on_save_callback_store_.func) {
|
||||
this->on_blend_save_handler_unregister();
|
||||
}
|
||||
}
|
||||
|
||||
void AssetLibrary::foreach_loaded(FunctionRef<void(AssetLibrary &)> fn,
|
||||
const bool include_all_library)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
service->foreach_loaded_asset_library(fn, include_all_library);
|
||||
}
|
||||
|
||||
void AssetLibrary::force_remote_listing_download() const
|
||||
{
|
||||
/* Default implementation is a no-op. */
|
||||
}
|
||||
|
||||
bool AssetLibrary::use_relative_paths() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<StringRefNull> AssetLibrary::remote_url() const
|
||||
{
|
||||
/* Remote asset library support is implemented in #RemoteAssetLibrary::remote_url(). */
|
||||
return {};
|
||||
};
|
||||
|
||||
AssetCatalogService &AssetLibrary::catalog_service() const
|
||||
{
|
||||
std::lock_guard lock{catalog_service_mutex_};
|
||||
return *catalog_service_;
|
||||
}
|
||||
|
||||
std::shared_ptr<AssetCatalogService> AssetLibrary::catalog_service_ptr() const
|
||||
{
|
||||
std::lock_guard lock{catalog_service_mutex_};
|
||||
return catalog_service_;
|
||||
}
|
||||
|
||||
void AssetLibrary::refresh_catalogs()
|
||||
{
|
||||
/* To be implemented by a subclass, like #OnDiskAssetLibrary::refresh_catalogs. */
|
||||
}
|
||||
|
||||
void AssetLibrary::load_or_reload_catalogs()
|
||||
{
|
||||
std::lock_guard lock{catalog_service_mutex_};
|
||||
|
||||
/* Should never actually be the case, catalog service gets allocated with the asset library. */
|
||||
if (catalog_service_ == nullptr) {
|
||||
auto catalog_service = std::make_unique<AssetCatalogService>(*root_path_);
|
||||
catalog_service->load_from_disk();
|
||||
catalog_service_ = std::move(catalog_service);
|
||||
return;
|
||||
}
|
||||
|
||||
/* The catalog service was created before without being associated with a definition file. */
|
||||
if (catalog_service_->get_catalog_definition_file() == nullptr) {
|
||||
catalog_service_->load_from_disk();
|
||||
if (library_type() == ASSET_LIBRARY_ESSENTIALS) {
|
||||
this->refresh_catalogs();
|
||||
}
|
||||
}
|
||||
else {
|
||||
this->refresh_catalogs();
|
||||
}
|
||||
}
|
||||
|
||||
std::weak_ptr<AssetRepresentation> AssetLibrary::add_external_on_disk_asset(
|
||||
StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
const int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata)
|
||||
{
|
||||
std::scoped_lock lock{asset_storage_.external_assets_mutex};
|
||||
return asset_storage_.external_assets.lookup_key_or_add(std::make_shared<AssetRepresentation>(
|
||||
relative_asset_path, name, id_type, std::move(metadata), *this));
|
||||
}
|
||||
|
||||
std::weak_ptr<AssetRepresentation> AssetLibrary::add_external_online_asset(
|
||||
StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
const int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata,
|
||||
OnlineAssetInfo online_info)
|
||||
{
|
||||
std::scoped_lock lock{asset_storage_.external_assets_mutex};
|
||||
return asset_storage_.external_assets.lookup_key_or_add(std::make_shared<AssetRepresentation>(
|
||||
relative_asset_path, name, id_type, std::move(metadata), *this, online_info));
|
||||
}
|
||||
|
||||
std::weak_ptr<AssetRepresentation> AssetLibrary::add_local_id_asset(ID &id)
|
||||
{
|
||||
std::scoped_lock lock{asset_storage_.local_id_assets_mutex};
|
||||
return asset_storage_.local_id_assets.lookup_key_or_add(
|
||||
std::make_shared<AssetRepresentation>(id, *this));
|
||||
}
|
||||
|
||||
bool AssetLibrary::remove_asset(AssetRepresentation &asset)
|
||||
{
|
||||
/* Make sure this is forwarded to the library actually owning the asset if needed. For example
|
||||
* the "All Libraries" library doesn't own the assets itself. */
|
||||
if (&asset.owner_asset_library_ != this) {
|
||||
return asset.owner_asset_library_.remove_asset(asset);
|
||||
}
|
||||
|
||||
std::scoped_lock lock{asset_storage_.external_assets_mutex,
|
||||
asset_storage_.local_id_assets_mutex};
|
||||
|
||||
BLI_assert(asset_storage_.local_id_assets.contains_as(&asset) ||
|
||||
asset_storage_.external_assets.contains_as(&asset));
|
||||
|
||||
if (asset_storage_.local_id_assets.remove_as(&asset)) {
|
||||
return true;
|
||||
}
|
||||
return asset_storage_.external_assets.remove_as(&asset);
|
||||
}
|
||||
|
||||
void AssetLibrary::remap_ids_and_remove_invalid(const bke::id::IDRemapper &mappings)
|
||||
{
|
||||
Set<AssetRepresentation *> removed_assets;
|
||||
|
||||
{
|
||||
std::scoped_lock lock{asset_storage_.local_id_assets_mutex};
|
||||
|
||||
for (const auto &asset_ptr : asset_storage_.local_id_assets) {
|
||||
AssetRepresentation &asset = *asset_ptr;
|
||||
BLI_assert(asset.is_local_id());
|
||||
|
||||
const IDRemapperApplyResult result = mappings.apply(&std::get<ID *>(asset.asset_),
|
||||
ID_REMAP_APPLY_DEFAULT);
|
||||
|
||||
/* Entirely remove assets whose ID is unset. We don't want assets with a null ID pointer. */
|
||||
if (result == ID_REMAP_RESULT_SOURCE_UNASSIGNED) {
|
||||
removed_assets.add(&asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (AssetRepresentation *asset : removed_assets) {
|
||||
this->remove_asset(*asset);
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
void asset_library_on_save_post(Main *bmain,
|
||||
PointerRNA **pointers,
|
||||
const int num_pointers,
|
||||
void *arg)
|
||||
{
|
||||
AssetLibrary *asset_lib = static_cast<AssetLibrary *>(arg);
|
||||
|
||||
/* Transform 'runtime' current file library into 'on-disk' current file library. */
|
||||
if (asset_lib->library_type() == ASSET_LIBRARY_LOCAL && asset_lib->root_path().is_empty()) {
|
||||
BLI_assert(dynamic_cast<RuntimeAssetLibrary *>(asset_lib) != nullptr);
|
||||
|
||||
if (AssetLibrary *on_disk_lib =
|
||||
AssetLibraryService::move_runtime_current_file_into_on_disk_library(*bmain))
|
||||
{
|
||||
/* Allow undoing to the state before merging in catalogs from disk. */
|
||||
on_disk_lib->catalog_service().undo_push();
|
||||
|
||||
/* Force refresh to merge on-disk catalogs with the ones stolen from the runtime library. */
|
||||
asset_lib = AssetLibraryService::get()->get_asset_library_on_disk_builtin(
|
||||
ASSET_LIBRARY_LOCAL, on_disk_lib->root_path());
|
||||
BLI_assert(asset_lib == on_disk_lib);
|
||||
}
|
||||
}
|
||||
|
||||
asset_lib->on_blend_save_post(bmain, pointers, num_pointers);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AssetLibrary::on_blend_save_handler_register()
|
||||
{
|
||||
/* The callback system doesn't own `on_save_callback_store_`. */
|
||||
on_save_callback_store_.alloc = false;
|
||||
|
||||
on_save_callback_store_.func = asset_library_on_save_post;
|
||||
on_save_callback_store_.arg = this;
|
||||
|
||||
BKE_callback_add(&on_save_callback_store_, BKE_CB_EVT_SAVE_POST);
|
||||
}
|
||||
|
||||
void AssetLibrary::on_blend_save_handler_unregister()
|
||||
{
|
||||
BKE_callback_remove(&on_save_callback_store_, BKE_CB_EVT_SAVE_POST);
|
||||
on_save_callback_store_.func = nullptr;
|
||||
on_save_callback_store_.arg = nullptr;
|
||||
}
|
||||
|
||||
void AssetLibrary::on_blend_save_post(Main *bmain,
|
||||
PointerRNA ** /*pointers*/,
|
||||
const int /*num_pointers*/)
|
||||
{
|
||||
if (save_catalogs_when_file_is_saved && !this->catalog_service().is_read_only()) {
|
||||
this->catalog_service().write_to_disk(bmain->filepath);
|
||||
}
|
||||
}
|
||||
|
||||
std::string AssetLibrary::resolve_asset_weak_reference_to_full_path(
|
||||
const AssetWeakReference &asset_reference)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
return service->resolve_asset_weak_reference_to_full_path(asset_reference);
|
||||
}
|
||||
|
||||
void AssetLibrary::refresh_catalog_simplename(AssetMetaData *asset_data)
|
||||
{
|
||||
if (BLI_uuid_is_nil(asset_data->catalog_id)) {
|
||||
asset_data->catalog_simple_name[0] = '\0';
|
||||
return;
|
||||
}
|
||||
const AssetCatalog *catalog = this->catalog_service().find_catalog(asset_data->catalog_id);
|
||||
if (catalog == nullptr) {
|
||||
/* No-op if the catalog cannot be found. This could be the kind of "the catalog definition file
|
||||
* is corrupt/lost" scenario that the simple name is meant to help recover from. */
|
||||
return;
|
||||
}
|
||||
STRNCPY(asset_data->catalog_simple_name, catalog->simple_name.c_str());
|
||||
}
|
||||
|
||||
eAssetLibraryType AssetLibrary::library_type() const
|
||||
{
|
||||
return library_type_;
|
||||
}
|
||||
|
||||
StringRefNull AssetLibrary::name() const
|
||||
{
|
||||
return name_;
|
||||
}
|
||||
|
||||
StringRefNull AssetLibrary::root_path() const
|
||||
{
|
||||
return *root_path_;
|
||||
}
|
||||
|
||||
bool AssetLibrary::is_read_only() const
|
||||
{
|
||||
return is_read_only_;
|
||||
}
|
||||
|
||||
Vector<AssetLibraryReference> all_valid_asset_library_refs()
|
||||
{
|
||||
Vector<AssetLibraryReference> result;
|
||||
{
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_ESSENTIALS;
|
||||
result.append(library_ref);
|
||||
}
|
||||
|
||||
const bool include_remote_libraries = USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
|
||||
const bool include_online_essentials = (U.asset_flag & USER_ASSETS_USE_ONLINE_ESSENTIALS) != 0;
|
||||
if (include_remote_libraries && include_online_essentials) {
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_ONLINE_ESSENTIALS;
|
||||
result.append(library_ref);
|
||||
}
|
||||
|
||||
for (const auto [i, asset_library] : U.asset_libraries.enumerate()) {
|
||||
if (!BKE_preferences_asset_library_is_valid(&U, &asset_library, true)) {
|
||||
continue;
|
||||
}
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = i;
|
||||
library_ref.type = ASSET_LIBRARY_CUSTOM;
|
||||
result.append(library_ref);
|
||||
}
|
||||
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_LOCAL;
|
||||
result.append(library_ref);
|
||||
return result;
|
||||
}
|
||||
|
||||
AssetLibraryReference all_library_reference()
|
||||
{
|
||||
AssetLibraryReference all_library_ref{};
|
||||
all_library_ref.custom_library_index = -1;
|
||||
all_library_ref.type = ASSET_LIBRARY_ALL;
|
||||
return all_library_ref;
|
||||
}
|
||||
|
||||
AssetLibraryReference essentials_library_reference()
|
||||
{
|
||||
AssetLibraryReference all_library_ref{};
|
||||
all_library_ref.custom_library_index = -1;
|
||||
all_library_ref.type = ASSET_LIBRARY_ESSENTIALS;
|
||||
return all_library_ref;
|
||||
}
|
||||
|
||||
AssetLibraryReference current_file_library_reference()
|
||||
{
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_LOCAL;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
AssetLibraryReference online_essentials_library_reference()
|
||||
{
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_ONLINE_ESSENTIALS;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
void all_library_tag_catalogs_dirty()
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
service->tag_all_library_catalogs_dirty();
|
||||
}
|
||||
|
||||
void all_library_reload_catalogs_if_dirty()
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
service->reload_all_library_catalogs_if_dirty();
|
||||
}
|
||||
|
||||
bool is_or_contains_remote_libraries(const AssetLibraryReference &reference)
|
||||
{
|
||||
switch (reference.type) {
|
||||
/* Also returns true since it contains the online essentials. */
|
||||
case ASSET_LIBRARY_ALL:
|
||||
return true;
|
||||
case ASSET_LIBRARY_ESSENTIALS:
|
||||
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
|
||||
return true;
|
||||
case ASSET_LIBRARY_CUSTOM: {
|
||||
if (bUserAssetLibrary *asset_library =
|
||||
AssetLibraryService::find_custom_asset_library_from_library_ref(reference))
|
||||
{
|
||||
if (asset_library->flag & ASSET_LIBRARY_USE_REMOTE_URL) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ASSET_LIBRARY_LOCAL:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool contains_assets_from_remote_url(const AssetLibrary &library, const StringRef remote_url)
|
||||
{
|
||||
switch (library.library_type()) {
|
||||
case ASSET_LIBRARY_ALL: {
|
||||
if (is_online_essentials_url(remote_url)) {
|
||||
return true;
|
||||
}
|
||||
bool has_match = false;
|
||||
AssetLibrary::foreach_loaded(
|
||||
[&](const AssetLibrary &nested) {
|
||||
if (nested.remote_url() == remote_url) {
|
||||
has_match = true;
|
||||
}
|
||||
},
|
||||
/*include_all_library=*/false);
|
||||
return has_match;
|
||||
}
|
||||
case ASSET_LIBRARY_ESSENTIALS:
|
||||
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
|
||||
return is_online_essentials_url(remote_url);
|
||||
case ASSET_LIBRARY_CUSTOM:
|
||||
return library.remote_url() == remote_url;
|
||||
case ASSET_LIBRARY_LOCAL:
|
||||
return false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,763 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include "BKE_blender.hh"
|
||||
#include "BKE_preferences.h"
|
||||
|
||||
#include "BLI_fileops.h" // IWYU pragma: keep
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "DNA_asset_types.h"
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
#include "AS_essentials_library.hh"
|
||||
#include "all_library.hh"
|
||||
#include "asset_catalog_collection.hh"
|
||||
#include "asset_catalog_definition_file.hh" // IWYU pragma: keep
|
||||
#include "asset_library_service.hh"
|
||||
#include "essentials_library.hh"
|
||||
#include "on_disk_library.hh"
|
||||
#include "preferences_on_disk_library.hh"
|
||||
#include "remote_library.hh"
|
||||
#include "runtime_library.hh"
|
||||
#include "utils.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
/* When enabled, use a pre file load handler (#BKE_CB_EVT_LOAD_PRE) callback to destroy the asset
|
||||
* library service. Without this an explicit call from the file loading code is needed to do this,
|
||||
* which is not as nice.
|
||||
*
|
||||
* TODO Currently disabled because UI data depends on asset library data, so we have to make sure
|
||||
* it's freed in the right order (UI first). Pre-load handlers don't give us this order.
|
||||
* Should be addressed with a proper ownership model for the asset system:
|
||||
* https://developer.blender.org/docs/features/asset_system/backend/#ownership-model
|
||||
*/
|
||||
// #define WITH_DESTROY_VIA_LOAD_HANDLER
|
||||
|
||||
static CLG_LogRef LOG = {"asset.library"};
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
std::unique_ptr<AssetLibraryService> AssetLibraryService::instance_;
|
||||
bool AssetLibraryService::atexit_handler_registered_ = false;
|
||||
|
||||
AssetLibraryService *AssetLibraryService::get()
|
||||
{
|
||||
if (!instance_) {
|
||||
allocate_service_instance();
|
||||
}
|
||||
return instance_.get();
|
||||
}
|
||||
|
||||
void AssetLibraryService::destroy()
|
||||
{
|
||||
if (!instance_) {
|
||||
return;
|
||||
}
|
||||
instance_->app_handler_unregister();
|
||||
instance_.reset();
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library(
|
||||
const Main *bmain, const AssetLibraryReference &library_reference)
|
||||
{
|
||||
const eAssetLibraryType type = eAssetLibraryType(library_reference.type);
|
||||
|
||||
switch (type) {
|
||||
case ASSET_LIBRARY_ESSENTIALS: {
|
||||
const StringRefNull root_path = essentials_directory_path();
|
||||
if (root_path.is_empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return this->get_asset_library_on_disk_builtin(type, root_path);
|
||||
}
|
||||
case ASSET_LIBRARY_ONLINE_ESSENTIALS: {
|
||||
return this->get_online_essentials_asset_library();
|
||||
}
|
||||
case ASSET_LIBRARY_LOCAL: {
|
||||
/* For the "Current File" library we get the asset library root path based on main. */
|
||||
std::string root_path = bmain ? AS_asset_library_find_suitable_root_path_from_main(bmain) :
|
||||
"";
|
||||
|
||||
if (root_path.empty()) {
|
||||
/* File wasn't saved yet. */
|
||||
return this->get_asset_library_current_file();
|
||||
}
|
||||
return this->get_asset_library_on_disk_builtin(type, root_path);
|
||||
}
|
||||
case ASSET_LIBRARY_ALL:
|
||||
return this->get_asset_library_all(bmain);
|
||||
case ASSET_LIBRARY_CUSTOM: {
|
||||
bUserAssetLibrary *custom_library = find_custom_asset_library_from_library_ref(
|
||||
library_reference);
|
||||
if (!custom_library) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (custom_library->flag & ASSET_LIBRARY_USE_REMOTE_URL) {
|
||||
if (is_online_essentials_url(custom_library->remote_url)) {
|
||||
return this->get_online_essentials_asset_library();
|
||||
}
|
||||
return this->get_preferences_remote_asset_library(*custom_library);
|
||||
}
|
||||
|
||||
std::string root_path = custom_library->dirpath;
|
||||
if (root_path.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AssetLibrary *library = this->get_asset_library_on_disk_custom_preferences(custom_library);
|
||||
library->may_override_import_method_ = true;
|
||||
|
||||
return library;
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_online_essentials_asset_library()
|
||||
{
|
||||
if (online_essentials_library_) {
|
||||
CLOG_DEBUG(&LOG, "get online essentials lib (cached)");
|
||||
online_essentials_library_->load_or_reload_catalogs();
|
||||
}
|
||||
else {
|
||||
CLOG_DEBUG(&LOG, "get online essentials lib (loaded)");
|
||||
online_essentials_library_ = std::make_unique<OnlineEssentialsLibrary>();
|
||||
}
|
||||
|
||||
AssetLibrary *lib = online_essentials_library_.get();
|
||||
return lib;
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_preferences_remote_asset_library(
|
||||
const bUserAssetLibrary &custom_library)
|
||||
{
|
||||
if (!custom_library.remote_url[0]) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
const StringRefNull remote_url = custom_library.remote_url;
|
||||
|
||||
/* Lock for the entire "lookup and if not found -> create and insert" scope, so no two threads do
|
||||
* this in parallel and interfere with each other. */
|
||||
std::scoped_lock lock{remote_libraries_mutex_};
|
||||
|
||||
std::unique_ptr<PreferencesRemoteAssetLibrary> *lib_uptr_ptr = remote_libraries_.lookup_ptr(
|
||||
remote_url);
|
||||
if (lib_uptr_ptr != nullptr) {
|
||||
CLOG_DEBUG(&LOG, "get \"%s\" (cached)", remote_url.c_str());
|
||||
AssetLibrary *lib = lib_uptr_ptr->get();
|
||||
lib->load_or_reload_catalogs();
|
||||
return lib;
|
||||
}
|
||||
|
||||
std::unique_ptr<PreferencesRemoteAssetLibrary> lib_uptr =
|
||||
std::make_unique<PreferencesRemoteAssetLibrary>(custom_library);
|
||||
AssetLibrary *lib = lib_uptr.get();
|
||||
lib->load_or_reload_catalogs();
|
||||
|
||||
remote_libraries_.add_new(remote_url, std::move(lib_uptr));
|
||||
CLOG_DEBUG(&LOG, "get \"%s\" (loaded)", remote_url.c_str());
|
||||
return lib;
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library_on_disk(
|
||||
eAssetLibraryType library_type,
|
||||
StringRef name,
|
||||
StringRefNull root_path,
|
||||
const bool load_catalogs,
|
||||
bUserAssetLibrary *preferences_library)
|
||||
{
|
||||
const std::string normalized_root_path = utils::normalize_directory_path(root_path);
|
||||
|
||||
/* Lock for the entire "lookup and if not found -> create and insert" scope, so no two threads do
|
||||
* this in parallel and interfere with each other. */
|
||||
std::scoped_lock lock{on_disk_libraries_mutex_};
|
||||
|
||||
if (OnDiskAssetLibrary *lib = this->lookup_on_disk_library(library_type, normalized_root_path)) {
|
||||
CLOG_DEBUG(&LOG, "get \"%s\" (cached)", normalized_root_path.c_str());
|
||||
if (load_catalogs) {
|
||||
lib->load_or_reload_catalogs();
|
||||
}
|
||||
return lib;
|
||||
}
|
||||
|
||||
std::unique_ptr<OnDiskAssetLibrary> lib_uptr;
|
||||
switch (library_type) {
|
||||
case ASSET_LIBRARY_CUSTOM:
|
||||
if (preferences_library) {
|
||||
lib_uptr = std::make_unique<PreferencesOnDiskAssetLibrary>(*preferences_library);
|
||||
}
|
||||
else {
|
||||
/* Only used by unit tests. */
|
||||
lib_uptr = std::make_unique<OnDiskAssetLibrary>(
|
||||
library_type, name, normalized_root_path, /*is_read_only=*/false);
|
||||
}
|
||||
break;
|
||||
case ASSET_LIBRARY_ESSENTIALS:
|
||||
lib_uptr = std::make_unique<EssentialsAssetLibrary>();
|
||||
break;
|
||||
case ASSET_LIBRARY_LOCAL:
|
||||
lib_uptr = std::make_unique<OnDiskAssetLibrary>(
|
||||
library_type, name, normalized_root_path, /*is_read_only=*/false);
|
||||
break;
|
||||
default:
|
||||
lib_uptr = std::make_unique<OnDiskAssetLibrary>(
|
||||
library_type, name, normalized_root_path, /*is_read_only=*/true);
|
||||
break;
|
||||
}
|
||||
|
||||
/* Get underlying pointer before moving. */
|
||||
AssetLibrary *lib = lib_uptr.get();
|
||||
on_disk_libraries_.add_new({library_type, normalized_root_path}, std::move(lib_uptr));
|
||||
CLOG_DEBUG(&LOG, "get \"%s\" (loaded)", normalized_root_path.c_str());
|
||||
|
||||
if (load_catalogs) {
|
||||
lib->load_or_reload_catalogs();
|
||||
}
|
||||
|
||||
return lib;
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library_on_disk_custom(StringRef name,
|
||||
StringRefNull root_path)
|
||||
{
|
||||
return this->get_asset_library_on_disk(ASSET_LIBRARY_CUSTOM, name, root_path);
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library_on_disk_custom_preferences(
|
||||
bUserAssetLibrary *custom_library)
|
||||
{
|
||||
return this->get_asset_library_on_disk(
|
||||
ASSET_LIBRARY_CUSTOM, custom_library->name, custom_library->dirpath, true, custom_library);
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library_on_disk_builtin(eAssetLibraryType type,
|
||||
StringRefNull root_path)
|
||||
{
|
||||
BLI_assert_msg(
|
||||
type != ASSET_LIBRARY_CUSTOM,
|
||||
"Use `get_asset_library_on_disk_custom()` for libraries of type `ASSET_LIBRARY_CUSTOM`");
|
||||
|
||||
/* Builtin asset libraries don't need a name, the #eAssetLibraryType is enough to identify them
|
||||
* (and doesn't change, unlike the name). */
|
||||
return this->get_asset_library_on_disk(type, {}, root_path);
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library_current_file()
|
||||
{
|
||||
if (current_file_library_) {
|
||||
CLOG_DEBUG(&LOG, "get current file lib (cached)");
|
||||
current_file_library_->refresh_catalogs();
|
||||
}
|
||||
else {
|
||||
CLOG_DEBUG(&LOG, "get current file lib (loaded)");
|
||||
current_file_library_ = std::make_unique<RuntimeAssetLibrary>();
|
||||
}
|
||||
|
||||
AssetLibrary *lib = current_file_library_.get();
|
||||
return lib;
|
||||
}
|
||||
|
||||
void AssetLibraryService::tag_all_library_catalogs_dirty()
|
||||
{
|
||||
if (all_library_) {
|
||||
all_library_->tag_catalogs_dirty();
|
||||
}
|
||||
}
|
||||
|
||||
void AssetLibraryService::reload_all_library_catalogs_if_dirty()
|
||||
{
|
||||
if (all_library_ && all_library_->is_catalogs_dirty()) {
|
||||
/* Don't reload catalogs from nested libraries from disk, just reflect their currently known
|
||||
* state in the "All" library. Loading catalog changes from disk is only done with a
|
||||
* #AS_asset_library_load()/#AssetLibraryService:get_asset_library() call. */
|
||||
const bool reload_nested_catalogs = false;
|
||||
all_library_->rebuild_catalogs_from_nested(reload_nested_catalogs);
|
||||
}
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::move_runtime_current_file_into_on_disk_library(
|
||||
const Main &bmain)
|
||||
{
|
||||
AssetLibraryService &library_service = *AssetLibraryService::get();
|
||||
|
||||
const std::string root_path = AS_asset_library_find_suitable_root_path_from_main(&bmain);
|
||||
if (root_path.empty()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
{
|
||||
std::scoped_lock lock{library_service.on_disk_libraries_mutex_};
|
||||
BLI_assert_msg(!library_service.lookup_on_disk_library(ASSET_LIBRARY_LOCAL, root_path),
|
||||
"On-disk \"Current File\" asset library shouldn't exist yet, it should only be "
|
||||
"created now in response to initially saving the file - catalog service "
|
||||
"will be overridden");
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Create on disk library without loading catalogs. We'll steal the catalog service from the
|
||||
* runtime library below. */
|
||||
AssetLibrary *on_disk_library = library_service.get_asset_library_on_disk(
|
||||
ASSET_LIBRARY_LOCAL,
|
||||
{},
|
||||
root_path,
|
||||
/*load_catalogs=*/false);
|
||||
|
||||
{
|
||||
/* These should always be completely separate, just sanity check since it would cause a
|
||||
* deadlock below. */
|
||||
BLI_assert(on_disk_library != library_service.current_file_library_.get());
|
||||
|
||||
std::lock_guard lock_on_disk{on_disk_library->catalog_service_mutex_};
|
||||
std::lock_guard lock_runtime{library_service.current_file_library_->catalog_service_mutex_};
|
||||
on_disk_library->catalog_service_.swap(
|
||||
library_service.current_file_library_->catalog_service_);
|
||||
}
|
||||
|
||||
AssetCatalogService &catalog_service = on_disk_library->catalog_service();
|
||||
catalog_service.asset_library_root_ = on_disk_library->root_path();
|
||||
/* The catalogs are not stored on disk, so there should not be any CDF. Otherwise, we'd have to
|
||||
* remap their stored file-path too (#AssetCatalogDefinitionFile.file_path). */
|
||||
BLI_assert_msg(catalog_service.get_catalog_definition_file() == nullptr,
|
||||
"new on-disk library shouldn't have catalog definition files - root path "
|
||||
"changed, so they would have to be relocated");
|
||||
|
||||
/* Create a CDF with the runtime catalogs that on-disk catalogs can be merged into. Only do if
|
||||
* there's catalogs to write, otherwise we create empty CDFs on disk on every new .blend save. */
|
||||
if (!catalog_service.catalog_collection_->is_empty()) {
|
||||
char asset_lib_cdf_path[PATH_MAX];
|
||||
BLI_path_join(asset_lib_cdf_path,
|
||||
sizeof(asset_lib_cdf_path),
|
||||
on_disk_library->root_path().c_str(),
|
||||
AssetCatalogService::DEFAULT_CATALOG_FILENAME.c_str());
|
||||
catalog_service.catalog_collection_->catalog_definition_file_ =
|
||||
catalog_service.construct_cdf_in_memory(asset_lib_cdf_path);
|
||||
}
|
||||
|
||||
library_service.current_file_library_ = nullptr;
|
||||
|
||||
return on_disk_library;
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::get_asset_library_all(const Main *bmain)
|
||||
{
|
||||
/* (Re-)load all other asset libraries. */
|
||||
for (AssetLibraryReference &library_ref : all_valid_asset_library_refs()) {
|
||||
/* Skip self :) */
|
||||
if (library_ref.type == ASSET_LIBRARY_ALL) {
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Ensure all asset libraries are loaded. */
|
||||
this->get_asset_library(bmain, library_ref);
|
||||
}
|
||||
|
||||
if (!all_library_) {
|
||||
CLOG_DEBUG(&LOG, "get all lib (loaded)");
|
||||
all_library_ = std::make_unique<AllAssetLibrary>();
|
||||
}
|
||||
else {
|
||||
CLOG_DEBUG(&LOG, "get all lib (cached)");
|
||||
}
|
||||
|
||||
/* Don't reload catalogs, they've just been loaded above. */
|
||||
all_library_->rebuild_catalogs_from_nested(/*reload_nested_catalogs=*/false);
|
||||
|
||||
return all_library_.get();
|
||||
}
|
||||
|
||||
OnDiskAssetLibrary *AssetLibraryService::lookup_on_disk_library(eAssetLibraryType library_type,
|
||||
StringRefNull root_path)
|
||||
{
|
||||
BLI_assert_msg(!root_path.is_empty(),
|
||||
"top level directory must be given for on-disk asset library");
|
||||
|
||||
std::string normalized_root_path = utils::normalize_directory_path(root_path);
|
||||
|
||||
std::scoped_lock lock{on_disk_libraries_mutex_};
|
||||
std::unique_ptr<OnDiskAssetLibrary> *lib_uptr_ptr = on_disk_libraries_.lookup_ptr(
|
||||
{library_type, normalized_root_path});
|
||||
return lib_uptr_ptr ? lib_uptr_ptr->get() : nullptr;
|
||||
}
|
||||
|
||||
bUserAssetLibrary *AssetLibraryService::find_custom_preferences_asset_library_from_asset_weak_ref(
|
||||
const AssetWeakReference &asset_reference)
|
||||
{
|
||||
if (!ELEM(asset_reference.asset_library_type, ASSET_LIBRARY_CUSTOM)) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return BKE_preferences_asset_library_find_by_name(&U, asset_reference.asset_library_identifier);
|
||||
}
|
||||
|
||||
AssetLibrary *AssetLibraryService::find_loaded_on_disk_asset_library_from_name(
|
||||
StringRef name) const
|
||||
{
|
||||
std::scoped_lock lock{on_disk_libraries_mutex_};
|
||||
for (const std::unique_ptr<OnDiskAssetLibrary> &library : on_disk_libraries_.values()) {
|
||||
if (library->name_ == name) {
|
||||
return library.get();
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::string AssetLibraryService::resolve_asset_weak_reference_to_library_path(
|
||||
const AssetWeakReference &asset_reference)
|
||||
{
|
||||
StringRefNull library_dirpath;
|
||||
|
||||
switch (eAssetLibraryType(asset_reference.asset_library_type)) {
|
||||
case ASSET_LIBRARY_CUSTOM: {
|
||||
bUserAssetLibrary *custom_lib = find_custom_preferences_asset_library_from_asset_weak_ref(
|
||||
asset_reference);
|
||||
if (custom_lib) {
|
||||
library_dirpath = custom_lib->dirpath;
|
||||
break;
|
||||
}
|
||||
|
||||
/* A bit of an odd-ball, the API supports loading custom libraries from arbitrary paths (used
|
||||
* by unit tests). So check all loaded on-disk libraries too. */
|
||||
AssetLibrary *loaded_custom_lib = this->find_loaded_on_disk_asset_library_from_name(
|
||||
asset_reference.asset_library_identifier);
|
||||
if (!loaded_custom_lib) {
|
||||
return "";
|
||||
}
|
||||
|
||||
library_dirpath = *loaded_custom_lib->root_path_;
|
||||
break;
|
||||
}
|
||||
case ASSET_LIBRARY_ESSENTIALS:
|
||||
library_dirpath = essentials_directory_path();
|
||||
break;
|
||||
case ASSET_LIBRARY_ONLINE_ESSENTIALS:
|
||||
library_dirpath = online_essentials_cache_directory_path();
|
||||
break;
|
||||
case ASSET_LIBRARY_LOCAL:
|
||||
case ASSET_LIBRARY_ALL:
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string normalized_library_dirpath = utils::normalize_path(library_dirpath);
|
||||
return normalized_library_dirpath;
|
||||
}
|
||||
|
||||
int64_t AssetLibraryService::rfind_blendfile_extension(StringRef path)
|
||||
{
|
||||
const std::vector<StringRefNull> blendfile_extensions = {".blend" SEP_STR,
|
||||
".blend.gz" SEP_STR,
|
||||
".ble" SEP_STR,
|
||||
".blend" ALTSEP_STR,
|
||||
".blend.gz" ALTSEP_STR,
|
||||
".ble" ALTSEP_STR};
|
||||
int64_t blendfile_extension_pos = StringRef::not_found;
|
||||
|
||||
for (StringRefNull blendfile_ext : blendfile_extensions) {
|
||||
const int64_t iter_ext_pos = path.rfind(blendfile_ext);
|
||||
if (iter_ext_pos == StringRef::not_found) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((blendfile_extension_pos == StringRef::not_found) ||
|
||||
(blendfile_extension_pos < iter_ext_pos))
|
||||
{
|
||||
blendfile_extension_pos = iter_ext_pos;
|
||||
}
|
||||
}
|
||||
|
||||
return blendfile_extension_pos;
|
||||
}
|
||||
|
||||
std::string AssetLibraryService::normalize_asset_weak_reference_relative_asset_identifier(
|
||||
const AssetWeakReference &asset_reference)
|
||||
{
|
||||
StringRefNull relative_asset_identifier = asset_reference.relative_asset_identifier;
|
||||
|
||||
int64_t blend_ext_pos = rfind_blendfile_extension(asset_reference.relative_asset_identifier);
|
||||
const bool has_blend_ext = blend_ext_pos != StringRef::not_found;
|
||||
|
||||
int64_t blend_path_len = 0;
|
||||
/* Get the position of the path separator after the blend file extension. */
|
||||
if (has_blend_ext) {
|
||||
blend_path_len = relative_asset_identifier.find_first_of(SEP_STR ALTSEP_STR, blend_ext_pos);
|
||||
|
||||
/* If there is a blend file in the relative asset path, then there should be group and id name
|
||||
* after it. */
|
||||
BLI_assert(blend_path_len != StringRef::not_found);
|
||||
/* Skip slash. */
|
||||
blend_path_len += 1;
|
||||
}
|
||||
|
||||
/* Find the first path separator (after the blend file extension if any). This will be the one
|
||||
* separating the group from the name. */
|
||||
const int64_t group_name_sep_pos = relative_asset_identifier.find_first_of(SEP_STR ALTSEP_STR,
|
||||
blend_path_len);
|
||||
|
||||
return utils::normalize_path(relative_asset_identifier,
|
||||
(group_name_sep_pos == StringRef::not_found) ?
|
||||
StringRef::not_found :
|
||||
group_name_sep_pos + 1);
|
||||
}
|
||||
|
||||
std::string AssetLibraryService::resolve_asset_weak_reference_to_full_path(
|
||||
const AssetWeakReference &asset_reference)
|
||||
{
|
||||
/* TODO currently only works for asset libraries on disk (custom or essentials asset libraries).
|
||||
* Once there is a proper registry of asset libraries, this could contain an asset library
|
||||
* locator and/or identifier, so a full path (not necessarily file path) can be built for all
|
||||
* asset libraries. */
|
||||
|
||||
if (asset_reference.relative_asset_identifier[0] == '\0') {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string library_dirpath = resolve_asset_weak_reference_to_library_path(asset_reference);
|
||||
if (library_dirpath.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
std::string normalized_full_path = utils::normalize_path(library_dirpath + SEP_STR) +
|
||||
normalize_asset_weak_reference_relative_asset_identifier(
|
||||
asset_reference);
|
||||
|
||||
return normalized_full_path;
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryService::ExplodedPath> AssetLibraryService::
|
||||
resolve_asset_weak_reference_to_exploded_path(const AssetWeakReference &asset_reference)
|
||||
{
|
||||
if (asset_reference.relative_asset_identifier[0] == '\0') {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
switch (eAssetLibraryType(asset_reference.asset_library_type)) {
|
||||
case ASSET_LIBRARY_LOCAL: {
|
||||
std::string path_in_file = this->normalize_asset_weak_reference_relative_asset_identifier(
|
||||
asset_reference);
|
||||
const int64_t group_len = int64_t(path_in_file.find(SEP));
|
||||
|
||||
ExplodedPath exploded;
|
||||
exploded.full_path = std::make_unique<std::string>(path_in_file);
|
||||
exploded.group_component = StringRef(*exploded.full_path).substr(0, group_len);
|
||||
exploded.name_component = StringRef(*exploded.full_path).substr(group_len + 1);
|
||||
|
||||
return exploded;
|
||||
}
|
||||
case ASSET_LIBRARY_CUSTOM:
|
||||
case ASSET_LIBRARY_ESSENTIALS:
|
||||
case ASSET_LIBRARY_ONLINE_ESSENTIALS: {
|
||||
std::string full_path = this->resolve_asset_weak_reference_to_full_path(asset_reference);
|
||||
/* #full_path uses native slashes, so others don't need to be considered in the following. */
|
||||
|
||||
if (full_path.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
int64_t blendfile_extension_pos = this->rfind_blendfile_extension(full_path);
|
||||
BLI_assert(blendfile_extension_pos != StringRef::not_found);
|
||||
|
||||
size_t group_pos = full_path.find(SEP, blendfile_extension_pos);
|
||||
BLI_assert(group_pos != std::string::npos);
|
||||
|
||||
size_t name_pos = full_path.find(SEP, group_pos + 1);
|
||||
BLI_assert(group_pos != std::string::npos);
|
||||
|
||||
const int64_t dir_len = int64_t(group_pos);
|
||||
const int64_t group_len = int64_t(name_pos - group_pos - 1);
|
||||
|
||||
ExplodedPath exploded;
|
||||
exploded.full_path = std::make_unique<std::string>(full_path);
|
||||
StringRef full_path_ref = *exploded.full_path;
|
||||
exploded.dir_component = full_path_ref.substr(0, dir_len);
|
||||
exploded.group_component = full_path_ref.substr(dir_len + 1, group_len);
|
||||
exploded.name_component = full_path_ref.substr(dir_len + 1 + group_len + 1);
|
||||
|
||||
return exploded;
|
||||
}
|
||||
case ASSET_LIBRARY_ALL:
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
bUserAssetLibrary *AssetLibraryService::find_custom_asset_library_from_library_ref(
|
||||
const AssetLibraryReference &library_reference)
|
||||
{
|
||||
BLI_assert(library_reference.type == ASSET_LIBRARY_CUSTOM);
|
||||
BLI_assert(library_reference.custom_library_index >= 0);
|
||||
|
||||
return BKE_preferences_asset_library_find_index(&U, library_reference.custom_library_index);
|
||||
}
|
||||
|
||||
std::string AssetLibraryService::root_path_from_library_ref(
|
||||
const AssetLibraryReference &library_reference)
|
||||
{
|
||||
if (ELEM(library_reference.type, ASSET_LIBRARY_ALL, ASSET_LIBRARY_LOCAL)) {
|
||||
return "";
|
||||
}
|
||||
if (ELEM(library_reference.type, ASSET_LIBRARY_ESSENTIALS)) {
|
||||
return essentials_directory_path();
|
||||
}
|
||||
if (library_reference.type == ASSET_LIBRARY_ONLINE_ESSENTIALS) {
|
||||
return online_essentials_cache_directory_path();
|
||||
}
|
||||
|
||||
bUserAssetLibrary *custom_library = find_custom_asset_library_from_library_ref(
|
||||
library_reference);
|
||||
if (!custom_library || !custom_library->dirpath[0]) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return custom_library->dirpath;
|
||||
}
|
||||
|
||||
void AssetLibraryService::allocate_service_instance()
|
||||
{
|
||||
instance_ = std::make_unique<AssetLibraryService>();
|
||||
instance_->app_handler_register();
|
||||
|
||||
if (!atexit_handler_registered_) {
|
||||
/* Ensure the instance gets freed before Blender's memory leak detector runs. */
|
||||
BKE_blender_atexit_register([](void * /*user_data*/) { AssetLibraryService::destroy(); },
|
||||
nullptr);
|
||||
atexit_handler_registered_ = true;
|
||||
}
|
||||
}
|
||||
|
||||
static void on_blendfile_load(Main * /*bmain*/,
|
||||
PointerRNA ** /*pointers*/,
|
||||
const int /*num_pointers*/,
|
||||
void * /*arg*/)
|
||||
{
|
||||
#ifdef WITH_DESTROY_VIA_LOAD_HANDLER
|
||||
AssetLibraryService::destroy();
|
||||
#endif
|
||||
}
|
||||
|
||||
void AssetLibraryService::app_handler_register()
|
||||
{
|
||||
/* The callback system doesn't own `on_load_callback_store_`. */
|
||||
on_load_callback_store_.alloc = false;
|
||||
|
||||
on_load_callback_store_.func = &on_blendfile_load;
|
||||
on_load_callback_store_.arg = this;
|
||||
|
||||
BKE_callback_add(&on_load_callback_store_, BKE_CB_EVT_LOAD_PRE);
|
||||
}
|
||||
|
||||
void AssetLibraryService::app_handler_unregister()
|
||||
{
|
||||
BKE_callback_remove(&on_load_callback_store_, BKE_CB_EVT_LOAD_PRE);
|
||||
on_load_callback_store_.func = nullptr;
|
||||
on_load_callback_store_.arg = nullptr;
|
||||
}
|
||||
|
||||
bool AssetLibraryService::has_any_unsaved_catalogs() const
|
||||
{
|
||||
bool has_unsaved_changes = false;
|
||||
|
||||
foreach_loaded_asset_library(
|
||||
[&has_unsaved_changes](AssetLibrary &library) {
|
||||
if (library.catalog_service().has_unsaved_changes()) {
|
||||
has_unsaved_changes = true;
|
||||
}
|
||||
},
|
||||
true);
|
||||
return has_unsaved_changes;
|
||||
}
|
||||
|
||||
void AssetLibraryService::foreach_loaded_asset_library(FunctionRef<void(AssetLibrary &)> fn,
|
||||
const bool include_all_library) const
|
||||
{
|
||||
/* Collect the libraries to visit first, then invoke the callback without holding any of the
|
||||
* library mutexes. The callback may re-enter the asset library service, e.g. the "All" library
|
||||
* reading triggers a catalog rebuild, which itself calls #foreach_loaded() - so running it while
|
||||
* holding these mutexes can deadlock.
|
||||
*
|
||||
* Holding on to the raw pointers is safe as long as loaded libraries are not freed concurrently.
|
||||
*/
|
||||
Vector<AssetLibrary *, 16> libraries;
|
||||
|
||||
if (include_all_library && all_library_) {
|
||||
libraries.append(all_library_.get());
|
||||
}
|
||||
|
||||
if (current_file_library_) {
|
||||
libraries.append(current_file_library_.get());
|
||||
}
|
||||
|
||||
{
|
||||
std::scoped_lock lock{on_disk_libraries_mutex_};
|
||||
/* Do essentials library first. Plenty of general features use the essentials, these features
|
||||
* should be available as soon as possible. Not only after other, potentially big libraries are
|
||||
* loaded. */
|
||||
for (const auto &asset_lib_uptr : on_disk_libraries_.values()) {
|
||||
if (asset_lib_uptr->library_type() != ASSET_LIBRARY_ESSENTIALS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (asset_lib_uptr->is_enabled()) {
|
||||
libraries.append(asset_lib_uptr.get());
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const bool include_remote_libraries = USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
|
||||
|
||||
if (include_remote_libraries && online_essentials_library_ &&
|
||||
(U.asset_flag & USER_ASSETS_USE_ONLINE_ESSENTIALS))
|
||||
{
|
||||
libraries.append(online_essentials_library_.get());
|
||||
}
|
||||
|
||||
{
|
||||
std::scoped_lock lock{on_disk_libraries_mutex_};
|
||||
for (const auto &asset_lib_uptr : on_disk_libraries_.values()) {
|
||||
/* Already handled above. */
|
||||
if (asset_lib_uptr->library_type() == ASSET_LIBRARY_ESSENTIALS) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (asset_lib_uptr->is_enabled()) {
|
||||
libraries.append(asset_lib_uptr.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (include_remote_libraries) {
|
||||
std::scoped_lock lock{remote_libraries_mutex_};
|
||||
for (const auto &asset_lib_uptr : remote_libraries_.values()) {
|
||||
if (asset_lib_uptr->is_enabled()) {
|
||||
libraries.append(asset_lib_uptr.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (AssetLibrary *library : libraries) {
|
||||
fn(*library);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,218 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <utility>
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
|
||||
#include "BLI_function_ref.hh"
|
||||
#include "BLI_map.hh"
|
||||
|
||||
#include "essentials_library.hh"
|
||||
|
||||
namespace blender {
|
||||
|
||||
struct AssetLibraryReference;
|
||||
struct bUserAssetLibrary;
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
class AllAssetLibrary;
|
||||
class OnDiskAssetLibrary;
|
||||
class PreferencesRemoteAssetLibrary;
|
||||
class RuntimeAssetLibrary;
|
||||
|
||||
/**
|
||||
* Global singleton-ish that provides access to individual #AssetLibrary instances.
|
||||
*
|
||||
* Whenever a blend file is loaded, the existing instance of AssetLibraryService is destructed, and
|
||||
* a new one is created -- hence the "singleton-ish". This ensures only information about relevant
|
||||
* asset libraries is loaded.
|
||||
*
|
||||
* \note How Asset libraries are identified may change in the future.
|
||||
* For now they are assumed to be:
|
||||
* - on disk (identified by the absolute directory), or
|
||||
* - the "current file" library (which is in memory but could have catalogs
|
||||
* loaded from a file on disk).
|
||||
*/
|
||||
class AssetLibraryService {
|
||||
static std::unique_ptr<AssetLibraryService> instance_;
|
||||
|
||||
/**
|
||||
* Identify libraries with the library type, and the absolute path of the library's root path
|
||||
* (normalize with #normalize_directory_path()!). The type is relevant since the current file
|
||||
* library may point to the same path as a custom library.
|
||||
*/
|
||||
using OnDiskLibraryIdentifier = std::pair<eAssetLibraryType, std::string>;
|
||||
/** Mapping of a (type, root path) pair to the AssetLibrary instance.
|
||||
* Always protect access with #on_disk_libraries_mutex_ below. */
|
||||
Map<OnDiskLibraryIdentifier, std::unique_ptr<OnDiskAssetLibrary>> on_disk_libraries_;
|
||||
mutable std::recursive_mutex on_disk_libraries_mutex_;
|
||||
using URLLibraryIdentifier = std::string;
|
||||
/** Always protect access with #remote_libraries_mutex_ below. */
|
||||
Map<URLLibraryIdentifier, std::unique_ptr<PreferencesRemoteAssetLibrary>> remote_libraries_;
|
||||
mutable std::recursive_mutex remote_libraries_mutex_;
|
||||
/**
|
||||
* Library without a known path, i.e. the "Current File" library if the file isn't saved yet. If
|
||||
* the file was saved, a valid path for the library can be determined and #on_disk_libraries_
|
||||
* above should be used.
|
||||
*/
|
||||
std::unique_ptr<RuntimeAssetLibrary> current_file_library_;
|
||||
/** The "all" asset library, merging all other libraries into one. */
|
||||
std::unique_ptr<AllAssetLibrary> all_library_;
|
||||
std::unique_ptr<OnlineEssentialsLibrary> online_essentials_library_;
|
||||
|
||||
/** Handlers for managing the life cycle of the AssetLibraryService instance. */
|
||||
bCallbackFuncStore on_load_callback_store_;
|
||||
static bool atexit_handler_registered_;
|
||||
|
||||
public:
|
||||
AssetLibraryService() = default;
|
||||
~AssetLibraryService() = default;
|
||||
|
||||
/** Return the AssetLibraryService singleton, allocating it if necessary. */
|
||||
static AssetLibraryService *get();
|
||||
|
||||
/** Destroy the AssetLibraryService singleton. It will be reallocated by #get() if necessary. */
|
||||
static void destroy();
|
||||
|
||||
static std::string root_path_from_library_ref(const AssetLibraryReference &library_reference);
|
||||
static bUserAssetLibrary *find_custom_asset_library_from_library_ref(
|
||||
const AssetLibraryReference &library_reference);
|
||||
static bUserAssetLibrary *find_custom_preferences_asset_library_from_asset_weak_ref(
|
||||
const AssetWeakReference &asset_reference);
|
||||
/**
|
||||
* Turn the runtime current file library into an on-disk current file library, preserving
|
||||
* catalog data like undo/redo history, deleted catalog info, catalog saving state, etc.
|
||||
* Note that this creates a new on-disk asset library and destroys the runtime one.
|
||||
*
|
||||
* Call when the `.blend` file is saved to disk.
|
||||
*
|
||||
* \return the new on-disk current file asset library (null in case of failure to find a path to
|
||||
* store the library in, based on the #Main.filepath from \a main).
|
||||
*/
|
||||
static AssetLibrary *move_runtime_current_file_into_on_disk_library(const Main &bmain);
|
||||
|
||||
AssetLibrary *get_asset_library(const Main *bmain,
|
||||
const AssetLibraryReference &library_reference);
|
||||
|
||||
/**
|
||||
* Get an asset library of type #ASSET_LIBRARY_CUSTOM from a directory path. Use
|
||||
* #get_asset_library_on_disk_custom_preferences() for asset libraries registered in the
|
||||
* Preferences.
|
||||
*/
|
||||
AssetLibrary *get_asset_library_on_disk_custom(StringRef name, StringRefNull root_path);
|
||||
/**
|
||||
* Get an asset library of type #ASSET_LIBRARY_CUSTOM from an asset library definition in the
|
||||
* Preferences.
|
||||
*/
|
||||
AssetLibrary *get_asset_library_on_disk_custom_preferences(bUserAssetLibrary *custom_library);
|
||||
/** Get a builtin (not user defined) asset library. I.e. a library that is **not** of type
|
||||
* #ASSET_LIBRARY_CUSTOM. */
|
||||
AssetLibrary *get_asset_library_on_disk_builtin(eAssetLibraryType type, StringRefNull root_path);
|
||||
/** Get the "Current File" asset library. */
|
||||
AssetLibrary *get_asset_library_current_file();
|
||||
/** Get the "All" asset library, which loads all others and merges them into one. */
|
||||
AssetLibrary *get_asset_library_all(const Main *bmain);
|
||||
/**
|
||||
* Tag the "All" asset library as needing to reload catalogs. This should be called when catalog
|
||||
* data of other asset libraries changes. Note that changes to the catalog definition file on
|
||||
* disk don't ever affect this "dirty" flag. It only reflects changes from this Blender session.
|
||||
*/
|
||||
void tag_all_library_catalogs_dirty();
|
||||
void reload_all_library_catalogs_if_dirty();
|
||||
|
||||
/**
|
||||
* Return the start position of the last blend-file extension in given path,
|
||||
* or #std::string::npos if not found. Works with both kind of path separators.
|
||||
*/
|
||||
int64_t rfind_blendfile_extension(StringRef path);
|
||||
/**
|
||||
* Return a normalized version of #AssetWeakReference.relative_asset_identifier.
|
||||
* Special care is required here because slashes or backslashes should not be converted in the ID
|
||||
* name itself.
|
||||
*/
|
||||
std::string normalize_asset_weak_reference_relative_asset_identifier(
|
||||
const AssetWeakReference &asset_reference);
|
||||
/** Get a valid library path from the weak reference. Empty if e.g. the reference is to a local
|
||||
* asset. */
|
||||
std::string resolve_asset_weak_reference_to_library_path(
|
||||
const AssetWeakReference &asset_reference);
|
||||
/**
|
||||
* Attempt to build a full path to an asset based on the currently available (not necessary
|
||||
* loaded) asset libraries. The path is not guaranteed to exist. The returned path will be
|
||||
* normalized and using native slashes.
|
||||
*
|
||||
* \note Only works for asset libraries on disk (others can't be resolved).
|
||||
*/
|
||||
std::string resolve_asset_weak_reference_to_full_path(const AssetWeakReference &asset_reference);
|
||||
/** Struct to hold results from path explosion functions
|
||||
* (#resolve_asset_weak_reference_to_exploded_path()). */
|
||||
struct ExplodedPath {
|
||||
/** The string buffer containing the fully resolved path, if resolving was successful. Pointer
|
||||
* so that the contained string address doesn't change when moving this object. */
|
||||
std::unique_ptr<std::string> full_path;
|
||||
/** Reference into the part of #full_path that is the library directory path. That is, it ends
|
||||
* with the library .blend file ("directory" is misleading). */
|
||||
StringRef dir_component = "";
|
||||
/** Reference into the part of #full_path that is the ID group name ("Object", "Material",
|
||||
* "Brush", ...). */
|
||||
StringRef group_component = "";
|
||||
/** Reference into the part of #full_path that is the ID name. */
|
||||
StringRef name_component = "";
|
||||
};
|
||||
/** Similar to #BKE_blendfile_library_path_explode, returns the full path as
|
||||
* #resolve_asset_weak_reference_to_library_path, with StringRefs to the `dir` (i.e. blendfile
|
||||
* path), `group` (i.e. ID type) and `name` (i.e. ID name) parts. */
|
||||
std::optional<ExplodedPath> resolve_asset_weak_reference_to_exploded_path(
|
||||
const AssetWeakReference &asset_reference);
|
||||
|
||||
/** Returns whether there are any known asset libraries with unsaved catalog edits. */
|
||||
bool has_any_unsaved_catalogs() const;
|
||||
|
||||
/** See AssetLibrary::foreach_loaded(). */
|
||||
void foreach_loaded_asset_library(FunctionRef<void(AssetLibrary &)> fn,
|
||||
bool include_all_library) const;
|
||||
|
||||
protected:
|
||||
/** Allocate a new instance of the service and assign it to `instance_`. */
|
||||
static void allocate_service_instance();
|
||||
|
||||
OnDiskAssetLibrary *lookup_on_disk_library(eAssetLibraryType type, StringRefNull root_path);
|
||||
|
||||
AssetLibrary *find_loaded_on_disk_asset_library_from_name(StringRef name) const;
|
||||
|
||||
AssetLibrary *get_online_essentials_asset_library();
|
||||
AssetLibrary *get_preferences_remote_asset_library(const bUserAssetLibrary &custom_library);
|
||||
/**
|
||||
* Get the given asset library. Opens it (i.e. creates a new AssetLibrary instance) if necessary.
|
||||
*
|
||||
* \param root_path: The top level directory.
|
||||
* \param preferences_library: The definition of the library from the Preferences. Set this to
|
||||
* null if the library is not registered in the Preferences (but non-null if it is!).
|
||||
*/
|
||||
AssetLibrary *get_asset_library_on_disk(eAssetLibraryType library_type,
|
||||
StringRef name,
|
||||
StringRefNull root_path,
|
||||
bool load_catalogs = true,
|
||||
bUserAssetLibrary *preferences_library = nullptr);
|
||||
/**
|
||||
* Ensure the AssetLibraryService instance is destroyed before a new blend file is loaded.
|
||||
* This makes memory management simple, and ensures a fresh start for every blend file. */
|
||||
void app_handler_register();
|
||||
void app_handler_unregister();
|
||||
};
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,350 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <stdexcept>
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include "BKE_blendfile.hh"
|
||||
#include "BKE_icons.hh"
|
||||
#include "BKE_idtype.hh"
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_preview_image.hh"
|
||||
|
||||
#include "DNA_ID.h"
|
||||
#include "DNA_asset_types.h"
|
||||
|
||||
#include "IMB_thumbs.hh"
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
#include "AS_asset_representation.hh"
|
||||
#include "AS_remote_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
AssetRepresentation::AssetRepresentation(StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
const int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata,
|
||||
AssetLibrary &owner_asset_library)
|
||||
: owner_asset_library_(owner_asset_library),
|
||||
relative_identifier_(relative_asset_path),
|
||||
asset_(AssetRepresentation::ExternalAsset{name, id_type, std::move(metadata)})
|
||||
{
|
||||
}
|
||||
|
||||
AssetRepresentation::AssetRepresentation(StringRef relative_asset_path,
|
||||
StringRef name,
|
||||
const int id_type,
|
||||
std::unique_ptr<AssetMetaData> metadata,
|
||||
AssetLibrary &owner_asset_library,
|
||||
OnlineAssetInfo online_info)
|
||||
: owner_asset_library_(owner_asset_library),
|
||||
relative_identifier_(relative_asset_path),
|
||||
asset_(AssetRepresentation::ExternalAsset{
|
||||
name,
|
||||
id_type,
|
||||
std::move(metadata),
|
||||
nullptr,
|
||||
RemoteAssetFileStatus::UNSET,
|
||||
std::make_unique<OnlineAssetInfo>(std::move(online_info))})
|
||||
{
|
||||
}
|
||||
|
||||
AssetRepresentation::AssetRepresentation(ID &id, AssetLibrary &owner_asset_library)
|
||||
: owner_asset_library_(owner_asset_library), asset_(&id)
|
||||
{
|
||||
if (!id.asset_data) {
|
||||
throw std::invalid_argument("Passed ID is not an asset");
|
||||
}
|
||||
}
|
||||
|
||||
AssetRepresentation::~AssetRepresentation()
|
||||
{
|
||||
if (const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
extern_asset && extern_asset->preview_)
|
||||
{
|
||||
BKE_previewimg_cached_release(this->full_path().c_str());
|
||||
}
|
||||
}
|
||||
|
||||
AssetWeakReference AssetRepresentation::make_weak_reference() const
|
||||
{
|
||||
return AssetWeakReference::make_reference(owner_asset_library_, library_relative_identifier());
|
||||
}
|
||||
|
||||
void AssetRepresentation::ensure_previewable(const bContext &C, ReportList *reports)
|
||||
{
|
||||
if (ID *id = this->local_id()) {
|
||||
PreviewImage *preview = BKE_previewimg_id_get(id);
|
||||
BKE_icon_preview_ensure(id, preview);
|
||||
return;
|
||||
}
|
||||
|
||||
ExternalAsset &extern_asset = std::get<ExternalAsset>(asset_);
|
||||
|
||||
if (extern_asset.preview_ && extern_asset.preview_->runtime->icon_id) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* The asset may be in multiple libraries, so multiple #AssetRepresentation's may refer to the
|
||||
* same preview. Use user counting so the preview is only released with the last representation.
|
||||
*/
|
||||
const bool count_preview_users = true;
|
||||
|
||||
/* Only use the remote thumbnail when there is no asset file on disk. Otherwise use the on-disk
|
||||
* file. */
|
||||
if (this->is_online_only()) {
|
||||
if (!extern_asset.online_info_->preview_url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const std::string preview_path = remote_library_asset_preview_path(*this);
|
||||
/* Doesn't do the actual reading, just allocates and attaches the derived load info. */
|
||||
extern_asset.preview_ = BKE_previewimg_online_thumbnail_read(
|
||||
this->full_path().c_str(), preview_path.c_str(), false, count_preview_users);
|
||||
remote_library_request_preview_download(C, *this, preview_path, reports);
|
||||
}
|
||||
else {
|
||||
/* Use the full path as preview name, it's the only unique identifier we have. */
|
||||
const std::string full_path = this->full_path();
|
||||
|
||||
/* Doesn't do the actual reading, just allocates and attaches the derived load info. */
|
||||
extern_asset.preview_ = BKE_previewimg_cached_thumbnail_read(
|
||||
full_path.c_str(), full_path.c_str(), THB_SOURCE_BLEND, false, count_preview_users);
|
||||
}
|
||||
|
||||
BKE_icon_preview_ensure(nullptr, extern_asset.preview_);
|
||||
}
|
||||
|
||||
PreviewImage *AssetRepresentation::get_preview() const
|
||||
{
|
||||
if (const ID *id = this->local_id()) {
|
||||
return BKE_previewimg_id_get(id);
|
||||
}
|
||||
|
||||
return std::get<ExternalAsset>(asset_).preview_;
|
||||
}
|
||||
|
||||
StringRefNull AssetRepresentation::get_name() const
|
||||
{
|
||||
if (const ID *id = this->local_id()) {
|
||||
return id->name + 2;
|
||||
}
|
||||
return std::get<ExternalAsset>(asset_).name;
|
||||
}
|
||||
|
||||
ID_Type AssetRepresentation::get_id_type() const
|
||||
{
|
||||
if (const ID *id = this->local_id()) {
|
||||
return GS(id->name);
|
||||
}
|
||||
return ID_Type(std::get<ExternalAsset>(asset_).id_type);
|
||||
}
|
||||
|
||||
AssetMetaData &AssetRepresentation::get_metadata() const
|
||||
{
|
||||
if (const ID *id = this->local_id()) {
|
||||
return *id->asset_data;
|
||||
}
|
||||
return *std::get<ExternalAsset>(asset_).metadata_;
|
||||
}
|
||||
|
||||
StringRefNull AssetRepresentation::library_relative_identifier() const
|
||||
{
|
||||
if (const ID *id = this->local_id()) {
|
||||
StringRef idname = BKE_id_name(*id);
|
||||
/* Lazy-create/-update with the latest ID name. */
|
||||
if (!StringRef{relative_identifier_}.endswith(idname)) {
|
||||
relative_identifier_ = StringRef{BKE_idtype_idcode_to_name(GS(id->name))} + SEP_STR + idname;
|
||||
}
|
||||
}
|
||||
|
||||
return relative_identifier_;
|
||||
}
|
||||
|
||||
std::string AssetRepresentation::full_path() const
|
||||
{
|
||||
char filepath[FILE_MAX];
|
||||
BLI_path_join(filepath,
|
||||
sizeof(filepath),
|
||||
owner_asset_library_.root_path().c_str(),
|
||||
library_relative_identifier().c_str());
|
||||
return filepath;
|
||||
}
|
||||
|
||||
std::string AssetRepresentation::full_library_path() const
|
||||
{
|
||||
std::string asset_path = full_path();
|
||||
|
||||
char blend_path[/*FILE_MAX_LIBEXTRA*/ 1282];
|
||||
if (!BKE_blendfile_library_path_explode(asset_path.c_str(), blend_path, nullptr, nullptr)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return blend_path;
|
||||
}
|
||||
|
||||
Span<OnlineAssetFile> AssetRepresentation::online_asset_files() const
|
||||
{
|
||||
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset || !extern_asset->online_info_) {
|
||||
return {};
|
||||
}
|
||||
return extern_asset->online_info_->files;
|
||||
}
|
||||
|
||||
std::optional<int64_t> AssetRepresentation::online_asset_files_combined_size_in_bytes() const
|
||||
{
|
||||
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset || !extern_asset->online_info_) {
|
||||
return {};
|
||||
}
|
||||
int64_t size = 0;
|
||||
for (const OnlineAssetFile &file : online_asset_files()) {
|
||||
size += file.size_in_bytes;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
std::optional<StringRefNull> AssetRepresentation::online_asset_preview_url() const
|
||||
{
|
||||
if (!this->is_online_only()) {
|
||||
return {};
|
||||
}
|
||||
std::optional<URLWithHash> &url_with_hash =
|
||||
std::get<ExternalAsset>(asset_).online_info_->preview_url;
|
||||
if (!url_with_hash) {
|
||||
return {};
|
||||
}
|
||||
return url_with_hash->url;
|
||||
}
|
||||
|
||||
std::optional<StringRefNull> AssetRepresentation::online_asset_preview_hash() const
|
||||
{
|
||||
if (!this->is_online_only()) {
|
||||
return {};
|
||||
}
|
||||
std::optional<URLWithHash> &url_with_hash =
|
||||
std::get<ExternalAsset>(asset_).online_info_->preview_url;
|
||||
if (!url_with_hash) {
|
||||
return {};
|
||||
}
|
||||
return url_with_hash->hash;
|
||||
}
|
||||
|
||||
void AssetRepresentation::online_asset_mark_downloaded()
|
||||
{
|
||||
ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset) {
|
||||
return;
|
||||
}
|
||||
/* Since it was just downloaded, let's assume the file matches the listed hash. If not, the
|
||||
* next refresh will show the correct status.
|
||||
* TODO: ensure that the file status is actually checked, instead of just making assumptions. */
|
||||
extern_asset->remote_file_status_ = RemoteAssetFileStatus::MATCH;
|
||||
}
|
||||
|
||||
std::optional<eAssetImportMethod> AssetRepresentation::get_import_method() const
|
||||
{
|
||||
const AssetMetaData &metadata = this->get_metadata();
|
||||
if (metadata.flag & ASSETDATA_USE_OWN_IMPORT_METHOD) {
|
||||
return metadata.preferred_import_method;
|
||||
}
|
||||
return owner_asset_library_.import_method();
|
||||
}
|
||||
|
||||
bool AssetRepresentation::may_override_import_method() const
|
||||
{
|
||||
if (!owner_asset_library_.import_method()) {
|
||||
return true;
|
||||
}
|
||||
return owner_asset_library_.may_override_import_method_;
|
||||
}
|
||||
|
||||
bool AssetRepresentation::get_use_relative_path() const
|
||||
{
|
||||
return owner_asset_library_.use_relative_paths();
|
||||
}
|
||||
|
||||
ID *AssetRepresentation::local_id() const
|
||||
{
|
||||
return this->is_local_id() ? std::get<ID *>(asset_) : nullptr;
|
||||
}
|
||||
|
||||
bool AssetRepresentation::is_local_id() const
|
||||
{
|
||||
return std::holds_alternative<ID *>(asset_);
|
||||
}
|
||||
|
||||
bool AssetRepresentation::is_online_only() const
|
||||
{
|
||||
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset || !extern_asset->online_info_) {
|
||||
return false;
|
||||
}
|
||||
/* An asset is considered 'online' if there is no file on disk for it.
|
||||
*
|
||||
* About also allowing UNSET: This function is (indirectly) called from all kinds of
|
||||
* places, like `get_node_tools_type_data()` in `node_group_operators.cc` to figure out which
|
||||
* node tools are available. Since that happens on startup, the actual on-disk file status may
|
||||
* not have been checked yet. Until that time, just assume that having `online_info_` means "it
|
||||
* is online". */
|
||||
return ELEM(extern_asset->remote_file_status_,
|
||||
RemoteAssetFileStatus::NOT_ON_DISK,
|
||||
RemoteAssetFileStatus::UNSET);
|
||||
}
|
||||
|
||||
bool AssetRepresentation::is_potentially_editable_asset_blend() const
|
||||
{
|
||||
if (this->owner_asset_library().is_read_only()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string lib_path = this->full_library_path();
|
||||
return StringRef(lib_path).endswith(BLENDER_ASSET_FILE_SUFFIX);
|
||||
}
|
||||
|
||||
RemoteAssetFileStatus AssetRepresentation::remote_file_status() const
|
||||
{
|
||||
const ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset) {
|
||||
return RemoteAssetFileStatus::UNSET;
|
||||
}
|
||||
return extern_asset->remote_file_status_;
|
||||
}
|
||||
|
||||
void AssetRepresentation::online_info_set(OnlineAssetInfo info)
|
||||
{
|
||||
ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset) {
|
||||
return;
|
||||
}
|
||||
extern_asset->online_info_ = std::make_unique<OnlineAssetInfo>(std::move(info));
|
||||
}
|
||||
|
||||
void AssetRepresentation::remote_file_status_set(const RemoteAssetFileStatus status)
|
||||
{
|
||||
ExternalAsset *extern_asset = std::get_if<ExternalAsset>(&asset_);
|
||||
if (!extern_asset) {
|
||||
return;
|
||||
}
|
||||
extern_asset->remote_file_status_ = status;
|
||||
}
|
||||
|
||||
bool AssetRepresentation::needs_download() const
|
||||
{
|
||||
return this->is_online_only() || this->remote_file_status() == RemoteAssetFileStatus::NO_MATCH;
|
||||
}
|
||||
|
||||
AssetLibrary &AssetRepresentation::owner_asset_library() const
|
||||
{
|
||||
return owner_asset_library_;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,183 @@
|
||||
/* SPDX-FileCopyrightText: 2025 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <optional>
|
||||
|
||||
#include "AS_disk_file_hash_service.hh"
|
||||
|
||||
#include "BKE_idprop.hh"
|
||||
|
||||
#ifdef WITH_PYTHON
|
||||
# include "BPY_extern_run.hh"
|
||||
#endif
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
static CLG_LogRef LOG = {"assets.disk_file_hash_service"};
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
std::unique_ptr<DiskFileHashService> disk_file_hash_service_get(const StringRef storage_path)
|
||||
{
|
||||
return std::make_unique<DiskFileHashService>(storage_path);
|
||||
}
|
||||
|
||||
DiskFileHashService::DiskFileHashService(const StringRef storage_path)
|
||||
: storage_path_(storage_path)
|
||||
{
|
||||
}
|
||||
|
||||
DiskFileHashService::~DiskFileHashService()
|
||||
{
|
||||
release_python();
|
||||
}
|
||||
|
||||
void DiskFileHashService::release_python()
|
||||
{
|
||||
#ifdef WITH_PYTHON
|
||||
constexpr const char *SCRIPT = R"(
|
||||
import _bpy_internal.disk_file_hash_service as dfhs
|
||||
from pathlib import Path
|
||||
|
||||
dfhs.release_service(Path(storage_path))
|
||||
)";
|
||||
std::unique_ptr locals = bke::idprop::create_group("locals");
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(this->storage_path_, "storage_path"));
|
||||
|
||||
BPY_run_string_exec_with_locals(nullptr, SCRIPT, *locals);
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string DiskFileHashService::get_hash(const StringRef filepath, const StringRef hash_algorithm)
|
||||
{
|
||||
#ifdef WITH_PYTHON
|
||||
/* NOTE: this is a somewhat inefficient implementation for frequently-repeated calls, as each
|
||||
* call repeats the calls to `dfhs.get_service(Path(...))`. However, this does mean that the C++
|
||||
* wrapper does not have to retain any references to Python objects itself, avoiding reference
|
||||
* counting bugs. If the performance starts to matter, do the lookup of the service itself once,
|
||||
* and cache the result. */
|
||||
constexpr const char *SCRIPT = R"(
|
||||
import _bpy_internal.disk_file_hash_service as dfhs
|
||||
from pathlib import Path
|
||||
|
||||
service = dfhs.get_service(Path(storage_path))
|
||||
_result = service.get_hash(Path(filepath), hash_algorithm)
|
||||
)";
|
||||
|
||||
/* Local variables for the script. */
|
||||
std::unique_ptr locals = bke::idprop::create_group("locals");
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(this->storage_path_, "storage_path"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(filepath, "filepath"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(hash_algorithm, "hash_algorithm"));
|
||||
|
||||
/* Run the script. */
|
||||
std::optional<IDProperty *> idprop_optptr = BPY_run_string_exec_with_locals_return_idprop(
|
||||
nullptr, SCRIPT, *locals, "_result");
|
||||
if (!idprop_optptr.has_value()) {
|
||||
const std::string filepath_str = filepath;
|
||||
CLOG_ERROR(&LOG, "Failed to run hash script for file [%s].", filepath_str.c_str());
|
||||
return "";
|
||||
}
|
||||
IDProperty *hash_idprop = *idprop_optptr;
|
||||
|
||||
/* Check the returned value. */
|
||||
if (hash_idprop == nullptr || hash_idprop->type != IDP_STRING) {
|
||||
IDP_FreeProperty(hash_idprop);
|
||||
const std::string filepath_str = filepath;
|
||||
CLOG_ERROR(&LOG,
|
||||
"Hash for file [%s] was not returned as string. Please report this as a bug.",
|
||||
filepath_str.c_str());
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string hash_value(IDP_string_get(hash_idprop));
|
||||
IDP_FreeProperty(hash_idprop);
|
||||
|
||||
return hash_value;
|
||||
#else
|
||||
UNUSED_VARS(filepath, hash_algorithm);
|
||||
const std::string filepath_str = filepath;
|
||||
CLOG_ERROR(&LOG,
|
||||
"Blender was built without Python support, cannot compute hash for file [%s]",
|
||||
filepath_str.c_str());
|
||||
return "";
|
||||
#endif
|
||||
}
|
||||
|
||||
bool DiskFileHashService::file_matches(const StringRef filepath,
|
||||
const StringRef hash_algorithm,
|
||||
const StringRef hexhash,
|
||||
const int64_t size_in_bytes)
|
||||
{
|
||||
#ifdef WITH_PYTHON
|
||||
/* NOTE: this is a somewhat inefficient implementation for frequently-repeated calls, as each
|
||||
* call repeats the calls to `dfhs.get_service(Path(...))`. However, this does mean that the C++
|
||||
* wrapper does not have to retain any references to Python objects itself, avoiding reference
|
||||
* counting bugs. If the performance starts to matter, do the lookup of the service itself once,
|
||||
* and cache the result. */
|
||||
constexpr const char *SCRIPT = R"(
|
||||
import _bpy_internal.disk_file_hash_service as dfhs
|
||||
from pathlib import Path
|
||||
|
||||
# The '& 0xFFFFFFFF' makes Python interpret the values as unsigned ints.
|
||||
size_in_bytes = ((size_in_bytes_high & 0xFFFFFFFF) << 32) | (size_in_bytes_low & 0xFFFFFFFF)
|
||||
|
||||
service = dfhs.get_service(Path(storage_path))
|
||||
_result = service.file_matches(Path(filepath), hash_algorithm, hexhash, size_in_bytes);
|
||||
)";
|
||||
|
||||
/* Since IDProperties don't support 64-bit integers, split it up into two 32-bit integers, and do
|
||||
* bit shifting in Python to get the value back. */
|
||||
BLI_assert(size_in_bytes >= 0);
|
||||
const int size_in_bytes_high = int((size_in_bytes >> 32) & 0xFFFFFFFF);
|
||||
const int size_in_bytes_low = int(size_in_bytes & 0xFFFFFFFF);
|
||||
|
||||
std::unique_ptr locals = bke::idprop::create_group("locals");
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(this->storage_path_, "storage_path"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(filepath, "filepath"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(hash_algorithm, "hash_algorithm"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewString(hexhash, "hexhash"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewInt(size_in_bytes_high, "size_in_bytes_high"));
|
||||
IDP_AddToGroup(locals.get(), IDP_NewInt(size_in_bytes_low, "size_in_bytes_low"));
|
||||
|
||||
/* Run the script. */
|
||||
std::optional<IDProperty *> idprop_optptr = BPY_run_string_exec_with_locals_return_idprop(
|
||||
nullptr, SCRIPT, *locals, "_result");
|
||||
if (!idprop_optptr.has_value()) {
|
||||
const std::string filepath_str = filepath;
|
||||
CLOG_ERROR(&LOG, "Failed to run hash match script for file [%s].", filepath_str.c_str());
|
||||
return false;
|
||||
}
|
||||
IDProperty *is_match_idprop = *idprop_optptr;
|
||||
|
||||
/* Check the returned value. */
|
||||
if (is_match_idprop == nullptr || is_match_idprop->type != IDP_BOOLEAN) {
|
||||
IDP_FreeProperty(is_match_idprop);
|
||||
const std::string filepath_str = filepath;
|
||||
CLOG_ERROR(
|
||||
&LOG,
|
||||
"Hash match check for file [%s] did not return a boolean. Please report this as a bug.",
|
||||
filepath_str.c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool is_match(IDP_bool_get(is_match_idprop));
|
||||
IDP_FreeProperty(is_match_idprop);
|
||||
|
||||
return is_match;
|
||||
#else
|
||||
UNUSED_VARS(filepath, hash_algorithm, hexhash, size_in_bytes);
|
||||
const std::string filepath_str = filepath;
|
||||
CLOG_ERROR(&LOG,
|
||||
"Blender was built without Python support, cannot check hash for file [%s]",
|
||||
filepath_str.c_str());
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,132 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include <memory>
|
||||
|
||||
/* For getting the experimental flag for remote library support. */
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "AS_remote_library.hh"
|
||||
|
||||
#include "all_library.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
static CLG_LogRef LOG = {"asset.library"};
|
||||
|
||||
namespace asset_system {
|
||||
|
||||
AllAssetLibrary::AllAssetLibrary()
|
||||
: AssetLibrary(ASSET_LIBRARY_ALL,
|
||||
/*is_read_only=*/true)
|
||||
{
|
||||
}
|
||||
|
||||
void AllAssetLibrary::force_remote_listing_download() const
|
||||
{
|
||||
/* This includes the online essentials as a separate library, if loaded. */
|
||||
AssetLibrary::foreach_loaded(
|
||||
[&](AssetLibrary &nested) {
|
||||
const std::optional<StringRefNull> url = nested.remote_url();
|
||||
if (url.has_value()) {
|
||||
remote_library_request_download(RemoteLibraryDefinitionRef{*url, nested.root_path()});
|
||||
}
|
||||
},
|
||||
/*include_all_library=*/false);
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryReference> AllAssetLibrary::library_reference() const
|
||||
{
|
||||
return all_library_reference();
|
||||
}
|
||||
|
||||
std::optional<eAssetImportMethod> AllAssetLibrary::import_method() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void AllAssetLibrary::rebuild_catalogs_from_nested(const bool reload_nested_catalogs)
|
||||
{
|
||||
/* Only one thread should rebuild at a time. If another thread is already rebuilding, wait for it
|
||||
* to finish and then skip rebuilding. The result would effectively be the same, so re-running
|
||||
* would just be wasted work. Waiting (rather than returning early) ensures callers don't see
|
||||
* partially rebuilt catalogs. */
|
||||
std::unique_lock rebuild_lock{rebuild_mutex_, std::try_to_lock};
|
||||
if (!rebuild_lock.owns_lock()) {
|
||||
/* Another thread holds the lock and is rebuilding. Block until it is done, then return. */
|
||||
rebuild_lock.lock();
|
||||
return;
|
||||
}
|
||||
|
||||
/* Start with empty catalog storage. Don't do this directly in #this.catalog_service to avoid
|
||||
* race conditions. Rather build into a new service and replace the current one when done. */
|
||||
std::unique_ptr<AssetCatalogService> new_catalog_service = std::make_unique<AssetCatalogService>(
|
||||
AssetCatalogService::read_only_tag());
|
||||
|
||||
const bool skip_remote_libraries = !USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
|
||||
|
||||
AssetLibrary::foreach_loaded(
|
||||
[&](AssetLibrary &nested) {
|
||||
const bool is_online_lib = nested.remote_url().has_value();
|
||||
if (is_online_lib && skip_remote_libraries) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (reload_nested_catalogs) {
|
||||
nested.catalog_service().reload_catalogs();
|
||||
}
|
||||
|
||||
new_catalog_service->add_from_existing(
|
||||
nested.catalog_service(),
|
||||
/*on_duplicate_items=*/[](const AssetCatalog &existing,
|
||||
const AssetCatalog &to_be_ignored) {
|
||||
if (existing.path == to_be_ignored.path) {
|
||||
CLOG_DEBUG(&LOG,
|
||||
"multiple definitions of catalog %s (path: %s), ignoring duplicate",
|
||||
existing.catalog_id.str().c_str(),
|
||||
existing.path.c_str());
|
||||
}
|
||||
else {
|
||||
/* This is bound to happen at some point, for example with the Online Essentials
|
||||
* catalogs diverging from this Blender version's bundled Essentials catalogs. */
|
||||
CLOG_INFO(&LOG,
|
||||
"multiple definitions of catalog %s with differing paths (%s vs. %s), "
|
||||
"ignoring second one",
|
||||
existing.catalog_id.str().c_str(),
|
||||
existing.path.c_str(),
|
||||
to_be_ignored.path.c_str());
|
||||
}
|
||||
});
|
||||
},
|
||||
false);
|
||||
|
||||
std::lock_guard lock{catalog_service_mutex_};
|
||||
catalog_service_ = std::move(new_catalog_service);
|
||||
catalogs_dirty_ = false;
|
||||
}
|
||||
|
||||
void AllAssetLibrary::tag_catalogs_dirty()
|
||||
{
|
||||
catalogs_dirty_ = true;
|
||||
}
|
||||
|
||||
bool AllAssetLibrary::is_catalogs_dirty() const
|
||||
{
|
||||
return catalogs_dirty_;
|
||||
}
|
||||
|
||||
void AllAssetLibrary::refresh_catalogs()
|
||||
{
|
||||
this->rebuild_catalogs_from_nested(/*reload_nested_catalogs=*/true);
|
||||
}
|
||||
|
||||
} // namespace asset_system
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,46 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class AllAssetLibrary : public AssetLibrary {
|
||||
std::atomic<bool> catalogs_dirty_ = true;
|
||||
|
||||
/** Serializes #rebuild_catalogs_from_nested so only one thread rebuilds at a time. */
|
||||
std::mutex rebuild_mutex_;
|
||||
|
||||
public:
|
||||
AllAssetLibrary();
|
||||
|
||||
void force_remote_listing_download() const override;
|
||||
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
std::optional<eAssetImportMethod> import_method() const override;
|
||||
void refresh_catalogs() override;
|
||||
|
||||
/**
|
||||
* Update the available catalogs and catalog tree from the nested asset libraries. Completely
|
||||
* recreates the catalog service (invalidating pointers to the previous one).
|
||||
*
|
||||
* \param reload_nested_catalogs: Re-read catalog definitions of nested libraries from disk and
|
||||
* merge them into the in-memory representations.
|
||||
*/
|
||||
void rebuild_catalogs_from_nested(bool reload_nested_catalogs);
|
||||
|
||||
void tag_catalogs_dirty();
|
||||
bool is_catalogs_dirty() const;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,34 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "common.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
UserAssetLibraryWrapper::UserAssetLibraryWrapper(const bUserAssetLibrary &user_asset_library)
|
||||
: user_asset_library_(&user_asset_library)
|
||||
{
|
||||
}
|
||||
|
||||
const bUserAssetLibrary *UserAssetLibraryWrapper::user_asset_library() const
|
||||
{
|
||||
if (user_asset_library_ == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
if (BLI_findindex(&U.asset_libraries, user_asset_library_) == -1) {
|
||||
user_asset_library_ = nullptr;
|
||||
return nullptr;
|
||||
}
|
||||
return user_asset_library_;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,41 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace blender {
|
||||
struct bUserAssetLibrary;
|
||||
}
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* Wrapper to get the #bUserAssetLibrary from the preferences (if still valid).
|
||||
*/
|
||||
class UserAssetLibraryWrapper {
|
||||
/**
|
||||
* Pointer to the user's asset library entry in the preferences.
|
||||
|
||||
* \warning This may be dangling or null! Only access this using #user_asset_library(), which
|
||||
* returns `nullptr` if the library is not found (meaning it was removed/freed). It will also
|
||||
* null the pointer in that case, to avoid holding on to the dangling pointer (that's why it's
|
||||
* mutable).
|
||||
*/
|
||||
mutable const bUserAssetLibrary *user_asset_library_;
|
||||
|
||||
public:
|
||||
explicit UserAssetLibraryWrapper(const bUserAssetLibrary &user_asset_library);
|
||||
|
||||
/**
|
||||
* Returns a pointer to the user's asset library entry in the preferences, or `nullptr` if not
|
||||
* found (meaning it was removed/freed).
|
||||
*/
|
||||
const bUserAssetLibrary *user_asset_library() const;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,206 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "AS_remote_library.hh"
|
||||
|
||||
#include "BKE_appdir.hh"
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "DNA_asset_types.h"
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "on_disk_library.hh"
|
||||
#include "remote_library.hh"
|
||||
#include "utils.hh"
|
||||
|
||||
#include "AS_essentials_library.hh"
|
||||
#include "essentials_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
static CLG_LogRef LOG = {"asset.library.essentials"};
|
||||
|
||||
EssentialsAssetLibrary::EssentialsAssetLibrary()
|
||||
: OnDiskAssetLibrary(ASSET_LIBRARY_ESSENTIALS,
|
||||
{},
|
||||
utils::normalize_directory_path(essentials_directory_path()),
|
||||
/*is_read_only=*/true)
|
||||
{
|
||||
}
|
||||
|
||||
void EssentialsAssetLibrary::force_remote_listing_download() const
|
||||
{
|
||||
remote_library_request_download(RemoteLibraryDefinitionRef{
|
||||
online_essentials_url(), online_essentials_cache_directory_path()});
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryReference> EssentialsAssetLibrary::library_reference() const
|
||||
{
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_ESSENTIALS;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
std::optional<eAssetImportMethod> EssentialsAssetLibrary::import_method() const
|
||||
{
|
||||
if (U.experimental.no_data_block_packing) {
|
||||
return ASSET_IMPORT_APPEND_REUSE;
|
||||
}
|
||||
return ASSET_IMPORT_PACK;
|
||||
}
|
||||
|
||||
void EssentialsAssetLibrary::refresh_catalogs()
|
||||
{
|
||||
/* Start with empty catalog storage. Don't do this directly in #this.catalog_service to avoid
|
||||
* race conditions. Rather build into a new service and replace the current one when done. */
|
||||
std::unique_ptr<AssetCatalogService> new_catalog_service = std::make_unique<AssetCatalogService>(
|
||||
AssetCatalogService::read_only_tag());
|
||||
|
||||
const bool skip_remote_libraries = !USER_EXPERIMENTAL_TEST(&U, use_remote_asset_libraries);
|
||||
|
||||
const auto load_catalogs_fn = [&](const AssetLibrary *library) {
|
||||
const bool is_online_lib = library->remote_url().has_value();
|
||||
if (is_online_lib && skip_remote_libraries) {
|
||||
return;
|
||||
}
|
||||
|
||||
library->catalog_service().reload_catalogs();
|
||||
|
||||
new_catalog_service->add_from_existing(
|
||||
library->catalog_service(),
|
||||
/*on_duplicate_items=*/[](const AssetCatalog &existing,
|
||||
const AssetCatalog &to_be_ignored) {
|
||||
if (existing.path == to_be_ignored.path) {
|
||||
CLOG_DEBUG(&LOG,
|
||||
"multiple definitions of catalog %s (path: %s), ignoring duplicate",
|
||||
existing.catalog_id.str().c_str(),
|
||||
existing.path.c_str());
|
||||
}
|
||||
else {
|
||||
/* This is to be expected at some point in the future. The Online Essentials library
|
||||
* may change its catalog paths, while whatever version of Blender is running right now
|
||||
* still has the same old bundled assets. This means the Bundled Essentials and Online
|
||||
* Essentials diverge. There is no need to bother users with this, as it's bound to
|
||||
* happen eventually.
|
||||
*
|
||||
* Note that this same check happens in the 'All' library as well, and that already
|
||||
* logs this at INFO level, so there really is no need to be louder than DEBUG here. */
|
||||
CLOG_DEBUG(&LOG,
|
||||
"multiple definitions of catalog %s with differing paths (%s vs. %s), "
|
||||
"ignoring second one",
|
||||
existing.catalog_id.str().c_str(),
|
||||
existing.path.c_str(),
|
||||
to_be_ignored.path.c_str());
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
load_catalogs_fn(this);
|
||||
if (U.asset_flag & USER_ASSETS_USE_ONLINE_ESSENTIALS) {
|
||||
load_catalogs_fn(AS_asset_library_load(nullptr, online_essentials_library_reference()));
|
||||
}
|
||||
|
||||
std::lock_guard lock{catalog_service_mutex_};
|
||||
catalog_service_ = std::move(new_catalog_service);
|
||||
}
|
||||
|
||||
StringRefNull essentials_directory_path()
|
||||
{
|
||||
static std::string path = []() {
|
||||
const std::optional<std::string> datafiles_path = BKE_appdir_folder_id(
|
||||
BLENDER_SYSTEM_DATAFILES, "assets");
|
||||
return datafiles_path.value_or("");
|
||||
}();
|
||||
return path;
|
||||
}
|
||||
|
||||
bool skip_experimental_asset_catalog(const UUID & /*catalog_id*/)
|
||||
{
|
||||
/* Return true when the catalog_id should be rejected based on experimental features:
|
||||
*
|
||||
* const UUID UUID_my_feature_catalog_id("11111111-2222-3333-4444-555555555555");
|
||||
* if (!U.experimental.use_my_feature && catalog_id == UUID_my_feature_catalog_id) {
|
||||
* return true;
|
||||
* }
|
||||
*/
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------- */
|
||||
/** \name Online Essentials Library
|
||||
*
|
||||
* Internally this is a separate library. To the user, it's part of the normal Essentials library.
|
||||
* \{ */
|
||||
|
||||
StringRefNull online_essentials_cache_directory_path()
|
||||
{
|
||||
static std::string path = []() {
|
||||
return remote_library_cache_directory_path("online-essentials");
|
||||
}();
|
||||
return path;
|
||||
}
|
||||
|
||||
StringRefNull online_essentials_url()
|
||||
{
|
||||
return OnlineEssentialsLibrary::URL;
|
||||
}
|
||||
|
||||
bool is_online_essentials_url(const StringRef url)
|
||||
{
|
||||
if (url.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (remote_library_url_ends_with_top_meta_file_name(url)) {
|
||||
BLI_assert(url.drop_suffix(REMOTE_LIBRARY_TOP_META_FILE_NAME.size()).back() == '/');
|
||||
return url.drop_suffix(REMOTE_LIBRARY_TOP_META_FILE_NAME.size()) ==
|
||||
OnlineEssentialsLibrary::URL;
|
||||
}
|
||||
|
||||
return url == OnlineEssentialsLibrary::URL;
|
||||
}
|
||||
|
||||
bool is_online_essentials_dirpath(StringRef dirpath)
|
||||
{
|
||||
if (dirpath.is_empty()) {
|
||||
return false;
|
||||
}
|
||||
if (dirpath.endswith(SEP_STR)) {
|
||||
dirpath = dirpath.drop_known_suffix(SEP_STR);
|
||||
}
|
||||
BLI_assert(!online_essentials_cache_directory_path().endswith(SEP_STR));
|
||||
|
||||
return dirpath == online_essentials_cache_directory_path();
|
||||
}
|
||||
|
||||
OnlineEssentialsLibrary::OnlineEssentialsLibrary()
|
||||
: RemoteAssetLibrary(ASSET_LIBRARY_ONLINE_ESSENTIALS,
|
||||
/*is_read_only=*/true,
|
||||
/*remote_url=*/URL,
|
||||
/*name=*/"Online Essentials",
|
||||
/*root_path=*/online_essentials_cache_directory_path())
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryReference> OnlineEssentialsLibrary::library_reference() const
|
||||
{
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.type = ASSET_LIBRARY_ONLINE_ESSENTIALS;
|
||||
library_ref.custom_library_index = -1;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
/** \} */
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,40 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
#include "on_disk_library.hh"
|
||||
#include "remote_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class EssentialsAssetLibrary : public OnDiskAssetLibrary {
|
||||
public:
|
||||
EssentialsAssetLibrary();
|
||||
|
||||
void force_remote_listing_download() const override;
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
std::optional<eAssetImportMethod> import_method() const override;
|
||||
|
||||
void refresh_catalogs() override;
|
||||
};
|
||||
|
||||
class OnlineEssentialsLibrary : public RemoteAssetLibrary {
|
||||
public:
|
||||
OnlineEssentialsLibrary();
|
||||
|
||||
/* Trailing slash matters! */
|
||||
static constexpr StringRefNull URL =
|
||||
"https://cdn.extensions.blender.org/asset-libraries/essentials/";
|
||||
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,52 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "on_disk_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
OnDiskAssetLibrary::OnDiskAssetLibrary(eAssetLibraryType library_type,
|
||||
StringRef name,
|
||||
StringRef root_path,
|
||||
const bool is_read_only)
|
||||
: AssetLibrary(library_type, /*is_read_only=*/is_read_only, name, root_path)
|
||||
{
|
||||
this->on_blend_save_handler_register();
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryReference> OnDiskAssetLibrary::library_reference() const
|
||||
{
|
||||
if (library_type() == ASSET_LIBRARY_LOCAL) {
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.custom_library_index = -1;
|
||||
library_ref.type = ASSET_LIBRARY_LOCAL;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
BLI_assert_msg(false,
|
||||
"Library references are only available for built-in libraries and libraries "
|
||||
"configured in the Preferences");
|
||||
return {};
|
||||
}
|
||||
|
||||
std::optional<eAssetImportMethod> OnDiskAssetLibrary::import_method() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
void OnDiskAssetLibrary::refresh_catalogs()
|
||||
{
|
||||
this->catalog_service().reload_catalogs();
|
||||
}
|
||||
|
||||
bool OnDiskAssetLibrary::is_enabled() const
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,29 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class OnDiskAssetLibrary : public AssetLibrary {
|
||||
public:
|
||||
OnDiskAssetLibrary(eAssetLibraryType library_type,
|
||||
StringRef name,
|
||||
StringRef root_path,
|
||||
bool is_read_only);
|
||||
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
std::optional<eAssetImportMethod> import_method() const override;
|
||||
void refresh_catalogs() override;
|
||||
|
||||
virtual bool is_enabled() const;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,78 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "BLI_assert.h"
|
||||
#include "BLI_listbase.h"
|
||||
|
||||
#include "DNA_userdef_types.h"
|
||||
|
||||
#include "common.hh"
|
||||
#include "preferences_on_disk_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
PreferencesOnDiskAssetLibrary::PreferencesOnDiskAssetLibrary(
|
||||
const bUserAssetLibrary &user_asset_library)
|
||||
: OnDiskAssetLibrary(ASSET_LIBRARY_CUSTOM,
|
||||
user_asset_library.name,
|
||||
user_asset_library.dirpath,
|
||||
/*is_read_only=*/false),
|
||||
user_library_(user_asset_library)
|
||||
{
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryReference> PreferencesOnDiskAssetLibrary::library_reference() const
|
||||
{
|
||||
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
|
||||
if (!library_definition) {
|
||||
return {};
|
||||
}
|
||||
const int index = BLI_findindex(&U.asset_libraries, library_definition);
|
||||
if (index == -1) {
|
||||
/* Should have been caught by the #user_asset_library() call above already. */
|
||||
BLI_assert_unreachable();
|
||||
return {};
|
||||
}
|
||||
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.type = ASSET_LIBRARY_CUSTOM;
|
||||
library_ref.custom_library_index = index;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
std::optional<eAssetImportMethod> PreferencesOnDiskAssetLibrary::import_method() const
|
||||
{
|
||||
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
|
||||
if (!library_definition) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return eAssetImportMethod(library_definition->import_method);
|
||||
}
|
||||
|
||||
bool PreferencesOnDiskAssetLibrary::use_relative_paths() const
|
||||
{
|
||||
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
|
||||
if (!library_definition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (library_definition->flag & ASSET_LIBRARY_RELATIVE_PATH) != 0;
|
||||
}
|
||||
|
||||
bool PreferencesOnDiskAssetLibrary::is_enabled() const
|
||||
{
|
||||
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
|
||||
if (!library_definition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (library_definition->flag & ASSET_LIBRARY_DISABLED) == 0;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,30 @@
|
||||
/* SPDX-FileCopyrightText: 2024 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common.hh"
|
||||
|
||||
#include "on_disk_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class PreferencesOnDiskAssetLibrary : public OnDiskAssetLibrary {
|
||||
/** Helper to get the #bUserAssetLibrary from the preferences (if still valid). */
|
||||
UserAssetLibraryWrapper user_library_;
|
||||
|
||||
public:
|
||||
explicit PreferencesOnDiskAssetLibrary(const bUserAssetLibrary &user_asset_library);
|
||||
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
std::optional<eAssetImportMethod> import_method() const override;
|
||||
bool use_relative_paths() const override;
|
||||
bool is_enabled() const override;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "common.hh"
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
/**
|
||||
* Abstract class for remote libraries. #PreferencesRemoteAssetLibrary and #OnlineEssentialsLibrary
|
||||
* derive from this.
|
||||
*/
|
||||
class RemoteAssetLibrary : public AssetLibrary {
|
||||
std::string remote_url_;
|
||||
|
||||
public:
|
||||
RemoteAssetLibrary(eAssetLibraryType library_type,
|
||||
bool is_read_only,
|
||||
StringRef remote_url,
|
||||
StringRef name,
|
||||
StringRef root_path);
|
||||
void force_remote_listing_download() const override;
|
||||
|
||||
std::optional<eAssetImportMethod> import_method() const override;
|
||||
std::optional<StringRefNull> remote_url() const override;
|
||||
void refresh_catalogs() override;
|
||||
};
|
||||
|
||||
class PreferencesRemoteAssetLibrary : public RemoteAssetLibrary {
|
||||
/** Helper to get the #bUserAssetLibrary from the preferences (if still valid). */
|
||||
UserAssetLibraryWrapper user_library_;
|
||||
|
||||
public:
|
||||
PreferencesRemoteAssetLibrary(const bUserAssetLibrary &custom_library);
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
bool is_enabled() const;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,32 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#include "runtime_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
RuntimeAssetLibrary::RuntimeAssetLibrary()
|
||||
: AssetLibrary(ASSET_LIBRARY_LOCAL, /*is_read_only=*/false)
|
||||
{
|
||||
this->on_blend_save_handler_register();
|
||||
}
|
||||
|
||||
std::optional<AssetLibraryReference> RuntimeAssetLibrary::library_reference() const
|
||||
{
|
||||
AssetLibraryReference library_ref{};
|
||||
library_ref.type = ASSET_LIBRARY_LOCAL;
|
||||
library_ref.custom_library_index = -1;
|
||||
return library_ref;
|
||||
}
|
||||
|
||||
std::optional<eAssetImportMethod> RuntimeAssetLibrary::import_method() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*
|
||||
* An asset library that is purely stored in-memory. Used for the "Current File" asset library
|
||||
* while the file has not been saved on disk yet.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "AS_asset_library.hh"
|
||||
|
||||
namespace blender::asset_system {
|
||||
|
||||
class RuntimeAssetLibrary : public AssetLibrary {
|
||||
public:
|
||||
RuntimeAssetLibrary();
|
||||
|
||||
std::optional<AssetLibraryReference> library_reference() const override;
|
||||
std::optional<eAssetImportMethod> import_method() const override;
|
||||
};
|
||||
|
||||
} // namespace blender::asset_system
|
||||
55
blender-5.2.0/source/blender/asset_system/intern/utils.cc
Normal file
55
blender-5.2.0/source/blender/asset_system/intern/utils.cc
Normal file
@@ -0,0 +1,55 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
/* For PATH_MAX (at least on Windows). */
|
||||
#include "BLI_fileops.h" // IWYU pragma: keep
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "MEM_guardedalloc.h"
|
||||
|
||||
#include "utils.hh"
|
||||
|
||||
namespace blender::asset_system::utils {
|
||||
|
||||
std::string normalize_directory_path(StringRef directory)
|
||||
{
|
||||
if (directory.is_empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
char dir_normalized[PATH_MAX];
|
||||
BLI_strncpy(dir_normalized,
|
||||
directory.data(),
|
||||
/* + 1 for null terminator. */
|
||||
std::min(directory.size() + 1, int64_t(sizeof(dir_normalized))));
|
||||
BLI_path_slash_native(dir_normalized);
|
||||
BLI_path_normalize_dir(dir_normalized, sizeof(dir_normalized));
|
||||
return std::string(dir_normalized);
|
||||
}
|
||||
|
||||
std::string normalize_path(StringRefNull path, int64_t max_len)
|
||||
{
|
||||
const int64_t len = (max_len == StringRef::not_found) ? path.size() :
|
||||
std::min(max_len, path.size());
|
||||
|
||||
char *buf = BLI_strdupn(path.c_str(), len);
|
||||
BLI_path_slash_native(buf);
|
||||
BLI_path_normalize(buf);
|
||||
|
||||
std::string normalized_path = buf;
|
||||
MEM_delete(buf);
|
||||
|
||||
if (len != path.size()) {
|
||||
normalized_path = normalized_path + path.substr(len);
|
||||
}
|
||||
|
||||
return normalized_path;
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::utils
|
||||
31
blender-5.2.0/source/blender/asset_system/intern/utils.hh
Normal file
31
blender-5.2.0/source/blender/asset_system/intern/utils.hh
Normal file
@@ -0,0 +1,31 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
/** \file
|
||||
* \ingroup asset_system
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "BLI_string_ref.hh"
|
||||
|
||||
namespace blender::asset_system::utils {
|
||||
|
||||
/**
|
||||
* Returns a normalized directory path with a trailing slash, and a maximum length of #PATH_MAX.
|
||||
* Slashes are converted to native format.
|
||||
*/
|
||||
std::string normalize_directory_path(StringRef directory);
|
||||
|
||||
/**
|
||||
* Normalize the given `path` (remove 'parent directory' and double-slashes element etc., and
|
||||
* convert to native path separators).
|
||||
*
|
||||
* If \a max_len is not #StringRef::not_found (default value), only the first part of the given
|
||||
* string up to the given length is processed, the rest remains unchanged. Needed to avoid
|
||||
* modifying ID name part of linked library paths.
|
||||
*/
|
||||
std::string normalize_path(StringRefNull path, int64_t max_len = StringRef::not_found);
|
||||
|
||||
} // namespace blender::asset_system::utils
|
||||
@@ -0,0 +1,273 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "AS_asset_catalog_path.hh"
|
||||
|
||||
#include "BLI_set.hh"
|
||||
#include "BLI_vector.hh"
|
||||
|
||||
#include "BKE_gtest_base.hh"
|
||||
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
class AssetCatalogPathTest : public bke::BlenderGTestBase {};
|
||||
|
||||
TEST_F(AssetCatalogPathTest, construction)
|
||||
{
|
||||
AssetCatalogPath default_constructed;
|
||||
/* Use `.str()` to use `std:string`'s comparison operators here, not our own (which are tested
|
||||
* later). */
|
||||
EXPECT_EQ(default_constructed.str(), "");
|
||||
|
||||
/* C++ considers this construction special, it doesn't call the default constructor but does
|
||||
* recursive, member-wise value initialization. See https://stackoverflow.com/a/4982720. */
|
||||
AssetCatalogPath value_initialized = AssetCatalogPath();
|
||||
EXPECT_EQ(value_initialized.str(), "");
|
||||
|
||||
AssetCatalogPath from_char_literal("the/path");
|
||||
|
||||
const std::string str_const = "the/path";
|
||||
AssetCatalogPath from_string_constant(str_const);
|
||||
|
||||
std::string str_variable = "the/path";
|
||||
AssetCatalogPath from_string_variable(str_variable);
|
||||
|
||||
std::string long_string = "this is a long/string/with/a/path in the middle";
|
||||
StringRef long_string_ref(long_string);
|
||||
StringRef middle_bit = long_string_ref.substr(10, 23);
|
||||
AssetCatalogPath from_string_ref(middle_bit);
|
||||
EXPECT_EQ(from_string_ref, "long/string/with/a/path");
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, length)
|
||||
{
|
||||
const AssetCatalogPath one("1");
|
||||
EXPECT_EQ(1, one.length());
|
||||
|
||||
const AssetCatalogPath empty("");
|
||||
EXPECT_EQ(0, empty.length());
|
||||
|
||||
const AssetCatalogPath utf8("some/родитель");
|
||||
EXPECT_EQ(21, utf8.length()) << "13 characters should be 21 bytes.";
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, name)
|
||||
{
|
||||
EXPECT_EQ(StringRefNull(""), AssetCatalogPath("").name());
|
||||
EXPECT_EQ(StringRefNull("word"), AssetCatalogPath("word").name());
|
||||
EXPECT_EQ(StringRefNull("Пермь"), AssetCatalogPath("дорога/в/Пермь").name());
|
||||
EXPECT_EQ(StringRefNull("windows\\paths"),
|
||||
AssetCatalogPath("these/are/not/windows\\paths").name());
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, comparison_operators)
|
||||
{
|
||||
const AssetCatalogPath empty("");
|
||||
const AssetCatalogPath the_path("the/path");
|
||||
const AssetCatalogPath the_path_child("the/path/child");
|
||||
const AssetCatalogPath unrelated_path("unrelated/path");
|
||||
const AssetCatalogPath other_instance_same_path("the/path");
|
||||
|
||||
EXPECT_LT(empty, the_path);
|
||||
EXPECT_LT(the_path, the_path_child);
|
||||
EXPECT_LT(the_path, unrelated_path);
|
||||
|
||||
EXPECT_EQ(empty, empty) << "Identical empty instances should compare equal.";
|
||||
EXPECT_EQ(empty, "") << "Comparison to empty string should be possible.";
|
||||
EXPECT_EQ(the_path, the_path) << "Identical non-empty instances should compare equal.";
|
||||
EXPECT_EQ(the_path, "the/path") << "Comparison to string should be possible.";
|
||||
EXPECT_EQ(the_path, other_instance_same_path)
|
||||
<< "Different instances with equal path should compare equal.";
|
||||
|
||||
EXPECT_NE(the_path, the_path_child);
|
||||
EXPECT_NE(the_path, unrelated_path);
|
||||
EXPECT_NE(the_path, empty);
|
||||
|
||||
EXPECT_FALSE(empty);
|
||||
EXPECT_TRUE(the_path);
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, move_semantics)
|
||||
{
|
||||
AssetCatalogPath source_path("source/path");
|
||||
EXPECT_TRUE(source_path);
|
||||
|
||||
AssetCatalogPath dest_path = std::move(source_path);
|
||||
EXPECT_FALSE(source_path); /* NOLINT: bugprone-use-after-move */
|
||||
EXPECT_TRUE(dest_path);
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, concatenation)
|
||||
{
|
||||
AssetCatalogPath some_parent("some/родитель");
|
||||
AssetCatalogPath child = some_parent / "ребенок";
|
||||
|
||||
EXPECT_EQ(some_parent, "some/родитель")
|
||||
<< "Appending a child path should not modify the parent.";
|
||||
EXPECT_EQ(child, "some/родитель/ребенок");
|
||||
|
||||
AssetCatalogPath appended_compound_path = some_parent / "ребенок/внук";
|
||||
EXPECT_EQ(appended_compound_path, "some/родитель/ребенок/внук");
|
||||
|
||||
AssetCatalogPath empty("");
|
||||
AssetCatalogPath child_of_the_void = empty / "child";
|
||||
EXPECT_EQ(child_of_the_void, "child")
|
||||
<< "Appending to an empty path should not create an initial slash.";
|
||||
|
||||
AssetCatalogPath parent_of_the_void = some_parent / empty;
|
||||
EXPECT_EQ(parent_of_the_void, "some/родитель")
|
||||
<< "Prepending to an empty path should not create a trailing slash.";
|
||||
|
||||
std::string subpath = "child";
|
||||
AssetCatalogPath concatenated_with_string = some_parent / subpath;
|
||||
EXPECT_EQ(concatenated_with_string, "some/родитель/child");
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, hashable)
|
||||
{
|
||||
AssetCatalogPath path("heyyyyy");
|
||||
|
||||
std::set<AssetCatalogPath> path_std_set;
|
||||
path_std_set.insert(path);
|
||||
|
||||
Set<AssetCatalogPath> path_blender_set;
|
||||
path_blender_set.add(path);
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, stream_operator)
|
||||
{
|
||||
AssetCatalogPath path("путь/в/Пермь");
|
||||
std::stringstream sstream;
|
||||
sstream << path;
|
||||
EXPECT_EQ("путь/в/Пермь", sstream.str());
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, is_contained_in)
|
||||
{
|
||||
const AssetCatalogPath catpath("simple/path/child");
|
||||
EXPECT_FALSE(catpath.is_contained_in("unrelated"));
|
||||
EXPECT_FALSE(catpath.is_contained_in("sim"));
|
||||
EXPECT_FALSE(catpath.is_contained_in("simple/pathx"));
|
||||
EXPECT_FALSE(catpath.is_contained_in("simple/path/c"));
|
||||
EXPECT_FALSE(catpath.is_contained_in("simple/path/child/grandchild"));
|
||||
EXPECT_FALSE(catpath.is_contained_in("simple/path/"))
|
||||
<< "Non-normalized paths are not expected to work.";
|
||||
|
||||
EXPECT_TRUE(catpath.is_contained_in(""));
|
||||
EXPECT_TRUE(catpath.is_contained_in("simple"));
|
||||
EXPECT_TRUE(catpath.is_contained_in("simple/path"));
|
||||
|
||||
/* Test with some UTF8 non-ASCII characters. */
|
||||
AssetCatalogPath some_parent("some/родитель");
|
||||
AssetCatalogPath child = some_parent / "ребенок";
|
||||
|
||||
EXPECT_TRUE(child.is_contained_in(some_parent));
|
||||
EXPECT_TRUE(child.is_contained_in("some"));
|
||||
|
||||
AssetCatalogPath appended_compound_path = some_parent / "ребенок/внук";
|
||||
EXPECT_TRUE(appended_compound_path.is_contained_in(some_parent));
|
||||
EXPECT_TRUE(appended_compound_path.is_contained_in(child));
|
||||
|
||||
/* Test "going up" directory-style. */
|
||||
AssetCatalogPath child_with_dotdot = some_parent / "../../other/hierarchy/part";
|
||||
EXPECT_TRUE(child_with_dotdot.is_contained_in(some_parent))
|
||||
<< "dotdot path components should have no meaning";
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, cleanup)
|
||||
{
|
||||
{
|
||||
AssetCatalogPath ugly_path("/ some / родитель / ");
|
||||
AssetCatalogPath clean_path = ugly_path.cleanup();
|
||||
EXPECT_EQ(AssetCatalogPath("/ some / родитель / "), ugly_path)
|
||||
<< "cleanup should not modify the path instance itself";
|
||||
EXPECT_EQ(AssetCatalogPath("some/родитель"), clean_path);
|
||||
}
|
||||
{
|
||||
AssetCatalogPath double_slashed("some//родитель");
|
||||
EXPECT_EQ(AssetCatalogPath("some/родитель"), double_slashed.cleanup());
|
||||
}
|
||||
{
|
||||
AssetCatalogPath with_colons("some/key:subkey=value/path");
|
||||
EXPECT_EQ(AssetCatalogPath("some/key-subkey=value/path"), with_colons.cleanup());
|
||||
}
|
||||
{
|
||||
const AssetCatalogPath with_backslashes("windows\\for\\life");
|
||||
EXPECT_EQ(AssetCatalogPath("windows/for/life"), with_backslashes.cleanup());
|
||||
}
|
||||
{
|
||||
const AssetCatalogPath with_mixed("windows\\for/life");
|
||||
EXPECT_EQ(AssetCatalogPath("windows/for/life"), with_mixed.cleanup());
|
||||
}
|
||||
{
|
||||
const AssetCatalogPath with_punctuation("is!/this?/¿valid?");
|
||||
EXPECT_EQ(AssetCatalogPath("is!/this?/¿valid?"), with_punctuation.cleanup());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, iterate_components)
|
||||
{
|
||||
AssetCatalogPath path("путь/в/Пермь");
|
||||
Vector<std::pair<std::string, bool>> seen_components;
|
||||
|
||||
path.iterate_components([&seen_components](StringRef component_name, bool is_last_component) {
|
||||
std::pair<std::string, bool> parameter_pair = std::make_pair<std::string, bool>(
|
||||
component_name, bool(is_last_component));
|
||||
seen_components.append(parameter_pair);
|
||||
});
|
||||
|
||||
ASSERT_EQ(3, seen_components.size());
|
||||
|
||||
EXPECT_EQ("путь", seen_components[0].first);
|
||||
EXPECT_EQ("в", seen_components[1].first);
|
||||
EXPECT_EQ("Пермь", seen_components[2].first);
|
||||
|
||||
EXPECT_FALSE(seen_components[0].second);
|
||||
EXPECT_FALSE(seen_components[1].second);
|
||||
EXPECT_TRUE(seen_components[2].second);
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, rebase)
|
||||
{
|
||||
AssetCatalogPath path("some/path/to/some/catalog");
|
||||
EXPECT_EQ(path.rebase("some/path", "new/base"), "new/base/to/some/catalog");
|
||||
EXPECT_EQ(path.rebase("", "new/base"), "new/base/some/path/to/some/catalog");
|
||||
|
||||
EXPECT_EQ(path.rebase("some/path/to/some/catalog", "some/path/to/some/catalog"),
|
||||
"some/path/to/some/catalog")
|
||||
<< "Rebasing to itself should not change the path.";
|
||||
|
||||
EXPECT_EQ(path.rebase("path/to", "new/base"), "")
|
||||
<< "Non-matching base path should return empty string to indicate 'NO'.";
|
||||
|
||||
/* Empty strings should be handled without crashing or other nasty side-effects. */
|
||||
AssetCatalogPath empty("");
|
||||
EXPECT_EQ(empty.rebase("path/to", "new/base"), "");
|
||||
EXPECT_EQ(empty.rebase("", "new/base"), "new/base");
|
||||
EXPECT_EQ(empty.rebase("", ""), "");
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogPathTest, parent)
|
||||
{
|
||||
const AssetCatalogPath ascii_path("path/with/missing/parents");
|
||||
EXPECT_EQ(ascii_path.parent(), "path/with/missing");
|
||||
|
||||
const AssetCatalogPath path("путь/в/Пермь/долог/и/далек");
|
||||
EXPECT_EQ(path.parent(), "путь/в/Пермь/долог/и");
|
||||
EXPECT_EQ(path.parent().parent(), "путь/в/Пермь/долог");
|
||||
EXPECT_EQ(path.parent().parent().parent(), "путь/в/Пермь");
|
||||
|
||||
const AssetCatalogPath one_level("one");
|
||||
EXPECT_EQ(one_level.parent(), "");
|
||||
|
||||
const AssetCatalogPath empty("");
|
||||
EXPECT_EQ(empty.parent(), "");
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,161 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include "asset_library_test_common.hh"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
class AssetCatalogTreeTest : public AssetLibraryTestBase, public AssetCatalogTreeTestFunctions {};
|
||||
|
||||
TEST_F(AssetCatalogTreeTest, insert_item_into_tree)
|
||||
{
|
||||
{
|
||||
AssetCatalogTree tree;
|
||||
std::unique_ptr<AssetCatalog> catalog_empty_path = AssetCatalog::from_path("");
|
||||
tree.insert_item(*catalog_empty_path);
|
||||
|
||||
expect_tree_items(tree, {});
|
||||
}
|
||||
|
||||
{
|
||||
AssetCatalogTree tree;
|
||||
|
||||
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("item");
|
||||
tree.insert_item(*catalog);
|
||||
expect_tree_items(tree, {"item"});
|
||||
|
||||
/* Insert child after parent already exists. */
|
||||
std::unique_ptr<AssetCatalog> child_catalog = AssetCatalog::from_path("item/child");
|
||||
tree.insert_item(*catalog);
|
||||
expect_tree_items(tree, {"item", "item/child"});
|
||||
|
||||
std::vector<AssetCatalogPath> expected_paths;
|
||||
|
||||
/* Test inserting multi-component sub-path. */
|
||||
std::unique_ptr<AssetCatalog> grandgrandchild_catalog = AssetCatalog::from_path(
|
||||
"item/child/grandchild/grandgrandchild");
|
||||
tree.insert_item(*catalog);
|
||||
expected_paths = {
|
||||
"item", "item/child", "item/child/grandchild", "item/child/grandchild/grandgrandchild"};
|
||||
expect_tree_items(tree, expected_paths);
|
||||
|
||||
std::unique_ptr<AssetCatalog> root_level_catalog = AssetCatalog::from_path("root level");
|
||||
tree.insert_item(*catalog);
|
||||
expected_paths = {"item",
|
||||
"item/child",
|
||||
"item/child/grandchild",
|
||||
"item/child/grandchild/grandgrandchild",
|
||||
"root level"};
|
||||
expect_tree_items(tree, expected_paths);
|
||||
}
|
||||
|
||||
{
|
||||
AssetCatalogTree tree;
|
||||
|
||||
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("item/child");
|
||||
tree.insert_item(*catalog);
|
||||
expect_tree_items(tree, {"item", "item/child"});
|
||||
}
|
||||
|
||||
{
|
||||
AssetCatalogTree tree;
|
||||
|
||||
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("white space");
|
||||
tree.insert_item(*catalog);
|
||||
expect_tree_items(tree, {"white space"});
|
||||
}
|
||||
|
||||
{
|
||||
AssetCatalogTree tree;
|
||||
|
||||
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("/item/white space");
|
||||
tree.insert_item(*catalog);
|
||||
expect_tree_items(tree, {"item", "item/white space"});
|
||||
}
|
||||
|
||||
{
|
||||
AssetCatalogTree tree;
|
||||
|
||||
std::unique_ptr<AssetCatalog> catalog_unicode_path = AssetCatalog::from_path("Ružena");
|
||||
tree.insert_item(*catalog_unicode_path);
|
||||
expect_tree_items(tree, {"Ružena"});
|
||||
|
||||
catalog_unicode_path = AssetCatalog::from_path("Ružena/Ružena");
|
||||
tree.insert_item(*catalog_unicode_path);
|
||||
expect_tree_items(tree, {"Ružena", "Ružena/Ružena"});
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogTreeTest, load_single_file_into_tree)
|
||||
{
|
||||
AssetCatalogService service(asset_library_root_);
|
||||
service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
|
||||
|
||||
/* Contains not only paths from the CDF but also the missing parents (implicitly defined
|
||||
* catalogs). */
|
||||
std::vector<AssetCatalogPath> expected_paths{
|
||||
"character",
|
||||
"character/Ellie",
|
||||
"character/Ellie/backslashes",
|
||||
"character/Ellie/poselib",
|
||||
"character/Ellie/poselib/tailslash",
|
||||
"character/Ellie/poselib/white space",
|
||||
"character/Ružena",
|
||||
"character/Ružena/poselib",
|
||||
"character/Ružena/poselib/face",
|
||||
"character/Ružena/poselib/hand",
|
||||
"path", /* Implicit. */
|
||||
"path/without", /* Implicit. */
|
||||
"path/without/simplename", /* From CDF. */
|
||||
};
|
||||
|
||||
const std::shared_ptr<const AssetCatalogTree> tree = service.catalog_tree();
|
||||
expect_tree_items(*tree, expected_paths);
|
||||
}
|
||||
|
||||
TEST_F(AssetCatalogTreeTest, foreach_in_tree)
|
||||
{
|
||||
{
|
||||
AssetCatalogTree tree{};
|
||||
const std::vector<AssetCatalogPath> no_catalogs{};
|
||||
|
||||
expect_tree_items(tree, no_catalogs);
|
||||
expect_tree_root_items(tree, no_catalogs);
|
||||
/* Need a root item to check child items. */
|
||||
std::unique_ptr<AssetCatalog> catalog = AssetCatalog::from_path("something");
|
||||
tree.insert_item(*catalog);
|
||||
tree.foreach_root_item([&no_catalogs](const AssetCatalogTreeItem &item) {
|
||||
expect_tree_item_child_items(item, no_catalogs);
|
||||
});
|
||||
}
|
||||
|
||||
AssetCatalogService service(asset_library_root_);
|
||||
service.load_from_disk(asset_library_root_ + SEP_STR + "blender_assets.cats.txt");
|
||||
|
||||
std::vector<AssetCatalogPath> expected_root_items{{"character", "path"}};
|
||||
const std::shared_ptr<const AssetCatalogTree> tree = service.catalog_tree();
|
||||
expect_tree_root_items(*tree, expected_root_items);
|
||||
|
||||
/* Test if the direct children of the root item are what's expected. */
|
||||
std::vector<std::vector<AssetCatalogPath>> expected_root_child_items = {
|
||||
/* Children of the "character" root item. */
|
||||
{"character/Ellie", "character/Ružena"},
|
||||
/* Children of the "path" root item. */
|
||||
{"path/without"},
|
||||
};
|
||||
int i = 0;
|
||||
tree->foreach_root_item([&expected_root_child_items, &i](const AssetCatalogTreeItem &item) {
|
||||
expect_tree_item_child_items(item, expected_root_child_items[i]);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
@@ -0,0 +1,380 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "asset_library_service.hh"
|
||||
|
||||
#include "BLI_fileops.h" /* For PATH_MAX (at least on Windows). */
|
||||
#include "BLI_path_utils.hh"
|
||||
#include "BLI_string.h"
|
||||
|
||||
#include "BKE_appdir.hh"
|
||||
#include "BKE_callbacks.hh"
|
||||
#include "BKE_gtest_base.hh"
|
||||
#include "BKE_main.hh"
|
||||
|
||||
#include "DNA_asset_types.h"
|
||||
|
||||
#include "CLG_log.h"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
const UUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
|
||||
|
||||
class AssetLibraryServiceTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
CatalogFilePath asset_library_root_;
|
||||
CatalogFilePath temp_library_path_;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
|
||||
if (test_files_dir.empty()) {
|
||||
FAIL();
|
||||
}
|
||||
asset_library_root_ = test_files_dir + SEP_STR + "asset_library";
|
||||
temp_library_path_ = "";
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
AssetLibraryService::destroy();
|
||||
|
||||
if (!temp_library_path_.empty()) {
|
||||
BLI_delete(temp_library_path_.c_str(), true, true);
|
||||
temp_library_path_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
/* Register a temporary path, which will be removed at the end of the test.
|
||||
* The returned path ends in a slash. */
|
||||
CatalogFilePath use_temp_path()
|
||||
{
|
||||
BKE_tempdir_init(nullptr);
|
||||
const CatalogFilePath tempdir = BKE_tempdir_session();
|
||||
temp_library_path_ = tempdir + "test-temporary-path" + SEP_STR;
|
||||
return temp_library_path_;
|
||||
}
|
||||
|
||||
CatalogFilePath create_temp_path()
|
||||
{
|
||||
CatalogFilePath path = use_temp_path();
|
||||
BLI_dir_create_recursive(path.c_str());
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, get_destroy)
|
||||
{
|
||||
AssetLibraryService *const service = AssetLibraryService::get();
|
||||
EXPECT_EQ(service, AssetLibraryService::get())
|
||||
<< "Calling twice without destroying in between should return the same instance.";
|
||||
|
||||
/* This should not crash. */
|
||||
AssetLibraryService::destroy();
|
||||
AssetLibraryService::destroy();
|
||||
|
||||
/* NOTE: there used to be a test for the opposite here, that after a call to
|
||||
* AssetLibraryService::destroy() the above calls should return freshly allocated objects. This
|
||||
* cannot be reliably tested by just pointer comparison, though. */
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, library_pointers)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
|
||||
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__,
|
||||
asset_library_root_);
|
||||
AssetLibrary *const curfile_lib = service->get_asset_library_current_file();
|
||||
|
||||
EXPECT_EQ(lib, service->get_asset_library_on_disk_custom(__func__, asset_library_root_))
|
||||
<< "Calling twice without destroying in between should return the same instance.";
|
||||
EXPECT_EQ(curfile_lib, service->get_asset_library_current_file())
|
||||
<< "Calling twice without destroying in between should return the same instance.";
|
||||
|
||||
/* NOTE: there used to be a test for the opposite here, that after a call to
|
||||
* AssetLibraryService::destroy() the above calls should return freshly allocated objects. This
|
||||
* cannot be reliably tested by just pointer comparison, though. */
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, library_from_reference)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
|
||||
AssetLibrary *const curfile_lib = service->get_asset_library_current_file();
|
||||
|
||||
AssetLibraryReference ref{};
|
||||
ref.type = ASSET_LIBRARY_LOCAL;
|
||||
EXPECT_EQ(curfile_lib, service->get_asset_library(nullptr, ref))
|
||||
<< "Getting the local (current file) reference without a main saved on disk should return "
|
||||
"the current file library";
|
||||
|
||||
{
|
||||
Main dummy_main{};
|
||||
std::string dummy_filepath = asset_library_root_ + SEP + "dummy.blend";
|
||||
STRNCPY(dummy_main.filepath, dummy_filepath.c_str());
|
||||
|
||||
AssetLibrary *custom_lib = service->get_asset_library_on_disk_custom(__func__,
|
||||
asset_library_root_);
|
||||
AssetLibrary *tmp_curfile_lib = service->get_asset_library(&dummy_main, ref);
|
||||
|
||||
/* Requested a current file library with a (fake) file saved in the same directory as a custom
|
||||
* asset library. The resulting library should never match the custom asset library, even
|
||||
* though the paths match. */
|
||||
|
||||
EXPECT_NE(custom_lib, tmp_curfile_lib)
|
||||
<< "Getting an asset library from a local (current file) library reference should never "
|
||||
"match any custom asset library";
|
||||
EXPECT_EQ(custom_lib->root_path(), tmp_curfile_lib->root_path());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, library_path_trailing_slashes)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
|
||||
char asset_lib_no_slash[PATH_MAX];
|
||||
char asset_lib_with_slash[PATH_MAX];
|
||||
STRNCPY(asset_lib_no_slash, asset_library_root_.c_str());
|
||||
STRNCPY(asset_lib_with_slash, asset_library_root_.c_str());
|
||||
|
||||
/* Ensure #asset_lib_no_slash has no trailing slash, regardless of what was passed on the CLI to
|
||||
* the unit test. */
|
||||
while (strlen(asset_lib_no_slash) &&
|
||||
ELEM(asset_lib_no_slash[strlen(asset_lib_no_slash) - 1], SEP, ALTSEP))
|
||||
{
|
||||
asset_lib_no_slash[strlen(asset_lib_no_slash) - 1] = '\0';
|
||||
}
|
||||
|
||||
BLI_path_slash_ensure(asset_lib_with_slash, PATH_MAX);
|
||||
|
||||
AssetLibrary *const lib_no_slash = service->get_asset_library_on_disk_custom(__func__,
|
||||
asset_lib_no_slash);
|
||||
|
||||
EXPECT_EQ(lib_no_slash,
|
||||
service->get_asset_library_on_disk_custom(__func__, asset_lib_with_slash))
|
||||
<< "With or without trailing slash shouldn't matter.";
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, catalogs_loaded)
|
||||
{
|
||||
AssetLibraryService *const service = AssetLibraryService::get();
|
||||
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__,
|
||||
asset_library_root_);
|
||||
AssetCatalogService &cat_service = lib->catalog_service();
|
||||
|
||||
const UUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
|
||||
EXPECT_NE(nullptr, cat_service.find_catalog(UUID_POSES_ELLIE))
|
||||
<< "Catalogs should be loaded after getting an asset library from disk.";
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, has_any_unsaved_catalogs)
|
||||
{
|
||||
AssetLibraryService *const service = AssetLibraryService::get();
|
||||
EXPECT_FALSE(service->has_any_unsaved_catalogs())
|
||||
<< "Empty AssetLibraryService should have no unsaved catalogs";
|
||||
|
||||
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__,
|
||||
asset_library_root_);
|
||||
AssetCatalogService &cat_service = lib->catalog_service();
|
||||
EXPECT_FALSE(service->has_any_unsaved_catalogs())
|
||||
<< "Unchanged AssetLibrary should have no unsaved catalogs";
|
||||
|
||||
const UUID UUID_POSES_ELLIE("df60e1f6-2259-475b-93d9-69a1b4a8db78");
|
||||
cat_service.prune_catalogs_by_id(UUID_POSES_ELLIE);
|
||||
EXPECT_FALSE(service->has_any_unsaved_catalogs())
|
||||
<< "Deletion of catalogs via AssetCatalogService should not automatically tag as 'unsaved "
|
||||
"changes'.";
|
||||
|
||||
const UUID UUID_POSES_RUZENA("79a4f887-ab60-4bd4-94da-d572e27d6aed");
|
||||
AssetCatalog *cat = cat_service.find_catalog(UUID_POSES_RUZENA);
|
||||
ASSERT_NE(nullptr, cat) << "Catalog " << UUID_POSES_RUZENA << " should be known";
|
||||
|
||||
cat_service.tag_has_unsaved_changes(cat);
|
||||
EXPECT_TRUE(service->has_any_unsaved_catalogs())
|
||||
<< "Tagging as having unsaved changes of a single catalog service should result in unsaved "
|
||||
"changes being reported.";
|
||||
EXPECT_TRUE(cat->flags.has_unsaved_changes);
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryServiceTest, has_any_unsaved_catalogs_after_write)
|
||||
{
|
||||
const CatalogFilePath writable_dir = create_temp_path(); /* Has trailing slash. */
|
||||
const CatalogFilePath original_cdf_file = asset_library_root_ + SEP_STR +
|
||||
"blender_assets.cats.txt";
|
||||
CatalogFilePath writable_cdf_file = writable_dir + AssetCatalogService::DEFAULT_CATALOG_FILENAME;
|
||||
BLI_path_slash_native(writable_cdf_file.data());
|
||||
ASSERT_EQ(0, BLI_copy(original_cdf_file.c_str(), writable_cdf_file.c_str()));
|
||||
|
||||
AssetLibraryService *const service = AssetLibraryService::get();
|
||||
AssetLibrary *const lib = service->get_asset_library_on_disk_custom(__func__, writable_dir);
|
||||
|
||||
EXPECT_FALSE(service->has_any_unsaved_catalogs())
|
||||
<< "Unchanged AssetLibrary should have no unsaved catalogs";
|
||||
|
||||
AssetCatalogService &cat_service = lib->catalog_service();
|
||||
AssetCatalog *cat = cat_service.find_catalog(UUID_POSES_ELLIE);
|
||||
|
||||
cat_service.tag_has_unsaved_changes(cat);
|
||||
|
||||
EXPECT_TRUE(service->has_any_unsaved_catalogs())
|
||||
<< "Tagging as having unsaved changes of a single catalog service should result in unsaved "
|
||||
"changes being reported.";
|
||||
EXPECT_TRUE(cat->flags.has_unsaved_changes);
|
||||
|
||||
cat_service.write_to_disk(writable_dir + "dummy_path.blend");
|
||||
EXPECT_FALSE(service->has_any_unsaved_catalogs())
|
||||
<< "Written AssetCatalogService should have no unsaved catalogs";
|
||||
EXPECT_FALSE(cat->flags.has_unsaved_changes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Call #AssetLibraryService::move_runtime_current_file_into_on_disk_library() with an on disk
|
||||
* location that contains no existing asset catalog definition file.
|
||||
*/
|
||||
TEST_F(AssetLibraryServiceTest, move_runtime_current_file_into_on_disk_library__empty_directory)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
|
||||
AssetLibrary *runtime_lib = service->get_asset_library_current_file();
|
||||
AssetCatalogService &runtime_catservice = runtime_lib->catalog_service();
|
||||
|
||||
/* Catalog created in the runtime lib that should be moved to the on-disk lib. */
|
||||
AssetCatalog *catalog = runtime_catservice.create_catalog("Some/Catalog/Path");
|
||||
runtime_catservice.undo_push();
|
||||
|
||||
{
|
||||
EXPECT_TRUE(catalog->flags.has_unsaved_changes);
|
||||
|
||||
EXPECT_EQ(nullptr, runtime_catservice.find_catalog(UUID_POSES_ELLIE))
|
||||
<< "Catalog not expected in the runtime asset library.";
|
||||
}
|
||||
|
||||
{
|
||||
Main dummy_main{};
|
||||
std::string dummy_filepath = create_temp_path() + "dummy.blend";
|
||||
STRNCPY(dummy_main.filepath, dummy_filepath.c_str());
|
||||
|
||||
AssetLibraryService::move_runtime_current_file_into_on_disk_library(dummy_main);
|
||||
|
||||
AssetLibraryReference ref{};
|
||||
ref.type = ASSET_LIBRARY_LOCAL;
|
||||
|
||||
/* Loads and merges the catalogs from disk. */
|
||||
AssetLibrary *on_disk_lib = service->get_asset_library(&dummy_main, ref);
|
||||
AssetCatalogService &on_disk_catservice = on_disk_lib->catalog_service();
|
||||
|
||||
/* Can only test the pointer equality here because the implementation keeps the runtime library
|
||||
* alive until all its contents are moved to the on-disk library. Otherwise the allocator might
|
||||
* choose the same address for the new on-disk library. Useful for testing, though not
|
||||
* required. */
|
||||
EXPECT_NE(on_disk_lib, runtime_lib);
|
||||
EXPECT_EQ(on_disk_lib->root_path(), temp_library_path_);
|
||||
|
||||
/* Check if catalog was moved correctly. */
|
||||
{
|
||||
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path);
|
||||
/* Compare catalog by pointer. #move_runtime_current_file_into_on_disk_library() doesn't
|
||||
* guarantee publicly that catalog pointers remain unchanged, but practically code might rely
|
||||
* on it. Good to know if this breaks. */
|
||||
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id), catalog);
|
||||
/* No writing happened, just merging. */
|
||||
EXPECT_TRUE(on_disk_catservice.find_catalog(catalog->catalog_id)->flags.has_unsaved_changes);
|
||||
}
|
||||
|
||||
EXPECT_EQ(nullptr, runtime_catservice.find_catalog(UUID_POSES_ELLIE))
|
||||
<< "Catalog not expected in the on disk asset library.";
|
||||
|
||||
/* Check if undo stack was moved correctly. */
|
||||
{
|
||||
on_disk_catservice.undo();
|
||||
const AssetCatalog *ellie_catalog = on_disk_catservice.find_catalog(UUID_POSES_ELLIE);
|
||||
EXPECT_EQ(nullptr, ellie_catalog) << "This catalog should not be present after undo";
|
||||
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path)
|
||||
<< "This catalog should still be present after undo";
|
||||
}
|
||||
|
||||
/* Force a new current file runtime library to be created. */
|
||||
EXPECT_NE(service->get_asset_library_current_file(), on_disk_lib);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call #AssetLibraryService::move_runtime_current_file_into_on_disk_library() with an on disk
|
||||
* location that contains an existing asset catalog definition file.
|
||||
* Result should be merged libraries.
|
||||
*/
|
||||
TEST_F(AssetLibraryServiceTest,
|
||||
move_runtime_current_file_into_on_disk_library__directory_with_catalogs)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
|
||||
AssetLibrary *runtime_lib = service->get_asset_library_current_file();
|
||||
AssetCatalogService &runtime_catservice = runtime_lib->catalog_service();
|
||||
|
||||
/* Catalog created in the runtime lib that should be moved to the on-disk lib. */
|
||||
AssetCatalog *catalog = runtime_catservice.create_catalog("Some/Catalog/Path");
|
||||
runtime_catservice.undo_push();
|
||||
|
||||
{
|
||||
EXPECT_TRUE(catalog->flags.has_unsaved_changes);
|
||||
|
||||
EXPECT_EQ(nullptr, runtime_catservice.find_catalog(UUID_POSES_ELLIE))
|
||||
<< "Catalog not expected in the runtime asset library.";
|
||||
}
|
||||
|
||||
{
|
||||
Main dummy_main{};
|
||||
std::string dummy_filepath = asset_library_root_ + SEP + "dummy.blend";
|
||||
STRNCPY(dummy_main.filepath, dummy_filepath.c_str());
|
||||
|
||||
AssetLibraryService::move_runtime_current_file_into_on_disk_library(dummy_main);
|
||||
|
||||
AssetLibraryReference ref{};
|
||||
ref.type = ASSET_LIBRARY_LOCAL;
|
||||
|
||||
/* Loads and merges the catalogs from disk. */
|
||||
AssetLibrary *on_disk_lib = service->get_asset_library(&dummy_main, ref);
|
||||
AssetCatalogService &on_disk_catservice = on_disk_lib->catalog_service();
|
||||
|
||||
EXPECT_NE(on_disk_lib, runtime_lib);
|
||||
EXPECT_EQ(BLI_path_cmp_normalized(on_disk_lib->root_path().c_str(),
|
||||
(asset_library_root_ + SEP).c_str()),
|
||||
0);
|
||||
|
||||
/* Check if catalog was moved correctly. */
|
||||
{
|
||||
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path);
|
||||
/* Compare catalog by pointer. #move_runtime_current_file_into_on_disk_library() doesn't
|
||||
* guarantee publicly that catalog pointers remain unchanged, but practically code might rely
|
||||
* on it. Good to know if this breaks. */
|
||||
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id), catalog);
|
||||
/* No writing happened, just merging. */
|
||||
EXPECT_TRUE(on_disk_catservice.find_catalog(catalog->catalog_id)->flags.has_unsaved_changes);
|
||||
}
|
||||
|
||||
/* Check if catalogs have been merged in from disk correctly (by #get_asset_library()). */
|
||||
{
|
||||
const AssetCatalog *ellie_catalog = on_disk_catservice.find_catalog(UUID_POSES_ELLIE);
|
||||
EXPECT_NE(nullptr, ellie_catalog)
|
||||
<< "Catalogs should be loaded after getting an asset library from disk.";
|
||||
EXPECT_FALSE(ellie_catalog->flags.has_unsaved_changes);
|
||||
}
|
||||
|
||||
/* Check if undo stack was moved correctly. */
|
||||
{
|
||||
on_disk_catservice.undo();
|
||||
const AssetCatalog *ellie_catalog = on_disk_catservice.find_catalog(UUID_POSES_ELLIE);
|
||||
EXPECT_EQ(nullptr, ellie_catalog) << "This catalog should not be present after undo";
|
||||
EXPECT_EQ(on_disk_catservice.find_catalog(catalog->catalog_id)->path, catalog->path)
|
||||
<< "This catalog should still be present after undo";
|
||||
}
|
||||
|
||||
/* Force a new current file runtime library to be created. */
|
||||
EXPECT_NE(service->get_asset_library_current_file(), on_disk_lib);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
@@ -0,0 +1,67 @@
|
||||
/* SPDX-FileCopyrightText: 2020 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_library.hh"
|
||||
|
||||
#include "BKE_gtest_base.hh"
|
||||
|
||||
#include "asset_library_service.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
class AssetLibraryTest : public bke::BlenderGTestBase {
|
||||
public:
|
||||
void TearDown() override
|
||||
{
|
||||
asset_system::AssetLibraryService::destroy();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AssetLibraryTest, AS_asset_library_load_from_directory)
|
||||
{
|
||||
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
|
||||
if (test_files_dir.empty()) {
|
||||
FAIL();
|
||||
}
|
||||
|
||||
/* Load the asset library. */
|
||||
const std::string library_dirpath = test_files_dir + "/" + "asset_library";
|
||||
AssetLibrary *library = AS_asset_library_load_from_directory(__func__, library_dirpath.data());
|
||||
ASSERT_NE(nullptr, library);
|
||||
|
||||
/* Check that it can be cast to the C++ type and has a Catalog Service. */
|
||||
const AssetCatalogService &service = library->catalog_service();
|
||||
|
||||
/* Check that the catalogs defined in the library are actually loaded. This just tests one single
|
||||
* catalog, as that indicates the file has been loaded. Testing that loading went OK is for
|
||||
* the asset catalog service tests. */
|
||||
const UUID uuid_poses_ellie("df60e1f6-2259-475b-93d9-69a1b4a8db78");
|
||||
AssetCatalog *poses_ellie = service.find_catalog(uuid_poses_ellie);
|
||||
ASSERT_NE(nullptr, poses_ellie) << "unable to find POSES_ELLIE catalog";
|
||||
EXPECT_EQ("character/Ellie/poselib", poses_ellie->path.str());
|
||||
}
|
||||
|
||||
TEST_F(AssetLibraryTest, load_nonexistent_directory)
|
||||
{
|
||||
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
|
||||
if (test_files_dir.empty()) {
|
||||
FAIL();
|
||||
}
|
||||
|
||||
/* Load the asset library. */
|
||||
const std::string library_dirpath = test_files_dir + "/" +
|
||||
"asset_library/this/subdir/does/not/exist";
|
||||
AssetLibrary *library = AS_asset_library_load_from_directory(__func__, library_dirpath.data());
|
||||
ASSERT_NE(nullptr, library);
|
||||
|
||||
/* Check that it can be cast to the C++ type and has a Catalog Service. */
|
||||
AssetCatalogService &service = library->catalog_service();
|
||||
/* Check that the catalog service doesn't have any catalogs. */
|
||||
EXPECT_TRUE(service.is_empty());
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
@@ -0,0 +1,166 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: Apache-2.0 */
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "AS_asset_catalog.hh"
|
||||
#include "AS_asset_catalog_tree.hh"
|
||||
|
||||
#include "asset_library_service.hh"
|
||||
|
||||
#include "BKE_appdir.hh"
|
||||
#include "BKE_gtest_base.hh"
|
||||
|
||||
#include "BLI_fileops.h"
|
||||
#include "BLI_path_utils.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender {
|
||||
|
||||
namespace asset_system {
|
||||
class AssetCatalogTree;
|
||||
class AssetCatalogTreeItem;
|
||||
class AssetCatalogPath;
|
||||
} // namespace asset_system
|
||||
|
||||
namespace asset_system::tests {
|
||||
|
||||
/**
|
||||
* Functionality to setup and access directories on disk within which asset library related testing
|
||||
* can be done.
|
||||
*/
|
||||
class AssetLibraryTestBase : public bke::BlenderGTestBase {
|
||||
protected:
|
||||
std::string asset_library_root_;
|
||||
std::string temp_library_path_;
|
||||
|
||||
void SetUp() override
|
||||
{
|
||||
const std::string test_files_dir = blender::tests::flags_test_asset_dir();
|
||||
if (test_files_dir.empty()) {
|
||||
FAIL();
|
||||
}
|
||||
|
||||
asset_library_root_ = test_files_dir + SEP_STR + "asset_library";
|
||||
temp_library_path_ = "";
|
||||
}
|
||||
|
||||
void TearDown() override
|
||||
{
|
||||
AssetLibraryService::destroy();
|
||||
|
||||
if (!temp_library_path_.empty()) {
|
||||
BLI_delete(temp_library_path_.c_str(), true, true);
|
||||
temp_library_path_ = "";
|
||||
}
|
||||
}
|
||||
|
||||
/* Register a temporary path, which will be removed at the end of the test.
|
||||
* The returned path ends in a slash. */
|
||||
std::string use_temp_path()
|
||||
{
|
||||
BKE_tempdir_init(nullptr);
|
||||
const std::string tempdir = BKE_tempdir_session();
|
||||
temp_library_path_ = tempdir + "test-temporary-path" + SEP_STR;
|
||||
return temp_library_path_;
|
||||
}
|
||||
|
||||
std::string create_temp_path()
|
||||
{
|
||||
std::string path = use_temp_path();
|
||||
BLI_dir_create_recursive(path.c_str());
|
||||
return path;
|
||||
}
|
||||
};
|
||||
|
||||
class AssetCatalogTreeTestFunctions {
|
||||
public:
|
||||
/**
|
||||
* Recursively iterate over all tree items using #AssetCatalogTree::foreach_item() and check if
|
||||
* the items map exactly to \a expected_paths.
|
||||
*/
|
||||
static void expect_tree_items(const AssetCatalogTree &tree,
|
||||
const std::vector<AssetCatalogPath> &expected_paths);
|
||||
|
||||
/**
|
||||
* Iterate over the root items of \a tree and check if the items map exactly to \a
|
||||
* expected_paths. Similar to #assert_expected_tree_items() but calls
|
||||
* #AssetCatalogTree::foreach_root_item() instead of #AssetCatalogTree::foreach_item().
|
||||
*/
|
||||
static void expect_tree_root_items(const AssetCatalogTree &tree,
|
||||
const std::vector<AssetCatalogPath> &expected_paths);
|
||||
|
||||
/**
|
||||
* Iterate over the child items of \a parent_item and check if the items map exactly to \a
|
||||
* expected_paths. Similar to #assert_expected_tree_items() but calls
|
||||
* #AssetCatalogTreeItem::foreach_child() instead of #AssetCatalogTree::foreach_item().
|
||||
*/
|
||||
static void expect_tree_item_child_items(const AssetCatalogTreeItem &parent_item,
|
||||
const std::vector<AssetCatalogPath> &expected_paths);
|
||||
};
|
||||
|
||||
static inline void compare_item_with_path(const AssetCatalogPath &expected_path,
|
||||
const AssetCatalogTreeItem &actual_item)
|
||||
{
|
||||
if (expected_path != actual_item.catalog_path().str()) {
|
||||
/* This will fail, but with a nicer error message than just calling FAIL(). */
|
||||
EXPECT_EQ(expected_path, actual_item.catalog_path());
|
||||
return;
|
||||
}
|
||||
|
||||
/* Is the catalog name as expected? "character", "Ellie", ... */
|
||||
EXPECT_EQ(expected_path.name(), actual_item.get_name());
|
||||
|
||||
/* Does the computed number of parents match? */
|
||||
const std::string expected_path_str = expected_path.str();
|
||||
const size_t expected_parent_count = std::count(
|
||||
expected_path_str.begin(), expected_path_str.end(), AssetCatalogPath::SEPARATOR);
|
||||
EXPECT_EQ(expected_parent_count, actual_item.count_parents());
|
||||
}
|
||||
|
||||
inline void AssetCatalogTreeTestFunctions::expect_tree_items(
|
||||
const AssetCatalogTree &tree, const std::vector<AssetCatalogPath> &expected_paths)
|
||||
{
|
||||
int i = 0;
|
||||
tree.foreach_item([&](const AssetCatalogTreeItem &actual_item) {
|
||||
ASSERT_LT(i, expected_paths.size())
|
||||
<< "More catalogs in tree than expected; did not expect " << actual_item.catalog_path();
|
||||
compare_item_with_path(expected_paths[i], actual_item);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
inline void AssetCatalogTreeTestFunctions::expect_tree_root_items(
|
||||
const AssetCatalogTree &tree, const std::vector<AssetCatalogPath> &expected_paths)
|
||||
{
|
||||
int i = 0;
|
||||
tree.foreach_root_item([&](const AssetCatalogTreeItem &actual_item) {
|
||||
ASSERT_LT(i, expected_paths.size())
|
||||
<< "More catalogs in tree root than expected; did not expect "
|
||||
<< actual_item.catalog_path();
|
||||
compare_item_with_path(expected_paths[i], actual_item);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
inline void AssetCatalogTreeTestFunctions::expect_tree_item_child_items(
|
||||
const AssetCatalogTreeItem &parent_item, const std::vector<AssetCatalogPath> &expected_paths)
|
||||
{
|
||||
int i = 0;
|
||||
parent_item.foreach_child([&](const AssetCatalogTreeItem &actual_item) {
|
||||
ASSERT_LT(i, expected_paths.size())
|
||||
<< "More catalogs in tree item than expected; did not expect "
|
||||
<< actual_item.catalog_path();
|
||||
compare_item_with_path(expected_paths[i], actual_item);
|
||||
i++;
|
||||
});
|
||||
}
|
||||
|
||||
} // namespace asset_system::tests
|
||||
|
||||
} // namespace blender
|
||||
@@ -0,0 +1,360 @@
|
||||
/* SPDX-FileCopyrightText: 2023 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "asset_library_service.hh"
|
||||
#include "asset_library_test_common.hh"
|
||||
|
||||
#include "AS_asset_representation.hh"
|
||||
|
||||
#include "BKE_lib_id.hh"
|
||||
#include "BKE_main.hh"
|
||||
|
||||
#if defined(WIN32)
|
||||
# include "BLI_string.h"
|
||||
#endif
|
||||
|
||||
#include "DNA_asset_types.h"
|
||||
#include "DNA_object_types.h"
|
||||
|
||||
#include "ED_asset_mark_clear.hh"
|
||||
|
||||
#include "../intern/utils.hh"
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
/**
|
||||
* Sets up asset library loading so we have a library to load asset representations into (required
|
||||
* for some functionality to perform work).
|
||||
*/
|
||||
class AssetRepresentationTest : public AssetLibraryTestBase {
|
||||
public:
|
||||
AssetLibrary *get_builtin_library_from_type(eAssetLibraryType type)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
|
||||
AssetLibraryReference ref{};
|
||||
ref.type = type;
|
||||
return service->get_asset_library(nullptr, ref);
|
||||
}
|
||||
|
||||
AssetRepresentation &add_dummy_asset(AssetLibrary &library, StringRef relative_path)
|
||||
{
|
||||
std::unique_ptr<AssetMetaData> dummy_metadata = std::make_unique<AssetMetaData>();
|
||||
return *library
|
||||
.add_external_on_disk_asset(
|
||||
relative_path, "Some asset name", 0, std::move(dummy_metadata))
|
||||
.lock();
|
||||
}
|
||||
|
||||
AssetRepresentation &add_dummy_id_asset(AssetLibrary &library, ID &id)
|
||||
{
|
||||
/* Ensure ID is marked as asset (no-op if already marked). */
|
||||
ed::asset::mark_id(&id);
|
||||
|
||||
return *library.add_local_id_asset(id).lock();
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AssetRepresentationTest, library_relative_identifier__id_name_change)
|
||||
{
|
||||
Main *bmain = BKE_main_new();
|
||||
Object *object = BKE_id_new<Object>(bmain, "Before rename");
|
||||
|
||||
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
|
||||
|
||||
AssetRepresentation &asset = add_dummy_id_asset(*library, object->id);
|
||||
|
||||
EXPECT_EQ(asset.library_relative_identifier(), "Object" SEP_STR "Before rename");
|
||||
|
||||
BKE_id_rename(*bmain, object->id, "Renamed!");
|
||||
EXPECT_EQ(asset.library_relative_identifier(), "Object" SEP_STR "Renamed!");
|
||||
|
||||
BKE_id_rename(*bmain, object->id, "Name/With\\Slashes/");
|
||||
EXPECT_EQ(asset.library_relative_identifier(), "Object" SEP_STR "Name/With\\Slashes/");
|
||||
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
|
||||
TEST_F(AssetRepresentationTest, weak_reference__current_file)
|
||||
{
|
||||
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
{
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
|
||||
EXPECT_EQ(weak_ref.asset_library_identifier, nullptr);
|
||||
EXPECT_STREQ(weak_ref.relative_asset_identifier, "path/to/an/asset");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AssetRepresentationTest, weak_reference__custom_library)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
{
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_CUSTOM);
|
||||
EXPECT_STREQ(weak_ref.asset_library_identifier, "My custom lib");
|
||||
EXPECT_STREQ(weak_ref.relative_asset_identifier, "path/to/an/asset");
|
||||
}
|
||||
}
|
||||
|
||||
/* Test if new weak references the ID name changes. */
|
||||
TEST_F(AssetRepresentationTest, weak_reference__id_name_change)
|
||||
{
|
||||
Main *bmain = BKE_main_new();
|
||||
Object *object = BKE_id_new<Object>(bmain, "Before rename");
|
||||
|
||||
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
|
||||
|
||||
AssetRepresentation &asset = add_dummy_id_asset(*library, object->id);
|
||||
|
||||
{
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
|
||||
EXPECT_STREQ(weak_ref.asset_library_identifier, nullptr);
|
||||
EXPECT_STREQ(weak_ref.relative_asset_identifier, "Object" SEP_STR "Before rename");
|
||||
}
|
||||
|
||||
BKE_id_rename(*bmain, object->id, "Renamed!");
|
||||
{
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
|
||||
EXPECT_STREQ(weak_ref.asset_library_identifier, nullptr);
|
||||
EXPECT_STREQ(weak_ref.relative_asset_identifier, "Object" SEP_STR "Renamed!");
|
||||
}
|
||||
|
||||
BKE_id_rename(*bmain, object->id, "Name/With\\Slashes/");
|
||||
{
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
EXPECT_EQ(weak_ref.asset_library_type, ASSET_LIBRARY_LOCAL);
|
||||
EXPECT_STREQ(weak_ref.asset_library_identifier, nullptr);
|
||||
EXPECT_STREQ(weak_ref.relative_asset_identifier, "Object" SEP_STR "Name/With\\Slashes/");
|
||||
}
|
||||
|
||||
BKE_main_free(bmain);
|
||||
}
|
||||
|
||||
TEST_F(AssetRepresentationTest, weak_reference__compare)
|
||||
{
|
||||
{
|
||||
AssetWeakReference a;
|
||||
AssetWeakReference b;
|
||||
EXPECT_EQ(a, b);
|
||||
|
||||
/* Arbitrary individual member changes to test how it affects the comparison. */
|
||||
b.asset_library_identifier = "My lib";
|
||||
/* Asset library identifier should be ignored unless the type is #ASSET_LIBRARY_CUSTOM. */
|
||||
EXPECT_EQ(a, b);
|
||||
a.asset_library_identifier = "My lib";
|
||||
EXPECT_EQ(a, b);
|
||||
a.asset_library_type = ASSET_LIBRARY_ESSENTIALS;
|
||||
EXPECT_NE(a, b);
|
||||
b.asset_library_type = ASSET_LIBRARY_LOCAL;
|
||||
EXPECT_NE(a, b);
|
||||
b.asset_library_type = ASSET_LIBRARY_ESSENTIALS;
|
||||
EXPECT_EQ(a, b);
|
||||
a.relative_asset_identifier = "Foo";
|
||||
EXPECT_NE(a, b);
|
||||
b.relative_asset_identifier = "Bar";
|
||||
EXPECT_NE(a, b);
|
||||
a.relative_asset_identifier = "Bar";
|
||||
EXPECT_EQ(a, b);
|
||||
|
||||
/* Make the destructor work. */
|
||||
a.asset_library_identifier = b.asset_library_identifier = nullptr;
|
||||
a.relative_asset_identifier = b.relative_asset_identifier = nullptr;
|
||||
}
|
||||
|
||||
{
|
||||
AssetWeakReference a;
|
||||
a.asset_library_type = ASSET_LIBRARY_LOCAL;
|
||||
a.asset_library_identifier = "My custom lib";
|
||||
a.relative_asset_identifier = "path/to/an/asset";
|
||||
|
||||
AssetWeakReference b;
|
||||
EXPECT_NE(a, b);
|
||||
|
||||
b.asset_library_type = ASSET_LIBRARY_LOCAL;
|
||||
b.asset_library_identifier = "My custom lib";
|
||||
b.relative_asset_identifier = "path/to/an/asset";
|
||||
EXPECT_EQ(a, b);
|
||||
|
||||
/* Make the destructor work. */
|
||||
a.asset_library_identifier = b.asset_library_identifier = nullptr;
|
||||
a.relative_asset_identifier = b.relative_asset_identifier = nullptr;
|
||||
}
|
||||
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
AssetWeakReference other;
|
||||
other.asset_library_type = ASSET_LIBRARY_CUSTOM;
|
||||
other.asset_library_identifier = "My custom lib";
|
||||
other.relative_asset_identifier = "path/to/an/asset";
|
||||
EXPECT_EQ(weak_ref, other);
|
||||
|
||||
other.relative_asset_identifier = "";
|
||||
EXPECT_NE(weak_ref, other);
|
||||
other.relative_asset_identifier = nullptr;
|
||||
EXPECT_NE(weak_ref, other);
|
||||
|
||||
/* Make the destructor work. */
|
||||
other.asset_library_identifier = nullptr;
|
||||
other.relative_asset_identifier = nullptr;
|
||||
}
|
||||
|
||||
/* Same but comparing windows and unix style paths. */
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
AssetWeakReference other;
|
||||
other.asset_library_type = ASSET_LIBRARY_CUSTOM;
|
||||
other.asset_library_identifier = "My custom lib";
|
||||
other.relative_asset_identifier = "path\\to\\an\\asset";
|
||||
EXPECT_EQ(weak_ref, other);
|
||||
|
||||
other.relative_asset_identifier = "";
|
||||
EXPECT_NE(weak_ref, other);
|
||||
other.relative_asset_identifier = nullptr;
|
||||
EXPECT_NE(weak_ref, other);
|
||||
|
||||
/* Make the destructor work. */
|
||||
other.asset_library_identifier = nullptr;
|
||||
other.relative_asset_identifier = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_full_path__current_file)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
|
||||
std::string resolved_path = service->resolve_asset_weak_reference_to_full_path(weak_ref);
|
||||
EXPECT_EQ(resolved_path, "");
|
||||
}
|
||||
|
||||
/* #AssetLibraryService::resolve_asset_weak_reference_to_full_path(). */
|
||||
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_full_path__custom_library)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
|
||||
std::string expected_path = utils::normalize_path(asset_library_root_ + "/" + "path/") +
|
||||
"to/an/asset";
|
||||
std::string resolved_path = service->resolve_asset_weak_reference_to_full_path(weak_ref);
|
||||
|
||||
EXPECT_EQ(BLI_path_cmp(resolved_path.c_str(), expected_path.c_str()), 0);
|
||||
}
|
||||
|
||||
TEST_F(AssetRepresentationTest,
|
||||
weak_reference__resolve_to_full_path__custom_library__windows_pathsep)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path\\to\\an\\asset");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
|
||||
std::string expected_path = utils::normalize_path(asset_library_root_ + "\\" + "path\\") +
|
||||
"to\\an\\asset";
|
||||
std::string resolved_path = service->resolve_asset_weak_reference_to_full_path(weak_ref);
|
||||
|
||||
EXPECT_EQ(BLI_path_cmp(resolved_path.c_str(), expected_path.c_str()), 0);
|
||||
}
|
||||
|
||||
/* #AssetLibraryService::resolve_asset_weak_reference_to_exploded_path(). */
|
||||
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_exploded_path__current_file)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *library = get_builtin_library_from_type(ASSET_LIBRARY_LOCAL);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "path/to/an/asset");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
|
||||
std::string expected_full_path = utils::normalize_path("path/to/an/asset", 5);
|
||||
std::optional<AssetLibraryService::ExplodedPath> resolved_path =
|
||||
service->resolve_asset_weak_reference_to_exploded_path(weak_ref);
|
||||
|
||||
EXPECT_EQ(*resolved_path->full_path, expected_full_path);
|
||||
EXPECT_EQ(resolved_path->dir_component, "");
|
||||
EXPECT_EQ(resolved_path->group_component, "path");
|
||||
/* ID names may contain slashes. */
|
||||
EXPECT_EQ(resolved_path->name_component, "to/an/asset");
|
||||
}
|
||||
|
||||
/* #AssetLibraryService::resolve_asset_weak_reference_to_exploded_path(). */
|
||||
TEST_F(AssetRepresentationTest, weak_reference__resolve_to_exploded_path__custom_library)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "some.blend/Material/asset/name");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
|
||||
std::string expected_full_path = utils::normalize_path(asset_library_root_ +
|
||||
"/some.blend/Material/") +
|
||||
"asset/name";
|
||||
std::optional<AssetLibraryService::ExplodedPath> resolved_path =
|
||||
service->resolve_asset_weak_reference_to_exploded_path(weak_ref);
|
||||
|
||||
EXPECT_EQ(BLI_path_cmp(resolved_path->full_path->c_str(), expected_full_path.c_str()), 0);
|
||||
EXPECT_EQ(BLI_path_cmp_normalized(std::string(resolved_path->dir_component).c_str(),
|
||||
std::string(asset_library_root_ + "/some.blend").c_str()),
|
||||
0);
|
||||
EXPECT_EQ(resolved_path->group_component, "Material");
|
||||
/* ID names may contain slashes. */
|
||||
EXPECT_EQ(resolved_path->name_component, "asset/name");
|
||||
}
|
||||
|
||||
/* #AssetLibraryService::resolve_asset_weak_reference_to_exploded_path(). */
|
||||
TEST_F(AssetRepresentationTest,
|
||||
weak_reference__resolve_to_exploded_path__custom_library__windows_pathsep)
|
||||
{
|
||||
AssetLibraryService *service = AssetLibraryService::get();
|
||||
AssetLibrary *const library = service->get_asset_library_on_disk_custom("My custom lib",
|
||||
asset_library_root_);
|
||||
AssetRepresentation &asset = add_dummy_asset(*library, "some.blend\\Material\\asset/name");
|
||||
|
||||
AssetWeakReference weak_ref = asset.make_weak_reference();
|
||||
|
||||
std::string expected_full_path = utils::normalize_path(asset_library_root_ +
|
||||
"\\some.blend\\Material\\") +
|
||||
"asset/name";
|
||||
std::optional<AssetLibraryService::ExplodedPath> resolved_path =
|
||||
service->resolve_asset_weak_reference_to_exploded_path(weak_ref);
|
||||
|
||||
EXPECT_EQ(BLI_path_cmp(resolved_path->full_path->c_str(), expected_full_path.c_str()), 0);
|
||||
EXPECT_EQ(BLI_path_cmp_normalized(std::string(resolved_path->dir_component).c_str(),
|
||||
std::string(asset_library_root_ + "\\some.blend").c_str()),
|
||||
0);
|
||||
EXPECT_EQ(resolved_path->group_component, "Material");
|
||||
/* ID names may contain slashes. */
|
||||
EXPECT_EQ(resolved_path->name_component, "asset/name");
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
@@ -0,0 +1,31 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include "AS_essentials_library.hh"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
TEST(EssentialsLibraryTest, is_online_essentials_url)
|
||||
{
|
||||
EXPECT_FALSE(is_online_essentials_url(""));
|
||||
EXPECT_FALSE(is_online_essentials_url("https://www.blender.org/asset-library/"));
|
||||
|
||||
EXPECT_TRUE(
|
||||
is_online_essentials_url("https://cdn.extensions.blender.org/asset-libraries/essentials/"));
|
||||
EXPECT_TRUE(is_online_essentials_url(
|
||||
"https://cdn.extensions.blender.org/asset-libraries/essentials/_asset-library-meta.json"));
|
||||
|
||||
EXPECT_FALSE(
|
||||
is_online_essentials_url("https://cdn.extensions.blender.org/asset-libraries/essentials"));
|
||||
EXPECT_FALSE(is_online_essentials_url(
|
||||
"https://cdn.extensions.blender.org/asset-libraries/essentials_asset-library-meta.json"));
|
||||
|
||||
/* http instead of https. */
|
||||
EXPECT_FALSE(
|
||||
is_online_essentials_url("http://cdn.extensions.blender.org/asset-libraries/essentials/"));
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
@@ -0,0 +1,29 @@
|
||||
/* SPDX-FileCopyrightText: 2026 Blender Authors
|
||||
*
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later */
|
||||
|
||||
#include "testing/testing.h"
|
||||
|
||||
#include "AS_remote_library.hh"
|
||||
|
||||
namespace blender::asset_system::tests {
|
||||
|
||||
TEST(RemoteLibraryTest, url_ends_with_top_meta_file_name)
|
||||
{
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name(""));
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("_asset-library-meta.json"));
|
||||
|
||||
EXPECT_TRUE(remote_library_url_ends_with_top_meta_file_name(
|
||||
"https://example.com/_asset-library-meta.json"));
|
||||
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("https://example.com/"));
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("https://example.com/abc"));
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name("https://example.com/abc/"));
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name(
|
||||
"https://example.com/_asset-library-meta.json/"));
|
||||
/* Missing slash. */
|
||||
EXPECT_FALSE(remote_library_url_ends_with_top_meta_file_name(
|
||||
"https://example.com_asset-library-meta.json"));
|
||||
}
|
||||
|
||||
} // namespace blender::asset_system::tests
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user