From 0468bea899fe4806d8358c1cab078f700ab84d22 Mon Sep 17 00:00:00 2001 From: demolke Date: Fri, 30 Aug 2024 22:40:11 +0200 Subject: [PATCH] Add per-bone meta to Skeleton3D Individual bones are not represented as `Node`s in Godot, in order to support meta functionality for them the skeleton has to carry the information similarly to how other per-bone properties are handled. - Also adds support for GLTF import/export --- doc/classes/Skeleton3D.xml | 32 ++++ editor/add_metadata_dialog.cpp | 118 +++++++++++++ editor/add_metadata_dialog.h | 66 +++++++ editor/editor_inspector.cpp | 100 +++-------- editor/editor_inspector.h | 4 +- editor/plugins/skeleton_3d_editor_plugin.cpp | 171 +++++++++++++++---- editor/plugins/skeleton_3d_editor_plugin.h | 22 ++- modules/gltf/gltf_document.cpp | 4 + modules/gltf/skin_tool.cpp | 5 + modules/gltf/tests/test_gltf_extras.h | 57 +++++++ scene/3d/skeleton_3d.cpp | 65 +++++++ scene/3d/skeleton_3d.h | 9 + tests/scene/test_skeleton_3d.h | 78 +++++++++ tests/test_main.cpp | 1 + 14 files changed, 617 insertions(+), 115 deletions(-) create mode 100644 editor/add_metadata_dialog.cpp create mode 100644 editor/add_metadata_dialog.h create mode 100644 tests/scene/test_skeleton_3d.h diff --git a/doc/classes/Skeleton3D.xml b/doc/classes/Skeleton3D.xml index cc3f61e1b29..f5b808be8e2 100644 --- a/doc/classes/Skeleton3D.xml +++ b/doc/classes/Skeleton3D.xml @@ -99,6 +99,21 @@ Returns the global rest transform for [param bone_idx]. + + + + + + Returns bone metadata for [param bone_idx] with [param key]. + + + + + + + Returns a list of all metadata keys for [param bone_idx]. + + @@ -171,6 +186,14 @@ Use for invalidating caches in IK solvers and other nodes which process bones. + + + + + + Returns whether there exists any bone metadata for [param bone_idx] with key [param key]. + + @@ -263,6 +286,15 @@ [b]Note:[/b] The pose transform needs to be a global pose! To convert a world transform from a [Node3D] to a global bone pose, multiply the [method Transform3D.affine_inverse] of the node's [member Node3D.global_transform] by the desired world transform. + + + + + + + Sets bone metadata for [param bone_idx], will set the [param key] meta to [param value]. + + diff --git a/editor/add_metadata_dialog.cpp b/editor/add_metadata_dialog.cpp new file mode 100644 index 00000000000..0a070e37b68 --- /dev/null +++ b/editor/add_metadata_dialog.cpp @@ -0,0 +1,118 @@ +/**************************************************************************/ +/* add_metadata_dialog.cpp */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#include "add_metadata_dialog.h" + +AddMetadataDialog::AddMetadataDialog() { + VBoxContainer *vbc = memnew(VBoxContainer); + add_child(vbc); + + HBoxContainer *hbc = memnew(HBoxContainer); + vbc->add_child(hbc); + hbc->add_child(memnew(Label(TTR("Name:")))); + + add_meta_name = memnew(LineEdit); + add_meta_name->set_custom_minimum_size(Size2(200 * EDSCALE, 1)); + hbc->add_child(add_meta_name); + hbc->add_child(memnew(Label(TTR("Type:")))); + + add_meta_type = memnew(OptionButton); + + hbc->add_child(add_meta_type); + + Control *spacing = memnew(Control); + vbc->add_child(spacing); + spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE)); + + set_ok_button_text(TTR("Add")); + register_text_enter(add_meta_name); + + validation_panel = memnew(EditorValidationPanel); + vbc->add_child(validation_panel); + validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid.")); + validation_panel->set_update_callback(callable_mp(this, &AddMetadataDialog::_check_meta_name)); + validation_panel->set_accept_button(get_ok_button()); + + add_meta_name->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); +} + +void AddMetadataDialog::_complete_init(const StringName &p_title) { + add_meta_name->grab_focus(); + add_meta_name->set_text(""); + validation_panel->update(); + + set_title(vformat(TTR("Add Metadata Property for \"%s\""), p_title)); + + // Skip if we already completed the initialization. + if (add_meta_type->get_item_count()) { + return; + } + + // Theme icons can be retrieved only the Window has been initialized. + for (int i = 0; i < Variant::VARIANT_MAX; i++) { + if (i == Variant::NIL || i == Variant::RID || i == Variant::CALLABLE || i == Variant::SIGNAL) { + continue; //not editable by inspector. + } + String type = i == Variant::OBJECT ? String("Resource") : Variant::get_type_name(Variant::Type(i)); + + add_meta_type->add_icon_item(get_editor_theme_icon(type), type, i); + } +} + +void AddMetadataDialog::open(const StringName p_title, List &p_existing_metas) { + this->_existing_metas = p_existing_metas; + _complete_init(p_title); + popup_centered(); +} + +StringName AddMetadataDialog::get_meta_name() { + return add_meta_name->get_text(); +} + +Variant AddMetadataDialog::get_meta_defval() { + Variant defval; + Callable::CallError ce; + Variant::construct(Variant::Type(add_meta_type->get_selected_id()), defval, nullptr, 0, ce); + return defval; +} + +void AddMetadataDialog::_check_meta_name() { + const String meta_name = add_meta_name->get_text(); + + if (meta_name.is_empty()) { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR); + } else if (!meta_name.is_valid_ascii_identifier()) { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR); + } else if (_existing_metas.find(meta_name)) { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR); + } else if (meta_name[0] == '_') { + validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR); + } +} diff --git a/editor/add_metadata_dialog.h b/editor/add_metadata_dialog.h new file mode 100644 index 00000000000..b1a244ddc6c --- /dev/null +++ b/editor/add_metadata_dialog.h @@ -0,0 +1,66 @@ +/**************************************************************************/ +/* add_metadata_dialog.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef ADD_METADATA_DIALOG_H +#define ADD_METADATA_DIALOG_H + +#include "core/object/callable_method_pointer.h" +#include "editor/editor_help.h" +#include "editor/editor_undo_redo_manager.h" +#include "editor/gui/editor_validation_panel.h" +#include "editor/themes/editor_scale.h" +#include "scene/gui/button.h" +#include "scene/gui/dialogs.h" +#include "scene/gui/item_list.h" +#include "scene/gui/line_edit.h" +#include "scene/gui/option_button.h" +#include "scene/gui/tree.h" + +class AddMetadataDialog : public ConfirmationDialog { + GDCLASS(AddMetadataDialog, ConfirmationDialog); + +public: + AddMetadataDialog(); + void open(const StringName p_title, List &p_existing_metas); + + StringName get_meta_name(); + Variant get_meta_defval(); + +private: + List _existing_metas; + + void _check_meta_name(); + void _complete_init(const StringName &p_label); + + LineEdit *add_meta_name = nullptr; + OptionButton *add_meta_type = nullptr; + EditorValidationPanel *validation_panel = nullptr; +}; +#endif // ADD_METADATA_DIALOG_H diff --git a/editor/editor_inspector.cpp b/editor/editor_inspector.cpp index da50ffc510b..4cd0761691a 100644 --- a/editor/editor_inspector.cpp +++ b/editor/editor_inspector.cpp @@ -32,6 +32,7 @@ #include "editor_inspector.compat.inc" #include "core/os/keyboard.h" +#include "editor/add_metadata_dialog.h" #include "editor/doc_tools.h" #include "editor/editor_feature_profile.h" #include "editor/editor_main_screen.h" @@ -4245,92 +4246,33 @@ Variant EditorInspector::get_property_clipboard() const { return property_clipboard; } -void EditorInspector::_add_meta_confirm() { - String name = add_meta_name->get_text(); - - object->editor_set_section_unfold("metadata", true); // Ensure metadata is unfolded when adding a new metadata. - - Variant defval; - Callable::CallError ce; - Variant::construct(Variant::Type(add_meta_type->get_selected_id()), defval, nullptr, 0, ce); - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(vformat(TTR("Add metadata %s"), name)); - undo_redo->add_do_method(object, "set_meta", name, defval); - undo_redo->add_undo_method(object, "remove_meta", name); - undo_redo->commit_action(); -} - -void EditorInspector::_check_meta_name() { - const String meta_name = add_meta_name->get_text(); - - if (meta_name.is_empty()) { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name can't be empty."), EditorValidationPanel::MSG_ERROR); - } else if (!meta_name.is_valid_ascii_identifier()) { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name must be a valid identifier."), EditorValidationPanel::MSG_ERROR); - } else if (object->has_meta(meta_name)) { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, vformat(TTR("Metadata with name \"%s\" already exists."), meta_name), EditorValidationPanel::MSG_ERROR); - } else if (meta_name[0] == '_') { - validation_panel->set_message(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Names starting with _ are reserved for editor-only metadata."), EditorValidationPanel::MSG_ERROR); - } -} - void EditorInspector::_show_add_meta_dialog() { if (!add_meta_dialog) { - add_meta_dialog = memnew(ConfirmationDialog); - - VBoxContainer *vbc = memnew(VBoxContainer); - add_meta_dialog->add_child(vbc); - - HBoxContainer *hbc = memnew(HBoxContainer); - vbc->add_child(hbc); - hbc->add_child(memnew(Label(TTR("Name:")))); - - add_meta_name = memnew(LineEdit); - add_meta_name->set_custom_minimum_size(Size2(200 * EDSCALE, 1)); - hbc->add_child(add_meta_name); - hbc->add_child(memnew(Label(TTR("Type:")))); - - add_meta_type = memnew(OptionButton); - for (int i = 0; i < Variant::VARIANT_MAX; i++) { - if (i == Variant::NIL || i == Variant::RID || i == Variant::CALLABLE || i == Variant::SIGNAL) { - continue; //not editable by inspector. - } - String type = i == Variant::OBJECT ? String("Resource") : Variant::get_type_name(Variant::Type(i)); - - add_meta_type->add_icon_item(get_editor_theme_icon(type), type, i); - } - hbc->add_child(add_meta_type); - - Control *spacing = memnew(Control); - vbc->add_child(spacing); - spacing->set_custom_minimum_size(Size2(0, 10 * EDSCALE)); - - add_meta_dialog->set_ok_button_text(TTR("Add")); - add_child(add_meta_dialog); - add_meta_dialog->register_text_enter(add_meta_name); + add_meta_dialog = memnew(AddMetadataDialog()); add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &EditorInspector::_add_meta_confirm)); - - validation_panel = memnew(EditorValidationPanel); - vbc->add_child(validation_panel); - validation_panel->add_line(EditorValidationPanel::MSG_ID_DEFAULT, TTR("Metadata name is valid.")); - validation_panel->set_update_callback(callable_mp(this, &EditorInspector::_check_meta_name)); - validation_panel->set_accept_button(add_meta_dialog->get_ok_button()); - - add_meta_name->connect(SceneStringName(text_changed), callable_mp(validation_panel, &EditorValidationPanel::update).unbind(1)); + add_child(add_meta_dialog); } + StringName dialog_title; Node *node = Object::cast_to(object); - if (node) { - add_meta_dialog->set_title(vformat(TTR("Add Metadata Property for \"%s\""), node->get_name())); - } else { - // This should normally be reached when the object is derived from Resource. - add_meta_dialog->set_title(vformat(TTR("Add Metadata Property for \"%s\""), object->get_class())); - } + // If object is derived from Node use node name, if derived from Resource use classname. + dialog_title = node ? node->get_name() : StringName(object->get_class()); - add_meta_dialog->popup_centered(); - add_meta_name->grab_focus(); - add_meta_name->set_text(""); - validation_panel->update(); + List existing_meta_keys; + object->get_meta_list(&existing_meta_keys); + add_meta_dialog->open(dialog_title, existing_meta_keys); +} + +void EditorInspector::_add_meta_confirm() { + // Ensure metadata is unfolded when adding a new metadata. + object->editor_set_section_unfold("metadata", true); + + String name = add_meta_dialog->get_meta_name(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Add metadata %s"), name)); + undo_redo->add_do_method(object, "set_meta", name, add_meta_dialog->get_meta_defval()); + undo_redo->add_undo_method(object, "remove_meta", name); + undo_redo->commit_action(); } void EditorInspector::_bind_methods() { diff --git a/editor/editor_inspector.h b/editor/editor_inspector.h index fda14430009..14b6ff0907a 100644 --- a/editor/editor_inspector.h +++ b/editor/editor_inspector.h @@ -31,6 +31,7 @@ #ifndef EDITOR_INSPECTOR_H #define EDITOR_INSPECTOR_H +#include "editor/add_metadata_dialog.h" #include "editor_property_name_processor.h" #include "scene/gui/box_container.h" #include "scene/gui/scroll_container.h" @@ -575,14 +576,13 @@ class EditorInspector : public ScrollContainer { bool _is_property_disabled_by_feature_profile(const StringName &p_property); - ConfirmationDialog *add_meta_dialog = nullptr; + AddMetadataDialog *add_meta_dialog = nullptr; LineEdit *add_meta_name = nullptr; OptionButton *add_meta_type = nullptr; EditorValidationPanel *validation_panel = nullptr; void _add_meta_confirm(); void _show_add_meta_dialog(); - void _check_meta_name(); protected: static void _bind_methods(); diff --git a/editor/plugins/skeleton_3d_editor_plugin.cpp b/editor/plugins/skeleton_3d_editor_plugin.cpp index 99cb03cdcd3..64b9522864c 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.cpp +++ b/editor/plugins/skeleton_3d_editor_plugin.cpp @@ -52,7 +52,7 @@ #include "scene/resources/skeleton_profile.h" #include "scene/resources/surface_tool.h" -void BoneTransformEditor::create_editors() { +void BonePropertiesEditor::create_editors() { section = memnew(EditorInspectorSection); section->setup("trf_properties", label, this, Color(0.0f, 0.0f, 0.0f), true); section->unfold(); @@ -61,7 +61,7 @@ void BoneTransformEditor::create_editors() { enabled_checkbox = memnew(EditorPropertyCheck()); enabled_checkbox->set_label("Pose Enabled"); enabled_checkbox->set_selectable(false); - enabled_checkbox->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); + enabled_checkbox->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); section->get_vbox()->add_child(enabled_checkbox); // Position property. @@ -69,8 +69,8 @@ void BoneTransformEditor::create_editors() { position_property->setup(-10000, 10000, 0.001, true); position_property->set_label("Position"); position_property->set_selectable(false); - position_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); - position_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed)); + position_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); + position_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed)); section->get_vbox()->add_child(position_property); // Rotation property. @@ -78,8 +78,8 @@ void BoneTransformEditor::create_editors() { rotation_property->setup(-10000, 10000, 0.001, true); rotation_property->set_label("Rotation"); rotation_property->set_selectable(false); - rotation_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); - rotation_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed)); + rotation_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); + rotation_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed)); section->get_vbox()->add_child(rotation_property); // Scale property. @@ -87,8 +87,8 @@ void BoneTransformEditor::create_editors() { scale_property->setup(-10000, 10000, 0.001, true, true); scale_property->set_label("Scale"); scale_property->set_selectable(false); - scale_property->connect("property_changed", callable_mp(this, &BoneTransformEditor::_value_changed)); - scale_property->connect("property_keyed", callable_mp(this, &BoneTransformEditor::_property_keyed)); + scale_property->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_value_changed)); + scale_property->connect("property_keyed", callable_mp(this, &BonePropertiesEditor::_property_keyed)); section->get_vbox()->add_child(scale_property); // Transform/Matrix section. @@ -102,50 +102,136 @@ void BoneTransformEditor::create_editors() { rest_matrix->set_label("Transform"); rest_matrix->set_selectable(false); rest_section->get_vbox()->add_child(rest_matrix); + + // Bone Metadata property + meta_section = memnew(EditorInspectorSection); + meta_section->setup("bone_meta", TTR("Bone Metadata"), this, Color(.0f, .0f, .0f), true); + section->get_vbox()->add_child(meta_section); + + add_metadata_button = EditorInspector::create_inspector_action_button(TTR("Add Bone Metadata")); + add_metadata_button->connect(SceneStringName(pressed), callable_mp(this, &BonePropertiesEditor::_show_add_meta_dialog)); + section->get_vbox()->add_child(add_metadata_button); + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->connect("version_changed", callable_mp(this, &BonePropertiesEditor::_update_properties)); + undo_redo->connect("history_changed", callable_mp(this, &BonePropertiesEditor::_update_properties)); } -void BoneTransformEditor::_notification(int p_what) { +void BonePropertiesEditor::_notification(int p_what) { switch (p_what) { case NOTIFICATION_THEME_CHANGED: { const Color section_color = get_theme_color(SNAME("prop_subsection"), EditorStringName(Editor)); section->set_bg_color(section_color); rest_section->set_bg_color(section_color); + add_metadata_button->set_icon(get_editor_theme_icon(SNAME("Add"))); } break; } } -void BoneTransformEditor::_value_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { - if (updating) { +void BonePropertiesEditor::_value_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { + if (updating || !skeleton) { return; } - if (skeleton) { - EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); - undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS); - undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property)); - undo_redo->add_do_property(skeleton, p_property, p_value); - Skeleton3DEditor *se = Skeleton3DEditor::get_singleton(); - if (se) { - undo_redo->add_do_method(se, "update_joint_tree"); - undo_redo->add_undo_method(se, "update_joint_tree"); - } + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(TTR("Set Bone Transform"), UndoRedo::MERGE_ENDS); + undo_redo->add_undo_property(skeleton, p_property, skeleton->get(p_property)); + undo_redo->add_do_property(skeleton, p_property, p_value); - undo_redo->commit_action(); + Skeleton3DEditor *se = Skeleton3DEditor::get_singleton(); + if (se) { + undo_redo->add_do_method(se, "update_joint_tree"); + undo_redo->add_undo_method(se, "update_joint_tree"); } + + undo_redo->commit_action(); } -BoneTransformEditor::BoneTransformEditor(Skeleton3D *p_skeleton) : +void BonePropertiesEditor::_meta_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing) { + if (!skeleton || p_property.get_slicec('/', 2) != "bone_meta") { + return; + } + + int bone = p_property.get_slicec('/', 1).to_int(); + if (bone >= skeleton->get_bone_count()) { + return; + } + + String key = p_property.get_slicec('/', 3); + if (!skeleton->has_bone_meta(1, key)) { + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Modify metadata '%s' for bone '%s'"), key, skeleton->get_bone_name(bone))); + undo_redo->add_do_property(skeleton, p_property, p_value); + undo_redo->add_do_method(meta_editors[p_property], "update_property"); + undo_redo->add_undo_property(skeleton, p_property, skeleton->get_bone_meta(bone, key)); + undo_redo->add_undo_method(meta_editors[p_property], "update_property"); + undo_redo->commit_action(); +} + +void BonePropertiesEditor::_meta_deleted(const String &p_property) { + if (!skeleton || p_property.get_slicec('/', 2) != "bone_meta") { + return; + } + + int bone = p_property.get_slicec('/', 1).to_int(); + if (bone >= skeleton->get_bone_count()) { + return; + } + + String key = p_property.get_slicec('/', 3); + if (!skeleton->has_bone_meta(1, key)) { + return; + } + + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Remove metadata '%s' from bone '%s'"), key, skeleton->get_bone_name(bone))); + undo_redo->add_do_property(skeleton, p_property, Variant()); + undo_redo->add_undo_property(skeleton, p_property, skeleton->get_bone_meta(bone, key)); + undo_redo->commit_action(); + + emit_signal(SNAME("property_deleted"), p_property); +} + +void BonePropertiesEditor::_show_add_meta_dialog() { + if (!add_meta_dialog) { + add_meta_dialog = memnew(AddMetadataDialog()); + add_meta_dialog->connect(SceneStringName(confirmed), callable_mp(this, &BonePropertiesEditor::_add_meta_confirm)); + add_child(add_meta_dialog); + } + + int bone = Skeleton3DEditor::get_singleton()->get_selected_bone(); + StringName dialog_title = skeleton->get_bone_name(bone); + + List existing_meta_keys; + skeleton->get_bone_meta_list(bone, &existing_meta_keys); + add_meta_dialog->open(dialog_title, existing_meta_keys); +} + +void BonePropertiesEditor::_add_meta_confirm() { + int bone = Skeleton3DEditor::get_singleton()->get_selected_bone(); + String name = add_meta_dialog->get_meta_name(); + EditorUndoRedoManager *undo_redo = EditorUndoRedoManager::get_singleton(); + undo_redo->create_action(vformat(TTR("Add metadata '%s' to bone '%s'"), name, skeleton->get_bone_name(bone))); + undo_redo->add_do_method(skeleton, "set_bone_meta", bone, name, add_meta_dialog->get_meta_defval()); + undo_redo->add_undo_method(skeleton, "set_bone_meta", bone, name, Variant()); + undo_redo->commit_action(); +} + +BonePropertiesEditor::BonePropertiesEditor(Skeleton3D *p_skeleton) : skeleton(p_skeleton) { create_editors(); } -void BoneTransformEditor::set_keyable(const bool p_keyable) { +void BonePropertiesEditor::set_keyable(const bool p_keyable) { position_property->set_keying(p_keyable); rotation_property->set_keying(p_keyable); scale_property->set_keying(p_keyable); } -void BoneTransformEditor::set_target(const String &p_prop) { +void BonePropertiesEditor::set_target(const String &p_prop) { enabled_checkbox->set_object_and_property(skeleton, p_prop + "enabled"); enabled_checkbox->update_property(); @@ -162,7 +248,7 @@ void BoneTransformEditor::set_target(const String &p_prop) { rest_matrix->update_property(); } -void BoneTransformEditor::_property_keyed(const String &p_path, bool p_advance) { +void BonePropertiesEditor::_property_keyed(const String &p_path, bool p_advance) { AnimationTrackEditor *te = AnimationPlayerEditor::get_singleton()->get_track_editor(); if (!te || !te->has_keying()) { return; @@ -183,16 +269,17 @@ void BoneTransformEditor::_property_keyed(const String &p_path, bool p_advance) } } -void BoneTransformEditor::_update_properties() { +void BonePropertiesEditor::_update_properties() { if (!skeleton) { return; } int selected = Skeleton3DEditor::get_singleton()->get_selected_bone(); List props; + HashSet meta_seen; skeleton->get_property_list(&props); for (const PropertyInfo &E : props) { PackedStringArray split = E.name.split("/"); - if (split.size() == 3 && split[0] == "bones") { + if (split.size() >= 3 && split[0] == "bones") { if (split[1].to_int() == selected) { if (split[2] == "enabled") { enabled_checkbox->set_read_only(E.usage & PROPERTY_USAGE_READ_ONLY); @@ -224,9 +311,35 @@ void BoneTransformEditor::_update_properties() { rest_matrix->update_editor_property_status(); rest_matrix->queue_redraw(); } + if (split[2] == "bone_meta") { + meta_seen.insert(E.name); + if (!meta_editors.find(E.name)) { + EditorProperty *editor = EditorInspectorDefaultPlugin::get_editor_for_property(skeleton, E.type, E.name, PROPERTY_HINT_NONE, "", E.usage); + editor->set_label(split[3]); + editor->set_object_and_property(skeleton, E.name); + editor->set_deletable(true); + editor->set_selectable(false); + editor->connect("property_changed", callable_mp(this, &BonePropertiesEditor::_meta_changed)); + editor->connect("property_deleted", callable_mp(this, &BonePropertiesEditor::_meta_deleted)); + + meta_section->get_vbox()->add_child(editor); + editor->update_property(); + editor->update_editor_property_status(); + editor->queue_redraw(); + + meta_editors[E.name] = editor; + } + } } } } + // UI for any bone metadata prop not seen during the iteration has to be deleted + for (KeyValue iter : meta_editors) { + if (!meta_seen.has(iter.key)) { + callable_mp((Node *)meta_section->get_vbox(), &Node::remove_child).call_deferred(iter.value); + meta_editors.remove(meta_editors.find(iter.key)); + } + } } Skeleton3DEditor *Skeleton3DEditor::singleton = nullptr; @@ -992,7 +1105,7 @@ void Skeleton3DEditor::create_editors() { SET_DRAG_FORWARDING_GCD(joint_tree, Skeleton3DEditor); s_con->add_child(joint_tree); - pose_editor = memnew(BoneTransformEditor(skeleton)); + pose_editor = memnew(BonePropertiesEditor(skeleton)); pose_editor->set_label(TTR("Bone Transform")); pose_editor->set_visible(false); add_child(pose_editor); diff --git a/editor/plugins/skeleton_3d_editor_plugin.h b/editor/plugins/skeleton_3d_editor_plugin.h index d4dee1f16f7..0265183dfaf 100644 --- a/editor/plugins/skeleton_3d_editor_plugin.h +++ b/editor/plugins/skeleton_3d_editor_plugin.h @@ -31,6 +31,7 @@ #ifndef SKELETON_3D_EDITOR_PLUGIN_H #define SKELETON_3D_EDITOR_PLUGIN_H +#include "editor/add_metadata_dialog.h" #include "editor/editor_properties.h" #include "editor/gui/editor_file_dialog.h" #include "editor/plugins/editor_plugin.h" @@ -50,8 +51,8 @@ class Tree; class TreeItem; class VSeparator; -class BoneTransformEditor : public VBoxContainer { - GDCLASS(BoneTransformEditor, VBoxContainer); +class BonePropertiesEditor : public VBoxContainer { + GDCLASS(BonePropertiesEditor, VBoxContainer); EditorInspectorSection *section = nullptr; @@ -63,6 +64,10 @@ class BoneTransformEditor : public VBoxContainer { EditorInspectorSection *rest_section = nullptr; EditorPropertyTransform3D *rest_matrix = nullptr; + EditorInspectorSection *meta_section = nullptr; + AddMetadataDialog *add_meta_dialog = nullptr; + Button *add_metadata_button = nullptr; + Rect2 background_rects[5]; Skeleton3D *skeleton = nullptr; @@ -79,11 +84,18 @@ class BoneTransformEditor : public VBoxContainer { void _property_keyed(const String &p_path, bool p_advance); + void _meta_changed(const String &p_property, const Variant &p_value, const String &p_name, bool p_changing); + void _meta_deleted(const String &p_property); + void _show_add_meta_dialog(); + void _add_meta_confirm(); + + HashMap meta_editors; + protected: void _notification(int p_what); public: - BoneTransformEditor(Skeleton3D *p_skeleton); + BonePropertiesEditor(Skeleton3D *p_skeleton); // Which transform target to modify. void set_target(const String &p_prop); @@ -123,8 +135,8 @@ class Skeleton3DEditor : public VBoxContainer { }; Tree *joint_tree = nullptr; - BoneTransformEditor *rest_editor = nullptr; - BoneTransformEditor *pose_editor = nullptr; + BonePropertiesEditor *rest_editor = nullptr; + BonePropertiesEditor *pose_editor = nullptr; HBoxContainer *topmenu_bar = nullptr; MenuButton *skeleton_options = nullptr; diff --git a/modules/gltf/gltf_document.cpp b/modules/gltf/gltf_document.cpp index 56dae658319..bd034cbdc5d 100644 --- a/modules/gltf/gltf_document.cpp +++ b/modules/gltf/gltf_document.cpp @@ -5534,6 +5534,10 @@ void GLTFDocument::_convert_skeleton_to_gltf(Skeleton3D *p_skeleton3d, Refset_name(_gen_unique_name(p_state, skeleton->get_bone_name(bone_i))); joint_node->transform = skeleton->get_bone_pose(bone_i); joint_node->joint = true; + + if (p_skeleton3d->has_bone_meta(bone_i, "extras")) { + joint_node->set_meta("extras", p_skeleton3d->get_bone_meta(bone_i, "extras")); + } GLTFNodeIndex current_node_i = p_state->nodes.size(); p_state->scene_nodes.insert(current_node_i, skeleton); p_state->nodes.push_back(joint_node); diff --git a/modules/gltf/skin_tool.cpp b/modules/gltf/skin_tool.cpp index a344334d939..1522c0e324d 100644 --- a/modules/gltf/skin_tool.cpp +++ b/modules/gltf/skin_tool.cpp @@ -602,6 +602,11 @@ Error SkinTool::_create_skeletons( skeleton->set_bone_pose_rotation(bone_index, node->transform.basis.get_rotation_quaternion()); skeleton->set_bone_pose_scale(bone_index, node->transform.basis.get_scale()); + // Store bone-level GLTF extras in skeleton per bone meta. + if (node->has_meta("extras")) { + skeleton->set_bone_meta(bone_index, "extras", node->get_meta("extras")); + } + if (node->parent >= 0 && nodes[node->parent]->skeleton == skel_i) { const int bone_parent = skeleton->find_bone(nodes[node->parent]->get_name()); ERR_FAIL_COND_V(bone_parent < 0, FAILED); diff --git a/modules/gltf/tests/test_gltf_extras.h b/modules/gltf/tests/test_gltf_extras.h index 96aadf30232..37c8f6925c6 100644 --- a/modules/gltf/tests/test_gltf_extras.h +++ b/modules/gltf/tests/test_gltf_extras.h @@ -41,6 +41,7 @@ #include "modules/gltf/gltf_document.h" #include "modules/gltf/gltf_state.h" #include "scene/3d/mesh_instance_3d.h" +#include "scene/3d/skeleton_3d.h" #include "scene/main/window.h" #include "scene/resources/3d/primitive_meshes.h" #include "scene/resources/material.h" @@ -158,6 +159,62 @@ TEST_CASE("[SceneTree][Node] GLTF test mesh and material meta export and import" memdelete(original); memdelete(loaded); } + +TEST_CASE("[SceneTree][Node] GLTF test skeleton and bone export and import") { + // Setup scene. + Skeleton3D *skeleton = memnew(Skeleton3D); + skeleton->set_name("skeleton"); + Dictionary skeleton_extras; + skeleton_extras["node_type"] = "skeleton"; + skeleton->set_meta("extras", skeleton_extras); + + skeleton->add_bone("parent"); + skeleton->set_bone_rest(0, Transform3D()); + Dictionary parent_bone_extras; + parent_bone_extras["bone"] = "i_am_parent_bone"; + skeleton->set_bone_meta(0, "extras", parent_bone_extras); + + skeleton->add_bone("child"); + skeleton->set_bone_rest(1, Transform3D()); + skeleton->set_bone_parent(1, 0); + Dictionary child_bone_extras; + child_bone_extras["bone"] = "i_am_child_bone"; + skeleton->set_bone_meta(1, "extras", child_bone_extras); + + // We have to have a mesh to link with skeleton or it will not get imported. + Ref meshdata = memnew(PlaneMesh); + meshdata->set_name("planemesh"); + + MeshInstance3D *mesh = memnew(MeshInstance3D); + mesh->set_mesh(meshdata); + mesh->set_name("mesh_instance_3d"); + + Node3D *scene = memnew(Node3D); + SceneTree::get_singleton()->get_root()->add_child(scene); + scene->add_child(skeleton); + scene->add_child(mesh); + scene->set_name("node3d"); + + // Now that both skeleton and mesh are part of scene, link them. + mesh->set_skeleton_path(mesh->get_path_to(skeleton)); + + // Convert to GLFT and back. + String tempfile = OS::get_singleton()->get_cache_path().path_join("gltf_bone_extras"); + Node *loaded = _gltf_export_then_import(scene, tempfile); + + // Compare the results. + CHECK(loaded->get_name() == "node3d"); + Skeleton3D *result = Object::cast_to(loaded->find_child("Skeleton3D", false, true)); + CHECK(result->get_bone_name(0) == "parent"); + CHECK(Dictionary(result->get_bone_meta(0, "extras"))["bone"] == "i_am_parent_bone"); + CHECK(result->get_bone_name(1) == "child"); + CHECK(Dictionary(result->get_bone_meta(1, "extras"))["bone"] == "i_am_child_bone"); + + memdelete(skeleton); + memdelete(mesh); + memdelete(scene); + memdelete(loaded); +} } // namespace TestGltfExtras #endif // TOOLS_ENABLED diff --git a/scene/3d/skeleton_3d.cpp b/scene/3d/skeleton_3d.cpp index c6ece84cdde..db9c4db30d0 100644 --- a/scene/3d/skeleton_3d.cpp +++ b/scene/3d/skeleton_3d.cpp @@ -103,6 +103,8 @@ bool Skeleton3D::_set(const StringName &p_path, const Variant &p_value) { set_bone_pose_rotation(which, p_value); } else if (what == "scale") { set_bone_pose_scale(which, p_value); + } else if (what == "bone_meta") { + set_bone_meta(which, path.get_slicec('/', 3), p_value); #ifndef DISABLE_DEPRECATED } else if (what == "pose" || what == "bound_children") { // Kept for compatibility from 3.x to 4.x. @@ -170,6 +172,8 @@ bool Skeleton3D::_get(const StringName &p_path, Variant &r_ret) const { r_ret = get_bone_pose_rotation(which); } else if (what == "scale") { r_ret = get_bone_pose_scale(which); + } else if (what == "bone_meta") { + r_ret = get_bone_meta(which, path.get_slicec('/', 3)); } else { return false; } @@ -187,6 +191,11 @@ void Skeleton3D::_get_property_list(List *p_list) const { p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("position"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); p_list->push_back(PropertyInfo(Variant::QUATERNION, prep + PNAME("rotation"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); p_list->push_back(PropertyInfo(Variant::VECTOR3, prep + PNAME("scale"), PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR)); + + for (const KeyValue &K : bones[i].metadata) { + PropertyInfo pi = PropertyInfo(bones[i].metadata[K.key].get_type(), prep + PNAME("bone_meta/") + K.key, PROPERTY_HINT_NONE, "", PROPERTY_USAGE_NO_EDITOR); + p_list->push_back(pi); + } } for (PropertyInfo &E : *p_list) { @@ -531,6 +540,57 @@ void Skeleton3D::set_bone_name(int p_bone, const String &p_name) { version++; } +Variant Skeleton3D::get_bone_meta(int p_bone, const StringName &p_key) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX_V(p_bone, bone_size, Variant()); + + if (!bones[p_bone].metadata.has(p_key)) { + return Variant(); + } + return bones[p_bone].metadata[p_key]; +} + +TypedArray Skeleton3D::_get_bone_meta_list_bind(int p_bone) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX_V(p_bone, bone_size, TypedArray()); + + TypedArray _metaret; + for (const KeyValue &K : bones[p_bone].metadata) { + _metaret.push_back(K.key); + } + return _metaret; +} + +void Skeleton3D::get_bone_meta_list(int p_bone, List *p_list) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX(p_bone, bone_size); + + for (const KeyValue &K : bones[p_bone].metadata) { + p_list->push_back(K.key); + } +} + +bool Skeleton3D::has_bone_meta(int p_bone, const StringName &p_key) const { + const int bone_size = bones.size(); + ERR_FAIL_INDEX_V(p_bone, bone_size, false); + + return bones[p_bone].metadata.has(p_key); +} + +void Skeleton3D::set_bone_meta(int p_bone, const StringName &p_key, const Variant &p_value) { + const int bone_size = bones.size(); + ERR_FAIL_INDEX(p_bone, bone_size); + + if (p_value.get_type() == Variant::NIL) { + if (bones.write[p_bone].metadata.has(p_key)) { + bones.write[p_bone].metadata.erase(p_key); + } + return; + } + + bones.write[p_bone].metadata.insert(p_key, p_value, false); +} + bool Skeleton3D::is_bone_parent_of(int p_bone, int p_parent_bone_id) const { int parent_of_bone = get_bone_parent(p_bone); @@ -1014,6 +1074,11 @@ void Skeleton3D::_bind_methods() { ClassDB::bind_method(D_METHOD("get_bone_name", "bone_idx"), &Skeleton3D::get_bone_name); ClassDB::bind_method(D_METHOD("set_bone_name", "bone_idx", "name"), &Skeleton3D::set_bone_name); + ClassDB::bind_method(D_METHOD("get_bone_meta", "bone_idx", "key"), &Skeleton3D::get_bone_meta); + ClassDB::bind_method(D_METHOD("get_bone_meta_list", "bone_idx"), &Skeleton3D::_get_bone_meta_list_bind); + ClassDB::bind_method(D_METHOD("has_bone_meta", "bone_idx", "key"), &Skeleton3D::has_bone_meta); + ClassDB::bind_method(D_METHOD("set_bone_meta", "bone_idx", "key", "value"), &Skeleton3D::set_bone_meta); + ClassDB::bind_method(D_METHOD("get_concatenated_bone_names"), &Skeleton3D::get_concatenated_bone_names); ClassDB::bind_method(D_METHOD("get_bone_parent", "bone_idx"), &Skeleton3D::get_bone_parent); diff --git a/scene/3d/skeleton_3d.h b/scene/3d/skeleton_3d.h index a009383f45c..07bdeccf2f7 100644 --- a/scene/3d/skeleton_3d.h +++ b/scene/3d/skeleton_3d.h @@ -116,6 +116,8 @@ private: } } + HashMap metadata; + #ifndef DISABLE_DEPRECATED Transform3D pose_global_no_override; real_t global_pose_override_amount = 0.0; @@ -193,6 +195,7 @@ protected: void _get_property_list(List *p_list) const; void _validate_property(PropertyInfo &p_property) const; void _notification(int p_what); + TypedArray _get_bone_meta_list_bind(int p_bone) const; static void _bind_methods(); virtual void add_child_notify(Node *p_child) override; @@ -238,6 +241,12 @@ public: void set_motion_scale(float p_motion_scale); float get_motion_scale() const; + // bone metadata + Variant get_bone_meta(int p_bone, const StringName &p_key) const; + void get_bone_meta_list(int p_bone, List *p_list) const; + bool has_bone_meta(int p_bone, const StringName &p_key) const; + void set_bone_meta(int p_bone, const StringName &p_key, const Variant &p_value); + // Posing API Transform3D get_bone_pose(int p_bone) const; Vector3 get_bone_pose_position(int p_bone) const; diff --git a/tests/scene/test_skeleton_3d.h b/tests/scene/test_skeleton_3d.h new file mode 100644 index 00000000000..b5cf49c4eba --- /dev/null +++ b/tests/scene/test_skeleton_3d.h @@ -0,0 +1,78 @@ +/**************************************************************************/ +/* test_skeleton_3d.h */ +/**************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/**************************************************************************/ +/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ +/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ +/* */ +/* Permission is hereby granted, free of charge, to any person obtaining */ +/* a copy of this software and associated documentation files (the */ +/* "Software"), to deal in the Software without restriction, including */ +/* without limitation the rights to use, copy, modify, merge, publish, */ +/* distribute, sublicense, and/or sell copies of the Software, and to */ +/* permit persons to whom the Software is furnished to do so, subject to */ +/* the following conditions: */ +/* */ +/* The above copyright notice and this permission notice shall be */ +/* included in all copies or substantial portions of the Software. */ +/* */ +/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ +/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ +/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ +/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ +/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ +/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ +/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +/**************************************************************************/ + +#ifndef TEST_SKELETON_3D_H +#define TEST_SKELETON_3D_H + +#include "tests/test_macros.h" + +#include "scene/3d/skeleton_3d.h" + +namespace TestSkeleton3D { + +TEST_CASE("[Skeleton3D] Test per-bone meta") { + Skeleton3D *skeleton = memnew(Skeleton3D); + skeleton->add_bone("root"); + skeleton->set_bone_rest(0, Transform3D()); + + // Adding meta to bone. + skeleton->set_bone_meta(0, "key1", "value1"); + skeleton->set_bone_meta(0, "key2", 12345); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key1") == "value1", "Bone meta missing."); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key2") == Variant(12345), "Bone meta missing."); + + // Rename bone and check if meta persists. + skeleton->set_bone_name(0, "renamed_root"); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key1") == "value1", "Bone meta missing."); + CHECK_MESSAGE(skeleton->get_bone_meta(0, "key2") == Variant(12345), "Bone meta missing."); + + // Retrieve list of keys. + List keys; + skeleton->get_bone_meta_list(0, &keys); + CHECK_MESSAGE(keys.size() == 2, "Wrong number of bone meta keys."); + CHECK_MESSAGE(keys.find("key1"), "key1 not found in bone meta list"); + CHECK_MESSAGE(keys.find("key2"), "key2 not found in bone meta list"); + + // Removing meta. + skeleton->set_bone_meta(0, "key1", Variant()); + skeleton->set_bone_meta(0, "key2", Variant()); + CHECK_MESSAGE(!skeleton->has_bone_meta(0, "key1"), "Bone meta key1 should be deleted."); + CHECK_MESSAGE(!skeleton->has_bone_meta(0, "key2"), "Bone meta key2 should be deleted."); + List should_be_empty_keys; + skeleton->get_bone_meta_list(0, &should_be_empty_keys); + CHECK_MESSAGE(should_be_empty_keys.size() == 0, "Wrong number of bone meta keys."); + + // Deleting non-existing key should succeed. + skeleton->set_bone_meta(0, "non-existing-key", Variant()); + memdelete(skeleton); +} +} // namespace TestSkeleton3D + +#endif // TEST_SKELETON_3D_H diff --git a/tests/test_main.cpp b/tests/test_main.cpp index 2b6461e9cae..949e4f0b330 100644 --- a/tests/test_main.cpp +++ b/tests/test_main.cpp @@ -160,6 +160,7 @@ #include "tests/scene/test_path_3d.h" #include "tests/scene/test_path_follow_3d.h" #include "tests/scene/test_primitives.h" +#include "tests/scene/test_skeleton_3d.h" #endif // _3D_DISABLED #include "modules/modules_tests.gen.h"