Dragging horizontally with DragGesture

I am trying to achieve a naturally feeling movement of an entity, but the drag gesture always gives me movement perpendicular the view. That means if I restrict y to the stay the same I can only move the model left and right. A movement (of the mouse) upwards would result exclusive into a movement up/down.

My model has many sub entities and I found that I need to create a box as collision shape so that I can move the entire model. Previously if I generated collision shapes it would only move the individual part.

This code is not working mostly, except the restriction for y which feels very much like a hack.

Is there a facility by the system that would rotate the movement vector along the x axis such that y becomes zero?

Or am I approaching this wrong? how would you move objects in immersive space usually?

import SwiftUI
import RealityKit
import RealityKitContent

struct ImmersiveView: View {
	
	@State private var initialDragOffset: SIMD3<Float> = .zero
	
	var body: some View {
		
		RealityView { content in
			
			async let model = Entity(named: "01-TFKmono2-Normal-PremiumGrey", in: realityKitContentBundle)
			
			if let model = try? await model
			{
				// Set the model's position to the world origin
				model.position = [0, 0, 0]
				
				// Offset the model in the y direction by half its height
				let boundingBox = model.visualBounds(relativeTo: nil)
				let boxSize = boundingBox.extents
				
				// Create the collision box and set it to the model
				let collision = CollisionComponent(shapes: [.generateBox(size: [boxSize.x, boxSize.y, boxSize.z]).offsetBy(translation: .init(x: 0, y: boxSize.y / 2, z: 0))], isStatic: false)
				model.components.set(collision)
				
				// Set the InputTargetComponent and add the model to the content
				model.components.set(InputTargetComponent())
				
				content.add(model)
				
				// move the mode 2 meters in front of viewer
				model.position.z = -2
			}
		}
		.gesture(DragGesture()
			.targetedToAnyEntity()
			.onChanged { value in
				
				if initialDragOffset == .zero {
					// Calculate the initial drag offset at the beginning of the drag
					let startLocation = value.convert(value.startLocation3D, from: .local, to: value.entity.parent!)
					initialDragOffset = value.entity.position - SIMD3<Float>(startLocation.x, startLocation.y, startLocation.z)
				}
				
				// Convert the 3D location of the drag gesture to the entity's parent coordinate space
				let dragLocation = value.convert(value.location3D, from: .local, to: value.entity.parent!)
				var newPosition = SIMD3<Float>(dragLocation.x, dragLocation.y, dragLocation.z) + initialDragOffset
				
				
				// don't modify y coordinate so that model remains standing on ground
				newPosition.y = value.entity.position.y
				
				// Set the entity's position to the new position
				value.entity.position = newPosition
			}.onEnded({ value in
				initialDragOffset = .zero
			}))
	}
}

#Preview {
	ImmersiveView()
		.previewLayout(.sizeThatFits)
}

the drag gesture always gives me movement perpendicular the view.

What do you mean ? horizontal drag lead to vertical move and vice versa ?

I found this to be working... is there a simpler way to achieve this?

This takes the translation3D we get from the drag gesture, records an initialOffset (otherwise my entity jumps upward, because it has its origin in the bottom center). then it rotates the translation vector around x such that y becomes 0. Finally it converts it into the coordinate system of the entity's parent.

Phew...

.gesture(DragGesture()
			.targetedToAnyEntity()
			.onChanged { value in
				
				if initialDragOffset == .zero {
					// Calculate the initial drag offset at the beginning of the drag
					let startLocation = value.convert(value.startLocation3D, from: .local, to: value.entity.parent!)
					initialDragOffset = value.entity.position - SIMD3<Float>(startLocation.x, startLocation.y, startLocation.z)
				}
				
				let translation3D = value.translation3D
				
				// Rotate the drag vector such that its y-component becomes zero
				let theta = atan2(translation3D.y, translation3D.z)
				let cosTheta = cos(theta)
				let sinTheta = sin(theta)
				let rotatedX = translation3D.x
				let rotatedY = 0  // Force y to be zero
				let rotatedZ = theta >= 0 ? translation3D.y * sinTheta + translation3D.z * cosTheta : translation3D.z * cosTheta - translation3D.y * sinTheta
				let rotatedVector = SIMD3<Float>(Float(rotatedX), 0, Float(rotatedZ))
				
				var newLocation3D = value.startLocation3D
				newLocation3D.x += Double(rotatedVector.x)
				newLocation3D.y += Double(rotatedVector.y)
				newLocation3D.z += Double(rotatedVector.z)

				// Convert the 3D location of the drag gesture to the entity's parent coordinate space
				let dragLocation = value.convert(newLocation3D, from: .local, to: value.entity.parent!)
				var newPosition = SIMD3<Float>(dragLocation.x, dragLocation.y, dragLocation.z) + initialDragOffset
				
				// don't modify y coordinate so that model remains standing on ground
				newPosition.y = value.entity.position.y
				
				// Set the entity's position to the new position
				value.entity.position = newPosition
			}
			.onEnded({ value in
				initialDragOffset = .zero
			}))

I had the same problem. Very strange that there is no obvious and easy way to handle this sort of interaction.

I wasted a lot of time here because it seems like the 2D location field of the drag gesture is still affected by the camera orientation in 3D space. I ended up using the location3D field, removing view rotation (e.g. transform to local space), calculating the horizontal offset, and reapplying the view rotation:

.gesture(DragGesture(minimumDistance: 0.0).targetedToAnyEntity().onChanged {e in
            if refs.entityDragStartPos == nil {
                refs.entityDragStartPos = e.entity.position
            }
            
            // Get the 3D drag delta
            let delta = simd_float3(e.gestureValue.location3D.vector) - simd_float3(e.gestureValue.startLocation3D.vector)
            
            // Get the look rotation
            let direction = e.inputDevicePose3D!.rotation.quaternion.act(simd_double3(0, 0, 1))
            let lookRot = simd_quatf(from: SIMD3<Float>(0, 0, 1), to: simd_normalize(simd_float3(Float(direction.x), 0, Float(direction.z))))
            
            // Get the 3D drag delta in local space (e.g. independent of view direction) 
            // by rotating using inverse look rotation
            let drag = lookRot.inverse.act(delta)
            
            // Calculate offset, y drag is applied to z axis to only get horizontal.
            // Rotate back into view space and calcualte new entity position
            let offset = lookRot.act(simd_float3(drag.x, 0, -(drag.y - drag.z))) * 0.001
            let pos = refs.entityDragStartPos! + offset
            
            e.entity.setPosition(pos, relativeTo: nil)
        }.onEnded { e in
            refs.entityDragStartPos = nil
        })
Dragging horizontally with DragGesture
 
 
Q