Skip to content

Commit

Permalink
Merge pull request #25 from maxxfrazer/vision-touches
Browse files Browse the repository at this point in the history
visionOS gesture input support

Docs need updating, but see RUIDragGesture and RUITapGesture in repo
  • Loading branch information
maxxfrazer committed Dec 21, 2023
2 parents fd963de + d66e52a commit d97a14b
Show file tree
Hide file tree
Showing 18 changed files with 477 additions and 78 deletions.
7 changes: 0 additions & 7 deletions Sources/RealityUI/HasRUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,6 @@ public extension HasRUI {
}
}

/// Replace the current RUIComponent
/// - Parameter rui: new RUIComponent
func replaceRUI(with rui: RUIComponent) {
self.rui = rui
(self as? HasRUIMaterials)?.materialsShouldChange()
}

/// RealityUI Component for the entity.
internal(set) var rui: RUIComponent {
get {
Expand Down
23 changes: 12 additions & 11 deletions Sources/RealityUI/RUIAnimations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ public extension Entity {
/// - completion: Action to take place once the last spin has completed.
/// This will not execute if the animation is interrupted.
func ruiSpin(
by axis: SIMD3<Float>, period: TimeInterval,
times: Int = -1, completion: (() -> Void)? = nil
by axis: SIMD3<Float>, period: TimeInterval, times: Int = -1, completion: (() -> Void)? = nil
) {
self.spinPrivate(by: axis, period: period, times: max(-1, times * 3 - 1), completion: completion)
}
Expand Down Expand Up @@ -57,20 +56,22 @@ public extension Entity {
RealityUI.anims.removeValue(forKey: self)
}

internal func transformAfterRotation(by axis: SIMD3<Float>, angle: Float) -> Transform {
Transform(
matrix: self.transform.matrix
* Transform(rotation: simd_quatf(angle: angle, axis: axis)).matrix
)
}

private func spinPrivate(
by axis: SIMD3<Float>, period: TimeInterval,
times: Int, completion: (() -> Void)? = nil
) {
let startPos = self.transform
let spun90 = matrix_multiply(
startPos.matrix,
Transform(scale: .one, rotation: simd_quatf(angle: 2 * .pi / 3, axis: axis), translation: .zero).matrix
)
let spun120 = transformAfterRotation(by: axis, angle: 2 * .pi / 3)
self.move(
to: Transform(matrix: spun90),
relativeTo: self.parent,
duration: period / 3,
timingFunction: times == 0 ? .easeOut : .linear)
to: spun120, relativeTo: self.parent,
duration: period / 3, timingFunction: times == 0 ? .easeOut : .linear
)
let spinCancellable = self.scene?.subscribe(to: AnimationEvents.PlaybackCompleted.self, on: self, { _ in
RealityUI.anims[self]?["spin"]?.cancel()
RealityUI.anims[self]?.removeValue(forKey: "spin")
Expand Down
3 changes: 3 additions & 0 deletions Sources/RealityUI/RUIButton.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class RUIButton: Entity, HasButton, HasModel, HasPhysics {
self.button = button ?? ButtonComponent()
self.ruiOrientation()
self.makeModels()
#if os(visionOS)
self.components.set(InputTargetComponent())
#endif
self.components.set(RUIDragComponent(type: .click, delegate: self))
}

Expand Down
44 changes: 29 additions & 15 deletions Sources/RealityUI/RUIDragComponent+DragEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@

import RealityKit

// Extension for SIMD3<Double>
extension SIMD3 where Scalar == Double {
func toFloat3() -> SIMD3<Float> {
return SIMD3<Float>(Float(self.x), Float(self.y), Float(self.z))
}
}

extension RUIDragComponent {
/// Called when a drag interaction starts.
///
Expand All @@ -20,16 +27,19 @@ extension RUIDragComponent {
) -> Bool {
let worldPos = ray.origin + ray.direction
let localPos = entity.convert(position: worldPos, from: nil)
let dist = simd_length(ray.direction)
var dist = simd_length(ray.direction)
switch self.type {
case .move:
self.touchState = .move(poi: localPos, distance: dist)
#if os(visionOS)
dist = 0 // we don't care about origin with visionos
#endif
self.dragState = .move(poi: localPos, distance: dist)
case .turn(let axis):
let plane = self.turnCollisionPlane(for: axis)
guard let pointOnPlane = self.findPointOnPlane(ray: ray, plane: plane) else { return false }
self.touchState = .turn(plane: plane, start: pointOnPlane)
guard let pointOnPlane = RUIDragComponent.findPointOnPlane(ray: ray, plane: plane) else { return false }
self.dragState = .turn(plane: plane, start: pointOnPlane)
case .click:
self.touchState = .click(isSelected: true)
self.dragState = .click(isSelected: true)
self.delegate?.ruiDrag(entity, selectedDidUpdate: true)
}
self.delegate?.ruiDrag(entity, dragDidStart: ray)
Expand All @@ -45,22 +55,26 @@ extension RUIDragComponent {
public func dragUpdated(
_ entity: Entity, ray: (origin: SIMD3<Float>, direction: SIMD3<Float>), hasCollided: Bool
) {
let worldPos = self.getCollisionPoints(with: ray)
let worldPos = RUIDragComponent.getCollisionPoints(with: ray, dragState: self.dragState)
var newTouchPos: SIMD3<Float>?
if let worldPos {
newTouchPos = entity.convert(position: worldPos, from: nil)
}
var outputRay = ray
guard let touchState else { return }
guard let dragState else { return }

switch touchState {
switch dragState {
case .move(let poi, let len):
handleMoveState(entity, newTouchPos, poi)
outputRay.direction = simd_normalize(ray.direction) * len
if let newMovePos = RUIDragComponent.handleMoveState(entity, newTouchPos, poi) {
entity.position = newMovePos
}
if len != 0 {
outputRay.direction = simd_normalize(ray.direction) * len
}
case .turn(let plane, let lastPoint): handleTurnState(entity, plane, lastPoint, &outputRay)
case .click(let selected):
if selected != hasCollided {
self.touchState = .click(isSelected: hasCollided)
self.dragState = .click(isSelected: hasCollided)
self.delegate?.ruiDrag(entity, selectedDidUpdate: hasCollided)
}
}
Expand All @@ -75,8 +89,8 @@ extension RUIDragComponent {
/// - ray: A tuple containing the origin and direction of the ray used in the drag interaction.
public func dragEnded(_ entity: Entity, ray: (origin: SIMD3<Float>, direction: SIMD3<Float>)) {
var outputRay = ray
switch self.touchState {
case .move(_, let len):
switch self.dragState {
case .move(_, let len) where len != 0:
outputRay.direction = simd_normalize(ray.direction) * len
case .click(let selected):
if selected {
Expand All @@ -87,15 +101,15 @@ extension RUIDragComponent {
self.delegate?.ruiDrag(entity, selectedDidUpdate: false)
default: break
}
touchState = nil
dragState = nil
self.delegate?.ruiDrag(entity, dragDidEnd: outputRay)
}

/// Called when a drag interaction is cancelled.
///
/// - Parameter entity: The entity involved in the drag interaction.
public func dragCancelled(_ entity: Entity) {
touchState = nil
dragState = nil
self.delegate?.ruiDragCancelled(entity)
}
}
47 changes: 31 additions & 16 deletions Sources/RealityUI/RUIDragComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public class RUIDragComponent: Component {
/// An optional delegate to handle drag events.
public weak var delegate: RUIDragDelegate?

/// Whether the drag gesture should be enabled for this Entity.
public var isEnabled: Bool = true

/// Initializes a new `RUIDragComponent` with a specific drag interaction type and an optional delegate.
///
/// - Parameters:
Expand All @@ -100,7 +103,7 @@ public class RUIDragComponent: Component {
}

/// The current touch state of the drag component.
public internal(set) var touchState: DragState?
public internal(set) var dragState: DragState?

/// `DragState` represents the state of the current in-progress touch in an AR/VR context.
///
Expand All @@ -126,10 +129,14 @@ public class RUIDragComponent: Component {
///
/// - Parameter ray: A tuple containing the origin and direction of the ray.
/// - Returns: The collision point as `SIMD3<Float>` if a collision occurs, otherwise `nil`.
internal func getCollisionPoints(with ray: (origin: SIMD3<Float>, direction: SIMD3<Float>)) -> SIMD3<Float>? {
switch self.touchState {
case .move(_, let distance): ray.origin + normalize(ray.direction) * distance
case .turn(let plane, _): self.findPointOnPlane(ray: ray, plane: plane)
internal static func getCollisionPoints(
with ray: (origin: SIMD3<Float>, direction: SIMD3<Float>), dragState: DragState?
) -> SIMD3<Float>? {
switch dragState {
case .move(_, let distance): ray.origin + (
distance == 0 ? ray.direction : (normalize(ray.direction) * distance)
)
case .turn(let plane, _): RUIDragComponent.findPointOnPlane(ray: ray, plane: plane)
case .click: ray.origin + ray.direction
case .none: nil
}
Expand All @@ -142,7 +149,7 @@ public class RUIDragComponent: Component {
}
}

internal var moveContraint: MoveConstraint? {
internal var moveConstraint: MoveConstraint? {
switch self.type {
case .move(let moveConstraint): moveConstraint
default: nil
Expand All @@ -167,7 +174,7 @@ public class RUIDragComponent: Component {
))
}

internal func findPointOnPlane(
internal static func findPointOnPlane(
ray: (origin: SIMD3<Float>, direction: SIMD3<Float>), plane: float4x4
) -> SIMD3<Float>? {
// Extract plane normal and a point on the plane from the matrix
Expand All @@ -186,26 +193,34 @@ public class RUIDragComponent: Component {
}
}

internal func handleMoveState(_ entity: Entity, _ newTouchPos: SIMD3<Float>?, _ poi: SIMD3<Float>) {
guard let newTouchPos else { return }
let parentSpaceNTP = entity.convert(position: newTouchPos, to: entity.parent)
let parentSpaceOTP = entity.convert(position: poi, to: entity.parent)
guard let arTouchComp = entity.components.get(RUIDragComponent.self) else { return }
let endPos = entity.position + parentSpaceNTP - parentSpaceOTP
entity.position = switch arTouchComp.moveContraint {
fileprivate static func getClampedPosition(
_ moveConstraint: MoveConstraint?, _ endPos: SIMD3<Float>
) -> SIMD3<Float> {
switch moveConstraint {
case .box(let bbox): bbox.clamp(endPos)
case .points(let points): RUIDragComponent.closestPoint(from: endPos, points: points)
case .clamp(let clampFoo): clampFoo(endPos)
case .none: endPos
}
}

internal static func handleMoveState(
_ entity: Entity, _ newTouchPos: SIMD3<Float>?, _ poi: SIMD3<Float>
) -> SIMD3<Float>? {
guard let newTouchPos else { return nil }
let parentSpaceNTP = entity.convert(position: newTouchPos, to: entity.parent)
let parentSpaceOTP = entity.convert(position: poi, to: entity.parent)
guard let arTouchComp = entity.components.get(RUIDragComponent.self) else { return nil }
let endPos = entity.position + parentSpaceNTP - parentSpaceOTP
return getClampedPosition(arTouchComp.moveConstraint, endPos)
}

internal func handleTurnState(
_ entity: Entity, _ plane: float4x4, _ lastPoint: SIMD3<Float>,
_ ray: inout (origin: SIMD3<Float>, direction: SIMD3<Float>)
) {
guard let rotateVector,
let newPoint = self.findPointOnPlane(ray: ray, plane: plane)
let newPoint = RUIDragComponent.findPointOnPlane(ray: ray, plane: plane)
else { return }

// calculate the unsigned angle
Expand All @@ -221,7 +236,7 @@ public class RUIDragComponent: Component {
// calculate the rotation quaternion, and apply
entity.orientation *= simd_quatf(angle: signedAngle, axis: rotateVector)
// update the turn state, so we only check the difference with the new angle
self.touchState = .turn(plane: plane, start: newPoint)
self.dragState = .turn(plane: plane, start: newPoint)
}
ray.direction = normalize(ray.direction) * simd_distance(ray.origin, newPoint)
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/RealityUI/RUIDragGestureRecognizer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ import Combine
func globalTouchBegan(touchInView: CGPoint) -> Bool {
guard let firstHit = self.arView.hitTest(
touchInView, query: .nearest, mask: RealityUI.longGestureMask
).first, firstHit.entity.components.has(RUIDragComponent.self) else {
).first, let dragComp = firstHit.entity.components.get(RUIDragComponent.self),
dragComp.isEnabled else {
return false
}
return self.dragBegan(
Expand Down
3 changes: 3 additions & 0 deletions Sources/RealityUI/RUISlider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,9 @@ public extension HasSlider {
mesh: .generateSphere(radius: 0.5), materials: []
)
thumb.collision = CollisionComponent(shapes: [.generateSphere(radius: 0.5)])
#if os(visionOS)
thumb.components.set(InputTargetComponent())
#endif
thumb.components.set(RUIDragComponent(
type: .move(.clamp(self.clampThumb)),
delegate: self as? RUIDragDelegate
Expand Down
4 changes: 4 additions & 0 deletions Sources/RealityUI/RUIStepper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,10 @@ internal extension HasStepper {
rightModel.components.set(collider)
leftModel.components.set(RUIDragComponent(type: .click, delegate: self as? RUIDragDelegate))
rightModel.components.set(RUIDragComponent(type: .click, delegate: self as? RUIDragDelegate))
#if os(visionOS)
leftModel.components.set(InputTargetComponent())
rightModel.components.set(InputTargetComponent())
#endif

let background = self.addModel(part: .background)
background.model = ModelComponent(mesh: .generateBox(size: [2, 1, 0.25], cornerRadius: 0.125), materials: [])
Expand Down
11 changes: 7 additions & 4 deletions Sources/RealityUI/RUISwitch.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ public class RUISwitch: Entity, HasSwitch {
self.getModel(part: .thumb)?.scale = .one * 0.95
thumbCompressed = true
}
func uncompressThumb() {
self.getModel(part: .thumb)?.scale = .one
thumbCompressed = false
}
// func uncompressThumb() {
// self.getModel(part: .thumb)?.scale = .one
// thumbCompressed = false
// }

var distanceTravelled: Float = 0

Expand Down Expand Up @@ -260,6 +260,9 @@ public extension HasSwitch {
let thumb = self.addModel(part: .thumb)
thumb.model = ModelComponent(mesh: .generateSphere(radius: (1 - padding) / 2), materials: [])
thumb.collision = CollisionComponent(shapes: [.generateSphere(radius: (1 - padding) / 2)])
#if os(visionOS)
thumb.components.set(InputTargetComponent())
#endif
thumb.components.set(RUIDragComponent(type: .move(.box(
BoundingBox(min: [-toggleXSpan, 0, 0], max: [toggleXSpan, 0, 0]))
), delegate: self as? RUIDragDelegate))
Expand Down
11 changes: 7 additions & 4 deletions Sources/RealityUI/RUITexture.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public struct RUITexture {
return try await self.generateTexture(systemName: systemName, config: config)
}
fileprivate static func generateCGImage(
systemName: String, config: UIImage.SymbolConfiguration
systemName: String, config: UIImage.SymbolConfiguration?
) throws -> CGImage {
#if canImport(UIKit)
guard let symbolImage = UIImage(
Expand All @@ -50,9 +50,12 @@ public struct RUITexture {
throw TextureError.cgImageFailed
}
#else
guard let symbolImage = UIImage(
guard var symbolImage = UIImage(
systemSymbolName: systemName, accessibilityDescription: nil
)?.withSymbolConfiguration(config) else { throw TextureError.invalidSystemName }
) else { throw TextureError.invalidSystemName }
if let config, let imgWithConfig = symbolImage.withSymbolConfiguration(config) {
symbolImage = imgWithConfig
}
guard let cgImage = symbolImage.cgImage(
forProposedRect: nil, context: nil, hints: nil
) else { throw TextureError.cgImageFailed }
Expand All @@ -65,7 +68,7 @@ public struct RUITexture {
/// - config: Image SymbolConfiguration for the SF Symbol Image.
/// - Returns: A new `TextureResource`.
public static func generateTexture(
systemName: String, config: UIImage.SymbolConfiguration
systemName: String, config: UIImage.SymbolConfiguration? = nil
) async throws -> TextureResource {
let cgImage = try self.generateCGImage(systemName: systemName, config: config)
return try await TextureResource.generate(
Expand Down
10 changes: 8 additions & 2 deletions Sources/RealityUI/RealityUI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,13 @@ import Combine
RealityUI.shared.logActivated()
}
/// Orientation of all RealityUI Entities upon creation. If nil, none will be set.
public static var startingOrientation: simd_quatf?
public static var startingOrientation: simd_quatf? {
#if os(visionOS)
simd_quatf(angle: .pi, axis: [0, 1, 0])
#else
nil
#endif
}

/// Mask to exclude entities from being hit by the long/panning gesture
public static var longGestureMask: CollisionGroup = .all
Expand Down Expand Up @@ -147,7 +153,7 @@ import Combine
self.installedGestures[arView]?.append(dragGesture)
}

fileprivate func tapActionChecker(_ arView: ARView, _ tapInView: CGPoint) {
internal func tapActionChecker(_ arView: ARView, _ tapInView: CGPoint) {
if let ccHit = arView.hitTest(tapInView, mask: RealityUI.tapGestureMask).first,
let comp = ccHit.entity.components.get(RUITapComponent.self) {
// if the element has RUIComponent, and it has `ruiEnabled` set to false
Expand Down
Loading

0 comments on commit d97a14b

Please sign in to comment.