mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
[flutter] macOS/iOS builds, fixed examples wrt new API
This commit is contained in:
parent
83f5bd0e0a
commit
4225214caf
@ -24,7 +24,7 @@ class AnimationStateEvents extends StatelessWidget {
|
|||||||
controller.animationState.setListener((type, trackEntry, event) {
|
controller.animationState.setListener((type, trackEntry, event) {
|
||||||
if (type == EventType.event) {
|
if (type == EventType.event) {
|
||||||
print(
|
print(
|
||||||
"User event: { name: ${event?.getData().getName()}, intValue: ${event?.getIntValue()}, floatValue: ${event?.getFloatValue()}, stringValue: ${event?.getStringValue()} }",
|
"User event: { name: ${event?.data.name}, intValue: ${event?.intValue}, floatValue: ${event?.floatValue}, stringValue: ${event?.stringValue} }",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class DebugRendering extends StatelessWidget {
|
|||||||
const debugRenderer = DebugRenderer();
|
const debugRenderer = DebugRenderer();
|
||||||
final controller = SpineWidgetController(
|
final controller = SpineWidgetController(
|
||||||
onInitialized: (controller) {
|
onInitialized: (controller) {
|
||||||
controller.animationState.setAnimationByName(0, "walk", true);
|
controller.animationState.setAnimation(0, "walk", true);
|
||||||
},
|
},
|
||||||
onBeforePaint: (controller, canvas) {
|
onBeforePaint: (controller, canvas) {
|
||||||
// Save the current transform and other canvas state
|
// Save the current transform and other canvas state
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class DressUp extends StatefulWidget {
|
|||||||
|
|
||||||
class DressUpState extends State<DressUp> {
|
class DressUpState extends State<DressUp> {
|
||||||
static const double thumbnailSize = 200;
|
static const double thumbnailSize = 200;
|
||||||
late SkeletonDrawable _drawable;
|
late SkeletonDrawableFlutter _drawable;
|
||||||
Skin? _customSkin;
|
Skin? _customSkin;
|
||||||
final Map<String, RawImageData> _skinImages = {};
|
final Map<String, RawImageData> _skinImages = {};
|
||||||
final Map<String, bool> _selectedSkins = {};
|
final Map<String, bool> _selectedSkins = {};
|
||||||
@ -50,17 +50,19 @@ class DressUpState extends State<DressUp> {
|
|||||||
void initState() {
|
void initState() {
|
||||||
reportLeaks();
|
reportLeaks();
|
||||||
super.initState();
|
super.initState();
|
||||||
SkeletonDrawable.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel").then((drawable) async {
|
SkeletonDrawableFlutter.fromAsset("assets/mix-and-match.atlas", "assets/mix-and-match-pro.skel")
|
||||||
|
.then((drawable) async {
|
||||||
_drawable = drawable;
|
_drawable = drawable;
|
||||||
for (var skin in drawable.skeletonData.getSkins()) {
|
for (var skin in drawable.skeletonData.skins) {
|
||||||
if (skin.getName() == "default") continue;
|
if (skin == null) continue;
|
||||||
|
if (skin.name == "default") continue;
|
||||||
var skeleton = drawable.skeleton;
|
var skeleton = drawable.skeleton;
|
||||||
skeleton.setSkin(skin);
|
skeleton.setSkin2(skin);
|
||||||
skeleton.setToSetupPose();
|
skeleton.setupPose();
|
||||||
skeleton.update(0);
|
skeleton.update(0);
|
||||||
skeleton.updateWorldTransform(Physics.update);
|
skeleton.updateWorldTransform(Physics.update);
|
||||||
_skinImages[skin.getName()] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize, 0xffffffff);
|
_skinImages[skin.name] = await drawable.renderToRawImageData(thumbnailSize, thumbnailSize, 0xffffffff);
|
||||||
_selectedSkins[skin.getName()] = false;
|
_selectedSkins[skin.name] = false;
|
||||||
}
|
}
|
||||||
_toggleSkin("full-skins/girl");
|
_toggleSkin("full-skins/girl");
|
||||||
setState(() {});
|
setState(() {});
|
||||||
@ -69,7 +71,7 @@ class DressUpState extends State<DressUp> {
|
|||||||
|
|
||||||
void _toggleSkin(String skinName) {
|
void _toggleSkin(String skinName) {
|
||||||
_selectedSkins[skinName] = !_selectedSkins[skinName]!;
|
_selectedSkins[skinName] = !_selectedSkins[skinName]!;
|
||||||
_drawable.skeleton.setSkinByName("default");
|
_drawable.skeleton.setSkin("default");
|
||||||
if (_customSkin != null) _customSkin?.dispose();
|
if (_customSkin != null) _customSkin?.dispose();
|
||||||
_customSkin = Skin("custom-skin");
|
_customSkin = Skin("custom-skin");
|
||||||
for (var skinName in _selectedSkins.keys) {
|
for (var skinName in _selectedSkins.keys) {
|
||||||
@ -78,15 +80,15 @@ class DressUpState extends State<DressUp> {
|
|||||||
if (skin != null) _customSkin?.addSkin(skin);
|
if (skin != null) _customSkin?.addSkin(skin);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_drawable.skeleton.setSkin(_customSkin!);
|
_drawable.skeleton.setSkin2(_customSkin!);
|
||||||
_drawable.skeleton.setSlotsToSetupPose();
|
_drawable.skeleton.setupPoseSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final controller = SpineWidgetController(
|
final controller = SpineWidgetController(
|
||||||
onInitialized: (controller) {
|
onInitialized: (controller) {
|
||||||
controller.animationState.setAnimationByName(0, "dance", true);
|
controller.animationState.setAnimation(0, "dance", true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
class SpineComponent extends PositionComponent {
|
class SpineComponent extends PositionComponent {
|
||||||
final BoundsProvider _boundsProvider;
|
final BoundsProvider _boundsProvider;
|
||||||
final SkeletonDrawable _drawable;
|
final SkeletonDrawableFlutter _drawable;
|
||||||
late final Bounds _bounds;
|
late final Bounds _bounds;
|
||||||
final bool _ownsDrawable;
|
final bool _ownsDrawable;
|
||||||
|
|
||||||
@ -70,7 +70,7 @@ class SpineComponent extends PositionComponent {
|
|||||||
int? priority,
|
int? priority,
|
||||||
}) async {
|
}) async {
|
||||||
return SpineComponent(
|
return SpineComponent(
|
||||||
await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle),
|
await SkeletonDrawableFlutter.fromAsset(atlasFile, skeletonFile, bundle: bundle),
|
||||||
ownsDrawable: true,
|
ownsDrawable: true,
|
||||||
boundsProvider: boundsProvider,
|
boundsProvider: boundsProvider,
|
||||||
position: position,
|
position: position,
|
||||||
@ -137,15 +137,15 @@ class SimpleFlameExample extends FlameGame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class DragonExample extends FlameGame {
|
class DragonExample extends FlameGame {
|
||||||
late final Atlas cachedAtlas;
|
late final AtlasFlutter cachedAtlas;
|
||||||
late final SkeletonData cachedSkeletonData;
|
late final SkeletonData cachedSkeletonData;
|
||||||
late final SpineComponent dragon;
|
late final SpineComponent dragon;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
cachedAtlas = await Atlas.fromAsset("assets/dragon.atlas");
|
cachedAtlas = await AtlasFlutter.fromAsset("assets/dragon.atlas");
|
||||||
cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/dragon-ess.skel");
|
cachedSkeletonData = await SkeletonDataFlutter.fromAsset(cachedAtlas, "assets/dragon-ess.skel");
|
||||||
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false);
|
final drawable = SkeletonDrawableFlutter(cachedAtlas, cachedSkeletonData, false);
|
||||||
dragon = SpineComponent(
|
dragon = SpineComponent(
|
||||||
drawable,
|
drawable,
|
||||||
scale: Vector2(0.4, 0.4),
|
scale: Vector2(0.4, 0.4),
|
||||||
@ -168,21 +168,21 @@ class DragonExample extends FlameGame {
|
|||||||
|
|
||||||
class PreloadAndShareSpineDataExample extends FlameGame {
|
class PreloadAndShareSpineDataExample extends FlameGame {
|
||||||
late final SkeletonData cachedSkeletonData;
|
late final SkeletonData cachedSkeletonData;
|
||||||
late final Atlas cachedAtlas;
|
late final AtlasFlutter cachedAtlas;
|
||||||
late final List<SpineComponent> spineboys = [];
|
late final List<SpineComponent> spineboys = [];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> onLoad() async {
|
Future<void> onLoad() async {
|
||||||
// Pre-load the atlas and skeleton data once.
|
// Pre-load the atlas and skeleton data once.
|
||||||
cachedAtlas = await Atlas.fromAsset("assets/spineboy.atlas");
|
cachedAtlas = await AtlasFlutter.fromAsset("assets/spineboy.atlas");
|
||||||
cachedSkeletonData = await SkeletonData.fromAsset(cachedAtlas, "assets/spineboy-pro.skel");
|
cachedSkeletonData = await SkeletonDataFlutter.fromAsset(cachedAtlas, "assets/spineboy-pro.skel");
|
||||||
|
|
||||||
// Instantiate many spineboys from the pre-loaded data. Each SpineComponent
|
// Instantiate many spineboys from the pre-loaded data. Each SpineComponent
|
||||||
// gets their own SkeletonDrawable copy derived from the cached data. The
|
// gets their own SkeletonDrawable copy derived from the cached data. The
|
||||||
// SkeletonDrawable copies do not own the underlying skeleton data and atlas.
|
// SkeletonDrawable copies do not own the underlying skeleton data and atlas.
|
||||||
final rng = Random();
|
final rng = Random();
|
||||||
for (int i = 0; i < 100; i++) {
|
for (int i = 0; i < 100; i++) {
|
||||||
final drawable = SkeletonDrawable(cachedAtlas, cachedSkeletonData, false);
|
final drawable = SkeletonDrawableFlutter(cachedAtlas, cachedSkeletonData, false);
|
||||||
final scale = 0.1 + rng.nextDouble() * 0.2;
|
final scale = 0.1 + rng.nextDouble() * 0.2;
|
||||||
final position = Vector2(rng.nextDouble() * size.x, rng.nextDouble() * size.y);
|
final position = Vector2(rng.nextDouble() * size.x, rng.nextDouble() * size.y);
|
||||||
final spineboy = SpineComponent(drawable, scale: Vector2(scale, scale), position: position);
|
final spineboy = SpineComponent(drawable, scale: Vector2(scale, scale), position: position);
|
||||||
|
|||||||
@ -48,17 +48,19 @@ class IkFollowingState extends State<IkFollowing> {
|
|||||||
controller = SpineWidgetController(
|
controller = SpineWidgetController(
|
||||||
onInitialized: (controller) {
|
onInitialized: (controller) {
|
||||||
// Set the walk animation on track 0, let it loop
|
// Set the walk animation on track 0, let it loop
|
||||||
controller.animationState.setAnimationByName(0, "walk", true);
|
controller.animationState.setAnimation(0, "walk", true);
|
||||||
controller.animationState.setAnimationByName(1, "aim", true);
|
controller.animationState.setAnimation(1, "aim", true);
|
||||||
},
|
},
|
||||||
onAfterUpdateWorldTransforms: (controller) {
|
onAfterUpdateWorldTransforms: (controller) {
|
||||||
final worldPosition = crossHairPosition;
|
final worldPosition = crossHairPosition;
|
||||||
if (worldPosition == null) return;
|
if (worldPosition == null) return;
|
||||||
final bone = controller.skeleton.findBone("crosshair")!;
|
final bone = controller.skeleton.findBone("crosshair")!;
|
||||||
final parent = bone.getParent()!;
|
final parent = bone.parent;
|
||||||
final position = parent.worldToLocal(worldPosition.dx, worldPosition.dy);
|
if (parent != null) {
|
||||||
bone.setX(position.x);
|
final position = parent.appliedPose.worldToLocal(worldPosition.dx, worldPosition.dy);
|
||||||
bone.setY(position.y);
|
bone.appliedPose.x = position.x;
|
||||||
|
bone.appliedPose.y = position.y;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ import 'package:spine_flutter/spine_flutter.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class PlayPauseAnimation extends StatefulWidget {
|
class PlayPauseAnimation extends StatefulWidget {
|
||||||
const PlayPauseAnimation({Key? key}) : super(key: key);
|
const PlayPauseAnimation({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
PlayPauseAnimationState createState() => PlayPauseAnimationState();
|
PlayPauseAnimationState createState() => PlayPauseAnimationState();
|
||||||
@ -45,7 +45,7 @@ class PlayPauseAnimationState extends State<PlayPauseAnimation> {
|
|||||||
super.initState();
|
super.initState();
|
||||||
controller = SpineWidgetController(
|
controller = SpineWidgetController(
|
||||||
onInitialized: (controller) {
|
onInitialized: (controller) {
|
||||||
controller.animationState.setAnimationByName(0, "flying", true);
|
controller.animationState.setAnimation(0, "flying", true);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,8 +48,8 @@ class PhysicsState extends State<PhysicsTest> {
|
|||||||
|
|
||||||
controller = SpineWidgetController(
|
controller = SpineWidgetController(
|
||||||
onInitialized: (controller) {
|
onInitialized: (controller) {
|
||||||
controller.animationState.setAnimationByName(0, "eyeblink-long", true);
|
controller.animationState.setAnimation(0, "eyeblink-long", true);
|
||||||
controller.animationState.setAnimationByName(1, "wings-and-feet", true);
|
controller.animationState.setAnimation(1, "wings-and-feet", true);
|
||||||
},
|
},
|
||||||
onAfterUpdateWorldTransforms: (controller) {
|
onAfterUpdateWorldTransforms: (controller) {
|
||||||
if (lastMousePosition == null) {
|
if (lastMousePosition == null) {
|
||||||
|
|||||||
@ -5,5 +5,6 @@
|
|||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,8 +15,8 @@ EXTERNAL SOURCES:
|
|||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
|
||||||
spine_flutter: 75f9d54a630ac150d238210f9c211529c37c11ba
|
spine_flutter: 8469a2cfb87c5a7101a7c87dc7c14ee49699ea3b
|
||||||
|
|
||||||
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
PODFILE CHECKSUM: 353c8bcc5d5b0994e508d035b5431cfe18c1dea7
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
|
||||||
@NSApplicationMain
|
@main
|
||||||
class AppDelegate: FlutterAppDelegate {
|
class AppDelegate: FlutterAppDelegate {
|
||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@ -21,6 +21,16 @@ if [ ! -d "codegen/node_modules" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Generating spine-c bindings
|
||||||
|
log_action "Generating spine-c bindings"
|
||||||
|
if LOG=$(cd ../spine-c && ./build.sh codegen 2>&1); then
|
||||||
|
log_ok
|
||||||
|
else
|
||||||
|
log_fail
|
||||||
|
log_error_output "$LOG"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Copy spine-c and spine-cpp sources
|
# Copy spine-c and spine-cpp sources
|
||||||
log_action "Setting up source files"
|
log_action "Setting up source files"
|
||||||
if ./setup.sh > /dev/null 2>&1; then
|
if ./setup.sh > /dev/null 2>&1; then
|
||||||
@ -32,19 +42,21 @@ fi
|
|||||||
|
|
||||||
# Run the codegen
|
# Run the codegen
|
||||||
log_action "Generating Dart bindings"
|
log_action "Generating Dart bindings"
|
||||||
if npx tsx codegen/src/index.ts > /dev/null 2>&1; then
|
if LOG=$(npx tsx codegen/src/index.ts 2>&1); then
|
||||||
log_ok
|
log_ok
|
||||||
else
|
else
|
||||||
log_fail
|
log_fail
|
||||||
|
log_error_output "$LOG"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Build test spine_flutter shared library
|
# Build test spine_flutter shared library
|
||||||
log_action "Building test library"
|
log_action "Building test library"
|
||||||
if (cd test && ./build.sh > /dev/null 2>&1); then
|
if LOG=$(cd test && ./build.sh 2>&1); then
|
||||||
log_ok
|
log_ok
|
||||||
else
|
else
|
||||||
log_fail
|
log_fail
|
||||||
|
log_error_output "$LOG"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
// Relative import to be able to reuse the C sources.
|
|
||||||
// See the comment in ../{projectName}}.podspec for more information.
|
|
||||||
#include "../../src/spine-cpp-lite/spine-cpp-lite.cpp"
|
|
||||||
@ -12,14 +12,9 @@ A new Flutter FFI plugin project.
|
|||||||
s.homepage = 'http://example.com'
|
s.homepage = 'http://example.com'
|
||||||
s.license = { :file => '../LICENSE' }
|
s.license = { :file => '../LICENSE' }
|
||||||
s.author = { 'Your Company' => 'email@example.com' }
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
|
||||||
# This will ensure the source files in Classes/ are included in the native
|
|
||||||
# builds of apps using this FFI plugin. Podspec does not support relative
|
|
||||||
# paths, so Classes contains a forwarder C file that relatively imports
|
|
||||||
# `../src/*` so that the C sources can be shared among all target platforms.
|
|
||||||
s.source = { :path => '.' }
|
s.source = { :path => '.' }
|
||||||
s.source_files = 'Classes/**/*.{cpp}'
|
s.source_files = 'Classes/**/*.{cpp}'
|
||||||
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include"' }
|
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include" "$(PODS_TARGET_SRCROOT)/Classes/spine-c/include"' }
|
||||||
s.dependency 'Flutter'
|
s.dependency 'Flutter'
|
||||||
s.platform = :ios, '9.0'
|
s.platform = :ios, '9.0'
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -80,10 +80,10 @@ class Animation {
|
|||||||
SpineBindings.bindings.spine_animation_set_duration(_ptr, value);
|
SpineBindings.bindings.spine_animation_set_duration(_ptr, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply(Skeleton skeleton, double lastTime, double time, bool loop, ArrayEvent? pEvents, double alpha,
|
void apply(Skeleton skeleton, double lastTime, double time, bool loop, ArrayEvent? events, double alpha,
|
||||||
MixBlend blend, MixDirection direction, bool appliedPose) {
|
MixBlend blend, MixDirection direction, bool appliedPose) {
|
||||||
SpineBindings.bindings.spine_animation_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time, loop,
|
SpineBindings.bindings.spine_animation_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time, loop,
|
||||||
pEvents?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose);
|
events?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get name {
|
String get name {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -52,10 +52,10 @@ abstract class Timeline {
|
|||||||
return Rtti.fromPointer(result);
|
return Rtti.fromPointer(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
void apply(Skeleton skeleton, double lastTime, double time, ArrayEvent? pEvents, double alpha, MixBlend blend,
|
void apply(Skeleton skeleton, double lastTime, double time, ArrayEvent? events, double alpha, MixBlend blend,
|
||||||
MixDirection direction, bool appliedPose) {
|
MixDirection direction, bool appliedPose) {
|
||||||
SpineBindings.bindings.spine_timeline_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time,
|
SpineBindings.bindings.spine_timeline_apply(_ptr, skeleton.nativePtr.cast(), lastTime, time,
|
||||||
pEvents?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose);
|
events?.nativePtr.cast() ?? Pointer.fromAddress(0), alpha, blend.value, direction.value, appliedPose);
|
||||||
}
|
}
|
||||||
|
|
||||||
int get frameEntries {
|
int get frameEntries {
|
||||||
|
|||||||
@ -50,6 +50,7 @@ import 'generated/event.dart';
|
|||||||
import 'generated/event_type.dart';
|
import 'generated/event_type.dart';
|
||||||
import 'generated/render_command.dart';
|
import 'generated/render_command.dart';
|
||||||
import 'generated/physics.dart';
|
import 'generated/physics.dart';
|
||||||
|
import 'generated/bone_pose.dart';
|
||||||
|
|
||||||
// Export generated classes
|
// Export generated classes
|
||||||
export 'generated/api.dart';
|
export 'generated/api.dart';
|
||||||
@ -321,20 +322,24 @@ extension TrackEntryExtensions on TrackEntry {
|
|||||||
|
|
||||||
/// Represents a bounding box with position and dimensions
|
/// Represents a bounding box with position and dimensions
|
||||||
class Bounds {
|
class Bounds {
|
||||||
final double x;
|
double x;
|
||||||
final double y;
|
double y;
|
||||||
final double width;
|
double width;
|
||||||
final double height;
|
double height;
|
||||||
|
|
||||||
const Bounds({
|
Bounds({
|
||||||
required this.x,
|
required this.x,
|
||||||
required this.y,
|
required this.y,
|
||||||
required this.width,
|
required this.width,
|
||||||
required this.height,
|
required this.height,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
class Vector {
|
||||||
String toString() => 'Bounds(x: $x, y: $y, width: $width, height: $height)';
|
double x;
|
||||||
|
double y;
|
||||||
|
|
||||||
|
Vector({required this.x, required this.y});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extension to add bounds property to Skeleton
|
/// Extension to add bounds property to Skeleton
|
||||||
@ -349,6 +354,33 @@ extension SkeletonExtensions on Skeleton {
|
|||||||
height: spineBounds.height,
|
height: spineBounds.height,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Vector getPosition() {
|
||||||
|
final position = SpineBindings.bindings.spine_skeleton_get_position_v(nativePtr.cast());
|
||||||
|
return Vector(x: position.x, y: position.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension BonePoseExtensions on BonePose {
|
||||||
|
Vector worldToLocal(double worldX, double worldY) {
|
||||||
|
final result = SpineBindings.bindings.spine_bone_pose_world_to_local_v(nativePtr.cast(), worldX, worldY);
|
||||||
|
return Vector(x: result.x, y: result.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector localToWorld(double localX, double localY) {
|
||||||
|
final result = SpineBindings.bindings.spine_bone_pose_local_to_world_v(nativePtr.cast(), localX, localY);
|
||||||
|
return Vector(x: result.x, y: result.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector worldToParent(double worldX, double worldY) {
|
||||||
|
final result = SpineBindings.bindings.spine_bone_pose_world_to_parent_v(nativePtr.cast(), worldX, worldY);
|
||||||
|
return Vector(x: result.x, y: result.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
Vector parentToWorld(double parentX, double parentY) {
|
||||||
|
final result = SpineBindings.bindings.spine_bone_pose_parent_to_world_v(nativePtr.cast(), parentX, parentY);
|
||||||
|
return Vector(x: result.x, y: result.y);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenient drawable that combines skeleton, animation state, and rendering
|
/// Convenient drawable that combines skeleton, animation state, and rendering
|
||||||
|
|||||||
@ -24,14 +24,13 @@ Future<void> initSpineFlutter({bool useStaticLinkage = false, bool enableMemoryD
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Flutter wrapper for Atlas that manages texture loading and Paint creation
|
/// Flutter wrapper for Atlas that manages texture loading and Paint creation
|
||||||
class AtlasFlutter {
|
class AtlasFlutter extends Atlas {
|
||||||
static FilterQuality filterQuality = FilterQuality.low;
|
static FilterQuality filterQuality = FilterQuality.low;
|
||||||
final Atlas atlas;
|
|
||||||
final List<Image> atlasPages;
|
final List<Image> atlasPages;
|
||||||
final List<Map<BlendMode, Paint>> atlasPagePaints;
|
final List<Map<BlendMode, Paint>> atlasPagePaints;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
AtlasFlutter._(this.atlas, this.atlasPages, this.atlasPagePaints);
|
AtlasFlutter._(super.ptr, this.atlasPages, this.atlasPagePaints) : super.fromPointer();
|
||||||
|
|
||||||
/// Internal method to load atlas and images
|
/// Internal method to load atlas and images
|
||||||
static Future<AtlasFlutter> _load(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async {
|
static Future<AtlasFlutter> _load(String atlasFileName, Future<Uint8List> Function(String name) loadFile) async {
|
||||||
@ -77,7 +76,7 @@ class AtlasFlutter {
|
|||||||
paints.add(pagePaints);
|
paints.add(pagePaints);
|
||||||
}
|
}
|
||||||
|
|
||||||
return AtlasFlutter._(atlas, pages, paints);
|
return AtlasFlutter._(atlas.nativePtr.cast(), pages, paints);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads an [AtlasFlutter] from the file [atlasFileName] in the root bundle or the optionally provided [bundle].
|
/// Loads an [AtlasFlutter] from the file [atlasFileName] in the root bundle or the optionally provided [bundle].
|
||||||
@ -103,10 +102,11 @@ class AtlasFlutter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Disposes all resources including the native atlas and images
|
/// Disposes all resources including the native atlas and images
|
||||||
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
atlas.dispose();
|
super.dispose();
|
||||||
for (final image in atlasPages) {
|
for (final image in atlasPages) {
|
||||||
image.dispose();
|
image.dispose();
|
||||||
}
|
}
|
||||||
@ -114,6 +114,64 @@ class AtlasFlutter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Flutter wrapper for SkeletonData that provides convenient loading methods
|
||||||
|
class SkeletonDataFlutter extends SkeletonData {
|
||||||
|
SkeletonDataFlutter._(super.ptr) : super.fromPointer();
|
||||||
|
|
||||||
|
/// Loads a [SkeletonDataFlutter] from the file [skeletonFile] in the root bundle or the optionally provided [bundle].
|
||||||
|
/// Uses the provided [atlasFlutter] to resolve attachment images.
|
||||||
|
///
|
||||||
|
/// Throws an [Exception] in case the skeleton data could not be loaded.
|
||||||
|
static Future<SkeletonDataFlutter> fromAsset(AtlasFlutter atlas, String skeletonFile, {AssetBundle? bundle}) async {
|
||||||
|
bundle ??= rootBundle;
|
||||||
|
if (skeletonFile.endsWith(".json")) {
|
||||||
|
final jsonData = await bundle.loadString(skeletonFile);
|
||||||
|
final skeletonData = loadSkeletonDataJson(atlas, jsonData, path: skeletonFile);
|
||||||
|
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
|
||||||
|
} else {
|
||||||
|
final binaryData = (await bundle.load(skeletonFile)).buffer.asUint8List();
|
||||||
|
final skeletonData = loadSkeletonDataBinary(atlas, binaryData, path: skeletonFile);
|
||||||
|
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a [SkeletonDataFlutter] from the file [skeletonFile]. Uses the provided [atlasFlutter] to resolve attachment images.
|
||||||
|
///
|
||||||
|
/// Throws an [Exception] in case the skeleton data could not be loaded.
|
||||||
|
static Future<SkeletonDataFlutter> fromFile(AtlasFlutter atlasFlutter, String skeletonFile) async {
|
||||||
|
if (skeletonFile.endsWith(".json")) {
|
||||||
|
final jsonData = await File(skeletonFile).readAsString();
|
||||||
|
final skeletonData = loadSkeletonDataJson(atlasFlutter, jsonData, path: skeletonFile);
|
||||||
|
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
|
||||||
|
} else {
|
||||||
|
final binaryData = await File(skeletonFile).readAsBytes();
|
||||||
|
final skeletonData = loadSkeletonDataBinary(atlasFlutter, binaryData, path: skeletonFile);
|
||||||
|
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads a [SkeletonDataFlutter] from the URL [skeletonURL]. Uses the provided [atlasFlutter] to resolve attachment images.
|
||||||
|
///
|
||||||
|
/// Throws an [Exception] in case the skeleton data could not be loaded.
|
||||||
|
static Future<SkeletonDataFlutter> fromHttp(AtlasFlutter atlasFlutter, String skeletonURL) async {
|
||||||
|
if (skeletonURL.endsWith(".json")) {
|
||||||
|
final response = await http.get(Uri.parse(skeletonURL));
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Failed to load skeleton from $skeletonURL: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
final skeletonData = loadSkeletonDataJson(atlasFlutter, response.body, path: skeletonURL);
|
||||||
|
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
|
||||||
|
} else {
|
||||||
|
final response = await http.get(Uri.parse(skeletonURL));
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('Failed to load skeleton from $skeletonURL: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
final skeletonData = loadSkeletonDataBinary(atlasFlutter, response.bodyBytes, path: skeletonURL);
|
||||||
|
return SkeletonDataFlutter._(skeletonData.nativePtr.cast());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Extension to convert Spine BlendMode to Flutter BlendMode
|
/// Extension to convert Spine BlendMode to Flutter BlendMode
|
||||||
extension BlendModeExtensions on BlendMode {
|
extension BlendModeExtensions on BlendMode {
|
||||||
rendering.BlendMode toFlutterBlendMode() {
|
rendering.BlendMode toFlutterBlendMode() {
|
||||||
@ -184,7 +242,6 @@ class RenderCommandFlutter {
|
|||||||
// See https://github.com/flutter/flutter/issues/127486
|
// See https://github.com/flutter/flutter/issues/127486
|
||||||
//
|
//
|
||||||
// We thus batch all meshes not only by atlas page and blend mode, but also vertex color.
|
// We thus batch all meshes not only by atlas page and blend mode, but also vertex color.
|
||||||
// See spine_flutter.cpp, batch_commands().
|
|
||||||
//
|
//
|
||||||
// If the vertex color equals (1, 1, 1, 1), we do not store
|
// If the vertex color equals (1, 1, 1, 1), we do not store
|
||||||
// colors, which will trigger the fast path in Impeller. Otherwise we have to go the slow path, which
|
// colors, which will trigger the fast path in Impeller. Otherwise we have to go the slow path, which
|
||||||
@ -218,13 +275,13 @@ class RenderCommandFlutter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A SkeletonDrawable bundles loading, updating, and rendering an [Atlas], [Skeleton], and [AnimationState]
|
/// A SkeletonDrawable bundles loading, updating, and rendering an [AtlasFlutter], [Skeleton], and [AnimationState]
|
||||||
/// into a single easy to use class.
|
/// into a single easy to use class.
|
||||||
///
|
///
|
||||||
/// Use the [fromAsset], [fromFile], or [fromHttp] methods to construct a SkeletonDrawable. To have
|
/// Use the [fromAsset], [fromFile], or [fromHttp] methods to construct a SkeletonDrawable. To have
|
||||||
/// multiple skeleton drawable instances share the same [Atlas] and [SkeletonData], use the constructor.
|
/// multiple skeleton drawable instances share the same [AtlasFlutter] and [SkeletonDataFlutter], use the constructor.
|
||||||
///
|
///
|
||||||
/// You can then directly access the [atlas], [skeletonData], [skeleton], [animationStateData], and [animationState]
|
/// You can then directly access the [atlasFlutter], [skeletonDataFlutter], [skeleton], [animationStateData], and [animationState]
|
||||||
/// to query and animate the skeleton. Use the [AnimationState] to queue animations on one or more tracks
|
/// to query and animate the skeleton. Use the [AnimationState] to queue animations on one or more tracks
|
||||||
/// via [AnimationState.setAnimation] or [AnimationState.addAnimation].
|
/// via [AnimationState.setAnimation] or [AnimationState.addAnimation].
|
||||||
///
|
///
|
||||||
@ -235,27 +292,18 @@ class RenderCommandFlutter {
|
|||||||
/// [renderToPng], or [renderToRawImageData], depending on your needs.
|
/// [renderToPng], or [renderToRawImageData], depending on your needs.
|
||||||
///
|
///
|
||||||
/// When the skeleton drawable is no longer needed, call the [dispose] method to release its resources. If
|
/// When the skeleton drawable is no longer needed, call the [dispose] method to release its resources. If
|
||||||
/// the skeleton drawable was constructed from a shared [Atlas] and [SkeletonData], make sure to dispose the
|
/// the skeleton drawable was constructed from a shared [AtlasFlutter] and [SkeletonDataFlutter], make sure to dispose the
|
||||||
/// atlas and skeleton data as well, if no skeleton drawable references them anymore.
|
/// atlas and skeleton data as well, if no skeleton drawable references them anymore.
|
||||||
class SkeletonDrawableFlutter {
|
class SkeletonDrawableFlutter extends SkeletonDrawable {
|
||||||
final AtlasFlutter atlasFlutter;
|
final AtlasFlutter atlasFlutter;
|
||||||
final SkeletonData skeletonData;
|
final SkeletonData skeletonData;
|
||||||
late final SkeletonDrawable _drawable;
|
|
||||||
late final Skeleton skeleton;
|
|
||||||
late final AnimationStateData animationStateData;
|
|
||||||
late final AnimationState animationState;
|
|
||||||
final bool _ownsAtlasAndSkeletonData;
|
final bool _ownsAtlasAndSkeletonData;
|
||||||
bool _disposed = false;
|
bool _disposed = false;
|
||||||
|
|
||||||
/// Constructs a new skeleton drawable from the given (possibly shared) [Atlas] and [SkeletonData]. If
|
/// Constructs a new skeleton drawable from the given (possibly shared) [AtlasFlutter] and [SkeletonDataFlutter]. If
|
||||||
/// the atlas and skeleton data are not shared, the drawable can take ownership by passing true for [_ownsAtlasAndSkeletonData].
|
/// the atlas and skeleton data are not shared, the drawable can take ownership by passing true for [_ownsAtlasAndSkeletonData].
|
||||||
/// In that case a call to [dispose] will also dispose the atlas and skeleton data.
|
/// In that case a call to [dispose] will also dispose the atlas and skeleton data.
|
||||||
SkeletonDrawableFlutter(this.atlasFlutter, this.skeletonData, this._ownsAtlasAndSkeletonData) {
|
SkeletonDrawableFlutter(this.atlasFlutter, this.skeletonData, this._ownsAtlasAndSkeletonData) : super(skeletonData);
|
||||||
_drawable = SkeletonDrawable(skeletonData);
|
|
||||||
skeleton = _drawable.skeleton;
|
|
||||||
animationStateData = _drawable.animationStateData;
|
|
||||||
animationState = _drawable.animationState;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile] from the root asset bundle
|
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile] from the root asset bundle
|
||||||
/// or the optionally provided [bundle].
|
/// or the optionally provided [bundle].
|
||||||
@ -264,13 +312,8 @@ class SkeletonDrawableFlutter {
|
|||||||
static Future<SkeletonDrawableFlutter> fromAsset(String atlasFile, String skeletonFile, {AssetBundle? bundle}) async {
|
static Future<SkeletonDrawableFlutter> fromAsset(String atlasFile, String skeletonFile, {AssetBundle? bundle}) async {
|
||||||
bundle ??= rootBundle;
|
bundle ??= rootBundle;
|
||||||
final atlasFlutter = await AtlasFlutter.fromAsset(atlasFile, bundle: bundle);
|
final atlasFlutter = await AtlasFlutter.fromAsset(atlasFile, bundle: bundle);
|
||||||
|
final skeletonDataFlutter = await SkeletonDataFlutter.fromAsset(atlasFlutter, skeletonFile, bundle: bundle);
|
||||||
final skeletonData = await bundle.loadString(skeletonFile);
|
return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
|
||||||
final skeleton = skeletonFile.endsWith('.json')
|
|
||||||
? loadSkeletonDataJson(atlasFlutter.atlas, skeletonData)
|
|
||||||
: throw Exception('Binary skeleton data loading from assets not yet implemented');
|
|
||||||
|
|
||||||
return SkeletonDrawableFlutter(atlasFlutter, skeleton, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile].
|
/// Constructs a new skeleton drawable from the [atlasFile] and [skeletonFile].
|
||||||
@ -278,17 +321,8 @@ class SkeletonDrawableFlutter {
|
|||||||
/// Throws an exception in case the data could not be loaded.
|
/// Throws an exception in case the data could not be loaded.
|
||||||
static Future<SkeletonDrawableFlutter> fromFile(String atlasFile, String skeletonFile) async {
|
static Future<SkeletonDrawableFlutter> fromFile(String atlasFile, String skeletonFile) async {
|
||||||
final atlasFlutter = await AtlasFlutter.fromFile(atlasFile);
|
final atlasFlutter = await AtlasFlutter.fromFile(atlasFile);
|
||||||
|
final skeletonDataFlutter = await SkeletonDataFlutter.fromFile(atlasFlutter, skeletonFile);
|
||||||
final SkeletonData skeleton;
|
return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
|
||||||
if (skeletonFile.endsWith('.json')) {
|
|
||||||
final skeletonData = await File(skeletonFile).readAsString();
|
|
||||||
skeleton = loadSkeletonDataJson(atlasFlutter.atlas, skeletonData);
|
|
||||||
} else {
|
|
||||||
final skeletonData = await File(skeletonFile).readAsBytes();
|
|
||||||
skeleton = loadSkeletonDataBinary(atlasFlutter.atlas, skeletonData);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SkeletonDrawableFlutter(atlasFlutter, skeleton, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Constructs a new skeleton drawable from the [atlasUrl] and [skeletonUrl].
|
/// Constructs a new skeleton drawable from the [atlasUrl] and [skeletonUrl].
|
||||||
@ -296,45 +330,22 @@ class SkeletonDrawableFlutter {
|
|||||||
/// Throws an exception in case the data could not be loaded.
|
/// Throws an exception in case the data could not be loaded.
|
||||||
static Future<SkeletonDrawableFlutter> fromHttp(String atlasUrl, String skeletonUrl) async {
|
static Future<SkeletonDrawableFlutter> fromHttp(String atlasUrl, String skeletonUrl) async {
|
||||||
final atlasFlutter = await AtlasFlutter.fromHttp(atlasUrl);
|
final atlasFlutter = await AtlasFlutter.fromHttp(atlasUrl);
|
||||||
|
final skeletonDataFlutter = await SkeletonDataFlutter.fromHttp(atlasFlutter, skeletonUrl);
|
||||||
final SkeletonData skeleton;
|
return SkeletonDrawableFlutter(atlasFlutter, skeletonDataFlutter, true);
|
||||||
if (skeletonUrl.endsWith('.json')) {
|
|
||||||
final skeletonResponse = await http.get(Uri.parse(skeletonUrl));
|
|
||||||
if (skeletonResponse.statusCode != 200) {
|
|
||||||
throw Exception('Failed to load skeleton from $skeletonUrl: ${skeletonResponse.statusCode}');
|
|
||||||
}
|
|
||||||
skeleton = loadSkeletonDataJson(atlasFlutter.atlas, skeletonResponse.body);
|
|
||||||
} else {
|
|
||||||
final skeletonResponse = await http.get(Uri.parse(skeletonUrl));
|
|
||||||
if (skeletonResponse.statusCode != 200) {
|
|
||||||
throw Exception('Failed to load skeleton from $skeletonUrl: ${skeletonResponse.statusCode}');
|
|
||||||
}
|
|
||||||
skeleton = loadSkeletonDataBinary(atlasFlutter.atlas, skeletonResponse.bodyBytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
return SkeletonDrawableFlutter(atlasFlutter, skeleton, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Updates the [AnimationState] using the [delta] time given in seconds, applies the
|
|
||||||
/// animation state to the [Skeleton] and updates the world transforms of the skeleton
|
|
||||||
/// to calculate its current pose.
|
|
||||||
void update(double delta) {
|
|
||||||
if (_disposed) return;
|
|
||||||
_drawable.update(delta);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renders to current skeleton pose to a list of [RenderCommandFlutter] instances. The render commands
|
/// Renders to current skeleton pose to a list of [RenderCommandFlutter] instances. The render commands
|
||||||
/// can be rendered via [Canvas.drawVertices].
|
/// can be rendered via [Canvas.drawVertices].
|
||||||
List<RenderCommandFlutter> render() {
|
List<RenderCommandFlutter> renderFlutter() {
|
||||||
if (_disposed) return [];
|
if (_disposed) return [];
|
||||||
|
|
||||||
var commands = <RenderCommandFlutter>[];
|
var commands = <RenderCommandFlutter>[];
|
||||||
var nativeCmd = _drawable.render();
|
var nativeCmd = render();
|
||||||
|
|
||||||
while (nativeCmd != null) {
|
while (nativeCmd != null) {
|
||||||
// Get page dimensions from atlas
|
// Get page dimensions from atlas
|
||||||
final pageIndex = nativeCmd.texture?.address ?? 0;
|
final pageIndex = nativeCmd.texture?.address ?? 0;
|
||||||
final pages = atlasFlutter.atlas.pages;
|
final pages = atlasFlutter.pages;
|
||||||
final page = pages[pageIndex];
|
final page = pages[pageIndex];
|
||||||
if (page != null) {
|
if (page != null) {
|
||||||
commands.add(RenderCommandFlutter._(nativeCmd, page.width.toDouble(), page.height.toDouble()));
|
commands.add(RenderCommandFlutter._(nativeCmd, page.width.toDouble(), page.height.toDouble()));
|
||||||
@ -350,7 +361,7 @@ class SkeletonDrawableFlutter {
|
|||||||
/// Renders the skeleton drawable's current pose to the given [canvas]. Does not perform any
|
/// Renders the skeleton drawable's current pose to the given [canvas]. Does not perform any
|
||||||
/// scaling or fitting.
|
/// scaling or fitting.
|
||||||
List<RenderCommandFlutter> renderToCanvas(Canvas canvas) {
|
List<RenderCommandFlutter> renderToCanvas(Canvas canvas) {
|
||||||
var commands = render();
|
var commands = renderFlutter();
|
||||||
|
|
||||||
for (final cmd in commands) {
|
for (final cmd in commands) {
|
||||||
// Get the paint for this atlas page and blend mode
|
// Get the paint for this atlas page and blend mode
|
||||||
@ -425,10 +436,11 @@ class SkeletonDrawableFlutter {
|
|||||||
/// Disposes the skeleton drawable's resources. If the skeleton drawable owns the atlas
|
/// Disposes the skeleton drawable's resources. If the skeleton drawable owns the atlas
|
||||||
/// and skeleton data, they are disposed as well. Must be called when the skeleton drawable
|
/// and skeleton data, they are disposed as well. Must be called when the skeleton drawable
|
||||||
/// is no longer in use.
|
/// is no longer in use.
|
||||||
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
_drawable.dispose();
|
super.dispose();
|
||||||
|
|
||||||
if (_ownsAtlasAndSkeletonData) {
|
if (_ownsAtlasAndSkeletonData) {
|
||||||
atlasFlutter.dispose();
|
atlasFlutter.dispose();
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
/// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
|
||||||
/// THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
/// THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
///
|
///
|
||||||
|
library;
|
||||||
|
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
@ -34,7 +35,7 @@ import 'package:flutter/scheduler.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'spine_dart.dart';
|
import 'spine_flutter.dart';
|
||||||
|
|
||||||
/// Controls how the skeleton of a [SpineWidget] is animated and rendered.
|
/// Controls how the skeleton of a [SpineWidget] is animated and rendered.
|
||||||
///
|
///
|
||||||
@ -54,7 +55,7 @@ import 'spine_dart.dart';
|
|||||||
/// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which
|
/// [SpineWidget] then renderes the skeleton's current pose, and finally calls the optional [onAfterPaint], which
|
||||||
/// can render additional objects on top of the skeleton.
|
/// can render additional objects on top of the skeleton.
|
||||||
///
|
///
|
||||||
/// The underlying [Atlas], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawable]
|
/// The underlying [AtlasFlutter], [SkeletonData], [Skeleton], [AnimationStateData], [AnimationState], and [SkeletonDrawableFlutter]
|
||||||
/// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
|
/// can be accessed through their respective getters to inspect and/or modify the skeleton and its associated data. Accessing
|
||||||
/// this data is only allowed if the [SpineWidget] and its data have been initialized and have not been disposed yet.
|
/// this data is only allowed if the [SpineWidget] and its data have been initialized and have not been disposed yet.
|
||||||
///
|
///
|
||||||
@ -62,7 +63,7 @@ import 'spine_dart.dart';
|
|||||||
/// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter
|
/// and rendering the skeleton. The [resume] method resumes updating and rendering the skeleton. The [isPlaying] getter
|
||||||
/// reports the current state.
|
/// reports the current state.
|
||||||
class SpineWidgetController {
|
class SpineWidgetController {
|
||||||
SkeletonDrawable? _drawable;
|
SkeletonDrawableFlutter? _drawable;
|
||||||
double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1;
|
double _offsetX = 0, _offsetY = 0, _scaleX = 1, _scaleY = 1;
|
||||||
bool _isPlaying = true;
|
bool _isPlaying = true;
|
||||||
_SpineRenderObject? _renderObject;
|
_SpineRenderObject? _renderObject;
|
||||||
@ -70,7 +71,8 @@ class SpineWidgetController {
|
|||||||
final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms;
|
final void Function(SpineWidgetController controller)? onBeforeUpdateWorldTransforms;
|
||||||
final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms;
|
final void Function(SpineWidgetController controller)? onAfterUpdateWorldTransforms;
|
||||||
final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint;
|
final void Function(SpineWidgetController controller, Canvas canvas)? onBeforePaint;
|
||||||
final void Function(SpineWidgetController controller, Canvas canvas, List<RenderCommand> commands)? onAfterPaint;
|
final void Function(SpineWidgetController controller, Canvas canvas, List<RenderCommandFlutter> commands)?
|
||||||
|
onAfterPaint;
|
||||||
|
|
||||||
/// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on
|
/// Constructs a new [SpineWidget] controller. See the class documentation of [SpineWidgetController] for information on
|
||||||
/// the optional arguments.
|
/// the optional arguments.
|
||||||
@ -82,16 +84,16 @@ class SpineWidgetController {
|
|||||||
this.onAfterPaint,
|
this.onAfterPaint,
|
||||||
});
|
});
|
||||||
|
|
||||||
void _initialize(SkeletonDrawable drawable) {
|
void _initialize(SkeletonDrawableFlutter drawable) {
|
||||||
var wasInitialized = _drawable != null;
|
var wasInitialized = _drawable != null;
|
||||||
_drawable = drawable;
|
_drawable = drawable;
|
||||||
if (!wasInitialized) onInitialized?.call(this);
|
if (!wasInitialized) onInitialized?.call(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [Atlas] from which images to render the skeleton are sourced.
|
/// The [AtlasFlutter] from which images to render the skeleton are sourced.
|
||||||
Atlas get atlas {
|
AtlasFlutter get atlasFlutter {
|
||||||
if (_drawable == null) throw Exception("Controller is not initialized yet.");
|
if (_drawable == null) throw Exception("Controller is not initialized yet.");
|
||||||
return _drawable!.atlas;
|
return _drawable!.atlasFlutter;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The setup-pose data used by the skeleton.
|
/// The setup-pose data used by the skeleton.
|
||||||
@ -119,8 +121,8 @@ class SpineWidgetController {
|
|||||||
return _drawable!.skeleton;
|
return _drawable!.skeleton;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The [SkeletonDrawable]
|
/// The [SkeletonDrawableFlutter]
|
||||||
SkeletonDrawable get drawable {
|
SkeletonDrawableFlutter get drawable {
|
||||||
if (_drawable == null) throw Exception("Controller is not initialized yet.");
|
if (_drawable == null) throw Exception("Controller is not initialized yet.");
|
||||||
return _drawable!;
|
return _drawable!;
|
||||||
}
|
}
|
||||||
@ -170,7 +172,7 @@ enum _AssetType { asset, file, http, drawable }
|
|||||||
abstract class BoundsProvider {
|
abstract class BoundsProvider {
|
||||||
const BoundsProvider();
|
const BoundsProvider();
|
||||||
|
|
||||||
Bounds computeBounds(SkeletonDrawable drawable);
|
Bounds computeBounds(SkeletonDrawableFlutter drawable);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible
|
/// A [BoundsProvider] that calculates the bounding box of the skeleton based on the visible
|
||||||
@ -179,8 +181,10 @@ class SetupPoseBounds extends BoundsProvider {
|
|||||||
const SetupPoseBounds();
|
const SetupPoseBounds();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Bounds computeBounds(SkeletonDrawable drawable) {
|
Bounds computeBounds(SkeletonDrawableFlutter drawable) {
|
||||||
return drawable.skeleton.getBounds();
|
drawable.skeleton.setupPose();
|
||||||
|
drawable.skeleton.updateWorldTransform(Physics.none);
|
||||||
|
return drawable.skeleton.bounds;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,8 +195,8 @@ class RawBounds extends BoundsProvider {
|
|||||||
RawBounds(this.x, this.y, this.width, this.height);
|
RawBounds(this.x, this.y, this.width, this.height);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Bounds computeBounds(SkeletonDrawable drawable) {
|
Bounds computeBounds(SkeletonDrawableFlutter drawable) {
|
||||||
return Bounds(x, y, width, height);
|
return Bounds(x: x, y: y, width: width, height: height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -211,17 +215,17 @@ class SkinAndAnimationBounds extends BoundsProvider {
|
|||||||
: skins = skins == null || skins.isEmpty ? ["default"] : skins;
|
: skins = skins == null || skins.isEmpty ? ["default"] : skins;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Bounds computeBounds(SkeletonDrawable drawable) {
|
Bounds computeBounds(SkeletonDrawableFlutter drawable) {
|
||||||
final data = drawable.skeletonData;
|
final data = drawable.skeletonData;
|
||||||
final oldSkin = drawable.skeleton.getSkin();
|
final oldSkin = drawable.skeleton.skin;
|
||||||
final customSkin = Skin("custom-skin");
|
final customSkin = Skin("custom-skin");
|
||||||
for (final skinName in skins) {
|
for (final skinName in skins) {
|
||||||
final skin = data.findSkin(skinName);
|
final skin = data.findSkin(skinName);
|
||||||
if (skin == null) continue;
|
if (skin == null) continue;
|
||||||
customSkin.addSkin(skin);
|
customSkin.addSkin(skin);
|
||||||
}
|
}
|
||||||
drawable.skeleton.setSkin(customSkin);
|
drawable.skeleton.setSkin2(customSkin);
|
||||||
drawable.skeleton.setToSetupPose();
|
drawable.skeleton.setupPose();
|
||||||
|
|
||||||
final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
final animation = this.animation != null ? data.findAnimation(this.animation!) : null;
|
||||||
double minX = double.infinity;
|
double minX = double.infinity;
|
||||||
@ -229,35 +233,37 @@ class SkinAndAnimationBounds extends BoundsProvider {
|
|||||||
double maxX = double.negativeInfinity;
|
double maxX = double.negativeInfinity;
|
||||||
double maxY = double.negativeInfinity;
|
double maxY = double.negativeInfinity;
|
||||||
if (animation == null) {
|
if (animation == null) {
|
||||||
final bounds = drawable.skeleton.getBounds();
|
drawable.skeleton.updateWorldTransform(Physics.none);
|
||||||
|
final bounds = drawable.skeleton.bounds;
|
||||||
minX = bounds.x;
|
minX = bounds.x;
|
||||||
minY = bounds.y;
|
minY = bounds.y;
|
||||||
maxX = minX + bounds.width;
|
maxX = minX + bounds.width;
|
||||||
maxY = minY + bounds.height;
|
maxY = minY + bounds.height;
|
||||||
} else {
|
} else {
|
||||||
drawable.animationState.setAnimation(0, animation, false);
|
drawable.animationState.setAnimation(0, animation.name, false);
|
||||||
final steps = max(animation.getDuration() / stepTime, 1.0).toInt();
|
final steps = max(animation.duration / stepTime, 1.0).toInt();
|
||||||
for (int i = 0; i < steps; i++) {
|
for (int i = 0; i < steps; i++) {
|
||||||
drawable.update(i > 0 ? stepTime : 0);
|
drawable.update(i > 0 ? stepTime : 0);
|
||||||
final bounds = drawable.skeleton.getBounds();
|
drawable.skeleton.updateWorldTransform(Physics.none);
|
||||||
|
final bounds = drawable.skeleton.bounds;
|
||||||
minX = min(minX, bounds.x);
|
minX = min(minX, bounds.x);
|
||||||
minY = min(minY, bounds.y);
|
minY = min(minY, bounds.y);
|
||||||
maxX = max(maxX, minX + bounds.width);
|
maxX = max(maxX, minX + bounds.width);
|
||||||
maxY = max(maxY, minY + bounds.height);
|
maxY = max(maxY, minY + bounds.height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
drawable.skeleton.setSkinByName("default");
|
drawable.skeleton.setSkin("default");
|
||||||
drawable.animationState.clearTracks();
|
drawable.animationState.clearTracks();
|
||||||
if (oldSkin != null) drawable.skeleton.setSkin(oldSkin);
|
if (oldSkin != null) drawable.skeleton.setSkin2(oldSkin);
|
||||||
drawable.skeleton.setToSetupPose();
|
drawable.skeleton.setupPose();
|
||||||
drawable.update(0);
|
drawable.update(0);
|
||||||
customSkin.dispose();
|
customSkin.dispose();
|
||||||
return Bounds(minX, minY, maxX - minX, maxY - minY);
|
return Bounds(x: minX, y: minY, width: maxX - minX, height: maxY - minY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A [StatefulWidget] to display a Spine skeleton. The skeleton can be loaded from an asset bundle ([SpineWidget.fromAsset],
|
/// A [StatefulWidget] to display a Spine skeleton. The skeleton can be loaded from an asset bundle ([SpineWidget.fromAsset],
|
||||||
/// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], or a pre-loaded [SkeletonDrawable] ([SpineWidget.fromDrawable]).
|
/// local files [SpineWidget.fromFile], URLs [SpineWidget.fromHttp], or a pre-loaded [SkeletonDrawableFlutter] ([SpineWidget.fromDrawable]).
|
||||||
///
|
///
|
||||||
/// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController].
|
/// The skeleton displayed by a `SpineWidget` can be controlled via a [SpineWidgetController].
|
||||||
///
|
///
|
||||||
@ -268,7 +274,7 @@ class SpineWidget extends StatefulWidget {
|
|||||||
final AssetBundle? _bundle;
|
final AssetBundle? _bundle;
|
||||||
final String? _skeletonFile;
|
final String? _skeletonFile;
|
||||||
final String? _atlasFile;
|
final String? _atlasFile;
|
||||||
final SkeletonDrawable? _drawable;
|
final SkeletonDrawableFlutter? _drawable;
|
||||||
final SpineWidgetController _controller;
|
final SpineWidgetController _controller;
|
||||||
final BoxFit _fit;
|
final BoxFit _fit;
|
||||||
final Alignment _alignment;
|
final Alignment _alignment;
|
||||||
@ -361,7 +367,7 @@ class SpineWidget extends StatefulWidget {
|
|||||||
_sizedByBounds = sizedByBounds ?? false,
|
_sizedByBounds = sizedByBounds ?? false,
|
||||||
_drawable = null;
|
_drawable = null;
|
||||||
|
|
||||||
/// Constructs a new [SpineWidget] from a [SkeletonDrawable].
|
/// Constructs a new [SpineWidget] from a [SkeletonDrawableFlutter].
|
||||||
///
|
///
|
||||||
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
|
/// After initialization is complete, the provided [_controller] is invoked as per the [SpineWidgetController] semantics, to allow
|
||||||
/// modifying how the skeleton inside the widget is animated and rendered.
|
/// modifying how the skeleton inside the widget is animated and rendered.
|
||||||
@ -394,7 +400,7 @@ class SpineWidget extends StatefulWidget {
|
|||||||
|
|
||||||
class _SpineWidgetState extends State<SpineWidget> {
|
class _SpineWidgetState extends State<SpineWidget> {
|
||||||
late Bounds _computedBounds;
|
late Bounds _computedBounds;
|
||||||
SkeletonDrawable? _drawable;
|
SkeletonDrawableFlutter? _drawable;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -436,7 +442,7 @@ class _SpineWidgetState extends State<SpineWidget> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void loadDrawable(SkeletonDrawable drawable) {
|
void loadDrawable(SkeletonDrawableFlutter drawable) {
|
||||||
_drawable = drawable;
|
_drawable = drawable;
|
||||||
_computedBounds = widget._boundsProvider.computeBounds(drawable);
|
_computedBounds = widget._boundsProvider.computeBounds(drawable);
|
||||||
widget._controller._initialize(drawable);
|
widget._controller._initialize(drawable);
|
||||||
@ -446,13 +452,13 @@ class _SpineWidgetState extends State<SpineWidget> {
|
|||||||
void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async {
|
void loadFromAsset(AssetBundle? bundle, String atlasFile, String skeletonFile, _AssetType assetType) async {
|
||||||
switch (assetType) {
|
switch (assetType) {
|
||||||
case _AssetType.asset:
|
case _AssetType.asset:
|
||||||
loadDrawable(await SkeletonDrawable.fromAsset(atlasFile, skeletonFile, bundle: bundle));
|
loadDrawable(await SkeletonDrawableFlutter.fromAsset(atlasFile, skeletonFile, bundle: bundle));
|
||||||
break;
|
break;
|
||||||
case _AssetType.file:
|
case _AssetType.file:
|
||||||
loadDrawable(await SkeletonDrawable.fromFile(atlasFile, skeletonFile));
|
loadDrawable(await SkeletonDrawableFlutter.fromFile(atlasFile, skeletonFile));
|
||||||
break;
|
break;
|
||||||
case _AssetType.http:
|
case _AssetType.http:
|
||||||
loadDrawable(await SkeletonDrawable.fromHttp(atlasFile, skeletonFile));
|
loadDrawable(await SkeletonDrawableFlutter.fromHttp(atlasFile, skeletonFile));
|
||||||
break;
|
break;
|
||||||
case _AssetType.drawable:
|
case _AssetType.drawable:
|
||||||
throw Exception("Drawable can not be loaded via loadFromAsset().");
|
throw Exception("Drawable can not be loaded via loadFromAsset().");
|
||||||
@ -483,7 +489,7 @@ class _SpineWidgetState extends State<SpineWidget> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SpineRenderObjectWidget extends LeafRenderObjectWidget {
|
class _SpineRenderObjectWidget extends LeafRenderObjectWidget {
|
||||||
final SkeletonDrawable _skeletonDrawable;
|
final SkeletonDrawableFlutter _skeletonDrawable;
|
||||||
final SpineWidgetController _controller;
|
final SpineWidgetController _controller;
|
||||||
final BoxFit _fit;
|
final BoxFit _fit;
|
||||||
final Alignment _alignment;
|
final Alignment _alignment;
|
||||||
@ -515,7 +521,7 @@ class _SpineRenderObjectWidget extends LeafRenderObjectWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SpineRenderObject extends RenderBox {
|
class _SpineRenderObject extends RenderBox {
|
||||||
SkeletonDrawable _skeletonDrawable;
|
SkeletonDrawableFlutter _skeletonDrawable;
|
||||||
final SpineWidgetController _controller;
|
final SpineWidgetController _controller;
|
||||||
double _deltaTime = 0;
|
double _deltaTime = 0;
|
||||||
final Stopwatch _stopwatch = Stopwatch();
|
final Stopwatch _stopwatch = Stopwatch();
|
||||||
@ -535,7 +541,7 @@ class _SpineRenderObject extends RenderBox {
|
|||||||
this._sizedByBounds,
|
this._sizedByBounds,
|
||||||
);
|
);
|
||||||
|
|
||||||
set skeletonDrawable(SkeletonDrawable skeletonDrawable) {
|
set skeletonDrawable(SkeletonDrawableFlutter skeletonDrawable) {
|
||||||
if (_skeletonDrawable == skeletonDrawable) return;
|
if (_skeletonDrawable == skeletonDrawable) return;
|
||||||
|
|
||||||
_skeletonDrawable = skeletonDrawable;
|
_skeletonDrawable = skeletonDrawable;
|
||||||
@ -721,7 +727,11 @@ class _SpineRenderObject extends RenderBox {
|
|||||||
|
|
||||||
if (_firstUpdated) {
|
if (_firstUpdated) {
|
||||||
_controller.onBeforePaint?.call(_controller, canvas);
|
_controller.onBeforePaint?.call(_controller, canvas);
|
||||||
final commands = _skeletonDrawable.renderToCanvas(canvas);
|
final commands = _skeletonDrawable.renderFlutter();
|
||||||
|
for (final cmd in commands) {
|
||||||
|
final paint = _skeletonDrawable.atlasFlutter.atlasPagePaints[cmd.atlasPageIndex][cmd.blendMode]!;
|
||||||
|
canvas.drawVertices(cmd.vertices, rendering.BlendMode.modulate, paint);
|
||||||
|
}
|
||||||
_controller.onAfterPaint?.call(_controller, canvas, commands);
|
_controller.onAfterPaint?.call(_controller, canvas, commands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
// Relative import to be able to reuse the C sources.
|
|
||||||
// See the comment in ../{projectName}}.podspec for more information.
|
|
||||||
#include "../../src/spine-cpp-lite/spine-cpp-lite.cpp"
|
|
||||||
@ -12,14 +12,9 @@ A new Flutter FFI plugin project.
|
|||||||
s.homepage = 'http://example.com'
|
s.homepage = 'http://example.com'
|
||||||
s.license = { :file => '../LICENSE' }
|
s.license = { :file => '../LICENSE' }
|
||||||
s.author = { 'Your Company' => 'email@example.com' }
|
s.author = { 'Your Company' => 'email@example.com' }
|
||||||
|
|
||||||
# This will ensure the source files in Classes/ are included in the native
|
|
||||||
# builds of apps using this FFI plugin. Podspec does not support relative
|
|
||||||
# paths, so Classes contains a forwarder C file that relatively imports
|
|
||||||
# `../src/*` so that the C sources can be shared among all target platforms.
|
|
||||||
s.source = { :path => '.' }
|
s.source = { :path => '.' }
|
||||||
s.source_files = 'Classes/**/*.{cpp}'
|
s.source_files = 'Classes/spine-c/src/**/*.{cpp,h}', 'Classes/spine-cpp/src/**/*.{cpp,h}'
|
||||||
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include"' }
|
s.xcconfig = { 'HEADER_SEARCH_PATHS' => '"$(PODS_TARGET_SRCROOT)/Classes/spine-cpp/include" "$(PODS_TARGET_SRCROOT)/Classes/spine-c/include"' }
|
||||||
s.dependency 'FlutterMacOS'
|
s.dependency 'FlutterMacOS'
|
||||||
|
|
||||||
s.platform = :osx, '10.11'
|
s.platform = :osx, '10.11'
|
||||||
|
|||||||
@ -44,5 +44,3 @@ flutter:
|
|||||||
assets:
|
assets:
|
||||||
- lib/assets/libspine_flutter.js
|
- lib/assets/libspine_flutter.js
|
||||||
- lib/assets/libspine_flutter.wasm
|
- lib/assets/libspine_flutter.wasm
|
||||||
- src/spine-cpp-lite/spine-cpp-lite.cpp
|
|
||||||
- src/spine-cpp-lite/spine-cpp-lite.h
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user