mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
feat(spine-ios): Implement SpineSwift high-level API mirroring spine_dart.dart
- Created SkeletonDrawable class wrapping spine_skeleton_drawable C functions - Implemented AnimationStateEventManager singleton for event listener management - Added helper types: Bounds and Vector structs - Added extensions for Skeleton (bounds, getPosition) - Added extensions for Skin (getEntries to iterate attachments) - Added extensions for BonePose (coordinate transformations) - Added extensions for AnimationState and TrackEntry (event listeners) - Created skeleton_drawable_test_swift.swift using SpineSwift high-level API - Updated test Package.swift to include SpineSwift dependency - SpineSwift module now compiles with 0 errors This completes the port of the high-level API from spine-flutter's spine_dart.dart to SpineSwift, providing a clean Swift API that mirrors the Dart implementation.
This commit is contained in:
parent
9fdc0f0033
commit
cc43fd549b
@ -0,0 +1,128 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* 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
|
||||||
|
|
||||||
|
/// Event listener callback for animation state events
|
||||||
|
public typealias AnimationStateListener = (EventType, TrackEntry, Event?) -> Void
|
||||||
|
|
||||||
|
/// Manager for animation state event listeners
|
||||||
|
public class AnimationStateEventManager {
|
||||||
|
// Use pointer addresses as keys since Swift wrapper objects might be recreated
|
||||||
|
private var stateListeners: [Int: AnimationStateListener?] = [:]
|
||||||
|
private var trackEntryListeners: [Int: [Int: AnimationStateListener]] = [:]
|
||||||
|
|
||||||
|
public static let instance = AnimationStateEventManager()
|
||||||
|
private init() {}
|
||||||
|
|
||||||
|
func setStateListener(_ state: AnimationState, _ listener: AnimationStateListener?) {
|
||||||
|
let key = Int(bitPattern: state._ptr)
|
||||||
|
if listener == nil {
|
||||||
|
stateListeners.removeValue(forKey: key)
|
||||||
|
} else {
|
||||||
|
stateListeners[key] = listener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStateListener(_ state: AnimationState) -> AnimationStateListener? {
|
||||||
|
let key = Int(bitPattern: state._ptr)
|
||||||
|
return stateListeners[key] ?? nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func setTrackEntryListener(_ entry: TrackEntry, _ listener: AnimationStateListener?) {
|
||||||
|
// Get the animation state from the track entry itself
|
||||||
|
guard let state = entry.animationState else {
|
||||||
|
fatalError("TrackEntry does not have an associated AnimationState")
|
||||||
|
}
|
||||||
|
|
||||||
|
let stateKey = Int(bitPattern: state._ptr)
|
||||||
|
let entryKey = Int(bitPattern: entry._ptr)
|
||||||
|
|
||||||
|
if trackEntryListeners[stateKey] == nil {
|
||||||
|
trackEntryListeners[stateKey] = [:]
|
||||||
|
}
|
||||||
|
|
||||||
|
if listener == nil {
|
||||||
|
trackEntryListeners[stateKey]?.removeValue(forKey: entryKey)
|
||||||
|
} else {
|
||||||
|
trackEntryListeners[stateKey]?[entryKey] = listener
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTrackEntryListener(_ state: AnimationState, _ entry: TrackEntry) -> AnimationStateListener? {
|
||||||
|
let stateKey = Int(bitPattern: state._ptr)
|
||||||
|
let entryKey = Int(bitPattern: entry._ptr)
|
||||||
|
return trackEntryListeners[stateKey]?[entryKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTrackEntry(_ state: AnimationState, _ entry: TrackEntry) {
|
||||||
|
let stateKey = Int(bitPattern: state._ptr)
|
||||||
|
let entryKey = Int(bitPattern: entry._ptr)
|
||||||
|
trackEntryListeners[stateKey]?.removeValue(forKey: entryKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clearState(_ state: AnimationState) {
|
||||||
|
let key = Int(bitPattern: state._ptr)
|
||||||
|
stateListeners.removeValue(forKey: key)
|
||||||
|
trackEntryListeners.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Debug method to inspect current state of the manager
|
||||||
|
public func debugPrint() {
|
||||||
|
print("\nAnimationStateEventManager contents:")
|
||||||
|
print(" State listeners: \(stateListeners.keys) (\(stateListeners.count) total)")
|
||||||
|
print(" Track entry listeners by state:")
|
||||||
|
for (stateKey, entries) in trackEntryListeners {
|
||||||
|
print(" State \(stateKey): \(entries.keys) (\(entries.count) entries)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - AnimationState Extensions
|
||||||
|
|
||||||
|
extension AnimationState {
|
||||||
|
/// Set a listener for all animation state events
|
||||||
|
public func setListener(_ listener: AnimationStateListener?) {
|
||||||
|
AnimationStateEventManager.instance.setStateListener(self, listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current state listener
|
||||||
|
public var listener: AnimationStateListener? {
|
||||||
|
return AnimationStateEventManager.instance.getStateListener(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - TrackEntry Extensions
|
||||||
|
|
||||||
|
extension TrackEntry {
|
||||||
|
/// Set a listener for events from this track entry
|
||||||
|
public func setListener(_ listener: AnimationStateListener?) {
|
||||||
|
AnimationStateEventManager.instance.setTrackEntryListener(self, listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,75 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* 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 SpineC
|
||||||
|
|
||||||
|
// MARK: - BonePose Extensions
|
||||||
|
|
||||||
|
extension BonePose {
|
||||||
|
/// Transform world coordinates to local coordinates
|
||||||
|
public func worldToLocal(worldX: Float, worldY: Float) -> Vector {
|
||||||
|
let output = ArrayFloat()
|
||||||
|
spine_bone_pose_world_to_local_v(_ptr.assumingMemoryBound(to: spine_bone_pose_wrapper.self),
|
||||||
|
worldX, worldY,
|
||||||
|
output._ptr.assumingMemoryBound(to: spine_array_float_wrapper.self))
|
||||||
|
let vector = Vector(x: output[0], y: output[1])
|
||||||
|
return vector
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform local coordinates to world coordinates
|
||||||
|
public func localToWorld(localX: Float, localY: Float) -> Vector {
|
||||||
|
let output = ArrayFloat()
|
||||||
|
spine_bone_pose_local_to_world_v(_ptr.assumingMemoryBound(to: spine_bone_pose_wrapper.self),
|
||||||
|
localX, localY,
|
||||||
|
output._ptr.assumingMemoryBound(to: spine_array_float_wrapper.self))
|
||||||
|
let vector = Vector(x: output[0], y: output[1])
|
||||||
|
return vector
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform world coordinates to parent coordinates
|
||||||
|
public func worldToParent(worldX: Float, worldY: Float) -> Vector {
|
||||||
|
let output = ArrayFloat()
|
||||||
|
spine_bone_pose_world_to_parent_v(_ptr.assumingMemoryBound(to: spine_bone_pose_wrapper.self),
|
||||||
|
worldX, worldY,
|
||||||
|
output._ptr.assumingMemoryBound(to: spine_array_float_wrapper.self))
|
||||||
|
let vector = Vector(x: output[0], y: output[1])
|
||||||
|
return vector
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform parent coordinates to world coordinates
|
||||||
|
public func parentToWorld(parentX: Float, parentY: Float) -> Vector {
|
||||||
|
let output = ArrayFloat()
|
||||||
|
spine_bone_pose_parent_to_world_v(_ptr.assumingMemoryBound(to: spine_bone_pose_wrapper.self),
|
||||||
|
parentX, parentY,
|
||||||
|
output._ptr.assumingMemoryBound(to: spine_array_float_wrapper.self))
|
||||||
|
let vector = Vector(x: output[0], y: output[1])
|
||||||
|
return vector
|
||||||
|
}
|
||||||
|
}
|
||||||
130
spine-ios/Sources/SpineSwift/Extensions/SkeletonDrawable.swift
Normal file
130
spine-ios/Sources/SpineSwift/Extensions/SkeletonDrawable.swift
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* 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 SpineC
|
||||||
|
|
||||||
|
/// Convenient drawable that combines skeleton, animation state, and rendering
|
||||||
|
public class SkeletonDrawable {
|
||||||
|
private let _drawable: UnsafeMutablePointer<spine_skeleton_drawable_wrapper>
|
||||||
|
|
||||||
|
public let skeleton: Skeleton
|
||||||
|
public let animationState: AnimationState
|
||||||
|
public let animationStateData: AnimationStateData
|
||||||
|
|
||||||
|
public init(skeletonData: SkeletonData) {
|
||||||
|
guard let drawable = spine_skeleton_drawable_create(skeletonData._ptr.assumingMemoryBound(to: spine_skeleton_data_wrapper.self)) else {
|
||||||
|
fatalError("Failed to create skeleton drawable")
|
||||||
|
}
|
||||||
|
self._drawable = drawable
|
||||||
|
|
||||||
|
// Get references to the skeleton and animation state
|
||||||
|
guard let skeletonPtr = spine_skeleton_drawable_get_skeleton(drawable) else {
|
||||||
|
spine_skeleton_drawable_dispose(drawable)
|
||||||
|
fatalError("Failed to get skeleton from drawable")
|
||||||
|
}
|
||||||
|
self.skeleton = Skeleton(fromPointer: skeletonPtr)
|
||||||
|
|
||||||
|
guard let animationStatePtr = spine_skeleton_drawable_get_animation_state(drawable) else {
|
||||||
|
spine_skeleton_drawable_dispose(drawable)
|
||||||
|
fatalError("Failed to get animation state from drawable")
|
||||||
|
}
|
||||||
|
self.animationState = AnimationState(fromPointer: animationStatePtr)
|
||||||
|
|
||||||
|
guard let animationStateDataPtr = spine_skeleton_drawable_get_animation_state_data(drawable) else {
|
||||||
|
spine_skeleton_drawable_dispose(drawable)
|
||||||
|
fatalError("Failed to get animation state data from drawable")
|
||||||
|
}
|
||||||
|
self.animationStateData = AnimationStateData(fromPointer: animationStateDataPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the animation state and process events
|
||||||
|
public func update(_ delta: Float) {
|
||||||
|
// Update animation state
|
||||||
|
animationState.update(delta)
|
||||||
|
|
||||||
|
// Process events
|
||||||
|
if let eventsPtr = spine_skeleton_drawable_get_animation_state_events(_drawable) {
|
||||||
|
let numEvents = Int(spine_animation_state_events_get_num_events(eventsPtr))
|
||||||
|
|
||||||
|
for i in 0..<numEvents {
|
||||||
|
// Get event type
|
||||||
|
let eventTypeValue = spine_animation_state_events_get_event_type(eventsPtr, Int32(i))
|
||||||
|
guard let type = EventType.fromValue(Int32(eventTypeValue)) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get track entry
|
||||||
|
if let trackEntryPtr = spine_animation_state_events_get_track_entry(eventsPtr, Int32(i)) {
|
||||||
|
let trackEntry = TrackEntry(fromPointer: trackEntryPtr)
|
||||||
|
|
||||||
|
// Get event (may be null)
|
||||||
|
let eventPtr = spine_animation_state_events_get_event(eventsPtr, Int32(i))
|
||||||
|
let event = eventPtr != nil ? Event(fromPointer: eventPtr!) : nil
|
||||||
|
|
||||||
|
// Call track entry listener if registered
|
||||||
|
if let trackListener = AnimationStateEventManager.instance.getTrackEntryListener(animationState, trackEntry) {
|
||||||
|
trackListener(type, trackEntry, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call global state listener
|
||||||
|
animationState.listener?(type, trackEntry, event)
|
||||||
|
|
||||||
|
// Remove listener if track entry is being disposed
|
||||||
|
if type == .dispose {
|
||||||
|
AnimationStateEventManager.instance.removeTrackEntry(animationState, trackEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset events for next frame
|
||||||
|
spine_animation_state_events_reset(eventsPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply animation state to skeleton
|
||||||
|
_ = animationState.apply(skeleton)
|
||||||
|
|
||||||
|
// Update skeleton physics and world transforms
|
||||||
|
skeleton.update(delta)
|
||||||
|
skeleton.updateWorldTransform(Physics.update)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render the skeleton and get render commands
|
||||||
|
public func render() -> RenderCommand? {
|
||||||
|
guard let renderCommand = spine_skeleton_drawable_render(_drawable) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return RenderCommand(fromPointer: renderCommand)
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
AnimationStateEventManager.instance.clearState(animationState)
|
||||||
|
spine_skeleton_drawable_dispose(_drawable)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* 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 SpineC
|
||||||
|
|
||||||
|
// MARK: - Skeleton Extensions
|
||||||
|
|
||||||
|
extension Skeleton {
|
||||||
|
/// Get the axis-aligned bounding box (AABB) containing all world vertices of the skeleton
|
||||||
|
public var bounds: Bounds {
|
||||||
|
let output = ArrayFloat()
|
||||||
|
spine_skeleton_get_bounds(_ptr.assumingMemoryBound(to: spine_skeleton_wrapper.self),
|
||||||
|
output._ptr.assumingMemoryBound(to: spine_array_float_wrapper.self))
|
||||||
|
let bounds = Bounds(
|
||||||
|
x: output[0],
|
||||||
|
y: output[1],
|
||||||
|
width: output[2],
|
||||||
|
height: output[3]
|
||||||
|
)
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the position of the skeleton
|
||||||
|
public func getPosition() -> Vector {
|
||||||
|
let output = ArrayFloat()
|
||||||
|
spine_skeleton_get_position_v(_ptr.assumingMemoryBound(to: spine_skeleton_wrapper.self),
|
||||||
|
output._ptr.assumingMemoryBound(to: spine_array_float_wrapper.self))
|
||||||
|
let position = Vector(x: output[0], y: output[1])
|
||||||
|
return position
|
||||||
|
}
|
||||||
|
}
|
||||||
100
spine-ios/Sources/SpineSwift/Extensions/SkinExtensions.swift
Normal file
100
spine-ios/Sources/SpineSwift/Extensions/SkinExtensions.swift
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* 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 SpineC
|
||||||
|
|
||||||
|
// MARK: - Skin Extensions
|
||||||
|
|
||||||
|
extension Skin {
|
||||||
|
/// Get all entries (slot/attachment pairs) in this skin
|
||||||
|
public func getEntries() -> [SkinEntry] {
|
||||||
|
guard let entriesPtr = spine_skin_get_entries(_ptr.assumingMemoryBound(to: spine_skin_wrapper.self)) else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
spine_skin_entries_dispose(entriesPtr)
|
||||||
|
}
|
||||||
|
|
||||||
|
let numEntries = Int(spine_skin_entries_get_num_entries(entriesPtr))
|
||||||
|
var entries: [SkinEntry] = []
|
||||||
|
|
||||||
|
for i in 0..<numEntries {
|
||||||
|
guard let entryPtr = spine_skin_entries_get_entry(entriesPtr, Int32(i)) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let slotIndex = Int(spine_skin_entry_get_slot_index(entryPtr))
|
||||||
|
|
||||||
|
guard let namePtr = spine_skin_entry_get_name(entryPtr) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let name = String(cString: namePtr)
|
||||||
|
|
||||||
|
let attachmentPtr = spine_skin_entry_get_attachment(entryPtr)
|
||||||
|
var attachment: Attachment? = nil
|
||||||
|
|
||||||
|
if let attachmentPtr = attachmentPtr {
|
||||||
|
// Use RTTI to determine the concrete attachment type
|
||||||
|
let rtti = spine_attachment_get_rtti(attachmentPtr)
|
||||||
|
guard let classNamePtr = spine_rtti_get_class_name(rtti) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let className = String(cString: classNamePtr)
|
||||||
|
|
||||||
|
switch className {
|
||||||
|
case "spine_region_attachment":
|
||||||
|
attachment = RegionAttachment(fromPointer: UnsafeMutableRawPointer(attachmentPtr).assumingMemoryBound(to: spine_region_attachment_wrapper.self))
|
||||||
|
case "spine_mesh_attachment":
|
||||||
|
attachment = MeshAttachment(fromPointer: UnsafeMutableRawPointer(attachmentPtr).assumingMemoryBound(to: spine_mesh_attachment_wrapper.self))
|
||||||
|
case "spine_bounding_box_attachment":
|
||||||
|
attachment = BoundingBoxAttachment(fromPointer: UnsafeMutableRawPointer(attachmentPtr).assumingMemoryBound(to: spine_bounding_box_attachment_wrapper.self))
|
||||||
|
case "spine_clipping_attachment":
|
||||||
|
attachment = ClippingAttachment(fromPointer: UnsafeMutableRawPointer(attachmentPtr).assumingMemoryBound(to: spine_clipping_attachment_wrapper.self))
|
||||||
|
case "spine_path_attachment":
|
||||||
|
attachment = PathAttachment(fromPointer: UnsafeMutableRawPointer(attachmentPtr).assumingMemoryBound(to: spine_path_attachment_wrapper.self))
|
||||||
|
case "spine_point_attachment":
|
||||||
|
attachment = PointAttachment(fromPointer: UnsafeMutableRawPointer(attachmentPtr).assumingMemoryBound(to: spine_point_attachment_wrapper.self))
|
||||||
|
default:
|
||||||
|
// Unknown attachment type, treat as generic Attachment
|
||||||
|
attachment = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entries.append(SkinEntry(
|
||||||
|
slotIndex: slotIndex,
|
||||||
|
name: name,
|
||||||
|
attachment: attachment
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
}
|
||||||
71
spine-ios/Sources/SpineSwift/Extensions/Types.swift
Normal file
71
spine-ios/Sources/SpineSwift/Extensions/Types.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
/******************************************************************************
|
||||||
|
* 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
|
||||||
|
|
||||||
|
// MARK: - Helper Types
|
||||||
|
|
||||||
|
/// Represents a bounding box with position and dimensions
|
||||||
|
public struct Bounds {
|
||||||
|
public var x: Float
|
||||||
|
public var y: Float
|
||||||
|
public var width: Float
|
||||||
|
public var height: Float
|
||||||
|
|
||||||
|
public init(x: Float, y: Float, width: Float, height: Float) {
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
self.width = width
|
||||||
|
self.height = height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a 2D vector
|
||||||
|
public struct Vector {
|
||||||
|
public var x: Float
|
||||||
|
public var y: Float
|
||||||
|
|
||||||
|
public init(x: Float, y: Float) {
|
||||||
|
self.x = x
|
||||||
|
self.y = y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents an entry in a skin
|
||||||
|
public struct SkinEntry {
|
||||||
|
public let slotIndex: Int
|
||||||
|
public let name: String
|
||||||
|
public let attachment: Attachment?
|
||||||
|
|
||||||
|
internal init(slotIndex: Int, name: String, attachment: Attachment?) {
|
||||||
|
self.slotIndex = slotIndex
|
||||||
|
self.name = name
|
||||||
|
self.attachment = attachment
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,7 +15,8 @@ let package = Package(
|
|||||||
.executableTarget(
|
.executableTarget(
|
||||||
name: "SpineTest",
|
name: "SpineTest",
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "SpineC", package: "spine-runtimes")
|
.product(name: "SpineC", package: "spine-runtimes"),
|
||||||
|
.product(name: "SpineSwift", package: "spine-runtimes")
|
||||||
],
|
],
|
||||||
path: "src"
|
path: "src"
|
||||||
)
|
)
|
||||||
|
|||||||
14
spine-ios/test/src/main.swift
Normal file
14
spine-ios/test/src/main.swift
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import Foundation
|
||||||
|
import SpineC
|
||||||
|
import SpineSwift
|
||||||
|
|
||||||
|
print("Spine-C test package is working!")
|
||||||
|
print("Spine version: \(spine_major_version()).\(spine_minor_version())")
|
||||||
|
|
||||||
|
// Run the original C API test
|
||||||
|
print("\n=== Testing C API directly ===")
|
||||||
|
runSkeletonDrawableTest()
|
||||||
|
|
||||||
|
// Run the new SpineSwift API test
|
||||||
|
print("\n=== Testing SpineSwift API ===")
|
||||||
|
runSkeletonDrawableTestSwift()
|
||||||
@ -170,5 +170,4 @@ func runSkeletonDrawableTest() {
|
|||||||
print("\n✓ Test complete")
|
print("\n✓ Test complete")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run the test
|
// Test function is called from main.swift
|
||||||
runSkeletonDrawableTest()
|
|
||||||
|
|||||||
162
spine-ios/test/src/skeleton_drawable_test_swift.swift
Normal file
162
spine-ios/test/src/skeleton_drawable_test_swift.swift
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
import Foundation
|
||||||
|
import SpineSwift
|
||||||
|
|
||||||
|
func runSkeletonDrawableTestSwift() {
|
||||||
|
print("Testing SkeletonDrawable with SpineSwift API...")
|
||||||
|
|
||||||
|
// Enable debug extension if needed
|
||||||
|
enableDebugExtension(false)
|
||||||
|
|
||||||
|
// Load atlas and skeleton data
|
||||||
|
let atlasPath = "../../spine-ts/assets/spineboy.atlas"
|
||||||
|
let jsonPath = "../../spine-ts/assets/spineboy-pro.json"
|
||||||
|
|
||||||
|
// Read atlas file
|
||||||
|
guard let atlasData = try? String(contentsOfFile: atlasPath, encoding: .utf8) else {
|
||||||
|
print("❌ Failed to read atlas file: \(atlasPath)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load atlas
|
||||||
|
let atlas: Atlas
|
||||||
|
do {
|
||||||
|
atlas = try loadAtlas(atlasData)
|
||||||
|
print("✓ Atlas loaded successfully")
|
||||||
|
} catch {
|
||||||
|
print("❌ Failed to load atlas: \(error)")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read skeleton JSON
|
||||||
|
guard let skeletonJson = try? String(contentsOfFile: jsonPath, encoding: .utf8) else {
|
||||||
|
print("❌ Failed to read skeleton JSON file: \(jsonPath)")
|
||||||
|
// atlas will be freed when out of scope
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load skeleton data
|
||||||
|
let skeletonData: SkeletonData
|
||||||
|
do {
|
||||||
|
skeletonData = try loadSkeletonDataJson(atlas: atlas, jsonData: skeletonJson, path: jsonPath)
|
||||||
|
print("✓ Skeleton data loaded successfully")
|
||||||
|
} catch {
|
||||||
|
print("❌ Failed to load skeleton data: \(error)")
|
||||||
|
// atlas will be freed when out of scope
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create skeleton drawable
|
||||||
|
let drawable = SkeletonDrawable(skeletonData: skeletonData)
|
||||||
|
print("✓ SkeletonDrawable created successfully")
|
||||||
|
|
||||||
|
// Test skeleton bounds
|
||||||
|
print("\nTesting skeleton bounds:")
|
||||||
|
let initialBounds = drawable.skeleton.bounds
|
||||||
|
print(" Initial bounds: x=\(initialBounds.x), y=\(initialBounds.y), width=\(initialBounds.width), height=\(initialBounds.height)")
|
||||||
|
|
||||||
|
// Set skeleton to pose and update bounds
|
||||||
|
drawable.skeleton.setupPose()
|
||||||
|
drawable.skeleton.updateWorldTransform(Physics.none)
|
||||||
|
|
||||||
|
let boundsAfterPose = drawable.skeleton.bounds
|
||||||
|
print(" Bounds after setupPose: x=\(boundsAfterPose.x), y=\(boundsAfterPose.y), width=\(boundsAfterPose.width), height=\(boundsAfterPose.height)")
|
||||||
|
|
||||||
|
// Test position
|
||||||
|
let position = drawable.skeleton.getPosition()
|
||||||
|
print(" Skeleton position: x=\(position.x), y=\(position.y)")
|
||||||
|
|
||||||
|
// Set up animation state listener
|
||||||
|
var eventCount = 0
|
||||||
|
drawable.animationState.setListener { type, trackEntry, event in
|
||||||
|
eventCount += 1
|
||||||
|
print(" AnimationState event #\(eventCount): type=\(type), track=\(trackEntry.trackIndex)")
|
||||||
|
if let event = event {
|
||||||
|
print(" Event name: \(event.data.name)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set an animation
|
||||||
|
let trackEntry = drawable.animationState.setAnimation(0, "walk", true)
|
||||||
|
print("✓ Set animation: walk")
|
||||||
|
|
||||||
|
// Set track entry listener
|
||||||
|
trackEntry.setListener { type, entry, event in
|
||||||
|
print(" TrackEntry event: type=\(type)")
|
||||||
|
if let event = event {
|
||||||
|
print(" Event data: \(event.data.name)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update several times to trigger events
|
||||||
|
print("\nUpdating animation state...")
|
||||||
|
for i in 0..<5 {
|
||||||
|
drawable.update(0.016) // ~60fps
|
||||||
|
print(" Frame \(i): updated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test switching animations
|
||||||
|
print("\nSwitching to run animation...")
|
||||||
|
_ = drawable.animationState.setAnimation(0, "run", true)
|
||||||
|
|
||||||
|
// Update a few more times
|
||||||
|
for i in 0..<3 {
|
||||||
|
drawable.update(0.016)
|
||||||
|
print(" Frame \(i): updated after switching")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test bounds after animation updates
|
||||||
|
print("\nTesting bounds after animation:")
|
||||||
|
drawable.skeleton.updateWorldTransform(Physics.none)
|
||||||
|
let boundsAfterAnimation = drawable.skeleton.bounds
|
||||||
|
print(" Bounds after animation: x=\(boundsAfterAnimation.x), y=\(boundsAfterAnimation.y), width=\(boundsAfterAnimation.width), height=\(boundsAfterAnimation.height)")
|
||||||
|
|
||||||
|
// Test with different animations that might have different bounds
|
||||||
|
print("\nTesting bounds with jump animation:")
|
||||||
|
_ = drawable.animationState.setAnimation(0, "jump", false)
|
||||||
|
drawable.update(0.5) // Update to middle of jump
|
||||||
|
|
||||||
|
let boundsAfterJump = drawable.skeleton.bounds
|
||||||
|
print(" Bounds during jump: x=\(boundsAfterJump.x), y=\(boundsAfterJump.y), width=\(boundsAfterJump.width), height=\(boundsAfterJump.height)")
|
||||||
|
|
||||||
|
// Test skin entries
|
||||||
|
print("\nTesting skin entries:")
|
||||||
|
if let skin = drawable.skeleton.skin {
|
||||||
|
let entries = skin.getEntries()
|
||||||
|
print(" Skin has \(entries.count) entries")
|
||||||
|
if entries.count > 0 {
|
||||||
|
let firstEntry = entries[0]
|
||||||
|
print(" First entry: slot=\(firstEntry.slotIndex), name=\(firstEntry.name), has attachment=\(firstEntry.attachment != nil)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test bone pose transformations
|
||||||
|
print("\nTesting bone pose transformations:")
|
||||||
|
if let rootBone = drawable.skeleton.rootBone {
|
||||||
|
let worldPoint = Vector(x: 100, y: 100)
|
||||||
|
// Note: worldToLocal is on BonePose, not Bone
|
||||||
|
// We would need to get the pose from the bone
|
||||||
|
// For now, skip this test
|
||||||
|
print(" Bone pose transformations test skipped (need to access BonePose)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test render command
|
||||||
|
print("\nTesting render command:")
|
||||||
|
if let renderCommand = drawable.render() {
|
||||||
|
print(" Got render command with blend mode: \(renderCommand.blendMode)")
|
||||||
|
// Note: atlasPage and vertices are accessed via getters, not properties
|
||||||
|
print(" Render command received successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear listener before cleanup
|
||||||
|
drawable.animationState.setListener(nil)
|
||||||
|
|
||||||
|
// Cleanup happens automatically via deinit
|
||||||
|
// skeletonData and atlas will be freed when out of scope
|
||||||
|
|
||||||
|
// Report memory leaks if debug extension is enabled
|
||||||
|
reportLeaks()
|
||||||
|
|
||||||
|
print("\n✓ SpineSwift API test complete")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test function is called from main.swift
|
||||||
@ -255,20 +255,83 @@ The SpineSwift module now compiles successfully! Remaining 27 errors are in Spin
|
|||||||
- [x] All type conversions and memory management working correctly
|
- [x] All type conversions and memory management working correctly
|
||||||
|
|
||||||
### TODO - Next Steps
|
### TODO - Next Steps
|
||||||
- [ ] Test SpineSwift module on iOS platform (current 27 errors are macOS/UIKit incompatibility)
|
- [x] Investigate how SkeletonDrawable is implemented in spine-flutter
|
||||||
- [ ] Complete SpineSwift high-level API (port from spine_dart.dart)
|
- [x] Check spine_flutter/lib/spine_dart.dart for extensions
|
||||||
|
- [x] Understand how SkeletonDrawable is exposed in Dart
|
||||||
|
- [x] Check if it's manually implemented or generated
|
||||||
|
|
||||||
- [ ] Complete SpineSwift high-level API (port from spine_dart.dart)
|
### Investigation Results - spine_dart.dart Structure
|
||||||
- [ ] SkeletonDrawable class
|
|
||||||
- [ ] AnimationStateEventManager
|
|
||||||
- [ ] Bounds and Vector types
|
|
||||||
- [ ] Skin extensions (getEntries)
|
|
||||||
- [ ] BonePose coordinate transformations
|
|
||||||
- [ ] Animation state listener management
|
|
||||||
|
|
||||||
- [ ] Create skeleton_drawable_test_swift to use SpineSwift API
|
The Dart implementation has these manually-written high-level components in spine_dart.dart:
|
||||||
- Port skeleton_drawable_test_swift.swift which uses C bindings directly
|
|
||||||
- Should test the high-level Swift API once working
|
1. **SkeletonDrawable class** (lines 409-492)
|
||||||
|
- Wraps spine_skeleton_drawable C functions
|
||||||
|
- Combines skeleton, animation state, and rendering
|
||||||
|
- Handles update cycle and event processing
|
||||||
|
- Methods: update(delta), render(), dispose()
|
||||||
|
|
||||||
|
2. **AnimationStateEventManager** (lines 232-305)
|
||||||
|
- Singleton for managing animation event listeners
|
||||||
|
- Maps native pointer addresses to listeners
|
||||||
|
- Handles both state-level and track-entry-level listeners
|
||||||
|
|
||||||
|
3. **Helper classes**:
|
||||||
|
- **Bounds** (lines 327-339): AABB with x, y, width, height
|
||||||
|
- **Vector** (lines 341-346): 2D vector with x, y
|
||||||
|
- **SkinEntry** (lines 155-162): Slot index and attachment pair
|
||||||
|
|
||||||
|
4. **Extensions**:
|
||||||
|
- **SkinExtensions** (lines 164-229): getEntries() method
|
||||||
|
- **AnimationStateListeners** (lines 308-318): listener property setter/getter
|
||||||
|
- **TrackEntryExtensions** (lines 319-324): listener property setter/getter
|
||||||
|
- **SkeletonExtensions** (lines 349-371): bounds property, getPosition()
|
||||||
|
- **BonePoseExtensions** (lines 373-406): worldToLocal, localToWorld coordinate transforms
|
||||||
|
|
||||||
|
5. **Top-level functions**:
|
||||||
|
- loadAtlas(String atlasData)
|
||||||
|
- loadSkeletonData(Atlas atlas, String jsonData)
|
||||||
|
- loadSkeletonDataBinary(Atlas atlas, Uint8List binaryData)
|
||||||
|
|
||||||
|
### COMPLETED - SpineSwift High-Level API Implementation ✅
|
||||||
|
- [x] Port SkeletonDrawable and extensions to SpineSwift
|
||||||
|
- [x] Created SkeletonDrawable class wrapping spine_skeleton_drawable functions
|
||||||
|
- [x] Implemented AnimationStateEventManager singleton
|
||||||
|
- [x] Added Bounds and Vector helper structs
|
||||||
|
- [x] Added extensions for Skeleton, Skin, AnimationState, TrackEntry, BonePose
|
||||||
|
- [x] Mirrored the update/render cycle from Dart implementation
|
||||||
|
|
||||||
|
- [x] Complete SpineSwift high-level API (port from spine_dart.dart)
|
||||||
|
- [x] SkeletonDrawable class (`Extensions/SkeletonDrawable.swift`)
|
||||||
|
- [x] AnimationStateEventManager (`Extensions/AnimationStateEventManager.swift`)
|
||||||
|
- [x] Bounds and Vector types (`Extensions/Types.swift`)
|
||||||
|
- [x] Skin extensions - getEntries (`Extensions/SkinExtensions.swift`)
|
||||||
|
- [x] BonePose coordinate transformations (`Extensions/BonePoseExtensions.swift`)
|
||||||
|
- [x] Animation state listener management (`Extensions/AnimationStateEventManager.swift`)
|
||||||
|
- [x] Skeleton extensions - bounds, getPosition (`Extensions/SkeletonExtensions.swift`)
|
||||||
|
|
||||||
|
- [x] Create skeleton_drawable_test_swift to use SpineSwift API
|
||||||
|
- [x] Created test that uses the high-level SpineSwift API
|
||||||
|
- [x] Tests loading atlas and skeleton data with Swift API
|
||||||
|
- [x] Tests SkeletonDrawable update/render cycle
|
||||||
|
- [x] Tests animation state listeners and events
|
||||||
|
- [x] Tests bounds and position tracking
|
||||||
|
- [x] Tests skin entries iteration
|
||||||
|
|
||||||
|
### Files Created (Session 5)
|
||||||
|
- `spine-ios/Sources/SpineSwift/Extensions/SkeletonDrawable.swift`
|
||||||
|
- `spine-ios/Sources/SpineSwift/Extensions/AnimationStateEventManager.swift`
|
||||||
|
- `spine-ios/Sources/SpineSwift/Extensions/Types.swift`
|
||||||
|
- `spine-ios/Sources/SpineSwift/Extensions/SkeletonExtensions.swift`
|
||||||
|
- `spine-ios/Sources/SpineSwift/Extensions/SkinExtensions.swift`
|
||||||
|
- `spine-ios/Sources/SpineSwift/Extensions/BonePoseExtensions.swift`
|
||||||
|
- `spine-ios/test/src/skeleton_drawable_test_swift.swift`
|
||||||
|
- `spine-ios/test/src/main.swift`
|
||||||
|
|
||||||
|
### Build Status
|
||||||
|
- SpineSwift module compiles successfully with 0 errors ✅
|
||||||
|
- Test package builds and runs successfully ✅
|
||||||
|
- C API test passes all checks ✅
|
||||||
|
- SpineSwift API test implementation complete (may have runtime issues to debug)
|
||||||
|
|
||||||
### File Locations Reference
|
### File Locations Reference
|
||||||
- Codegen: `spine-ios/codegen/src/swift-writer.ts`
|
- Codegen: `spine-ios/codegen/src/swift-writer.ts`
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user