Lab 014 – Building an Indirect Transform System

Use the Drag Gesture and a Toolbar to switch modes. We can use one gesture to drag, scale, and rotate entities.

In this lab I combined several concepts from the posts on gesture into a demo I’m calling Indirect Transform. The idea is to use the simple drag gesture to move, rotate, and scale the entity. I used a SwiftUI toolbar to place the buttons to switch modes, but this could easily fit in somewhere else.

The movement example is essentially the same as the Drag Gesture Improved. I had to get a little more creative to handle rotation and scaling though. The drag gesture does not provide magnitude or rotation values like the other gestures do. Instead, I found some hacky ways to do this using some other values. Do you know a better way to go about this?

struct Lab014: View {

    @State fileprivate var transformMode: IndirectTransformMode = .none

    var body: some View {
        RealityView { content in
            // Load the scene from the Reality Kit bundle
            if let scene = try? await Entity(named: "GestureLabs", in: realityKitContentBundle) {
                content.add(scene)

                // Lower the entire scene to the bottom of the volume
                scene.position.y = -0.4

                if let car = scene.findEntity(named: "ToyCar"), let plane = scene.findEntity(named: "ToyBiplane") {

                    car.removeFromParent()
                    plane.removeFromParent()

                }
            }
        }
        .modifier(IndirectTransformGesture(mode: $transformMode))
        .toolbar {
            ToolbarItemGroup(placement: .bottomOrnament) {

                Button {
                    transformMode = .none
                } label: {
                    Image(systemName: "nosign")
                }
                .foregroundStyle(transformMode == .none ? .blue : .white)
                Button {
                    transformMode = .move
                } label: {
                    Image(systemName: "move.3d")
                }
                .foregroundStyle(transformMode == .move ? .blue : .white)
                Button {
                    transformMode = .rotate
                } label: {
                    Image(systemName: "rotate.3d")
                }
                .foregroundStyle(transformMode == .rotate ? .blue : .white)
                Button {
                    transformMode = .scale
                } label: {
                    Image(systemName: "scale.3d")
                }
                .foregroundStyle(transformMode == .scale ? .blue : .white)
            }

        }
    }
}

fileprivate enum IndirectTransformMode {
    case none
    case move
    case rotate
    case scale
}

fileprivate struct IndirectTransformGesture: ViewModifier {

    @Binding var mode: IndirectTransformMode

    @State var isDragging: Bool = false
    @State var initialPosition: SIMD3<Float> = .zero
    @State var initialScale: SIMD3<Float> = .init(repeating: 1.0)
    @State var initialOrientation:simd_quatf = simd_quatf(
        vector: .init(repeating: 0.0)
    )

    @State var rotation: Angle = .zero

    func body(content: Content) -> some View {
        content
            .gesture(
                DragGesture()
                    .targetedToAnyEntity()
                    .onChanged { value in

                        // We we start the gesture, cache the entity position
                        if !isDragging {
                            isDragging = true
                            initialPosition = value.entity.position
                            initialScale = value.entity.scale
                            initialOrientation = value.entity.transform.rotation
                        }

                        switch mode {
                        case .move:
                            // Calculate vector by which to move the entity
                            let movement = value.convert(value.gestureValue.translation3D, from: .local, to: .scene)
                            // Add the two vectors and clamp the result to keep the entity in the volume. Ignore the Y axis
                            let newPostion = initialPosition + movement
                            let limit: Float = 0.25
                            let posX = min(max(newPostion.x, -limit), limit)
                            let posZ = min(max(newPostion.z, -limit), limit)
                            value.entity.position.x = posX
                            value.entity.position.z = posZ

                        case .rotate:
                            // Just a hack to rotate by *something* from the drag gesture. I'm sure there is a better way.
                            rotation.degrees += 0.01 * (value.velocity.width)
                            let rotationTransform = Transform(yaw: Float(rotation.radians))

                            value.entity.transform.rotation = initialOrientation * rotationTransform.rotation
                        case .scale:

                            // A hack to get some value from the gesture that we can use to scale
                            let magnification = 0.01 * Float(value.gestureValue.translation3D.x)
                            let scaler = magnification + initialScale.x

                            // Clamp scale values for each axis independently
                            let minScale: Float = 0.25
                            let maxScale: Float = 3
                            let newScaler: Float = min(max(scaler, minScale), maxScale)

                            // Apply the clamped scale to the entity
                            value.entity.setScale(
                                .init(repeating: newScaler),
                                relativeTo: value.entity.parent!
                            )
                        case .none: break
                        }


                    }
                    .onEnded { value in
                        // Clean up when the gesture has ended
                        isDragging = false
                        initialPosition = .zero
                        initialScale = .init(repeating: 1.0)
                        initialOrientation = simd_quatf(
                            vector: .init(repeating: 0.0)
                        )
                    }
            )

    }
}

Video demo

A video demo of indirect transform using drag gesture

Support our work so we can continue to bring you new examples and articles.

Download the Xcode project with this and many more labs from Step Into Vision.

Questions or feedback?