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.

Questions or feedback?