From 06b114b3b5f7a2d046937d3e9f2eade969278a2a Mon Sep 17 00:00:00 2001 From: Luke Ingram Date: Tue, 17 Mar 2026 16:24:53 -0400 Subject: [PATCH] [godot] Resolves #3036, adds GDExtension implementation of animation mixes inspector panel --- spine-godot/spine_godot/SpineCommon.h | 1 + spine-godot/spine_godot/SpineEditorPlugin.cpp | 210 +++++++++++++++++- spine-godot/spine_godot/SpineEditorPlugin.h | 22 ++ .../spine_godot/SpineSkeletonDataResource.cpp | 1 + spine-godot/spine_godot/register_types.cpp | 1 + 5 files changed, 232 insertions(+), 3 deletions(-) diff --git a/spine-godot/spine_godot/SpineCommon.h b/spine-godot/spine_godot/SpineCommon.h index 29e8e6ff8..0c1811000 100644 --- a/spine-godot/spine_godot/SpineCommon.h +++ b/spine-godot/spine_godot/SpineCommon.h @@ -32,6 +32,7 @@ #ifdef SPINE_GODOT_EXTENSION #include +#include // When running scons with deprecated=no, these are not defined in version.h in Godot 4.5.1 // but our code for older versions of Godot relies on them. diff --git a/spine-godot/spine_godot/SpineEditorPlugin.cpp b/spine-godot/spine_godot/SpineEditorPlugin.cpp index 9e4b7ac96..0b4902052 100644 --- a/spine-godot/spine_godot/SpineEditorPlugin.cpp +++ b/spine-godot/spine_godot/SpineEditorPlugin.cpp @@ -35,6 +35,11 @@ #if VERSION_MAJOR > 3 #ifdef SPINE_GODOT_EXTENSION #include +#include +#include +#include +#include +#include #else #include "editor/editor_undo_redo_manager.h" #endif @@ -225,7 +230,6 @@ bool SpineSkeletonDataResourceInspectorPlugin::parse_property(Object *object, co bool SpineSkeletonDataResourceInspectorPlugin::parse_property(Object *object, Variant::Type type, const String &path, PropertyHint hint, const String &hint_text, int usage) { #endif -// FIXME can't do this in godot-cpp #ifndef SPINE_GODOT_EXTENSION if (path == "animation_mixes") { Ref skeleton_data = Object::cast_to(object); @@ -235,11 +239,19 @@ bool SpineSkeletonDataResourceInspectorPlugin::parse_property(Object *object, Va add_property_editor(path, mixes_property); return true; } +#else + if (path == "animation_mixes") { + Ref skeleton_data = Object::cast_to(object); + if (!skeleton_data.is_valid() || !skeleton_data->is_skeleton_data_loaded()) return true; + auto mixes_property = memnew(SpineEditorPropertyAnimationMixes); + mixes_property->setup(skeleton_data); + add_property_editor(path, mixes_property); + return true; + } #endif return false; } -// FIXME can't do this in godot-cpp #ifndef SPINE_GODOT_EXTENSION SpineEditorPropertyAnimationMixes::SpineEditorPropertyAnimationMixes() : skeleton_data(nullptr), container(nullptr), updating(false) { INSTANTIATE(array_object); @@ -471,6 +483,198 @@ void SpineEditorPropertyAnimationMix::update_property() { updating = false; } + +// *** NEW: GDExtension implementation of the animation mixes editor. *** +// Uses plain Godot Controls (OptionButton, SpinBox, Button) instead of +// internal editor classes (EditorPropertyTextEnum, EditorPropertyFloat, +// EditorPropertyArrayObject) which are not exposed to GDExtension. +// Undo/redo is supported via EditorUndoRedoManager, which IS available in godot-cpp. +#else +SpineEditorPropertyAnimationMixes::SpineEditorPropertyAnimationMixes() : container(nullptr), updating(false) { +} + +void SpineEditorPropertyAnimationMixes::_bind_methods() { + ClassDB::bind_method(D_METHOD("rebuild_ui"), &SpineEditorPropertyAnimationMixes::rebuild_ui); +} + +void SpineEditorPropertyAnimationMixes::setup(const Ref &_skeleton_data) { + this->skeleton_data = _skeleton_data; + rebuild_ui(); +} + +void SpineEditorPropertyAnimationMixes::_update_property() { + if (updating) return; + rebuild_ui(); +} + +void SpineEditorPropertyAnimationMixes::rebuild_ui() { + updating = true; + + if (container) { + remove_child(container); + memdelete(container); + container = nullptr; + } + + if (!skeleton_data.is_valid() || !skeleton_data->is_skeleton_data_loaded()) { + updating = false; + return; + } + + container = memnew(VBoxContainer); + add_child(container); + set_bottom_editor(container); + + Array mixes = skeleton_data->get_animation_mixes(); + PackedStringArray animation_names; + skeleton_data->get_animation_names(animation_names); + + for (int i = 0; i < mixes.size(); i++) { + Ref mix = mixes[i]; + if (mix.is_null()) continue; + + auto hbox = memnew(HBoxContainer); + hbox->set_h_size_flags(SIZE_EXPAND_FILL); + container->add_child(hbox); + + auto from_option = memnew(OptionButton); + from_option->set_h_size_flags(SIZE_EXPAND_FILL); + for (int j = 0; j < animation_names.size(); j++) { + from_option->add_item(animation_names[j]); + if (animation_names[j] == mix->get_from()) { + from_option->select(j); + } + } + + from_option->connect("item_selected", callable_mp(this, &SpineEditorPropertyAnimationMixes::on_from_changed).bind(i), CONNECT_DEFERRED); + hbox->add_child(from_option); + + auto to_option = memnew(OptionButton); + to_option->set_h_size_flags(SIZE_EXPAND_FILL); + for (int j = 0; j < animation_names.size(); j++) { + to_option->add_item(animation_names[j]); + if (animation_names[j] == mix->get_to()) { + to_option->select(j); + } + } + to_option->connect("item_selected", callable_mp(this, &SpineEditorPropertyAnimationMixes::on_to_changed).bind(i), CONNECT_DEFERRED); + hbox->add_child(to_option); + + auto spin_box = memnew(SpinBox); + spin_box->set_h_size_flags(SIZE_EXPAND_FILL); + spin_box->set_min(0.0); + spin_box->set_max(9999.0); + spin_box->set_step(0.01); + spin_box->set_value(mix->get_mix()); + spin_box->connect("value_changed", callable_mp(this, &SpineEditorPropertyAnimationMixes::on_mix_value_changed).bind(i), CONNECT_DEFERRED); + hbox->add_child(spin_box); + + auto delete_button = memnew(Button); + delete_button->set_text("Remove"); + delete_button->connect("pressed", callable_mp(this, &SpineEditorPropertyAnimationMixes::delete_mix).bind(i), CONNECT_DEFERRED); + hbox->add_child(delete_button); + } + + auto add_mix_button = memnew(Button); + add_mix_button->set_text("Add mix"); + add_mix_button->connect("pressed", callable_mp(this, &SpineEditorPropertyAnimationMixes::add_mix), CONNECT_DEFERRED); + container->add_child(add_mix_button); + + updating = false; +} + +void SpineEditorPropertyAnimationMixes::add_mix() { + if (updating || !skeleton_data.is_valid() || !skeleton_data->is_skeleton_data_loaded()) return; + + PackedStringArray animation_names; + skeleton_data->get_animation_names(animation_names); + + Ref mix = Ref(memnew(SpineAnimationMix)); + mix->set_from(animation_names[0]); + mix->set_to(animation_names[0]); + mix->set_mix(0); + + Array mixes = skeleton_data->get_animation_mixes().duplicate(); + mixes.push_back(mix); + emit_changed(get_edited_property(), mixes); +} + +void SpineEditorPropertyAnimationMixes::delete_mix(int idx) { + if (updating || !skeleton_data.is_valid() || !skeleton_data->is_skeleton_data_loaded()) return; + + Array mixes = skeleton_data->get_animation_mixes().duplicate(); + mixes.remove_at(idx); + emit_changed(get_edited_property(), mixes); +} + +void SpineEditorPropertyAnimationMixes::on_from_changed(int option_idx, int mix_idx) { + if (updating) return; + + PackedStringArray animation_names; + skeleton_data->get_animation_names(animation_names); + String new_value = animation_names[option_idx]; + + Array mixes = skeleton_data->get_animation_mixes(); + Ref mix = mixes[mix_idx]; + String old_value = mix->get_from(); + + auto undo_redo = EditorInterface::get_singleton()->get_editor_undo_redo(); + undo_redo->create_action("Set mix from animation"); + undo_redo->add_do_property(mix.ptr(), "from", new_value); + undo_redo->add_undo_property(mix.ptr(), "from", old_value); + undo_redo->add_do_method(this, "rebuild_ui"); + undo_redo->add_undo_method(this, "rebuild_ui"); + updating = true; + undo_redo->commit_action(); + updating = false; + + emit_changed(get_edited_property(), skeleton_data->get_animation_mixes()); +} + +void SpineEditorPropertyAnimationMixes::on_to_changed(int option_idx, int mix_idx) { + if (updating) return; + + PackedStringArray animation_names; + skeleton_data->get_animation_names(animation_names); + String new_value = animation_names[option_idx]; + + Array mixes = skeleton_data->get_animation_mixes(); + Ref mix = mixes[mix_idx]; + String old_value = mix->get_to(); + + auto undo_redo = EditorInterface::get_singleton()->get_editor_undo_redo(); + undo_redo->create_action("Set mix to animation"); + undo_redo->add_do_property(mix.ptr(), "to", new_value); + undo_redo->add_undo_property(mix.ptr(), "to", old_value); + undo_redo->add_do_method(this, "rebuild_ui"); + undo_redo->add_undo_method(this, "rebuild_ui"); + updating = true; + undo_redo->commit_action(); + updating = false; + + emit_changed(get_edited_property(), skeleton_data->get_animation_mixes()); +} + +void SpineEditorPropertyAnimationMixes::on_mix_value_changed(float value, int mix_idx) { + if (updating) return; + + Array mixes = skeleton_data->get_animation_mixes(); + Ref mix = mixes[mix_idx]; + float old_value = mix->get_mix(); + + auto undo_redo = EditorInterface::get_singleton()->get_editor_undo_redo(); + undo_redo->create_action("Set mix duration"); + undo_redo->add_do_property(mix.ptr(), "mix", value); + undo_redo->add_undo_property(mix.ptr(), "mix", old_value); + undo_redo->add_do_method(this, "rebuild_ui"); + undo_redo->add_undo_method(this, "rebuild_ui"); + updating = true; + undo_redo->commit_action(); + updating = false; + + emit_changed(get_edited_property(), skeleton_data->get_animation_mixes()); +} + #endif void SpineSpriteInspectorPlugin::_bind_methods() { @@ -498,4 +702,4 @@ void SpineSpriteInspectorPlugin::parse_begin(Object *object) { if (!sprite->get_skeleton_data_res().is_valid() || !sprite->get_skeleton_data_res()->is_skeleton_data_loaded()) return; } -#endif +#endif \ No newline at end of file diff --git a/spine-godot/spine_godot/SpineEditorPlugin.h b/spine-godot/spine_godot/SpineEditorPlugin.h index 37493824e..1382cfc00 100644 --- a/spine-godot/spine_godot/SpineEditorPlugin.h +++ b/spine-godot/spine_godot/SpineEditorPlugin.h @@ -34,6 +34,7 @@ #if VERSION_MAJOR > 3 #ifdef SPINE_GODOT_EXTENSION #include +#include #else #include "editor/import/editor_import_plugin.h" #endif @@ -371,6 +372,27 @@ public: void setup(SpineEditorPropertyAnimationMixes *mixes_property, const Ref &skeleton_data, int index); void update_property() override; }; +#else +class SpineEditorPropertyAnimationMixes : public EditorProperty { + GDCLASS(SpineEditorPropertyAnimationMixes, EditorProperty) + + Ref skeleton_data; + VBoxContainer *container; + bool updating; + + static void _bind_methods(); + void rebuild_ui(); + void add_mix(); + void delete_mix(int idx); + void on_from_changed(int option_idx, int mix_idx); + void on_to_changed(int option_idx, int mix_idx); + void on_mix_value_changed(float value, int mix_idx); + +public: + SpineEditorPropertyAnimationMixes(); + void setup(const Ref &skeletonData); + void _update_property() override; +}; #endif class SpineSpriteInspectorPlugin : public EditorInspectorPlugin { diff --git a/spine-godot/spine_godot/SpineSkeletonDataResource.cpp b/spine-godot/spine_godot/SpineSkeletonDataResource.cpp index f4e6ac273..9d37e0c7c 100644 --- a/spine-godot/spine_godot/SpineSkeletonDataResource.cpp +++ b/spine-godot/spine_godot/SpineSkeletonDataResource.cpp @@ -505,6 +505,7 @@ void SpineSkeletonDataResource::update_mixes() { animation_state_data->setDefaultMix(default_mix); for (int i = 0; i < animation_mixes.size(); i++) { Ref mix = animation_mixes[i]; + if (mix.is_null()) continue; spine::Animation *from = skeleton_data->findAnimation(mix->get_from().utf8().ptr()); spine::Animation *to = diff --git a/spine-godot/spine_godot/register_types.cpp b/spine-godot/spine_godot/register_types.cpp index c3eebd6c5..94aca4b7c 100644 --- a/spine-godot/spine_godot/register_types.cpp +++ b/spine-godot/spine_godot/register_types.cpp @@ -84,6 +84,7 @@ void initialize_spine_godot_module(ModuleInitializationLevel level) { GDREGISTER_CLASS(SpineJsonResourceImportPlugin); GDREGISTER_CLASS(SpineBinaryResourceImportPlugin); GDREGISTER_CLASS(SpineSkeletonDataResourceInspectorPlugin); + GDREGISTER_CLASS(SpineEditorPropertyAnimationMixes); GDREGISTER_CLASS(SpineEditorPlugin); EditorPlugins::add_plugin_class(StringName("SpineEditorPlugin")); #endif