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.

Follow Step Into Vision