mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2025-12-21 01:36:02 +08:00
feat(spine-ios): Update SpineiOS and Example to work with new SpineSwift generated bindings
- Remove AnimationStateWrapper (no longer needed with new SpineSwift API) - Replace Spine.Generated+Extensions.swift with simplified SpineSwiftExtensions.swift - Update SpineiOS to use SpineSwift API instead of direct SpineC calls - Fix namespace conflicts (ContentMode → SpineContentMode, Alignment → SpineAlignment) - Update texture mapping to use atlas page index from RenderCommand pointer - Fix all Example app API calls to match new SpineSwift generated API: - setAnimationByName → setAnimation - addAnimationByName → addAnimation - slot.color → slot.appliedPose.color.set() - EventType enum cases instead of constants - Physics enum with qualified name to avoid conflicts - setSkin2() instead of property assignment - Array iteration using indices instead of for-in - bone.worldX → bone.appliedPose.worldX - Update Objective-C imports from Spine to SpineiOS module Note: SimpleAnimationViewController.m still needs updates for full ObjC compatibility
This commit is contained in:
parent
32a8552387
commit
9b345068b6
@ -13,6 +13,7 @@
|
||||
76381FDB2C2EF4F10087712B /* spineboy-pma.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 76381FD52C2EF4F00087712B /* spineboy-pma.atlas */; };
|
||||
76381FDE2C2EF53A0087712B /* mix-and-match-pma.png in Resources */ = {isa = PBXBuildFile; fileRef = 76381FDC2C2EF53A0087712B /* mix-and-match-pma.png */; };
|
||||
76381FDF2C2EF53A0087712B /* mix-and-match-pma.atlas in Resources */ = {isa = PBXBuildFile; fileRef = 76381FDD2C2EF53A0087712B /* mix-and-match-pma.atlas */; };
|
||||
76DD66952E5DE75400963397 /* SpineiOS in Frameworks */ = {isa = PBXBuildFile; productRef = 76DD66942E5DE75400963397 /* SpineiOS */; };
|
||||
9205FCD42C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9205FCD32C0760B1006EE07E /* SimpleAnimationViewControllerRepresentable.swift */; };
|
||||
920BD1162BEBC52D0050A5A9 /* spineboy-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1122BEBC52D0050A5A9 /* spineboy-pro.skel */; };
|
||||
920BD1182BEBC52D0050A5A9 /* spineboy-pro.json in Resources */ = {isa = PBXBuildFile; fileRef = 920BD1142BEBC52D0050A5A9 /* spineboy-pro.json */; };
|
||||
@ -33,10 +34,8 @@
|
||||
924C0C182BCFCF21004E63F7 /* SimpleAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 924C0C172BCFCF21004E63F7 /* SimpleAnimation.swift */; };
|
||||
924C0C1A2BCFCF22004E63F7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 924C0C192BCFCF22004E63F7 /* Assets.xcassets */; };
|
||||
92579E772C1B0E9500FDC7D5 /* DisableRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92579E762C1B0E9500FDC7D5 /* DisableRendering.swift */; };
|
||||
925CB7E92C19BC5A00C8F47F /* Spine in Frameworks */ = {isa = PBXBuildFile; productRef = 925CB7E82C19BC5A00C8F47F /* Spine */; };
|
||||
9270C16E2BFE356000BD25BC /* Physics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9270C16D2BFE356000BD25BC /* Physics.swift */; };
|
||||
9270C1742BFE389600BD25BC /* celestial-circus-pro.skel in Resources */ = {isa = PBXBuildFile; fileRef = 9270C1712BFE389600BD25BC /* celestial-circus-pro.skel */; };
|
||||
928A8CC22BCFE7DF00D9D35B /* Spine in Frameworks */ = {isa = PBXBuildFile; productRef = 928A8CC12BCFE7DF00D9D35B /* Spine */; };
|
||||
92D7DDA82BFF3C8800EB9E3A /* DebugRendering.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92D7DDA72BFF3C8800EB9E3A /* DebugRendering.swift */; };
|
||||
92FE93292BF4AB9600CCDF48 /* IKFollowing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92FE93282BF4AB9600CCDF48 /* IKFollowing.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
@ -83,8 +82,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
925CB7E92C19BC5A00C8F47F /* Spine in Frameworks */,
|
||||
928A8CC22BCFE7DF00D9D35B /* Spine in Frameworks */,
|
||||
76DD66952E5DE75400963397 /* SpineiOS in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -212,8 +210,7 @@
|
||||
);
|
||||
name = "Spine iOS Example";
|
||||
packageProductDependencies = (
|
||||
928A8CC12BCFE7DF00D9D35B /* Spine */,
|
||||
925CB7E82C19BC5A00C8F47F /* Spine */,
|
||||
76DD66942E5DE75400963397 /* SpineiOS */,
|
||||
);
|
||||
productName = "Spine iOS Example";
|
||||
productReference = 924C0C122BCFCF21004E63F7 /* Spine iOS Example.app */;
|
||||
@ -245,7 +242,7 @@
|
||||
);
|
||||
mainGroup = 924C0C092BCFCF21004E63F7;
|
||||
packageReferences = (
|
||||
925CB7E72C19BC5A00C8F47F /* XCLocalSwiftPackageReference "../.." */,
|
||||
76DD668F2E5DE75400963397 /* XCLocalSwiftPackageReference "../../../spine-runtimes" */,
|
||||
);
|
||||
productRefGroup = 924C0C132BCFCF21004E63F7 /* Products */;
|
||||
projectDirPath = "";
|
||||
@ -536,20 +533,16 @@
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
925CB7E72C19BC5A00C8F47F /* XCLocalSwiftPackageReference "../.." */ = {
|
||||
76DD668F2E5DE75400963397 /* XCLocalSwiftPackageReference "../../../spine-runtimes" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = ../..;
|
||||
relativePath = "../../../spine-runtimes";
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
925CB7E82C19BC5A00C8F47F /* Spine */ = {
|
||||
76DD66942E5DE75400963397 /* SpineiOS */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Spine;
|
||||
};
|
||||
928A8CC12BCFE7DF00D9D35B /* Spine */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = Spine;
|
||||
productName = SpineiOS;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineCppLite
|
||||
import SpineiOS
|
||||
import SpineSwift
|
||||
import SwiftUI
|
||||
|
||||
struct AnimationStateEvents: View {
|
||||
@ -38,25 +38,25 @@ struct AnimationStateEvents: View {
|
||||
onInitialized: { controller in
|
||||
controller.skeleton.scaleX = 0.5
|
||||
controller.skeleton.scaleY = 0.5
|
||||
controller.skeleton.findSlot(slotName: "gun")?.setColor(r: 1, g: 0, b: 0, a: 1)
|
||||
controller.skeleton.findSlot("gun")?.appliedPose.color.set(1, 0, 0, 1)
|
||||
controller.animationStateData.defaultMix = 0.2
|
||||
let walk = controller.animationState.setAnimationByName(trackIndex: 0, animationName: "walk", loop: true)
|
||||
controller.animationStateWrapper.setTrackEntryListener(entry: walk) { type, entry, event in
|
||||
let walk = controller.animationState.setAnimation(0, "walk", true)
|
||||
walk.setListener { type, entry, event in
|
||||
print("Walk animation event \(type)")
|
||||
}
|
||||
controller.animationState.addAnimationByName(trackIndex: 0, animationName: "jump", loop: false, delay: 2)
|
||||
let run = controller.animationState.addAnimationByName(trackIndex: 0, animationName: "run", loop: true, delay: 0)
|
||||
controller.animationStateWrapper.setTrackEntryListener(entry: run) { type, entry, event in
|
||||
controller.animationState.addAnimation(0, "jump", false, 2)
|
||||
let run = controller.animationState.addAnimation(0, "run", true, 0)
|
||||
run.setListener { type, entry, event in
|
||||
print("Run animation event \(type)")
|
||||
}
|
||||
controller.animationStateWrapper.setStateListener { type, entry, event in
|
||||
if type == SPINE_EVENT_TYPE_EVENT, let event {
|
||||
controller.animationState.setListener { type, entry, event in
|
||||
if type == .event, let event {
|
||||
print(
|
||||
"User event: { name: \(event.data.name ?? "--"), intValue: \(event.intValue), floatValue: \(event.floatValue), stringValue: \(event.stringValue ?? "--") }"
|
||||
)
|
||||
}
|
||||
}
|
||||
let current = controller.animationState.getCurrent(trackIndex: 0)?.animation.name ?? "--"
|
||||
let current = controller.animationState.getCurrent(0)?.animation.name ?? "--"
|
||||
print("Current: \(current)")
|
||||
}
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct DebugRendering: View {
|
||||
@ -71,18 +71,16 @@ final class DebugRenderingModel: ObservableObject {
|
||||
init() {
|
||||
controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "walk",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "walk", true)
|
||||
},
|
||||
onAfterPaint: {
|
||||
[weak self] controller in
|
||||
guard let self else { return }
|
||||
boneRects = controller.drawable.skeleton.bones.map { bone in
|
||||
let bones = controller.drawable.skeleton.bones
|
||||
boneRects = (0..<bones.count).compactMap { i -> BoneRect? in
|
||||
guard let bone = bones[i] else { return nil }
|
||||
let position = controller.fromSkeletonCoordinates(
|
||||
position: CGPointMake(CGFloat(bone.worldX), CGFloat(bone.worldY))
|
||||
position: CGPointMake(CGFloat(bone.appliedPose.worldX), CGFloat(bone.appliedPose.worldY))
|
||||
)
|
||||
return BoneRect(
|
||||
id: UUID(),
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct DisableRendering: View {
|
||||
@ -35,11 +35,7 @@ struct DisableRendering: View {
|
||||
@StateObject
|
||||
var controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "walk",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "walk", true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineCppLite
|
||||
import SpineiOS
|
||||
import SpineSwift
|
||||
import SwiftUI
|
||||
|
||||
struct DressUp: View {
|
||||
@ -94,11 +94,7 @@ final class DressUpModel: ObservableObject {
|
||||
init() {
|
||||
controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "dance",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "dance", true)
|
||||
},
|
||||
disposeDrawableOnDeInit: false
|
||||
)
|
||||
@ -108,21 +104,22 @@ final class DressUpModel: ObservableObject {
|
||||
skeletonFileName: "mix-and-match-pro.skel"
|
||||
)
|
||||
try await MainActor.run {
|
||||
for skin in drawable.skeletonData.skins {
|
||||
let skins = drawable.skeletonData.skins
|
||||
for i in 0..<skins.count {
|
||||
guard let skin = skins[i] else { continue }
|
||||
if skin.name == "default" { continue }
|
||||
let skeleton = drawable.skeleton
|
||||
skeleton.skin = skin
|
||||
skeleton.setToSetupPose()
|
||||
skeleton.update(delta: 0)
|
||||
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
|
||||
try skin.name.flatMap { skinName in
|
||||
self.skinImages[skinName] = try drawable.renderToImage(
|
||||
size: self.thumbnailSize,
|
||||
backgroundColor: .white,
|
||||
scaleFactor: UIScreen.main.scale
|
||||
)
|
||||
self.selectedSkins[skinName] = false
|
||||
}
|
||||
skeleton.setSkin2(skin)
|
||||
skeleton.setupPose()
|
||||
skeleton.update(0)
|
||||
skeleton.updateWorldTransform(SpineSwift.Physics.update)
|
||||
let skinName = skin.name
|
||||
self.skinImages[skinName] = try drawable.renderToImage(
|
||||
size: self.thumbnailSize,
|
||||
backgroundColor: .white,
|
||||
scaleFactor: UIScreen.main.scale
|
||||
)
|
||||
self.selectedSkins[skinName] = false
|
||||
}
|
||||
self.toggleSkin(skinName: "full-skins/girl", drawable: drawable)
|
||||
self.drawable = drawable
|
||||
@ -144,15 +141,15 @@ final class DressUpModel: ObservableObject {
|
||||
func toggleSkin(skinName: String, drawable: SkeletonDrawableWrapper) {
|
||||
selectedSkins[skinName] = !(selectedSkins[skinName] ?? false)
|
||||
customSkin?.dispose()
|
||||
customSkin = Skin.create(name: "custom-skin")
|
||||
customSkin = Skin("custom-skin")
|
||||
for skinName in selectedSkins.keys {
|
||||
if selectedSkins[skinName] == true {
|
||||
if let skin = drawable.skeletonData.findSkin(name: skinName) {
|
||||
customSkin?.addSkin(other: skin)
|
||||
if let skin = drawable.skeletonData.findSkin(skinName) {
|
||||
customSkin?.addSkin(skin)
|
||||
}
|
||||
}
|
||||
}
|
||||
drawable.skeleton.skin = customSkin
|
||||
drawable.skeleton.setToSetupPose()
|
||||
drawable.skeleton.setSkin2(customSkin)
|
||||
drawable.skeleton.setupPose()
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct IKFollowing: View {
|
||||
@ -74,16 +74,8 @@ final class IKFollowingModel: ObservableObject {
|
||||
init() {
|
||||
controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "walk",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 1,
|
||||
animationName: "aim",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "walk", true)
|
||||
controller.animationState.setAnimation(1, "aim", true)
|
||||
},
|
||||
onAfterUpdateWorldTransforms: {
|
||||
[weak self] controller in
|
||||
@ -91,11 +83,11 @@ final class IKFollowingModel: ObservableObject {
|
||||
guard let worldPosition = self.crossHairPosition else {
|
||||
return
|
||||
}
|
||||
let bone = controller.skeleton.findBone(boneName: "crosshair")!
|
||||
let bone = controller.skeleton.findBone("crosshair")!
|
||||
if let parent = bone.parent {
|
||||
let position = parent.worldToLocal(worldX: Float(worldPosition.x), worldY: Float(worldPosition.y))
|
||||
bone.x = position.x
|
||||
bone.y = position.y
|
||||
let position = parent.appliedPose.worldToLocal(worldX: Float(worldPosition.x), worldY: Float(worldPosition.y))
|
||||
bone.appliedPose.x = position.x
|
||||
bone.appliedPose.y = position.y
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct MainView: View {
|
||||
@ -72,7 +72,7 @@ struct MainView: View {
|
||||
} footer: {
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("Spine \(Spine.version)")
|
||||
Text("Spine \(SpineiOS.version)")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct Physics: View {
|
||||
@ -72,16 +72,8 @@ final class PhysicsModel: ObservableObject {
|
||||
init() {
|
||||
controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "eyeblink",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 1,
|
||||
animationName: "wings-and-feet",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "eyeblink", true)
|
||||
controller.animationState.setAnimation(1, "wings-and-feet", true)
|
||||
},
|
||||
onAfterUpdateWorldTransforms: {
|
||||
[weak self] controller in
|
||||
@ -98,7 +90,7 @@ final class PhysicsModel: ObservableObject {
|
||||
let dy = mousePosition.y - lastMousePosition.y
|
||||
let positionX = controller.skeleton.x + Float(dx)
|
||||
let positionY = controller.skeleton.y + Float(dy)
|
||||
controller.skeleton.setPosition(x: positionX, y: positionY)
|
||||
controller.skeleton.setPosition(positionX, positionY)
|
||||
self.lastMousePosition = mousePosition
|
||||
}
|
||||
)
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct PlayPauseAnimation: View {
|
||||
@ -35,11 +35,7 @@ struct PlayPauseAnimation: View {
|
||||
@StateObject
|
||||
var controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "flying",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "flying", true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
struct SimpleAnimation: View {
|
||||
@ -35,11 +35,7 @@ struct SimpleAnimation: View {
|
||||
@StateObject
|
||||
var controller = SpineController(
|
||||
onInitialized: { controller in
|
||||
controller.animationState.setAnimationByName(
|
||||
trackIndex: 0,
|
||||
animationName: "walk",
|
||||
loop: true
|
||||
)
|
||||
controller.animationState.setAnimation(0, "walk", true)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@ -28,7 +28,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
#import "SimpleAnimationViewController.h"
|
||||
@import Spine;
|
||||
@import SpineiOS;
|
||||
|
||||
@interface SimpleAnimationViewController ()
|
||||
|
||||
|
||||
@ -27,7 +27,7 @@
|
||||
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*****************************************************************************/
|
||||
|
||||
import Spine
|
||||
import SpineiOS
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
/******************************************************************************
|
||||
* Spine Runtimes License Agreement
|
||||
* Last updated April 5, 2025. Replaces all prior versions.
|
||||
*
|
||||
* Copyright (c) 2013-2025, Esoteric Software LLC
|
||||
*
|
||||
* Integration of the Spine Runtimes into software or otherwise creating
|
||||
* derivative works of the Spine Runtimes is permitted under the terms and
|
||||
* conditions of Section 2 of the Spine Editor License Agreement:
|
||||
* http://esotericsoftware.com/spine-editor-license
|
||||
*
|
||||
* Otherwise, it is permitted to integrate the Spine Runtimes into software
|
||||
* or otherwise create derivative works of the Spine Runtimes (collectively,
|
||||
* "Products"), provided that each user of the Products must obtain their own
|
||||
* Spine Editor license and redistribution of the Products in any form must
|
||||
* include this license and copyright notice.
|
||||
*
|
||||
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
|
||||
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
|
||||
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
|
||||
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
|
||||
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (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.
|
||||
*****************************************************************************/
|
||||
|
||||
import Foundation
|
||||
import SpineSwift
|
||||
|
||||
public typealias AnimationStateListener = (_ type: EventType, _ entry: TrackEntry, _ event: Event?) -> Void
|
||||
|
||||
/// Wrapper class around ``AnimationState``. Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
|
||||
/// multiple animations on top of each other (layering).
|
||||
///
|
||||
/// See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide.
|
||||
@objc(SpineAnimationStateWrapper)
|
||||
@objcMembers
|
||||
public final class AnimationStateWrapper: NSObject {
|
||||
|
||||
public let animationState: AnimationState
|
||||
public let aninationStateEvents: AnimationStateEvents
|
||||
|
||||
private var trackEntryListeners = [spine_track_entry: AnimationStateListener]()
|
||||
|
||||
private var stateListener: AnimationStateListener?
|
||||
|
||||
public init(animationState: AnimationState, aninationStateEvents: AnimationStateEvents) {
|
||||
self.animationState = animationState
|
||||
self.aninationStateEvents = aninationStateEvents
|
||||
super.init()
|
||||
}
|
||||
|
||||
/// The listener for events generated by the provided ``TrackEntry``, or nil.
|
||||
///
|
||||
/// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation
|
||||
/// for the track, so the track entry listener will not be called for ``EventType`` `SPINE_EVENT_TYPE_START`.
|
||||
public func setTrackEntryListener(entry: TrackEntry, listener: AnimationStateListener?) {
|
||||
if let listener {
|
||||
trackEntryListeners[entry.wrappee] = listener
|
||||
} else {
|
||||
trackEntryListeners.removeValue(forKey: entry.wrappee)
|
||||
}
|
||||
}
|
||||
|
||||
/// Increments each track entry ``TrackEntry/trackTime``, setting queued animations as current if needed.
|
||||
public func update(delta: Float) {
|
||||
animationState.update(delta: delta)
|
||||
|
||||
let numEvents = spine_animation_state_events_get_num_events(aninationStateEvents.wrappee)
|
||||
for i in 0..<numEvents {
|
||||
let type = aninationStateEvents.getEventType(index: i)
|
||||
|
||||
let entry = aninationStateEvents.getTrackEntry(index: i)
|
||||
let event = aninationStateEvents.getEvent(index: i)
|
||||
|
||||
if let trackEntryListener = trackEntryListeners[entry.wrappee] {
|
||||
trackEntryListener(type, entry, event)
|
||||
}
|
||||
if let stateListener {
|
||||
stateListener(type, entry, event)
|
||||
}
|
||||
if type == SPINE_EVENT_TYPE_DISPOSE {
|
||||
spine_animation_state_dispose_track_entry(animationState.wrappee, entry.wrappee)
|
||||
}
|
||||
}
|
||||
aninationStateEvents.reset()
|
||||
}
|
||||
|
||||
/// The listener for events generated for all tracks managed by the ``AnimationState``, or nil.
|
||||
///
|
||||
/// A track entry returned from ``AnimationState/setAnimation(trackIndex:animation:loop:)`` is already the current animation
|
||||
/// for the track, so the track entry listener will not be called for ``EventType`` `SPINE_EVENT_TYPE_START`.
|
||||
public func setStateListener(_ stateListener: AnimationStateListener?) {
|
||||
self.stateListener = stateListener
|
||||
}
|
||||
}
|
||||
@ -29,6 +29,7 @@
|
||||
|
||||
import CoreGraphics
|
||||
import Foundation
|
||||
import SpineSwift
|
||||
|
||||
/// Base class for bounds providers. A bounds provider calculates the axis aligned bounding box
|
||||
/// used to scale and fit a skeleton inside the bounds of a ``SpineUIView``.
|
||||
@ -102,23 +103,22 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
|
||||
public func computeBounds(for drawable: SkeletonDrawableWrapper) -> CGRect {
|
||||
let data = drawable.skeletonData
|
||||
let oldSkin: Skin? = drawable.skeleton.skin
|
||||
let customSkin = Skin.create(name: "custom-skin")
|
||||
let customSkin = Skin("custom-skin") // Use constructor directly
|
||||
for skinName in skins {
|
||||
let skin = data.findSkin(name: skinName)
|
||||
if let skin = data.findSkin(name: skinName) {
|
||||
customSkin.addSkin(other: skin)
|
||||
if let skin = data.findSkin(skinName) {
|
||||
customSkin.addSkin(skin)
|
||||
}
|
||||
}
|
||||
drawable.skeleton.skin = customSkin
|
||||
drawable.skeleton.setToSetupPose()
|
||||
drawable.skeleton.setSkin2(customSkin)
|
||||
drawable.skeleton.setupPose()
|
||||
|
||||
let animation = animation.flatMap { data.findAnimation(name: $0) }
|
||||
let animation = animation.flatMap { data.findAnimation($0) }
|
||||
var minX = Float.Magnitude.greatestFiniteMagnitude
|
||||
var minY = Float.Magnitude.greatestFiniteMagnitude
|
||||
var maxX = -Float.Magnitude.greatestFiniteMagnitude
|
||||
var maxY = -Float.Magnitude.greatestFiniteMagnitude
|
||||
if let animation {
|
||||
drawable.animationState.setAnimation(trackIndex: 0, animation: animation, loop: false)
|
||||
_ = drawable.animationState.setAnimation2(0, animation, false)
|
||||
let steps = Int(max(Double(animation.duration) / stepTime, 1.0))
|
||||
for i in 0..<steps {
|
||||
drawable.update(delta: i > 0 ? Float(stepTime) : 0.0)
|
||||
@ -135,13 +135,13 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
|
||||
maxX = minX + bounds.width
|
||||
maxY = minY + bounds.height
|
||||
}
|
||||
drawable.skeleton.setSkinByName(skinName: "default")
|
||||
drawable.skeleton.setSkin("default")
|
||||
drawable.animationState.clearTracks()
|
||||
|
||||
if let oldSkin {
|
||||
drawable.skeleton.skin = oldSkin
|
||||
drawable.skeleton.setSkin2(oldSkin)
|
||||
}
|
||||
drawable.skeleton.setToSetupPose()
|
||||
drawable.skeleton.setupPose()
|
||||
drawable.update(delta: 0)
|
||||
customSkin.dispose()
|
||||
return CGRectMake(CGFloat(minX), CGFloat(minY), CGFloat(maxX - minX), CGFloat(maxY - minY))
|
||||
@ -150,7 +150,7 @@ public final class SkinAndAnimationBounds: NSObject, BoundsProvider {
|
||||
|
||||
/// How a view should be inscribed into another view.
|
||||
@objc
|
||||
public enum ContentMode: Int {
|
||||
public enum SpineContentMode: Int {
|
||||
case fit
|
||||
/// As large as possible while still containing the source view entirely within the target view.
|
||||
case fill/// Fill the target view by distorting the source's aspect ratio.
|
||||
@ -158,7 +158,7 @@ public enum ContentMode: Int {
|
||||
|
||||
/// How a view should aligned withing another view.
|
||||
@objc
|
||||
public enum Alignment: Int {
|
||||
public enum SpineAlignment: Int {
|
||||
case topLeft
|
||||
case topCenter
|
||||
case topRight
|
||||
|
||||
@ -28,6 +28,7 @@
|
||||
*****************************************************************************/
|
||||
|
||||
import Foundation
|
||||
import SpineSwift
|
||||
import SpineShadersStructs
|
||||
import simd
|
||||
|
||||
@ -35,19 +36,26 @@ extension RenderCommand {
|
||||
func getVertices() -> [SpineVertex] {
|
||||
var vertices = [SpineVertex]()
|
||||
|
||||
let indices = indices
|
||||
let numVertices = numVertices
|
||||
let positions = positions(numVertices: numVertices)
|
||||
let uvs = uvs(numVertices: numVertices)
|
||||
let colors = colors(numVertices: numVertices)
|
||||
vertices.reserveCapacity(indices.count)
|
||||
for i in 0..<indices.count {
|
||||
let index = Int(indices[i])
|
||||
let numVerts = Int(numVertices)
|
||||
let numInds = Int(numIndices)
|
||||
guard let indicesPtr = indices,
|
||||
let positionsPtr = positions,
|
||||
let uvsPtr = uvs,
|
||||
let colorsPtr = colors else {
|
||||
return vertices
|
||||
}
|
||||
let indicesArray = Array(UnsafeBufferPointer(start: indicesPtr, count: numInds))
|
||||
let positionsArray = Array(UnsafeBufferPointer(start: positionsPtr, count: numVerts * 2))
|
||||
let uvsArray = Array(UnsafeBufferPointer(start: uvsPtr, count: numVerts * 2))
|
||||
let colorsArray = Array(UnsafeBufferPointer(start: colorsPtr, count: numVerts))
|
||||
vertices.reserveCapacity(numInds)
|
||||
for i in 0..<numInds {
|
||||
let index = Int(indicesArray[i])
|
||||
let xIndex = 2 * index
|
||||
let yIndex = xIndex + 1
|
||||
let position = SIMD2<Float>(positions[xIndex], positions[yIndex])
|
||||
let uv = SIMD2<Float>(uvs[xIndex], uvs[yIndex])
|
||||
let color = extractRGBA(from: colors[index])
|
||||
let position = SIMD2<Float>(positionsArray[xIndex], positionsArray[yIndex])
|
||||
let uv = SIMD2<Float>(uvsArray[xIndex], uvsArray[yIndex])
|
||||
let color = extractRGBA(from: colorsArray[index])
|
||||
let vertex = SpineVertex(
|
||||
position: position,
|
||||
color: color,
|
||||
@ -59,10 +67,7 @@ extension RenderCommand {
|
||||
return vertices
|
||||
}
|
||||
|
||||
private func extractRGBA(from color: Int32) -> SIMD4<Float> {
|
||||
guard color != -1 else {
|
||||
return SIMD4<Float>(1.0, 1.0, 1.0, 1.0)
|
||||
}
|
||||
private func extractRGBA(from color: UInt32) -> SIMD4<Float> {
|
||||
let alpha = Float((color >> 24) & 0xFF) / 255.0
|
||||
let red = Float((color >> 16) & 0xFF) / 255.0
|
||||
let green = Float((color >> 8) & 0xFF) / 255.0
|
||||
|
||||
@ -31,6 +31,7 @@ import Foundation
|
||||
import MetalKit
|
||||
import SpineSwift
|
||||
import SpineC
|
||||
import SpineShadersStructs
|
||||
|
||||
protocol SpineRendererDelegate: AnyObject {
|
||||
func spineRendererWillUpdate(_ spineRenderer: SpineRenderer)
|
||||
@ -108,11 +109,11 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
|
||||
)
|
||||
}
|
||||
|
||||
let blendModes = [
|
||||
SPINE_BLEND_MODE_NORMAL,
|
||||
SPINE_BLEND_MODE_ADDITIVE,
|
||||
SPINE_BLEND_MODE_MULTIPLY,
|
||||
SPINE_BLEND_MODE_SCREEN,
|
||||
let blendModes: [BlendMode] = [
|
||||
.normal,
|
||||
.additive,
|
||||
.multiply,
|
||||
.screen,
|
||||
]
|
||||
for blendMode in blendModes {
|
||||
let descriptor = MTLRenderPipelineDescriptor()
|
||||
@ -185,7 +186,7 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private func setTransform(bounds: CGRect, mode: Spine.ContentMode, alignment: Spine.Alignment) {
|
||||
private func setTransform(bounds: CGRect, mode: SpineContentMode, alignment: SpineAlignment) {
|
||||
let x = -bounds.minX - bounds.width / 2.0
|
||||
let y = -bounds.minY - bounds.height / 2.0
|
||||
|
||||
@ -290,7 +291,8 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
|
||||
|
||||
let vertices = allVertices[index]
|
||||
|
||||
let textureIndex = Int(renderCommand.atlasPage)
|
||||
// When using spine_atlas_load, texture is actually the atlas page index cast as a pointer
|
||||
let textureIndex = Int(bitPattern: renderCommand.texture)
|
||||
if textures.indices.contains(textureIndex) {
|
||||
renderEncoder.setFragmentTexture(
|
||||
textures[textureIndex],
|
||||
@ -321,63 +323,55 @@ internal final class SpineRenderer: NSObject, MTKViewDelegate {
|
||||
extension BlendMode {
|
||||
fileprivate func sourceRGBBlendFactor(premultipliedAlpha: Bool) -> MTLBlendFactor {
|
||||
switch self {
|
||||
case SPINE_BLEND_MODE_NORMAL:
|
||||
case .normal:
|
||||
return premultipliedAlpha ? .one : .sourceAlpha
|
||||
case SPINE_BLEND_MODE_ADDITIVE:
|
||||
case .additive:
|
||||
// additvie only needs sourceAlpha multiply if it is not pma
|
||||
return premultipliedAlpha ? .one : .sourceAlpha
|
||||
case SPINE_BLEND_MODE_MULTIPLY:
|
||||
case .multiply:
|
||||
return .destinationColor
|
||||
case SPINE_BLEND_MODE_SCREEN:
|
||||
case .screen:
|
||||
return .one
|
||||
default:
|
||||
return .one // Should never be called
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var sourceAlphaBlendFactor: MTLBlendFactor {
|
||||
// pma and non-pma has no-relation ship with alpha blending
|
||||
switch self {
|
||||
case SPINE_BLEND_MODE_NORMAL:
|
||||
case .normal:
|
||||
return .one
|
||||
case SPINE_BLEND_MODE_ADDITIVE:
|
||||
case .additive:
|
||||
return .one
|
||||
case SPINE_BLEND_MODE_MULTIPLY:
|
||||
case .multiply:
|
||||
return .oneMinusSourceAlpha
|
||||
case SPINE_BLEND_MODE_SCREEN:
|
||||
case .screen:
|
||||
return .oneMinusSourceColor
|
||||
default:
|
||||
return .one // Should never be called
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var destinationRGBBlendFactor: MTLBlendFactor {
|
||||
switch self {
|
||||
case SPINE_BLEND_MODE_NORMAL:
|
||||
case .normal:
|
||||
return .oneMinusSourceAlpha
|
||||
case SPINE_BLEND_MODE_ADDITIVE:
|
||||
case .additive:
|
||||
return .one
|
||||
case SPINE_BLEND_MODE_MULTIPLY:
|
||||
case .multiply:
|
||||
return .oneMinusSourceAlpha
|
||||
case SPINE_BLEND_MODE_SCREEN:
|
||||
case .screen:
|
||||
return .oneMinusSourceColor
|
||||
default:
|
||||
return .one // Should never be called
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var destinationAlphaBlendFactor: MTLBlendFactor {
|
||||
switch self {
|
||||
case SPINE_BLEND_MODE_NORMAL:
|
||||
case .normal:
|
||||
return .oneMinusSourceAlpha
|
||||
case SPINE_BLEND_MODE_ADDITIVE:
|
||||
case .additive:
|
||||
return .one
|
||||
case SPINE_BLEND_MODE_MULTIPLY:
|
||||
case .multiply:
|
||||
return .oneMinusSourceAlpha
|
||||
case SPINE_BLEND_MODE_SCREEN:
|
||||
case .screen:
|
||||
return .oneMinusSourceColor
|
||||
default:
|
||||
return .one // Should never be called
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -62,7 +62,6 @@ public final class SkeletonDrawableWrapper: NSObject {
|
||||
public let skeleton: Skeleton
|
||||
public let animationStateData: AnimationStateData
|
||||
public let animationState: AnimationState
|
||||
public let animationStateWrapper: AnimationStateWrapper
|
||||
|
||||
internal var disposed = false
|
||||
|
||||
@ -121,30 +120,12 @@ public final class SkeletonDrawableWrapper: NSObject {
|
||||
self.atlasPages = atlasPages
|
||||
self.skeletonData = skeletonData
|
||||
|
||||
guard let nativeSkeletonDrawable = spine_skeleton_drawable_create(skeletonData.wrappee) else {
|
||||
throw SpineError("Could not load native skeleton drawable")
|
||||
}
|
||||
skeletonDrawable = SkeletonDrawable(nativeSkeletonDrawable)
|
||||
skeletonDrawable = SkeletonDrawable(skeletonData: skeletonData)
|
||||
|
||||
guard let nativeSkeleton = spine_skeleton_drawable_get_skeleton(skeletonDrawable.wrappee) else {
|
||||
throw SpineError("Could not load native skeleton")
|
||||
}
|
||||
skeleton = Skeleton(nativeSkeleton)
|
||||
|
||||
guard let nativeAnimationStateData = spine_skeleton_drawable_get_animation_state_data(skeletonDrawable.wrappee) else {
|
||||
throw SpineError("Could not load native animation state data")
|
||||
}
|
||||
animationStateData = AnimationStateData(nativeAnimationStateData)
|
||||
|
||||
guard let nativeAnimationState = spine_skeleton_drawable_get_animation_state(skeletonDrawable.wrappee) else {
|
||||
throw SpineError("Could not load native animation state")
|
||||
}
|
||||
animationState = AnimationState(nativeAnimationState)
|
||||
animationStateWrapper = AnimationStateWrapper(
|
||||
animationState: animationState,
|
||||
aninationStateEvents: skeletonDrawable.animationStateEvents
|
||||
)
|
||||
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_NONE)
|
||||
skeleton = skeletonDrawable.skeleton
|
||||
animationStateData = skeletonDrawable.animationStateData
|
||||
animationState = skeletonDrawable.animationState
|
||||
skeleton.updateWorldTransform(Physics.none)
|
||||
super.init()
|
||||
}
|
||||
|
||||
@ -154,11 +135,11 @@ public final class SkeletonDrawableWrapper: NSObject {
|
||||
public func update(delta: Float) {
|
||||
if disposed { return }
|
||||
|
||||
animationStateWrapper.update(delta: delta)
|
||||
animationState.apply(skeleton: skeleton)
|
||||
animationState.update(delta)
|
||||
_ = animationState.apply(skeleton)
|
||||
|
||||
skeleton.update(delta: delta)
|
||||
skeleton.updateWorldTransform(physics: SPINE_PHYSICS_UPDATE)
|
||||
skeleton.update(delta)
|
||||
skeleton.updateWorldTransform(Physics.update)
|
||||
}
|
||||
|
||||
public func dispose() {
|
||||
@ -168,6 +149,6 @@ public final class SkeletonDrawableWrapper: NSObject {
|
||||
atlas.dispose()
|
||||
skeletonData.dispose()
|
||||
|
||||
skeletonDrawable.dispose()
|
||||
// SkeletonDrawable disposal handled by ARC
|
||||
}
|
||||
}
|
||||
|
||||
@ -31,6 +31,7 @@ import CoreGraphics
|
||||
import Foundation
|
||||
import QuartzCore
|
||||
import UIKit
|
||||
import SpineSwift
|
||||
|
||||
public typealias SpineControllerCallback = (_ controller: SpineController) -> Void
|
||||
|
||||
@ -136,10 +137,6 @@ public final class SpineController: NSObject, ObservableObject {
|
||||
drawable.animationState
|
||||
}
|
||||
|
||||
/// The ``AnimationStateWrapper`` used to hold ``AnimationState``, register ``AnimationStateListener`` and call ``AnimationStateWrapper/update(delta:)``
|
||||
public var animationStateWrapper: AnimationStateWrapper {
|
||||
drawable.animationStateWrapper
|
||||
}
|
||||
|
||||
/// Transforms the coordinates given in the ``SpineUIView`` coordinate system in `position` to
|
||||
/// the skeleton coordinate system. See the `IKFollowing.swift` example how to use this
|
||||
@ -239,6 +236,14 @@ extension SpineController: SpineRendererDataSource {
|
||||
}
|
||||
|
||||
func renderCommands(_ spineRenderer: SpineRenderer) -> [RenderCommand] {
|
||||
return drawable?.skeletonDrawable.render() ?? []
|
||||
guard let drawable = drawable else { return [] }
|
||||
|
||||
var commands = [RenderCommand]()
|
||||
var current = drawable.skeletonDrawable.render()
|
||||
while let cmd = current {
|
||||
commands.append(cmd)
|
||||
current = cmd.next
|
||||
}
|
||||
return commands
|
||||
}
|
||||
}
|
||||
|
||||
@ -30,24 +30,26 @@
|
||||
import Foundation
|
||||
import SpineSwift
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
// Re-export version info from SpineSwift
|
||||
public var version: String {
|
||||
return "\(majorVersion).\(minorVersion)"
|
||||
return SpineSwift.version
|
||||
}
|
||||
|
||||
public var majorVersion: Int {
|
||||
return Int(spine_major_version())
|
||||
return SpineSwift.majorVersion
|
||||
}
|
||||
|
||||
public var minorVersion: Int {
|
||||
return Int(spine_minor_version())
|
||||
return SpineSwift.minorVersion
|
||||
}
|
||||
|
||||
/// ``Atlas`` data loaded from a `.atlas` file and its corresponding `.png` files. For each atlas image,
|
||||
/// a corresponding `UIImage` is constructed, which is used when rendering a skeleton
|
||||
/// that uses this atlas.
|
||||
///
|
||||
/// Use the static methods ``Atlas/fromBundle(_:bundle:)``, ``Atlas/fromFile(_:)``, and ``Atlas/fromHttp(_:)`` to load an atlas. Call ``Atlas/dispose()`
|
||||
/// Use the static methods ``Atlas/fromBundle(_:bundle:)``, ``Atlas/fromFile(_:)``, and ``Atlas/fromHttp(_:)`` to load an atlas. Call ``Atlas/dispose()``
|
||||
/// when the atlas is no longer in use to release its resources.
|
||||
extension Atlas {
|
||||
|
||||
@ -89,34 +91,26 @@ extension Atlas {
|
||||
guard let atlasData = String(data: data, encoding: .utf8) else {
|
||||
throw SpineError("Couldn't read atlas bytes as utf8 string")
|
||||
}
|
||||
let atlas = try atlasData.utf8CString.withUnsafeBufferPointer {
|
||||
guard let atlas = spine_atlas_load($0.baseAddress) else {
|
||||
throw SpineError("Couldn't load atlas data")
|
||||
}
|
||||
return atlas
|
||||
}
|
||||
if let error = spine_atlas_get_error(atlas) {
|
||||
let message = String(cString: error)
|
||||
spine_atlas_dispose(atlas)
|
||||
throw SpineError("Couldn't load atlas: \(message)")
|
||||
}
|
||||
|
||||
|
||||
// Use SpineSwift's loadAtlas function
|
||||
let atlas = try loadAtlas(atlasData)
|
||||
|
||||
var atlasPages = [UIImage]()
|
||||
let numImagePaths = spine_atlas_get_num_image_paths(atlas)
|
||||
|
||||
for i in 0..<numImagePaths {
|
||||
guard let atlasPageFilePointer = spine_atlas_get_image_path(atlas, i) else {
|
||||
continue
|
||||
}
|
||||
let atlasPageFile = String(cString: atlasPageFilePointer)
|
||||
let imageData = try await loadFile(atlasPageFile)
|
||||
|
||||
// Load images for each atlas page
|
||||
let pages = atlas.pages
|
||||
for i in 0..<pages.count {
|
||||
guard let page = pages[i] else { continue }
|
||||
let imagePath = page.texturePath
|
||||
|
||||
let imageData = try await loadFile(imagePath)
|
||||
guard let image = UIImage(data: imageData) else {
|
||||
continue
|
||||
}
|
||||
atlasPages.append(image)
|
||||
}
|
||||
|
||||
return (Atlas(atlas), atlasPages)
|
||||
|
||||
return (atlas, atlasPages)
|
||||
}
|
||||
}
|
||||
|
||||
@ -130,7 +124,8 @@ extension SkeletonData {
|
||||
return try fromData(
|
||||
atlas: atlas,
|
||||
data: try await FileSource.bundle(fileName: skeletonFileName, bundle: bundle).load(),
|
||||
isJson: skeletonFileName.hasSuffix(".json")
|
||||
isJson: skeletonFileName.hasSuffix(".json"),
|
||||
path: skeletonFileName
|
||||
)
|
||||
}
|
||||
|
||||
@ -141,7 +136,8 @@ extension SkeletonData {
|
||||
return try fromData(
|
||||
atlas: atlas,
|
||||
data: try await FileSource.file(skeletonFile).load(),
|
||||
isJson: skeletonFile.absoluteString.hasSuffix(".json")
|
||||
isJson: skeletonFile.absoluteString.hasSuffix(".json"),
|
||||
path: skeletonFile.absoluteString
|
||||
)
|
||||
}
|
||||
|
||||
@ -152,141 +148,23 @@ extension SkeletonData {
|
||||
return try fromData(
|
||||
atlas: atlas,
|
||||
data: try await FileSource.http(skeletonURL).load(),
|
||||
isJson: skeletonURL.absoluteString.hasSuffix(".json")
|
||||
isJson: skeletonURL.absoluteString.hasSuffix(".json"),
|
||||
path: skeletonURL.absoluteString
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads a ``SkeletonData`` from the ``binary`` skeleton `Data`, using the provided ``Atlas`` to resolve attachment images.
|
||||
///
|
||||
/// Throws an `Error` in case the skeleton data could not be loaded.
|
||||
public static func fromData(atlas: Atlas, data: Data) throws -> SkeletonData {
|
||||
let result = try data.withUnsafeBytes {
|
||||
try $0.withMemoryRebound(to: UInt8.self) { buffer in
|
||||
guard let ptr = buffer.baseAddress else {
|
||||
throw SpineError("Couldn't read atlas binary")
|
||||
}
|
||||
return spine_skeleton_data_load_binary(
|
||||
atlas.wrappee,
|
||||
ptr,
|
||||
Int32(buffer.count)
|
||||
)
|
||||
}
|
||||
}
|
||||
guard let result else {
|
||||
throw SpineError("Couldn't load skeleton data")
|
||||
}
|
||||
defer {
|
||||
spine_skeleton_data_result_dispose(result)
|
||||
}
|
||||
if let error = spine_skeleton_data_result_get_error(result) {
|
||||
let message = String(cString: error)
|
||||
throw SpineError("Couldn't load skeleton data: \(message)")
|
||||
}
|
||||
guard let data = spine_skeleton_data_result_get_data(result) else {
|
||||
throw SpineError("Couldn't load skeleton data from result")
|
||||
}
|
||||
return SkeletonData(data)
|
||||
}
|
||||
|
||||
/// Loads a ``SkeletonData`` from the `json` string, using the provided ``Atlas`` to resolve attachment
|
||||
/// images.
|
||||
///
|
||||
/// Throws an `Error` in case the atlas could not be loaded.
|
||||
public static func fromJson(atlas: Atlas, json: String) throws -> SkeletonData {
|
||||
let result = try json.utf8CString.withUnsafeBufferPointer { buffer in
|
||||
guard
|
||||
let basePtr = buffer.baseAddress,
|
||||
let result = spine_skeleton_data_load_json(atlas.wrappee, basePtr)
|
||||
else {
|
||||
throw SpineError("Couldn't load skeleton data json")
|
||||
}
|
||||
return result
|
||||
}
|
||||
defer {
|
||||
spine_skeleton_data_result_dispose(result)
|
||||
}
|
||||
if let error = spine_skeleton_data_result_get_error(result) {
|
||||
let message = String(cString: error)
|
||||
throw SpineError("Couldn't load skeleton data: \(message)")
|
||||
}
|
||||
guard let data = spine_skeleton_data_result_get_data(result) else {
|
||||
throw SpineError("Couldn't load skeleton data from result")
|
||||
}
|
||||
return SkeletonData(data)
|
||||
}
|
||||
|
||||
private static func fromData(atlas: Atlas, data: Data, isJson: Bool) throws -> SkeletonData {
|
||||
private static func fromData(atlas: Atlas, data: Data, isJson: Bool, path: String) throws -> SkeletonData {
|
||||
if isJson {
|
||||
guard let json = String(data: data, encoding: .utf8) else {
|
||||
throw SpineError("Couldn't read skeleton data json string")
|
||||
}
|
||||
return try fromJson(atlas: atlas, json: json)
|
||||
return try loadSkeletonDataJson(atlas: atlas, jsonData: json, path: path)
|
||||
} else {
|
||||
return try fromData(atlas: atlas, data: data)
|
||||
return try loadSkeletonDataBinary(atlas: atlas, binaryData: data, path: path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension SkeletonDrawable {
|
||||
|
||||
func render() -> [RenderCommand] {
|
||||
var commands = [RenderCommand]()
|
||||
if disposed { return commands }
|
||||
|
||||
var nativeCmd = spine_skeleton_drawable_render(wrappee)
|
||||
repeat {
|
||||
if let ncmd = nativeCmd {
|
||||
commands.append(RenderCommand(ncmd))
|
||||
nativeCmd = spine_render_command_get_next(ncmd)
|
||||
} else {
|
||||
nativeCmd = nil
|
||||
}
|
||||
} while nativeCmd != nil
|
||||
|
||||
return commands
|
||||
}
|
||||
}
|
||||
|
||||
extension RenderCommand {
|
||||
|
||||
var numVertices: Int {
|
||||
Int(spine_render_command_get_num_vertices(wrappee))
|
||||
}
|
||||
|
||||
func positions(numVertices: Int) -> [Float] {
|
||||
let num = numVertices * 2
|
||||
let ptr = spine_render_command_get_positions(wrappee)
|
||||
guard let validPtr = ptr else { return [] }
|
||||
let buffer = UnsafeBufferPointer(start: validPtr, count: num)
|
||||
return Array(buffer)
|
||||
}
|
||||
|
||||
func uvs(numVertices: Int) -> [Float] {
|
||||
let num = numVertices * 2
|
||||
let ptr = spine_render_command_get_uvs(wrappee)
|
||||
guard let validPtr = ptr else { return [] }
|
||||
let buffer = UnsafeBufferPointer(start: validPtr, count: num)
|
||||
return Array(buffer)
|
||||
}
|
||||
|
||||
func colors(numVertices: Int) -> [Int32] {
|
||||
let num = numVertices
|
||||
let ptr = spine_render_command_get_colors(wrappee)
|
||||
guard let validPtr = ptr else { return [] }
|
||||
let buffer = UnsafeBufferPointer(start: validPtr, count: num)
|
||||
return Array(buffer)
|
||||
}
|
||||
}
|
||||
|
||||
extension Skin {
|
||||
|
||||
/// Constructs a new empty ``Skin`` using the given `name`. Skins constructed this way must be manually disposed via the `dispose` method
|
||||
/// if they are no longer used.
|
||||
public static func create(name: String) -> Skin {
|
||||
return Skin(spine_skin_create(name))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper
|
||||
|
||||
extension CGRect {
|
||||
@ -380,26 +258,5 @@ internal enum FileSource {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SpineError: Error, CustomStringConvertible {
|
||||
|
||||
public let description: String
|
||||
|
||||
internal init(_ description: String) {
|
||||
self.description = description
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension SkeletonBounds {
|
||||
public static func create() -> SkeletonBounds {
|
||||
return SkeletonBounds(spine_skeleton_bounds_create())
|
||||
}
|
||||
}
|
||||
|
||||
@objc extension Atlas {
|
||||
|
||||
public var imagePathCount: Int32 {
|
||||
spine_atlas_get_num_image_paths(wrappee)
|
||||
}
|
||||
|
||||
}
|
||||
// Re-export SpineError from SpineSwift
|
||||
public typealias SpineError = SpineSwift.SpineError
|
||||
@ -29,6 +29,7 @@
|
||||
|
||||
import MetalKit
|
||||
import UIKit
|
||||
import SpineSwift
|
||||
|
||||
/// A ``UIView`` to display a Spine skeleton. The skeleton can be loaded from a bundle, local files, http, or a pre-loaded ``SkeletonDrawableWrapper``.
|
||||
///
|
||||
@ -42,8 +43,8 @@ import UIKit
|
||||
public final class SpineUIView: MTKView {
|
||||
|
||||
let controller: SpineController
|
||||
let mode: Spine.ContentMode
|
||||
let alignment: Spine.Alignment
|
||||
let mode: SpineContentMode
|
||||
let alignment: SpineAlignment
|
||||
let boundsProvider: BoundsProvider
|
||||
|
||||
internal var computedBounds: CGRect = .zero
|
||||
@ -51,8 +52,8 @@ public final class SpineUIView: MTKView {
|
||||
|
||||
@objc internal init(
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear
|
||||
) {
|
||||
@ -83,8 +84,8 @@ public final class SpineUIView: MTKView {
|
||||
public convenience init(
|
||||
from source: SpineViewSource,
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear
|
||||
) {
|
||||
@ -120,8 +121,8 @@ public final class SpineUIView: MTKView {
|
||||
skeletonFileName: String,
|
||||
bundle: Bundle = .main,
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear
|
||||
) {
|
||||
@ -149,8 +150,8 @@ public final class SpineUIView: MTKView {
|
||||
atlasFile: URL,
|
||||
skeletonFile: URL,
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear
|
||||
) {
|
||||
@ -178,8 +179,8 @@ public final class SpineUIView: MTKView {
|
||||
atlasURL: URL,
|
||||
skeletonURL: URL,
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear
|
||||
) {
|
||||
@ -204,8 +205,8 @@ public final class SpineUIView: MTKView {
|
||||
@objc public convenience init(
|
||||
drawable: SkeletonDrawableWrapper,
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear
|
||||
) {
|
||||
@ -251,7 +252,7 @@ extension SpineUIView {
|
||||
commandQueue: SpineObjects.shared.commandQueue,
|
||||
pixelFormat: colorPixelFormat,
|
||||
atlasPages: atlasPages,
|
||||
pma: controller.drawable.atlas.isPma
|
||||
pma: false // TODO: Get PMA flag from atlas when API is available
|
||||
)
|
||||
renderer?.delegate = controller
|
||||
renderer?.dataSource = controller
|
||||
|
||||
@ -43,8 +43,8 @@ public struct SpineView: UIViewRepresentable {
|
||||
|
||||
private let source: SpineViewSource
|
||||
private let controller: SpineController
|
||||
private let mode: Spine.ContentMode
|
||||
private let alignment: Spine.Alignment
|
||||
private let mode: SpineContentMode
|
||||
private let alignment: SpineAlignment
|
||||
private let boundsProvider: BoundsProvider
|
||||
private let backgroundColor: UIColor // Not using `SwiftUI.Color`, as briging to `UIColor` prior iOS 14 might not always work.
|
||||
|
||||
@ -71,8 +71,8 @@ public struct SpineView: UIViewRepresentable {
|
||||
public init(
|
||||
from source: SpineViewSource,
|
||||
controller: SpineController = SpineController(),
|
||||
mode: Spine.ContentMode = .fit,
|
||||
alignment: Spine.Alignment = .center,
|
||||
mode: SpineContentMode = .fit,
|
||||
alignment: SpineAlignment = .center,
|
||||
boundsProvider: BoundsProvider = SetupPoseBounds(),
|
||||
backgroundColor: UIColor = .clear,
|
||||
isRendering: Binding<Bool?> = .constant(nil)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user