[flutter] handle implicit enum values in codegen, extension for animation state listeners + test

This commit is contained in:
Mario Zechner 2025-07-29 22:52:06 +02:00
parent 71eb86c15e
commit 0e5de94529
18 changed files with 314 additions and 51 deletions

View File

@ -212,9 +212,10 @@ export class DartWriter {
private transformEnum (cEnum: CEnum): DartEnum { private transformEnum (cEnum: CEnum): DartEnum {
return { return {
name: this.toDartTypeName(cEnum.name), name: this.toDartTypeName(cEnum.name),
values: cEnum.values.map((value) => ({ values: cEnum.values.map((value, index) => ({
name: this.toDartEnumValueName(value.name, cEnum.name), 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
})) }))
}; };
} }

View File

@ -32,12 +32,12 @@
/// AttachmentType enum /// AttachmentType enum
enum AttachmentType { enum AttachmentType {
region(0), region(0),
boundingbox(0), boundingbox(1),
mesh(0), mesh(2),
linkedmesh(0), linkedmesh(3),
path(0), path(4),
point(0), point(5),
clipping(0); clipping(6);
const AttachmentType(this.value); const AttachmentType(this.value);
final int value; final int value;

View File

@ -32,9 +32,9 @@
/// BlendMode enum /// BlendMode enum
enum BlendMode { enum BlendMode {
normal(0), normal(0),
additive(0), additive(1),
multiply(0), multiply(2),
screen(0); screen(3);
const BlendMode(this.value); const BlendMode(this.value);
final int value; final int value;

View File

@ -32,11 +32,11 @@
/// EventType enum /// EventType enum
enum EventType { enum EventType {
start(0), start(0),
interrupt(0), interrupt(1),
end(0), end(2),
dispose(0), dispose(3),
complete(0), complete(4),
event(0); event(5);
const EventType(this.value); const EventType(this.value);
final int value; final int value;

View File

@ -32,12 +32,12 @@
/// Format enum /// Format enum
enum Format { enum Format {
alpha(0), alpha(0),
intensity(0), intensity(1),
luminanceAlpha(0), luminanceAlpha(2),
rgb565(0), rgb565(3),
rgba4444(0), rgba4444(4),
rgb888(0), rgb888(5),
rgba8888(0); rgba8888(6);
const Format(this.value); const Format(this.value);
final int value; final int value;

View File

@ -32,10 +32,10 @@
/// Inherit enum /// Inherit enum
enum Inherit { enum Inherit {
normal(0), normal(0),
onlyTranslation(0), onlyTranslation(1),
noRotationOrReflection(0), noRotationOrReflection(2),
noScale(0), noScale(3),
noScaleOrReflection(0); noScaleOrReflection(4);
const Inherit(this.value); const Inherit(this.value);
final int value; final int value;

View File

@ -32,9 +32,9 @@
/// MixBlend enum /// MixBlend enum
enum MixBlend { enum MixBlend {
setup(0), setup(0),
first(0), first(1),
replace(0), replace(2),
add(0); add(3);
const MixBlend(this.value); const MixBlend(this.value);
final int value; final int value;

View File

@ -32,7 +32,7 @@
/// MixDirection enum /// MixDirection enum
enum MixDirection { enum MixDirection {
directionIn(0), directionIn(0),
directionOut(0); directionOut(1);
const MixDirection(this.value); const MixDirection(this.value);
final int value; final int value;

View File

@ -32,9 +32,9 @@
/// Physics enum /// Physics enum
enum Physics { enum Physics {
none(0), none(0),
reset(0), reset(1),
update(0), update(2),
pose(0); pose(3);
const Physics(this.value); const Physics(this.value);
final int value; final int value;

View File

@ -32,7 +32,7 @@
/// PositionMode enum /// PositionMode enum
enum PositionMode { enum PositionMode {
fixed(0), fixed(0),
percent(0); percent(1);
const PositionMode(this.value); const PositionMode(this.value);
final int value; final int value;

View File

@ -32,8 +32,8 @@
/// RotateMode enum /// RotateMode enum
enum RotateMode { enum RotateMode {
tangent(0), tangent(0),
chain(0), chain(1),
chainScale(0); chainScale(2);
const RotateMode(this.value); const RotateMode(this.value);
final int value; final int value;

View File

@ -32,9 +32,9 @@
/// SpacingMode enum /// SpacingMode enum
enum SpacingMode { enum SpacingMode {
length(0), length(0),
fixed(0), fixed(1),
percent(0), percent(2),
proportional(0); proportional(3);
const SpacingMode(this.value); const SpacingMode(this.value);
final int value; final int value;

View File

@ -6749,6 +6749,83 @@ class SpineDartBindings {
_spine_skeleton_drawable_get_animation_state_eventsPtr _spine_skeleton_drawable_get_animation_state_eventsPtr
.asFunction<spine_animation_state_events Function(spine_skeleton_drawable)>(); .asFunction<spine_animation_state_events Function(spine_skeleton_drawable)>();
/// 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<ffi.NativeFunction<ffi.Int32 Function(spine_animation_state_events)>>(
'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 Function(spine_animation_state_events)>();
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<ffi.NativeFunction<ffi.Int32 Function(spine_animation_state_events, ffi.Int32)>>(
'spine_animation_state_events_get_event_type');
late final _spine_animation_state_events_get_event_type =
_spine_animation_state_events_get_event_typePtr.asFunction<int Function(spine_animation_state_events, int)>();
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<ffi.NativeFunction<spine_track_entry Function(spine_animation_state_events, ffi.Int32)>>(
'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_track_entry Function(spine_animation_state_events, int)>();
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<ffi.NativeFunction<spine_event Function(spine_animation_state_events, ffi.Int32)>>(
'spine_animation_state_events_get_event');
late final _spine_animation_state_events_get_event =
_spine_animation_state_events_get_eventPtr.asFunction<spine_event Function(spine_animation_state_events, int)>();
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<ffi.NativeFunction<ffi.Void Function(spine_animation_state_events)>>(
'spine_animation_state_events_reset');
late final _spine_animation_state_events_reset =
_spine_animation_state_events_resetPtr.asFunction<void Function(spine_animation_state_events)>();
/// Skin functions /// Skin functions
spine_skin_entries spine_skin_get_entries( spine_skin_entries spine_skin_get_entries(
spine_skin skin, spine_skin skin,

View File

@ -32,13 +32,13 @@
/// TextureFilter enum /// TextureFilter enum
enum TextureFilter { enum TextureFilter {
unknown(0), unknown(0),
nearest(0), nearest(1),
linear(0), linear(2),
mipMap(0), mipMap(3),
mipMapNearestNearest(0), mipMapNearestNearest(4),
mipMapLinearNearest(0), mipMapLinearNearest(5),
mipMapNearestLinear(0), mipMapNearestLinear(6),
mipMapLinearLinear(0); mipMapLinearLinear(7);
const TextureFilter(this.value); const TextureFilter(this.value);
final int value; final int value;

View File

@ -32,8 +32,8 @@
/// TextureWrap enum /// TextureWrap enum
enum TextureWrap { enum TextureWrap {
mirroredRepeat(0), mirroredRepeat(0),
clampToEdge(0), clampToEdge(1),
repeat(0); repeat(2);
const TextureWrap(this.value); const TextureWrap(this.value);
final int value; final int value;

View File

@ -43,6 +43,12 @@ import 'generated/clipping_attachment.dart';
import 'generated/path_attachment.dart'; import 'generated/path_attachment.dart';
import 'generated/point_attachment.dart'; import 'generated/point_attachment.dart';
import 'generated/rtti.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 classes
export 'generated/api.dart'; export 'generated/api.dart';
@ -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<spine_skeleton_drawable_wrapper> _drawable;
final Map<TrackEntry, AnimationStateListener> _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);
}
}

View File

@ -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 = <String>[];
// 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');
}