From 0e5de94529bad7a7ede2e1af849d598ab9b09ea7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Tue, 29 Jul 2025 22:52:06 +0200 Subject: [PATCH] [flutter] handle implicit enum values in codegen, extension for animation state listeners + test --- spine-flutter/codegen/src/dart-writer.ts | 5 +- .../lib/generated/attachment_type.dart | 12 +- spine-flutter/lib/generated/blend_mode.dart | 6 +- spine-flutter/lib/generated/event_type.dart | 10 +- spine-flutter/lib/generated/format.dart | 12 +- spine-flutter/lib/generated/inherit.dart | 8 +- spine-flutter/lib/generated/mix_blend.dart | 6 +- .../lib/generated/mix_direction.dart | 2 +- spine-flutter/lib/generated/physics.dart | 6 +- .../lib/generated/position_mode.dart | 2 +- spine-flutter/lib/generated/rotate_mode.dart | 4 +- spine-flutter/lib/generated/spacing_mode.dart | 6 +- .../spine_dart_bindings_generated.dart | 77 ++++++++++++ .../lib/generated/texture_filter.dart | 14 +-- spine-flutter/lib/generated/texture_wrap.dart | 4 +- spine-flutter/lib/spine_dart.dart | 115 +++++++++++++++++- spine-flutter/test/headless_test.dart | 2 +- .../test/skeleton_drawable_test.dart | 74 +++++++++++ 18 files changed, 314 insertions(+), 51 deletions(-) create mode 100644 spine-flutter/test/skeleton_drawable_test.dart diff --git a/spine-flutter/codegen/src/dart-writer.ts b/spine-flutter/codegen/src/dart-writer.ts index 443bfab86..08a36d5d0 100644 --- a/spine-flutter/codegen/src/dart-writer.ts +++ b/spine-flutter/codegen/src/dart-writer.ts @@ -212,9 +212,10 @@ export class DartWriter { private transformEnum (cEnum: CEnum): DartEnum { return { name: this.toDartTypeName(cEnum.name), - values: cEnum.values.map((value) => ({ + values: cEnum.values.map((value, index) => ({ name: this.toDartEnumValueName(value.name, cEnum.name), - value: Number.parseInt(value.value ?? "0") + // C enums without explicit values are implicitly numbered 0, 1, 2, etc. + value: value.value !== undefined ? Number.parseInt(value.value) : index })) }; } diff --git a/spine-flutter/lib/generated/attachment_type.dart b/spine-flutter/lib/generated/attachment_type.dart index 3aef91f6f..40595712c 100644 --- a/spine-flutter/lib/generated/attachment_type.dart +++ b/spine-flutter/lib/generated/attachment_type.dart @@ -32,12 +32,12 @@ /// AttachmentType enum enum AttachmentType { region(0), - boundingbox(0), - mesh(0), - linkedmesh(0), - path(0), - point(0), - clipping(0); + boundingbox(1), + mesh(2), + linkedmesh(3), + path(4), + point(5), + clipping(6); const AttachmentType(this.value); final int value; diff --git a/spine-flutter/lib/generated/blend_mode.dart b/spine-flutter/lib/generated/blend_mode.dart index cfc2e8aea..d148e5154 100644 --- a/spine-flutter/lib/generated/blend_mode.dart +++ b/spine-flutter/lib/generated/blend_mode.dart @@ -32,9 +32,9 @@ /// BlendMode enum enum BlendMode { normal(0), - additive(0), - multiply(0), - screen(0); + additive(1), + multiply(2), + screen(3); const BlendMode(this.value); final int value; diff --git a/spine-flutter/lib/generated/event_type.dart b/spine-flutter/lib/generated/event_type.dart index f8cb465f0..65543fc64 100644 --- a/spine-flutter/lib/generated/event_type.dart +++ b/spine-flutter/lib/generated/event_type.dart @@ -32,11 +32,11 @@ /// EventType enum enum EventType { start(0), - interrupt(0), - end(0), - dispose(0), - complete(0), - event(0); + interrupt(1), + end(2), + dispose(3), + complete(4), + event(5); const EventType(this.value); final int value; diff --git a/spine-flutter/lib/generated/format.dart b/spine-flutter/lib/generated/format.dart index 948c2420e..8df125ff6 100644 --- a/spine-flutter/lib/generated/format.dart +++ b/spine-flutter/lib/generated/format.dart @@ -32,12 +32,12 @@ /// Format enum enum Format { alpha(0), - intensity(0), - luminanceAlpha(0), - rgb565(0), - rgba4444(0), - rgb888(0), - rgba8888(0); + intensity(1), + luminanceAlpha(2), + rgb565(3), + rgba4444(4), + rgb888(5), + rgba8888(6); const Format(this.value); final int value; diff --git a/spine-flutter/lib/generated/inherit.dart b/spine-flutter/lib/generated/inherit.dart index c85483014..cc9e37d48 100644 --- a/spine-flutter/lib/generated/inherit.dart +++ b/spine-flutter/lib/generated/inherit.dart @@ -32,10 +32,10 @@ /// Inherit enum enum Inherit { normal(0), - onlyTranslation(0), - noRotationOrReflection(0), - noScale(0), - noScaleOrReflection(0); + onlyTranslation(1), + noRotationOrReflection(2), + noScale(3), + noScaleOrReflection(4); const Inherit(this.value); final int value; diff --git a/spine-flutter/lib/generated/mix_blend.dart b/spine-flutter/lib/generated/mix_blend.dart index 63bb15e95..ac4a41ae0 100644 --- a/spine-flutter/lib/generated/mix_blend.dart +++ b/spine-flutter/lib/generated/mix_blend.dart @@ -32,9 +32,9 @@ /// MixBlend enum enum MixBlend { setup(0), - first(0), - replace(0), - add(0); + first(1), + replace(2), + add(3); const MixBlend(this.value); final int value; diff --git a/spine-flutter/lib/generated/mix_direction.dart b/spine-flutter/lib/generated/mix_direction.dart index 72c8c3862..b509178e5 100644 --- a/spine-flutter/lib/generated/mix_direction.dart +++ b/spine-flutter/lib/generated/mix_direction.dart @@ -32,7 +32,7 @@ /// MixDirection enum enum MixDirection { directionIn(0), - directionOut(0); + directionOut(1); const MixDirection(this.value); final int value; diff --git a/spine-flutter/lib/generated/physics.dart b/spine-flutter/lib/generated/physics.dart index fa2b5b6cb..4009ee05f 100644 --- a/spine-flutter/lib/generated/physics.dart +++ b/spine-flutter/lib/generated/physics.dart @@ -32,9 +32,9 @@ /// Physics enum enum Physics { none(0), - reset(0), - update(0), - pose(0); + reset(1), + update(2), + pose(3); const Physics(this.value); final int value; diff --git a/spine-flutter/lib/generated/position_mode.dart b/spine-flutter/lib/generated/position_mode.dart index 6d69dbe93..1f7a56b89 100644 --- a/spine-flutter/lib/generated/position_mode.dart +++ b/spine-flutter/lib/generated/position_mode.dart @@ -32,7 +32,7 @@ /// PositionMode enum enum PositionMode { fixed(0), - percent(0); + percent(1); const PositionMode(this.value); final int value; diff --git a/spine-flutter/lib/generated/rotate_mode.dart b/spine-flutter/lib/generated/rotate_mode.dart index 4de24b5dc..42160444c 100644 --- a/spine-flutter/lib/generated/rotate_mode.dart +++ b/spine-flutter/lib/generated/rotate_mode.dart @@ -32,8 +32,8 @@ /// RotateMode enum enum RotateMode { tangent(0), - chain(0), - chainScale(0); + chain(1), + chainScale(2); const RotateMode(this.value); final int value; diff --git a/spine-flutter/lib/generated/spacing_mode.dart b/spine-flutter/lib/generated/spacing_mode.dart index 9521ae372..2d9262849 100644 --- a/spine-flutter/lib/generated/spacing_mode.dart +++ b/spine-flutter/lib/generated/spacing_mode.dart @@ -32,9 +32,9 @@ /// SpacingMode enum enum SpacingMode { length(0), - fixed(0), - percent(0), - proportional(0); + fixed(1), + percent(2), + proportional(3); const SpacingMode(this.value); final int value; diff --git a/spine-flutter/lib/generated/spine_dart_bindings_generated.dart b/spine-flutter/lib/generated/spine_dart_bindings_generated.dart index 02a7cecd6..00043ad41 100644 --- a/spine-flutter/lib/generated/spine_dart_bindings_generated.dart +++ b/spine-flutter/lib/generated/spine_dart_bindings_generated.dart @@ -6749,6 +6749,83 @@ class SpineDartBindings { _spine_skeleton_drawable_get_animation_state_eventsPtr .asFunction(); + /// Animation state events functions + int spine_animation_state_events_get_num_events( + spine_animation_state_events events, + ) { + return _spine_animation_state_events_get_num_events( + events, + ); + } + + late final _spine_animation_state_events_get_num_eventsPtr = + _lookup>( + 'spine_animation_state_events_get_num_events'); + late final _spine_animation_state_events_get_num_events = + _spine_animation_state_events_get_num_eventsPtr.asFunction(); + + int spine_animation_state_events_get_event_type( + spine_animation_state_events events, + int index, + ) { + return _spine_animation_state_events_get_event_type( + events, + index, + ); + } + + late final _spine_animation_state_events_get_event_typePtr = + _lookup>( + 'spine_animation_state_events_get_event_type'); + late final _spine_animation_state_events_get_event_type = + _spine_animation_state_events_get_event_typePtr.asFunction(); + + spine_track_entry spine_animation_state_events_get_track_entry( + spine_animation_state_events events, + int index, + ) { + return _spine_animation_state_events_get_track_entry( + events, + index, + ); + } + + late final _spine_animation_state_events_get_track_entryPtr = + _lookup>( + 'spine_animation_state_events_get_track_entry'); + late final _spine_animation_state_events_get_track_entry = _spine_animation_state_events_get_track_entryPtr + .asFunction(); + + spine_event spine_animation_state_events_get_event( + spine_animation_state_events events, + int index, + ) { + return _spine_animation_state_events_get_event( + events, + index, + ); + } + + late final _spine_animation_state_events_get_eventPtr = + _lookup>( + 'spine_animation_state_events_get_event'); + late final _spine_animation_state_events_get_event = + _spine_animation_state_events_get_eventPtr.asFunction(); + + void spine_animation_state_events_reset( + spine_animation_state_events events, + ) { + return _spine_animation_state_events_reset( + events, + ); + } + + late final _spine_animation_state_events_resetPtr = + _lookup>( + 'spine_animation_state_events_reset'); + late final _spine_animation_state_events_reset = + _spine_animation_state_events_resetPtr.asFunction(); + /// Skin functions spine_skin_entries spine_skin_get_entries( spine_skin skin, diff --git a/spine-flutter/lib/generated/texture_filter.dart b/spine-flutter/lib/generated/texture_filter.dart index 34b7a3099..8ba6c0bea 100644 --- a/spine-flutter/lib/generated/texture_filter.dart +++ b/spine-flutter/lib/generated/texture_filter.dart @@ -32,13 +32,13 @@ /// TextureFilter enum enum TextureFilter { unknown(0), - nearest(0), - linear(0), - mipMap(0), - mipMapNearestNearest(0), - mipMapLinearNearest(0), - mipMapNearestLinear(0), - mipMapLinearLinear(0); + nearest(1), + linear(2), + mipMap(3), + mipMapNearestNearest(4), + mipMapLinearNearest(5), + mipMapNearestLinear(6), + mipMapLinearLinear(7); const TextureFilter(this.value); final int value; diff --git a/spine-flutter/lib/generated/texture_wrap.dart b/spine-flutter/lib/generated/texture_wrap.dart index 108649e6d..72ec94889 100644 --- a/spine-flutter/lib/generated/texture_wrap.dart +++ b/spine-flutter/lib/generated/texture_wrap.dart @@ -32,8 +32,8 @@ /// TextureWrap enum enum TextureWrap { mirroredRepeat(0), - clampToEdge(0), - repeat(0); + clampToEdge(1), + repeat(2); const TextureWrap(this.value); final int value; diff --git a/spine-flutter/lib/spine_dart.dart b/spine-flutter/lib/spine_dart.dart index 1b9c46ebd..5c8d73653 100644 --- a/spine-flutter/lib/spine_dart.dart +++ b/spine-flutter/lib/spine_dart.dart @@ -43,6 +43,12 @@ import 'generated/clipping_attachment.dart'; import 'generated/path_attachment.dart'; import 'generated/point_attachment.dart'; import 'generated/rtti.dart'; +import 'generated/skeleton.dart'; +import 'generated/animation_state.dart'; +import 'generated/animation_state_data.dart'; +import 'generated/track_entry.dart'; +import 'generated/event.dart'; +import 'generated/event_type.dart'; // Export generated classes export 'generated/api.dart'; @@ -166,14 +172,14 @@ extension SkinExtensions on Skin { final slotIndex = SpineBindings.bindings.spine_skin_entry_get_slot_index(entryPtr.cast()); final namePtr = SpineBindings.bindings.spine_skin_entry_get_name(entryPtr.cast()); final name = namePtr.cast().toDartString(); - + final attachmentPtr = SpineBindings.bindings.spine_skin_entry_get_attachment(entryPtr.cast()); Attachment? attachment; if (attachmentPtr.address != 0) { // Use RTTI to determine the concrete attachment type final rtti = SpineBindings.bindings.spine_attachment_get_rtti(attachmentPtr); final className = SpineBindings.bindings.spine_rtti_get_class_name(rtti).cast().toDartString(); - + switch (className) { case 'spine_region_attachment': attachment = RegionAttachment.fromPointer(attachmentPtr.cast()); @@ -213,3 +219,108 @@ extension SkinExtensions on Skin { } } } + +/// Event listener callback for animation state events +typedef AnimationStateListener = void Function(EventType type, TrackEntry entry, Event? event); + +/// Convenient drawable that combines skeleton, animation state, and rendering +class SkeletonDrawable { + final Pointer _drawable; + final Map _trackEntryListeners = {}; + AnimationStateListener? _stateListener; + + late final Skeleton skeleton; + late final AnimationState animationState; + late final AnimationStateData animationStateData; + + SkeletonDrawable(SkeletonData skeletonData) + : _drawable = SpineBindings.bindings.spine_skeleton_drawable_create(skeletonData.nativePtr.cast()) { + if (_drawable == nullptr) { + throw Exception("Failed to create skeleton drawable"); + } + + // Get references to the skeleton and animation state + final skeletonPtr = SpineBindings.bindings.spine_skeleton_drawable_get_skeleton(_drawable.cast()); + skeleton = Skeleton.fromPointer(skeletonPtr); + + final animationStatePtr = SpineBindings.bindings.spine_skeleton_drawable_get_animation_state(_drawable.cast()); + animationState = AnimationState.fromPointer(animationStatePtr); + + final animationStateDataPtr = + SpineBindings.bindings.spine_skeleton_drawable_get_animation_state_data(_drawable.cast()); + animationStateData = AnimationStateData.fromPointer(animationStateDataPtr); + } + + /// Update the animation state and process events + void update(double delta) { + // Update animation state + animationState.update(delta); + + // Process events + final eventsPtr = SpineBindings.bindings.spine_skeleton_drawable_get_animation_state_events(_drawable.cast()); + if (eventsPtr != nullptr) { + final numEvents = SpineBindings.bindings.spine_animation_state_events_get_num_events(eventsPtr.cast()); + + for (int i = 0; i < numEvents; i++) { + // Get event type + final eventTypeValue = SpineBindings.bindings.spine_animation_state_events_get_event_type(eventsPtr.cast(), i); + final type = EventType.fromValue(eventTypeValue); + + // Get track entry + final trackEntryPtr = SpineBindings.bindings.spine_animation_state_events_get_track_entry(eventsPtr.cast(), i); + final trackEntry = TrackEntry.fromPointer(trackEntryPtr); + + // Get event (may be null) + final eventPtr = SpineBindings.bindings.spine_animation_state_events_get_event(eventsPtr.cast(), i); + final event = eventPtr.address == 0 ? null : Event.fromPointer(eventPtr); + + // Call track entry listener if registered + if (_trackEntryListeners.containsKey(trackEntry)) { + _trackEntryListeners[trackEntry]?.call(type, trackEntry, event); + } + + // Call global state listener + _stateListener?.call(type, trackEntry, event); + + // Remove listener if track entry is being disposed + if (type == EventType.dispose) { + _trackEntryListeners.remove(trackEntry); + } + } + + // Reset events for next frame + SpineBindings.bindings.spine_animation_state_events_reset(eventsPtr.cast()); + } + + // Apply animation state to skeleton + animationState.apply(skeleton); + } + + /// Set a listener for all animation state events + void setListener(AnimationStateListener? listener) { + _stateListener = listener; + } + + /// Internal method to set a listener for a specific track entry + void _setTrackEntryListener(TrackEntry entry, AnimationStateListener? listener) { + if (listener == null) { + _trackEntryListeners.remove(entry); + } else { + _trackEntryListeners[entry] = listener; + } + } + + void dispose() { + _trackEntryListeners.clear(); + _stateListener = null; + SpineBindings.bindings.spine_skeleton_drawable_dispose(_drawable.cast()); + } +} + +/// Extension to add setListener to TrackEntry +extension TrackEntryExtensions on TrackEntry { + /// Set a listener for events from this track entry + void setListener(SkeletonDrawable drawable, AnimationStateListener? listener) { + drawable._setTrackEntryListener(this, listener); + } +} diff --git a/spine-flutter/test/headless_test.dart b/spine-flutter/test/headless_test.dart index ffa5ee05c..ae343d8ee 100644 --- a/spine-flutter/test/headless_test.dart +++ b/spine-flutter/test/headless_test.dart @@ -33,7 +33,7 @@ void main() async { if (defaultSkin != null) { final entries = defaultSkin.getEntries(); print('Default skin has ${entries.length} entries'); - + // Print first few entries for (int i = 0; i < 5 && i < entries.length; i++) { final entry = entries[i]; diff --git a/spine-flutter/test/skeleton_drawable_test.dart b/spine-flutter/test/skeleton_drawable_test.dart new file mode 100644 index 000000000..745b5495b --- /dev/null +++ b/spine-flutter/test/skeleton_drawable_test.dart @@ -0,0 +1,74 @@ +import 'dart:io'; +import 'package:spine_flutter/spine_dart.dart'; + +void main() async { + print('Testing SkeletonDrawable and event listeners...'); + + // Initialize with debug extension enabled + await initSpineDart(enableMemoryDebugging: true); + + // Load atlas and skeleton data + final atlasData = File('../example/assets/spineboy.atlas').readAsStringSync(); + final atlas = loadAtlas(atlasData); + final skeletonJson = File('../example/assets/spineboy-pro.json').readAsStringSync(); + final skeletonData = loadSkeletonDataJson(atlas, skeletonJson); + + // Create skeleton drawable + final drawable = SkeletonDrawable(skeletonData); + print('SkeletonDrawable created successfully'); + + // Track events + final events = []; + + // Set global animation state listener + drawable.setListener((type, entry, event) { + final eventName = event?.data.name ?? 'null'; + events.add('State: $type, event: $eventName'); + }); + + // Set an animation + final trackEntry = drawable.animationState.setAnimation(0, 'walk', true); + print('Set animation: walk'); + + // Set track entry specific listener + trackEntry.setListener(drawable, (type, entry, event) { + final eventName = event?.data.name ?? 'null'; + events.add('TrackEntry: $type, event: $eventName'); + }); + + // Update several times to trigger events + print('\nUpdating animation state...'); + for (int i = 0; i < 5; i++) { + drawable.update(0.016); // ~60fps + } + + // Print collected events + print('\nCollected events:'); + for (final event in events) { + print(' $event'); + } + + // Test switching animations + print('\nSwitching to run animation...'); + events.clear(); + drawable.animationState.setAnimation(0, 'run', true); + + // Update a few more times + for (int i = 0; i < 3; i++) { + drawable.update(0.016); + } + + print('\nEvents after switching:'); + for (final event in events) { + print(' $event'); + } + + // Cleanup + drawable.dispose(); + skeletonData.dispose(); + atlas.dispose(); + + // Report memory leaks + reportLeaks(); + print('\nTest complete'); +}