EsotericSoftware / spine-runtimes

2D skeletal animation runtimes for Spine.
http://esotericsoftware.com/
Other
4.42k stars 2.92k forks source link

[ios] Provide the ability to convert an Attachment into a BoundingBoxAttachment #2671

Closed kikiloveswift closed 1 week ago

kikiloveswift commented 2 weeks ago

BoundingBoxAttachment is supposed to be a subclass of Attachment in SpineCppLite, but in Swift Spine, they’re not treated that way since both inherit from NSObject. When we’re trying to get the bounding box, we end up with an Attachment object, but SkeletonBounds needs a BoundingBoxAttachment to get the Polygon. So, we need to support type conversion for this.

Demo:

InteractiveSpineView

import Foundation
import SwiftUI
import Spine
import SpineCppLite

struct InteractiveSpineView: View {

    @StateObject var viewModel = InteractiveSpineViewModel()

    var body: some View {
        GeometryReader { geometry in
            VStack(alignment: .center, spacing: 20) {
                SpineView(
                    from: .bundle(atlasFileName: "role_jessica.atlas", skeletonFileName: "role_jessica.skel", bundle: .main),
                    controller: viewModel.controller ?? SpineController(),
                    mode: .fit,
                    alignment: .center,
                    boundsProvider: SkinAndAnimationBounds(animation: "action/happy1"),
                    backgroundColor: UIColor.white
                )
                .frame(width: geometry.size.width, height: 500)
                .gesture(
                    DragGesture(minimumDistance: 0, coordinateSpace: .local)
                        .onEnded { value in
                            let tapLocation = value.location
                            viewModel.tapPoint(tapLocation)
                        }
                )
                .padding(.top, 100)

                Text(viewModel.hitLabelText)
                    .padding()
                    .border(Color.gray, width: 1)
            }
            .navigationTitle("InteractiveSpine")
            .navigationBarTitleDisplayMode(.inline)
        }
    }
}

#Preview {
    InteractiveSpineView()
}

InteractiveSpineViewModel

import Foundation
import SwiftUI
import Spine
import SpineCppLite

class InteractiveSpineViewModel: NSObject, ObservableObject {

    @Published var controller: SpineController?

    @Published var hitLabelText: String = "Label indicator"

    private lazy var skeletonBounds: SkeletonBounds = {
        return SkeletonBounds.create()
    }()

    override init() {
        super.init()
        controller = SpineController(
            onInitialized: { controller in
                controller.animationState.setAnimationByName(trackIndex: 0, animationName: "action/highfive_waiting", loop: true)
        })
    }

    public func tapPoint(_ point: CGPoint) {
        guard let skeletonPoint = controller?.toSkeletonCoordinates(position: point),
              let skeleton = controller?.skeleton else {
            return
        }
        skeletonBounds.update(skeleton: skeleton, updateAabb: true)

        guard let attachment = controller?.skeleton.getAttachmentByName(slotName: "region", attachmentName: "regionAttachment"),
              let polygon = skeletonBounds.getPolygon(attachment: attachment.castToBoundingBoxAttachment()) else {
            return
        }
        let result = skeletonBounds.containsPoint(polygon: polygon, x: Float(skeletonPoint.x), y: Float(skeletonPoint.y))
        let text = result ? "hit bounding area" : "missed"
        hitLabelText = text
    }
}
badlogic commented 1 week ago

Awesome, thanks!