diff --git a/spine-ios/Sources/SpineSwift/Extensions/AnimationStateEventManager.swift b/spine-ios/Sources/SpineSwift/Extensions/AnimationStateEventManager.swift new file mode 100644 index 000000000..3a1e508e9 --- /dev/null +++ b/spine-ios/Sources/SpineSwift/Extensions/AnimationStateEventManager.swift @@ -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) + } +} \ No newline at end of file diff --git a/spine-ios/Sources/SpineSwift/Extensions/BonePoseExtensions.swift b/spine-ios/Sources/SpineSwift/Extensions/BonePoseExtensions.swift new file mode 100644 index 000000000..b7225834f --- /dev/null +++ b/spine-ios/Sources/SpineSwift/Extensions/BonePoseExtensions.swift @@ -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 + } +} \ No newline at end of file diff --git a/spine-ios/Sources/SpineSwift/Extensions/SkeletonDrawable.swift b/spine-ios/Sources/SpineSwift/Extensions/SkeletonDrawable.swift new file mode 100644 index 000000000..dc2052013 --- /dev/null +++ b/spine-ios/Sources/SpineSwift/Extensions/SkeletonDrawable.swift @@ -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 + + 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.. 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) + } +} \ No newline at end of file diff --git a/spine-ios/Sources/SpineSwift/Extensions/SkeletonExtensions.swift b/spine-ios/Sources/SpineSwift/Extensions/SkeletonExtensions.swift new file mode 100644 index 000000000..b51b9a919 --- /dev/null +++ b/spine-ios/Sources/SpineSwift/Extensions/SkeletonExtensions.swift @@ -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 + } +} \ No newline at end of file diff --git a/spine-ios/Sources/SpineSwift/Extensions/SkinExtensions.swift b/spine-ios/Sources/SpineSwift/Extensions/SkinExtensions.swift new file mode 100644 index 000000000..22ae28d6a --- /dev/null +++ b/spine-ios/Sources/SpineSwift/Extensions/SkinExtensions.swift @@ -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.. 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 \ No newline at end of file diff --git a/todos/work/2025-07-31-16-50-17-generate-swift-bindings/task.md b/todos/work/2025-07-31-16-50-17-generate-swift-bindings/task.md index 8e71a7315..f750ba4aa 100644 --- a/todos/work/2025-07-31-16-50-17-generate-swift-bindings/task.md +++ b/todos/work/2025-07-31-16-50-17-generate-swift-bindings/task.md @@ -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 ### TODO - Next Steps -- [ ] Test SpineSwift module on iOS platform (current 27 errors are macOS/UIKit incompatibility) -- [ ] Complete SpineSwift high-level API (port from spine_dart.dart) +- [x] Investigate how SkeletonDrawable is implemented in spine-flutter + - [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 + +### Investigation Results - spine_dart.dart Structure -- [ ] Complete SpineSwift high-level API (port from spine_dart.dart) - - [ ] SkeletonDrawable class - - [ ] AnimationStateEventManager - - [ ] Bounds and Vector types - - [ ] Skin extensions (getEntries) - - [ ] BonePose coordinate transformations - - [ ] Animation state listener management +The Dart implementation has these manually-written high-level components in spine_dart.dart: -- [ ] Create skeleton_drawable_test_swift to use SpineSwift API - - 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 - Codegen: `spine-ios/codegen/src/swift-writer.ts`