[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 {
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
}))
};
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

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

View File

@ -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;

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -6749,6 +6749,83 @@ class SpineDartBindings {
_spine_skeleton_drawable_get_animation_state_eventsPtr
.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
spine_skin_entries spine_skin_get_entries(
spine_skin skin,

View File

@ -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;

View File

@ -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;

View File

@ -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<Utf8>().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<Utf8>().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<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

@ -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];

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');
}