From 50c58f04bd99085308807f8f0ee76d609e38a2d9 Mon Sep 17 00:00:00 2001 From: RogPodge Date: Sun, 25 Dec 2022 21:28:51 -0800 Subject: [PATCH 01/10] Draft of new object manipulator implementation along with tests --- .../ObjectManipulator/NewObjectManipulator.cs | 577 ++++++++++ .../NewObjectManipulator.cs.meta | 11 + .../ObjectManipulator/ObjectManipulator.cs | 2 + .../PlacementHub.cs | 398 +++++++ .../PlacementHub.cs.meta | 11 + .../Runtime/NewObjectManipulatorTests.cs | 994 ++++++++++++++++++ .../Runtime/NewObjectManipulatorTests.cs.meta | 11 + 7 files changed, 2004 insertions(+) create mode 100644 com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs create mode 100644 com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs.meta create mode 100644 com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs create mode 100644 com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs.meta create mode 100644 com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs create mode 100644 com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs.meta diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs new file mode 100644 index 00000000000..4b37159a6e0 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs @@ -0,0 +1,577 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using Unity.Profiling; +using UnityEngine; +using UnityEngine.InputSystem; +using UnityEngine.Serialization; +using UnityEngine.XR.Interaction.Toolkit; + +namespace Microsoft.MixedReality.Toolkit.SpatialManipulation +{ + /// + /// ObjectManipulator allows for the manipulation (move, rotate, scale) + /// of an object by any interactor with a valid attach transform. + /// Multi-handed interactions and physics-enabled objects are also supported. + /// + /// + /// ObjectManipulator works with both rigidbody and non-rigidbody objects, + /// and allows for throwing and catching interactions. Any interactor + /// with a well-formed attach transform can interact with and manipulate + /// an ObjectManipulator. This is a drop-in replacement for the built-in + /// XRI XRGrabInteractable, that allows for flexible multi-handed interactions. + /// ObjectManipulator doesn't track controller velocity, so for precise fast-paced + /// throwing interactions that only need one hand, XRGrabInteractable may + /// give better results. + /// + [RequireComponent(typeof(PlacementHub))] + [AddComponentMenu("MRTK/Spatial Manipulation/New Object Manipulator")] + public class NewObjectManipulator : StatefulInteractable + { + #region Public Enums + + /// + /// Describes what pivot the manipulated object will rotate about when + /// you rotate your hand. This is not a description of any limits or + /// additional rotation logic. If no other factors (such as constraints) + /// are involved, rotating your hand by an amount should rotate the object + /// by the same amount. + /// For example a possible future value here is RotateAboutUserDefinedPoint + /// where the user could specify a pivot that the object is to rotate + /// around. + /// An example of a value that should not be found here is MaintainRotationToUser + /// as this restricts rotation of the object when we rotate the hand. + /// + public enum RotateAnchorType + { + RotateAboutObjectCenter, + RotateAboutGrabPoint + }; + + [System.Flags] + public enum ReleaseBehaviorType + { + KeepVelocity = 1 << 0, + KeepAngularVelocity = 1 << 1 + } + + #endregion Public Enums + + #region Serialized Fields + + [SerializeField] + [EnumFlags] + [Tooltip("What kinds of manipulation should be allowed?")] + private TransformFlags allowedManipulations = TransformFlags.Move | TransformFlags.Rotate | TransformFlags.Scale; + + /// + /// What kinds of manipulation should be allowed? + /// + public TransformFlags AllowedManipulations + { + get => allowedManipulations; + set => allowedManipulations = value; + } + + [SerializeField] + [Tooltip("Rotation behavior of object when using one hand near")] + private RotateAnchorType rotationAnchorNear = RotateAnchorType.RotateAboutGrabPoint; + + /// + /// Rotation behavior of object when using one hand near + /// + public RotateAnchorType RotationAnchorNear + { + get => rotationAnchorNear; + set => rotationAnchorNear = value; + } + + [SerializeField] + [Tooltip("Rotation behavior of object when using one hand at distance")] + private RotateAnchorType rotationAnchorFar = RotateAnchorType.RotateAboutGrabPoint; + + /// + /// Rotation behavior of object when using one hand at distance + /// + public RotateAnchorType RotationAnchorFar + { + get => rotationAnchorFar; + set => rotationAnchorFar = value; + } + + [SerializeField] + [EnumFlags] + [Tooltip("Rigid body behavior of the dragged object when releasing it.")] + private ReleaseBehaviorType releaseBehavior = ReleaseBehaviorType.KeepVelocity | ReleaseBehaviorType.KeepAngularVelocity; + + /// + /// Rigid body behavior of the dragged object when releasing it. + /// + public ReleaseBehaviorType ReleaseBehavior + { + get => releaseBehavior; + set => releaseBehavior = value; + } + + [FormerlySerializedAs("smoothingActive")] + [SerializeField] + [Tooltip("Frame-rate independent smoothing for far interactions. Far smoothing is enabled by default.")] + private bool smoothingFar = true; + + /// + /// Whether to enable frame-rate independent smoothing for far interactions. + /// + /// + /// Far smoothing is enabled by default. + /// + public bool SmoothingFar + { + get => smoothingFar; + set => smoothingFar = value; + } + + [SerializeField] + [Tooltip("Frame-rate independent smoothing for near interactions. Note that enabling near smoothing may be perceived as being 'disconnected' from the hand.")] + private bool smoothingNear = true; + + /// + /// Whether to enable frame-rate independent smoothing for near interactions. + /// + /// + /// Note that enabling near smoothing may be perceived as being 'disconnected' from the hand. + /// + public bool SmoothingNear + { + get => smoothingNear; + set => smoothingNear = value; + } + + [Serializable] + /// + /// The SystemTypes for the desired type of manipulation logic for move, rotate, and scale. + /// + public struct LogicType + { + [SerializeField] + [Tooltip("The concrete type of ManipulationLogic to use for moving.")] + [Extends(typeof(ManipulationLogic), TypeGrouping.ByNamespaceFlat)] + /// + /// The concrete type of to use for moving. + /// + public SystemType moveLogicType; + + [SerializeField] + [Tooltip("The concrete type of ManipulationLogic to use for rotating.")] + [Extends(typeof(ManipulationLogic), TypeGrouping.ByNamespaceFlat)] + /// + /// The concrete type of to use for rotating. + /// + public SystemType rotateLogicType; + + [SerializeField] + [Tooltip("The concrete type of ManipulationLogic to use for scaling.")] + [Extends(typeof(ManipulationLogic), TypeGrouping.ByNamespaceFlat)] + /// + /// The concrete type of to use for scaling. + /// + public SystemType scaleLogicType; + } + + [SerializeField] + [Tooltip("The concrete types of ManipulationLogic to use for manipulations.")] + private LogicType manipulationLogicTypes = new LogicType + { + moveLogicType = typeof(MoveLogic), + rotateLogicType = typeof(RotateLogic), + scaleLogicType = typeof(ScaleLogic) + }; + + /// + /// The concrete types of to use for manipulations. + /// + /// + /// Setting this field at runtime can be expensive (reflection) and interrupt/break + /// currently occurring manipulations. Use with caution. Best used at startup or when + /// instantiating ObjectManipulators from code. + /// + public LogicType ManipulationLogicTypes + { + get => manipulationLogicTypes; + set + { + // Re-instantiating manip logics is expensive and can interrupt ongoing interactions. + manipulationLogicTypes = value; + InstantiateManipulationTransformations(); + } + } + + #endregion Serialized Fields + + #region Protected Properties + + /// + /// The current for the current interaction. + /// + /// + /// Prioritizes near grab over ray selection, and ray selection over gaze selection. + /// Will return a one-hot . + /// + protected virtual InteractionFlags CurrentInteractionType + { + get + { + if (IsGrabSelected) + { + return InteractionFlags.Near; + } + else if (IsRaySelected) + { + return InteractionFlags.Ray; + } + else if (IsGazePinchSelected) + { + return InteractionFlags.Gaze; + } + else + { + return InteractionFlags.Generic; + } + } + } + + protected MoveTransformation moveManipulation; + + protected RotateTransformation rotateManipulation; + + protected ScaleTransformation scaleManipulation; + + #endregion Protected Properties + + #region Private Properties + + private PlacementHub placementHub; + + private bool ShouldSmooth => (IsGrabSelected && SmoothingNear) || (!IsGrabSelected && SmoothingFar); + + private bool wasSmoothed; + + private Rigidbody rigidBody; + + private bool wasGravity = false; + + private bool wasKinematic = false; + + // Reusable list for fetching interactionPoints from interactors. + private List interactionPoints = new List(); + + // Reusable list for fetching attachPoints from interactors. + private List attachPoints = new List(); + + // Reusable list for fetching grabPoints from interactors. + private List grabPoints = new List(); + + #endregion Private Properties + + #region MonoBehaviour Functions + + protected virtual void ApplyRequiredSettings() + { + // ObjectManipulator is never selected by poking. + DisableInteractorType(typeof(IPokeInteractor)); + } + + protected override void Reset() + { + base.Reset(); + ApplyRequiredSettings(); + selectMode = InteractableSelectMode.Multiple; + } + + private void OnValidate() + { + ApplyRequiredSettings(); + } + + protected override void Awake() + { + base.Awake(); + + placementHub = transform.GetComponent(); + + ApplyRequiredSettings(); + + rigidBody = GetComponent(); + + InstantiateManipulationTransformations(); + } + + #endregion + + private void InstantiateManipulationTransformations() + { + moveManipulation = new MoveTransformation(this); + rotateManipulation = new RotateTransformation(this); + scaleManipulation = new ScaleTransformation(this); + + moveManipulation.logic = Activator.CreateInstance(ManipulationLogicTypes.moveLogicType) as ManipulationLogic; + rotateManipulation.logic = Activator.CreateInstance(ManipulationLogicTypes.rotateLogicType) as ManipulationLogic; + scaleManipulation.logic = Activator.CreateInstance(ManipulationLogicTypes.scaleLogicType) as ManipulationLogic; + } + + /// + /// Override this class to provide the transform of the reference frame (e.g. the camera) against which to compute the damping. + /// + /// This intended for the situation of FPS-style controllers moving forward at constant speed while holding an object, + /// to prevent damping from pushing the body towards the player. + /// + /// Arguments of the OnSelectEntered event that called this function + /// The Transform that should be used to define the reference frame or null to use the global reference frame + protected virtual Transform GetReferenceFrameTransform(SelectEnterEventArgs args) => null; + + private static readonly ProfilerMarker OnSelectEnteredPerfMarker = + new ProfilerMarker("[MRTK] ObjectManipulator.OnSelectEntered"); + + /// + protected override void OnSelectEntered(SelectEnterEventArgs args) + { + using (OnSelectEnteredPerfMarker.Auto()) + { + base.OnSelectEntered(args); + + wasSmoothed = placementHub.UseSmoothing; + placementHub.UseSmoothing = (IsGrabSelected && SmoothingNear) || (!IsGrabSelected && SmoothingFar); + + // Only record rigidbody settings if this is the *first* + // selection event! Otherwise, we'll record the during-interaction + // rigidbody information, which we've already dirtied. + if (rigidBody != null && interactorsSelecting.Count == 1) + { + wasGravity = rigidBody.useGravity; + wasKinematic = rigidBody.isKinematic; + + rigidBody.useGravity = false; + rigidBody.isKinematic = false; + } + + // ideally, the reference frame should be that of the camera. Here the interactorObject transform is the best available alternative. + placementHub.SetReferenceFrameTransform(GetReferenceFrameTransform(args)); + + var initialTransform = new MixedRealityTransform(transform.position, transform.rotation, transform.localScale); + + moveManipulation.logic.Setup(interactorsSelecting, this, initialTransform); + rotateManipulation.logic.Setup(interactorsSelecting, this, initialTransform); + scaleManipulation.logic.Setup(interactorsSelecting, this, initialTransform); + + placementHub.transformations.Add(scaleManipulation); + placementHub.transformations.Add(rotateManipulation); + placementHub.transformations.Add(moveManipulation); + } + } + + private static readonly ProfilerMarker OnSelectExitedPerfMarker = + new ProfilerMarker("[MRTK] ObjectManipulator.OnSelectExited"); + + /// + protected override void OnSelectExited(SelectExitEventArgs args) + { + using (OnSelectExitedPerfMarker.Auto()) + { + base.OnSelectExited(args); + + placementHub.UseSmoothing = wasSmoothed; + + // Only release the rigidbody (restore rigidbody settings/configuration) + // if this is the last select event! + if (rigidBody != null && interactorsSelecting.Count == 0) + { + ReleaseRigidBody(rigidBody.velocity, rigidBody.angularVelocity); + } + + placementHub.transformations.Remove(moveManipulation); + placementHub.transformations.Remove(rotateManipulation); + placementHub.transformations.Remove(scaleManipulation); + } + } + + private static readonly ProfilerMarker ScaleLogicMarker = new ProfilerMarker("[MRTK] ScaleLogic.Update"); + private static readonly ProfilerMarker RotateLogicMarker = new ProfilerMarker("[MRTK] RotateLogic.Update"); + private static readonly ProfilerMarker MoveLogicMarker = new ProfilerMarker("[MRTK] MoveLogic.Update"); + + private static readonly ProfilerMarker ObjectManipulatorProcessInteractableMarker = + new ProfilerMarker("[MRTK] ObjectManipulator.ProcessInteractable"); + + /// + public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase) + { + using (ObjectManipulatorProcessInteractableMarker.Auto()) + { + base.ProcessInteractable(updatePhase); + + if(!isSelected) + { + return; + } + + // Evaluate user input in the UI Update() function. + // If we are using physics, targetTransform is not applied directly but instead deferred + // to the ApplyForcesToRigidbody() function called from FixedUpdate() + if (updatePhase == XRInteractionUpdateOrder.UpdatePhase.Dynamic) + { + RotateAnchorType rotateType = CurrentInteractionType == InteractionFlags.Near ? RotationAnchorNear : RotationAnchorFar; + bool useCenteredAnchor = rotateType == RotateAnchorType.RotateAboutObjectCenter; + bool isOneHanded = interactorsSelecting.Count == 1; + + using (ScaleLogicMarker.Auto()) + { + if (allowedManipulations.IsMaskSet(TransformFlags.Scale)) + { + scaleManipulation.interactorsSelecting = interactorsSelecting; + scaleManipulation.useCenteredAnchor = useCenteredAnchor; + } + } + + using (RotateLogicMarker.Auto()) + { + if (allowedManipulations.IsMaskSet(TransformFlags.Rotate)) + { + rotateManipulation.interactorsSelecting = interactorsSelecting; + rotateManipulation.useCenteredAnchor = useCenteredAnchor; + //targetTransform.Rotation = ManipulationLogic.rotateLogic.Update(interactorsSelecting, this, targetTransform, useCenteredAnchor); + } + } + + using (MoveLogicMarker.Auto()) + { + if (allowedManipulations.IsMaskSet(TransformFlags.Move)) + { + moveManipulation.interactorsSelecting = interactorsSelecting; + moveManipulation.useCenteredAnchor = useCenteredAnchor; + //targetTransform.Position = ManipulationLogic.moveLogic.Update(interactorsSelecting, this, targetTransform, useCenteredAnchor); + } + } + } + } + } + + private void ReleaseRigidBody(Vector3 velocity, Vector3 angularVelocity) + { + if (rigidBody != null) + { + rigidBody.useGravity = wasGravity; + rigidBody.isKinematic = wasKinematic; + + // Match the object's velocity to the controller for near interactions + // Otherwise keep the objects current velocity so that it's not dampened unnaturally + if (IsGrabSelected) + { + if ((releaseBehavior & ReleaseBehaviorType.KeepVelocity) == ReleaseBehaviorType.KeepVelocity) + { + rigidBody.velocity = velocity; + } + + if ((releaseBehavior & ReleaseBehaviorType.KeepAngularVelocity) == ReleaseBehaviorType.KeepAngularVelocity) + { + rigidBody.angularVelocity = angularVelocity; + } + } + } + } + + // TODO, may want to move this + // into an extension method on the controller, or into some utility box. + /// + /// Gets the absolute device (grip) rotation associated with the specified interactor. + /// Used to query actual grabbing rotation, vs a ray rotation. + /// + private bool TryGetGripRotation(IXRSelectInteractor interactor, out Quaternion rotation) + { + // We need to query the raw device rotation from the interactor; however, + // the controller may have its rotation bound to the pointerRotation, which is unsuitable + // for modeling rotations with far rays. Therefore, we cast down to the base TrackedDevice, + // and query the device rotation directly. If any of this is un-castable, we return the + // interactor's attachTransform's rotation. + if (interactor is XRBaseControllerInteractor controllerInteractor && + controllerInteractor.xrController is ActionBasedController abController && + abController.rotationAction.action?.activeControl?.device is TrackedDevice device) + { + rotation = device.deviceRotation.ReadValue(); + return true; + } + + rotation = interactor.GetAttachTransform(this).rotation; + return true; + } + + + public class MoveTransformation : ITransformation + { + private IXRSelectInteractable interactable; + + public MoveTransformation(IXRSelectInteractable affectedInteractable) + { + interactable = affectedInteractable; + } + + public ManipulationLogic logic; + internal List interactorsSelecting; + internal bool useCenteredAnchor; + + public int ExecutionPriority => throw new NotImplementedException(); + + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + { + var targetTransform = new MixedRealityTransform(initialPosition, initialRotation, initialLocalScale); + Vector3 newPosition = logic.Update(interactorsSelecting, interactable, targetTransform, useCenteredAnchor); + return (newPosition, initialRotation, initialLocalScale); + } + } + + public class RotateTransformation : ITransformation + { + private IXRSelectInteractable interactable; + + public RotateTransformation(IXRSelectInteractable affectedInteractable) + { + interactable = affectedInteractable; + } + + public ManipulationLogic logic; + internal List interactorsSelecting; + internal bool useCenteredAnchor; + + public int ExecutionPriority => throw new NotImplementedException(); + + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + { + var targetTransform = new MixedRealityTransform(initialPosition, initialRotation, initialLocalScale); + Quaternion newRotation = logic.Update(interactorsSelecting, interactable, targetTransform, useCenteredAnchor); + return (initialPosition, newRotation, initialLocalScale); + } + } + + public class ScaleTransformation : ITransformation + { + private IXRSelectInteractable interactable; + + public ScaleTransformation(IXRSelectInteractable affectedInteractable) + { + interactable = affectedInteractable; + } + + public ManipulationLogic logic; + internal List interactorsSelecting; + internal bool useCenteredAnchor; + + public int ExecutionPriority => throw new NotImplementedException(); + + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + { + var targetTransform = new MixedRealityTransform(initialPosition, initialRotation, initialLocalScale); + Vector3 newScale = logic.Update(interactorsSelecting, interactable, targetTransform, useCenteredAnchor); + return (initialPosition, initialRotation, newScale); + } + } + } +} diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs.meta b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs.meta new file mode 100644 index 00000000000..21647e178b4 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: a390a4254b6ce3647ba89bc9eaf899a2 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs index e7f9cacbc2c..052ed77e9fa 100644 --- a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs @@ -922,6 +922,7 @@ controllerInteractor.xrController is ActionBasedController abController && } } + #region ReleaseBehaviorEnum Extenions /// /// Extension methods specific to the enum. /// @@ -939,4 +940,5 @@ public static bool IsMaskSet(this ObjectManipulator.ReleaseBehaviorType a, Objec return ((a & b) == b); } } + #endregion } diff --git a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs new file mode 100644 index 00000000000..8cffedc0c06 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs @@ -0,0 +1,398 @@ +using Microsoft.MixedReality.Toolkit; +using Microsoft.MixedReality.Toolkit.SpatialManipulation; +using System; +using System.Collections; +using System.Collections.Generic; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.SpatialManipulation +{ + public class PlacementHub : MonoBehaviour + { + [SerializeReference] + [InterfaceSelector] + public List transformations =new List(); + + (Vector3, Quaternion, Vector3) targetPose; + + // Make this it's own class so we can potentially allow for alternate implementations which "blend" transformations or + // use a transformation's execution order differently. + private bool GetTargetTransform() + { + if (transformations.Count == 0) + { + return false; + } + + Vector3 currentPosition = transform.position; + Quaternion currentRotation = transform.rotation; + Vector3 currentLocalScale = transform.localScale; + + for (int i = 0; i < transformations.Count; i++) + { + var t = transformations[i]; + (currentPosition, currentRotation, currentLocalScale) = t.ApplyTransformation(currentPosition, currentRotation, currentLocalScale); + } + + targetPose = (currentPosition, currentRotation, currentLocalScale); + return true; + } + + // Update is called once per frame + void Update() + { + if (GetTargetTransform()) + { + ApplyTargetTransform(); + } + } + + + // When the player is carrying a Rigidbody, the physics damping of interaction should act within the moving frame of reference of the player. + // The reference frame logic allows compensating for that + private Transform referenceFrameTransform = null; + private bool referenceFrameHasLastPos = false; + private Vector3 referenceFrameLastPos; + public MixedRealityTransform targetTransform; + public bool isManipulated; + + [SerializeField] + private bool useForces; + private Rigidbody rigidBody => transform.GetComponent(); + + private void FixedUpdate() + { + if (useForces && rigidBody != null && GetTargetTransform()) + { + ApplyForcesToRigidbody(); + } + } + + /// + /// Once the has been determined, this method is called + /// to apply the target pose to the object. Calls before + /// applying, to adjust the pose with smoothing, constraints, etc. + /// + private void ApplyTargetTransform() + { + targetTransform = new MixedRealityTransform(targetPose.Item1, targetPose.Item2, targetPose.Item3); + + // modifiedTransformFlags currently unused. + TransformFlags modifiedTransformFlags = TransformFlags.None; + SmoothTargetPose(ref targetTransform, ref modifiedTransformFlags); + + if (rigidBody == null) + { + transform.SetPositionAndRotation(targetTransform.Position, targetTransform.Rotation); + transform.localScale = targetTransform.Scale; + } + else + { + // There is a Rigidbody. Potential different paths for near vs far manipulation + if (!useForces) + { + rigidBody.MovePosition(targetTransform.Position); + rigidBody.MoveRotation(targetTransform.Rotation); + } + + transform.localScale = targetTransform.Scale; + } + } + + #region smoothing + [SerializeField] + private bool useSmoothing = true; + + public bool UseSmoothing + { + get { return useSmoothing; } + set { useSmoothing = value; } + } + + [SerializeReference, InterfaceSelector(false)] + [DrawIf("shouldSmooth")] + private ITransformSmoothingLogic smoothingLogic = new DefaultTransformSmoothingLogic(); + + [SerializeField] + [Range(0, 1)] + [Tooltip("Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value.")] + private float moveLerpTime = 0.001f; + + /// + /// Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value. + /// + public float MoveLerpTime + { + get => moveLerpTime; + set => moveLerpTime = value; + } + + [SerializeField] + [Range(0, 1)] + [Tooltip("Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value.")] + private float rotateLerpTime = 0.001f; + + /// + /// Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value. + /// + public float RotateLerpTime + { + get => rotateLerpTime; + set => rotateLerpTime = value; + } + + [SerializeField] + [Range(0, 1)] + [Tooltip("Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value.")] + private float scaleLerpTime = 0.001f; + + /// + /// Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value. + /// + public float ScaleLerpTime + { + get => scaleLerpTime; + set => scaleLerpTime = value; + } + + /// + /// Called by ApplyTargetPose to modify the target pose with the relevant constraints, smoothing, + /// elastic, or any other derived/overridden behavior. + /// + /// + /// The target position, rotation, and scale, pre-smoothing, but post-input and post-constraints. Modified by-reference. + /// + /// + /// Flags which parts of the transform (position, rotation, scale) have been altered by an external source (like Elastics). + /// Modified by-reference. + /// + protected virtual void SmoothTargetPose(ref MixedRealityTransform targetPose, ref TransformFlags modifiedTransformFlags) + { + // TODO: Elastics. Compute elastics here and apply to modifiedTransformFlags. + + bool applySmoothing = UseSmoothing && smoothingLogic != null; + + targetPose.Position = (applySmoothing && !useForces) ? smoothingLogic.SmoothPosition(transform.position, targetPose.Position, moveLerpTime, Time.deltaTime) : targetPose.Position; + targetPose.Rotation = (applySmoothing && !useForces) ? smoothingLogic.SmoothRotation(transform.rotation, targetPose.Rotation, rotateLerpTime, Time.deltaTime) : targetPose.Rotation; + targetPose.Scale = applySmoothing ? smoothingLogic.SmoothScale(transform.localScale, targetPose.Scale, scaleLerpTime, Time.deltaTime) : targetPose.Scale; + } + + #endregion + + #region rigidbodies + [SerializeField] + [Tooltip( + "Apply torque to control orientation of the body")] + private bool applyTorque = true; + + /// + /// Apply torque to control orientation of the body + /// + public bool ApplyTorque + { + get => applyTorque; + set => applyTorque = value; + } + + [SerializeField] + [Range(0.001f, 2.0f)] + [Tooltip("The time scale at which a Rigidbody reacts to input movement defined as oscillation period of the dampened spring force.")] + private float springForceSoftness = 0.1f; + + /// + /// The time scale at which a Rigidbody reacts to input movement defined as oscillation period of the dampened spring force. + /// + public float SpringForceSoftness + { + get => springForceSoftness; + set => springForceSoftness = value; + } + + [SerializeField] + [Range(0.001f, 2.0f)] + [Tooltip("The time scale at which a Rigidbody reacts to input rotation defined as oscillation period of the dampened spring torque.")] + private float springTorqueSoftness = 0.1f; + + /// + /// The time scale at which a Rigidbody reacts to input rotation defined as oscillation period of the dampened angular spring force. + /// + public float SpringTorqueSoftness + { + get => springTorqueSoftness; + set => springTorqueSoftness = value; + } + + [SerializeField] + [Range(0, 2.0f)] + [Tooltip("The damping of the spring force&torque: 1.0f corresponds to critical damping, lower values lead to underdamping (i.e. oscillation).")] + private float springDamping = 1.0f; + + /// + /// The damping of the spring force&torque: 1.0f corresponds to critical damping, lower values lead to underdamping (i.e. oscillation). + /// + public float SpringDamping + { + get => springDamping; + set => springDamping = value; + } + + [SerializeField] + [Range(0, 10000f)] + [Tooltip("The maximum acceleration applied by the spring force to avoid trembling when pushing a body against a static object.")] + private float springForceLimit = 100.0f; + + /// + /// The maximum acceleration applied by the spring force to avoid trembling when pushing a body against a static object. + /// + public float SpringForceLimit + { + get => springForceLimit; + set => springForceLimit = value; + } + + /// + /// Override this class to provide the transform of the reference frame (e.g. the camera) against which to compute the damping. + /// + /// This intended for the situation of FPS-style controllers moving forward at constant speed while holding an object, + /// to prevent damping from pushing the body towards the player. + /// + /// The transform that used will be used to define the reference frame or null to use the global reference frame + public void SetReferenceFrameTransform(Transform t) + { + referenceFrameTransform = t; + referenceFrameHasLastPos = false; + } + + /// + /// In case a Rigidbody gets the targetTransform applied using physical forcees, this function is called within the + /// FixedUpdate() routine with physics-conforming time stepping. + /// + private void ApplyForcesToRigidbody() + { + var referenceFrameVelocity = Vector3.zero; + + if (referenceFrameTransform != null) + { + if (referenceFrameHasLastPos) + { + referenceFrameVelocity = (referenceFrameTransform.position - referenceFrameLastPos) / Time.fixedDeltaTime; + } + + referenceFrameLastPos = referenceFrameTransform.position; + referenceFrameHasLastPos = true; + } + + // implement critically dampened spring force, scaled to mass-independent frequency + float omega = Mathf.PI / springForceSoftness; // angular frequency, sqrt(k/m) + + Vector3 distance = transform.position - targetTransform.Position; + + // when player is moving, we need to anticipate where the targetTransform is going to be one time step from now + distance -= referenceFrameVelocity * Time.fixedDeltaTime; + + var velocity = rigidBody.velocity; + + var acceleration = -distance * omega * omega; // acceleration caused by spring force + + var accelerationMagnitude = acceleration.magnitude; + + // apply springForceLimit only for slow-moving body (e.g. pressed against wall) + // when body is already moving fast, also allow strong acceleration + var maxAcceleration = Mathf.Max(springForceLimit, 10 * velocity.magnitude / Time.fixedDeltaTime); + + if (accelerationMagnitude > maxAcceleration) + { + acceleration *= maxAcceleration / accelerationMagnitude; + } + + // Apply damping - mathematically, we need e^(-2 * omega * dt) + // To compensate for the finite time step, this is split in two equal factors, + // one applied before, the other after the spring force + // equivalent with applying damping as well as spring force continuously + float halfDampingFactor = Mathf.Exp(-springDamping * omega * Time.fixedDeltaTime); + + velocity -= referenceFrameVelocity; // change to the player's frame of reference before damping + + velocity *= halfDampingFactor; // 1/2 damping + velocity += acceleration * Time.fixedDeltaTime; // integration step of spring force + velocity *= halfDampingFactor; // 1/2 damping + + velocity += referenceFrameVelocity; // change back to global frame of reference + + rigidBody.velocity = velocity; + + if (applyTorque) + { + // Torque calculations: same calculation & parameters as for linear velocity + // skipping referenceFrameVelocity and springForceLimit which do not exactly apply here + + // implement critically dampened spring force, scaled to mass-independent frequency + float angularOmega = Mathf.PI / springTorqueSoftness; // angular frequency, sqrt(k/m) + + var angularDistance = transform.rotation * Quaternion.Inverse(targetTransform.Rotation); + angularDistance.ToAngleAxis(out float angle, out Vector3 axis); + + if (!axis.IsValidVector()) + { + // ToAngleAxis is numerically unstable, returning NaN axis for near-zero angles + angle = 0; + axis = Vector3.up; + } + + if (angle > 180f) + { + angle -= 360f; + } + + var angularVelocity = rigidBody.angularVelocity; + + var angularAcceleration = -angle * angularOmega * angularOmega; // acceleration caused by spring force + + angularVelocity *= halfDampingFactor; // 1/2 damping + angularVelocity += angularAcceleration * Time.fixedDeltaTime * Mathf.Deg2Rad * axis.normalized; // integration step of spring force + angularVelocity *= halfDampingFactor; // 1/2 damping + + rigidBody.angularVelocity = angularVelocity; + } + } + #endregion + } +} + +/// +/// Interface which describes a transformation that is applied to MixedRealityTransform +/// +public interface ITransformation +{ + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale); + + /// + /// Execution order priority of this constraint. Lower numbers will be executed before higher numbers. + /// + public int ExecutionPriority { get; } +} + +/// +/// A transformation which restricts the scale of of the MixedRealityTransform +/// +public class MinMaxConstraintTransformation: ITransformation +{ + public Vector3 minScale = Vector3.one * 0.2f; + + public Vector3 maxScale = Vector3.one * 2.0f; + + protected const int scale_priority = -1000; + protected const int rotation_priority = -1000; + protected const int position_priority = -1000; + protected const int constraint_priority_modifier = 1; + + public int ExecutionPriority => throw new NotImplementedException(); + + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + { + initialLocalScale.x = Mathf.Clamp(initialLocalScale.x, minScale.x, maxScale.x); + initialLocalScale.y = Mathf.Clamp(initialLocalScale.y, minScale.y, maxScale.y); + initialLocalScale.z = Mathf.Clamp(initialLocalScale.z, minScale.z, maxScale.z); + return (initialPosition, initialRotation, initialLocalScale); + } +} diff --git a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs.meta b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs.meta new file mode 100644 index 00000000000..51c291cc290 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9d4a28b08f6a07140825ed45fcd9a13d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs new file mode 100644 index 00000000000..cfe2b2da303 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs @@ -0,0 +1,994 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit; +using Microsoft.MixedReality.Toolkit.Core.Tests; +using Microsoft.MixedReality.Toolkit.Input.Tests; +using Microsoft.MixedReality.Toolkit.Input.Simulation; +using NUnit.Framework; +using System; +using System.Collections; +using UnityEngine; +using UnityEngine.TestTools; +using HandshapeId = Microsoft.MixedReality.Toolkit.Input.HandshapeTypes.HandshapeId; + +namespace Microsoft.MixedReality.Toolkit.SpatialManipulation.Runtime.Tests +{ + /// + /// Tests for NewObjectManipulator + /// + public class NewObjectManipulatorTests : BaseRuntimeInputTests + { + + /// + /// Verifies that creating an NewObjectManipulator at runtime properly + /// respects the various interactor filtering/interaction type rules. + /// + [UnityTest] + public IEnumerator TestObjManipInteractorRules() + { + GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); + cube.AddComponent(); + cube.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0.1f, 0.1f, 1)); + cube.transform.localScale = Vector3.one * 0.2f; + + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Verify that a ConstraintManager was automatically added. + Assert.IsTrue(cube.GetComponent() != null, "Runtime-spawned ObjManip didn't also spawn ConstraintManager"); + + NewObjectManipulator objManip = cube.GetComponent(); + + Assert.IsTrue(objManip.AllowedManipulations == (TransformFlags.Move | TransformFlags.Rotate | TransformFlags.Scale), + "ObjManip started out with incorrect AllowedManipulations"); + + var rightHand = new TestHand(Handedness.Right); + yield return rightHand.Show(InputTestUtilities.InFrontOfUser(0.5f)); + + yield return rightHand.MoveTo(cube.transform.position); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(objManip.IsPokeHovered, "ObjManip shouldn't get IsPokeHovered"); + Assert.IsTrue(objManip.IsGrabHovered, "ObjManip didn't report IsGrabHovered"); + + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsTrue(objManip.IsGrabSelected, "ObjManip didn't report IsGrabSelected"); + Assert.IsFalse(objManip.IsPokeSelected, "ObjManip was PokeSelected. Should not be possible."); + + yield return rightHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(objManip.isSelected, "ObjManip didn't de-select."); + + objManip.DisableInteractorType(typeof(IGrabInteractor)); + yield return RuntimeTestUtilities.WaitForUpdates(); + + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(objManip.IsGrabSelected, + "ObjManip was still grab selected after removing the grab interactor from the set of allowed interactors."); + Assert.IsFalse(objManip.IsPokeSelected, + "ObjManip was PokeSelected. Should not be possible."); + + yield return rightHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // We don't have full gaze support in sim yet, so this is an approximation. + // Set cube's position to straight ahead. + cube.transform.position = InputTestUtilities.InFrontOfUser(1.0f); + + // Put hand out in front, in-FOV, but not too close to cube as to + // disable the far interactors. + yield return rightHand.MoveTo(InputTestUtilities.InFrontOfUser(new Vector3(0.1f, 0, 0.5f))); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsTrue(objManip.IsGazePinchHovered, + "ObjManip didn't report IsGazePinchHovered"); + + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsTrue(objManip.IsGazePinchSelected, + "ObjManip didn't report IsGazePinchSelected"); + + yield return rightHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(objManip.IsGazePinchSelected, + "ObjManip was still GazePinchSelected after un-pinching."); + + objManip.DisableInteractorType(typeof(IGazeInteractor)); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(objManip.IsGazePinchSelected, + "ObjManip was still gaze selected after removing the gaze interactor from the set of allowed interactors."); + } + + /// + /// Verifies that an NewObjectManipulator created at runtime has proper smoothing characteristics. + /// + [UnityTest] + public IEnumerator TestObjManipSmoothingDrift() + { + GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); + NewObjectManipulator objManip = cube.AddComponent(); + cube.transform.position = InputTestUtilities.InFrontOfUser(new Vector3(0.1f, 0.1f, 1)); + cube.transform.localScale = Vector3.one * 0.2f; + + // Enable smoothing for near interaction. + objManip.SmoothingNear = true; + + yield return RuntimeTestUtilities.WaitForUpdates(); + + var rightHand = new TestHand(Handedness.Right); + yield return rightHand.Show(InputTestUtilities.InFrontOfUser(0.5f)); + + yield return rightHand.MoveTo(cube.transform.position); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsFalse(objManip.IsPokeHovered, "ObjManip shouldn't get IsPokeHovered"); + Assert.IsTrue(objManip.IsGrabHovered, "ObjManip didn't report IsGrabHovered"); + + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsTrue(objManip.IsGrabSelected, "ObjManip didn't report IsGrabSelected"); + + // Move the hand to the right. + Vector3 originalPosition = cube.transform.position; + Vector3 attachTransform = objManip.firstInteractorSelecting.GetAttachTransform(objManip).position; + Vector3 originalAttachOffset = attachTransform - originalPosition; + + Vector3 newPosition = originalPosition + Vector3.right * 0.5f; + yield return rightHand.MoveTo(newPosition); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Smoothing should mean that the cube has lagged behind the hand. + attachTransform = objManip.firstInteractorSelecting.GetAttachTransform(objManip).position; + Vector3 attachOffset = attachTransform - cube.transform.position; + Assert.IsTrue((attachOffset - originalAttachOffset).magnitude > 0.1f, + "Smoothing didn't seem to work. Current attachTransform offset should be different than the original, indicating lag."); + + // Wait long enough for the object to catch up. + yield return new WaitForSeconds(6.0f); + attachTransform = objManip.firstInteractorSelecting.GetAttachTransform(objManip).position; + attachOffset = attachTransform - cube.transform.position; + Debug.Log(attachTransform.ToString("F3")); + Debug.Log(originalAttachOffset.ToString("F3")); + Assert.IsTrue((attachOffset - originalAttachOffset).magnitude < 0.001f, + "Cube didn't catch up with the hand after waiting for a bit. Magnitude: " + (attachOffset - originalAttachOffset).magnitude.ToString("F4")); + + // Disable smoothing, to check that it properly sticks to the hand once disabled. + objManip.SmoothingNear = false; + + newPosition = originalPosition - Vector3.right * 1.5f; + yield return rightHand.MoveTo(newPosition); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Immediately check to make sure the cube matches our grab exactly. + attachTransform = objManip.firstInteractorSelecting.GetAttachTransform(objManip).position; + attachOffset = attachTransform - cube.transform.position; + Assert.IsTrue((attachOffset - originalAttachOffset).magnitude < 0.001f, + "Cube didn't match hand exactly after setting SmoothingNear to false."); + } + + /// + /// Test creating adding a NewObjectManipulator to GameObject programmatically. + /// Should be able to run scene without getting any exceptions. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorInstantiate() + { + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.2f; + + var NewObjectManipulator = testObject.AddComponent(); + // Wait for two frames to make sure we don't get null pointer exception. + yield return null; + yield return null; + + GameObject.Destroy(testObject); + // Wait for a frame to give Unity a change to actually destroy the object + yield return null; + } + + /// + /// Test creating NewObjectManipulator and receiving hover enter/exit events + /// from gaze provider. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorGazeHover() + { + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.2f; + + var NewObjectManipulator = testObject.AddComponent(); + int hoverEnterCount = 0; + int hoverExitCount = 0; + + NewObjectManipulator.hoverEntered.AddListener((eventData) => hoverEnterCount++); + NewObjectManipulator.hoverExited.AddListener((eventData) => hoverExitCount++); + + testObject.transform.position = InputTestUtilities.InFrontOfUser(1.0f); + + yield return new WaitForFixedUpdate(); + yield return null; + + Assert.AreEqual(1, hoverEnterCount, $"NewObjectManipulator did not receive hover enter event, count is {hoverEnterCount}"); + + testObject.transform.Translate(Vector3.up); + + // First yield for physics. Second for normal frame step. + // Without first one, second might happen before translation is applied. + // Without second one services will not be stepped. + yield return new WaitForFixedUpdate(); + yield return null; + + Assert.AreEqual(1, hoverExitCount, "NewObjectManipulator did not receive hover exit event"); + + testObject.transform.Translate(5 * Vector3.up); + + yield return new WaitForFixedUpdate(); + yield return null; + + Assert.IsTrue(hoverExitCount == 1, "NewObjectManipulator received the second hover event"); + + GameObject.Destroy(testObject); + + // Wait for a frame to give Unity a chance to actually destroy the object + yield return null; + } + + #region One Handed Manipulation Tests + + /// + /// This tests the one hand near movement while camera (character) is moving around. + /// The test will check the offset between object pivot and grab point and make sure we're not drifting + /// out of the object on pointer rotation - this test should be the same in all rotation setups + /// This test also has a sanity check to ensure behavior is still the same for objects of different scale + /// + [UnityTest] + public IEnumerator NewObjectManipulatorOneHandMoveNear() + { + // Set the anchor point to be the grab position + InputTestUtilities.SetHandAnchorPoint(Handedness.Left, Input.Simulation.ControllerAnchorPoint.Grab); + InputTestUtilities.SetHandAnchorPoint(Handedness.Right, Input.Simulation.ControllerAnchorPoint.Grab); + + // Disable gaze interactions for this unit test; + InputTestUtilities.DisableGaze(); + + // set up cube with manipulation handler + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + Vector3 initialObjectPosition = InputTestUtilities.InFrontOfUser(1f); + testObject.transform.position = initialObjectPosition; + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + yield return new WaitForFixedUpdate(); + yield return null; + + const int numCircleSteps = 10; + + Vector3 initialHandPosition = InputTestUtilities.InFrontOfUser(0.5f); + Vector3 initialGrabPosition = InputTestUtilities.InFrontOfUser(new Vector3(-0.05f, -0.05f, 1f)); // grab around the left bottom corner of the cube + Quaternion initialGrabRotation = Quaternion.identity; + TestHand hand = new TestHand(Handedness.Right); + + Vector3[] objectScales = new Vector3[] { Vector3.one * 0.2f, new Vector3(0.2f, 0.4f, 0.3f) }; + + foreach (var objectScale in objectScales) + { + testObject.transform.localScale = objectScale; + yield return RuntimeTestUtilities.WaitForUpdates(); + + // do this test for every one hand rotation mode + foreach (NewObjectManipulator.RotateAnchorType rotationAnchorType in Enum.GetValues(typeof(NewObjectManipulator.RotateAnchorType))) + { + NewObjectManipulator.RotationAnchorNear = rotationAnchorType; + + InputTestUtilities.InitializeCameraToOriginAndForward(); + + yield return hand.Show(initialHandPosition); + yield return hand.MoveTo(initialGrabPosition); + yield return hand.RotateTo(initialGrabRotation); + + yield return hand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsTrue(NewObjectManipulator.IsGrabSelected, $"NewObjectManipulator didn't get grabbed on pinch!"); + + // Ensure the object didn't move after pinching if using object centered rotation + // Todo: Re-enable when grab-anchoring no longer has a frame delay. (Will require + // updates to synthetic hands subsystem!) + // Vector3 initialPosition = testObject.transform.position; + // if (rotationAnchorType == NewObjectManipulator.RotateAnchorType.RotateAboutObjectCenter) + // { + // TestUtilities.AssertAboutEqual(testObject.transform.position, initialPosition, "object shifted during pinch", 0.01f); + // } + + // save relative pos grab point to object + // The firstInteractorSelecting is the one that is currently grabbing the object + Vector3 initialGrabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + Vector3 initialGrabPointInObject = testObject.transform.InverseTransformPoint(initialGrabPoint); + Vector3 initialGrabOffset = initialGrabPoint - testObject.transform.position; + + // full circle + const int degreeStep = 360 / numCircleSteps; + + // rotating the pointer in a circle around "the user" + for (int i = 1; i <= numCircleSteps; ++i) + { + // rotate main camera (user) + Vector3 rotationDelta = degreeStep * Vector3.up; + InputTestUtilities.RotateCamera(rotationDelta); + yield return new WaitForFixedUpdate(); + + // move hand with the camera + Vector3 newHandPosition = Quaternion.AngleAxis(degreeStep * i, Vector3.up) * initialGrabPosition; + yield return hand.MoveTo(newHandPosition); + yield return hand.RotateTo(Quaternion.AngleAxis(degreeStep * i, Vector3.up) * initialGrabRotation); + yield return new WaitForFixedUpdate(); + + if (rotationAnchorType == NewObjectManipulator.RotateAnchorType.RotateAboutObjectCenter) + { + // make sure that the offset between grab and object centre hasn't changed while rotating + Vector3 grabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + Vector3 offsetRotated = grabPoint - testObject.transform.position; + TestUtilities.AssertAboutEqual(offsetRotated, initialGrabOffset, $"Object offset changed during rotation using {rotationAnchorType}"); + } + else + { + // make sure that grab point has not changed relative to the object while rotating + Vector3 grabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + Vector3 grabPointRotated = testObject.transform.InverseTransformPoint(grabPoint); + TestUtilities.AssertAboutEqual(grabPointRotated, initialGrabPointInObject, $"Grab point on object changed during rotation using {rotationAnchorType}"); + } + } + + // Move the object forward and back + yield return hand.MoveTo(initialGrabPosition + Vector3.forward * 0.4f); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // make sure that the offset between grab and object centre hasn't changed while rotating + Vector3 currentGrabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + Vector3 currentOffset = currentGrabPoint - testObject.transform.position; + TestUtilities.AssertAboutEqual(currentOffset, initialGrabOffset, $"Object offset changed during move forward"); + + yield return hand.MoveTo(initialGrabPosition + Vector3.back * 0.4f); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // make sure that the offset between grab and object centre hasn't changed while moving + currentGrabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + currentOffset = currentGrabPoint - testObject.transform.position; + TestUtilities.AssertAboutEqual(currentOffset, initialGrabOffset, $"Object offset changed during move backward"); + + yield return hand.MoveTo(initialGrabPosition); + + yield return hand.SetHandshape(HandshapeId.Open); + yield return hand.Hide(); + } + } + } + + /// + /// This tests the one hand far movement while camera (character) is moving around. + /// The test will check the offset between object pivot and grab point and make sure we're not drifting + /// out of the object on pointer rotation - this test is the same for all objects that won't change + /// their orientation to camera while camera / pointer rotates as this will modify the far interaction grab point + /// This test also has a sanity check to ensure behavior is still the same for objects of different scale + /// + [UnityTest] + public IEnumerator NewObjectManipulatorOneHandMoveFar() + { + // Disable gaze interactions for this unit test; + InputTestUtilities.DisableGaze(); + + // set up cube with manipulation handler + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.3f; + Vector3 initialObjectPosition = InputTestUtilities.InFrontOfUser(1f); + testObject.transform.position = initialObjectPosition; + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + yield return new WaitForFixedUpdate(); + yield return null; + + const int numCircleSteps = 10; + + // Hand pointing at the cube + Vector3 initialHandPosition = InputTestUtilities.InFrontOfUser(0.6f); + Quaternion initialHandRotation = Quaternion.identity; + TestHand hand = new TestHand(Handedness.Right); + + Vector3[] objectScales = new Vector3[] { Vector3.one * 0.2f, new Vector3(0.2f, 0.4f, 0.3f) }; + foreach (var objectScale in objectScales) + { + // do this test for every one hand rotation mode + foreach (NewObjectManipulator.RotateAnchorType rotationAnchorType in Enum.GetValues(typeof(NewObjectManipulator.RotateAnchorType))) + { + NewObjectManipulator.RotationAnchorFar = rotationAnchorType; + + InputTestUtilities.InitializeCameraToOriginAndForward(); + + yield return hand.Show(initialHandPosition); + yield return hand.RotateTo(initialHandRotation); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Vector3 initialPosition = testObject.transform.position; + yield return hand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + Assert.IsTrue(NewObjectManipulator.isSelected, "NewObjectManipulator wasn't selected!"); + + // Ensure the object didn't move after pinching if using object centered rotation + if (rotationAnchorType == NewObjectManipulator.RotateAnchorType.RotateAboutObjectCenter) + { + TestUtilities.AssertAboutEqual(initialPosition, testObject.transform.position, "object shifted during pinch", 0.005f); + } + + // we do this because even though the interactor's position doesn't shift during the pinch + // what the hand considers to be the 'controller position' seems to shift slightly when performing the pinch gesture + yield return hand.MoveTo(initialHandPosition); + yield return hand.RotateTo(initialHandRotation); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // save relative pos grab point to object + // The firstInteractorSelecting is the one that is currently grabbing the object + Vector3 initialGrabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + Vector3 initialGrabPointInObject = testObject.transform.InverseTransformPoint(initialGrabPoint); + Vector3 initialGrabOffset = NewObjectManipulator.firstInteractorSelecting.transform.position - testObject.transform.position; + + // full circle + const int degreeStep = 360 / numCircleSteps; + + // rotating the pointer in a circle around "the user" + for (int i = 1; i <= numCircleSteps; ++i) + { + // rotate main camera (user) + Vector3 rotationDelta = degreeStep * Vector3.up; + InputTestUtilities.RotateCamera(rotationDelta); + yield return new WaitForFixedUpdate(); + + // move hand with the camera + Vector3 newHandPosition = Quaternion.AngleAxis(degreeStep * i, Vector3.up) * initialHandPosition; + yield return hand.MoveTo(newHandPosition); + yield return hand.RotateTo(Quaternion.AngleAxis(degreeStep * i, Vector3.up) * initialHandRotation); + yield return new WaitForFixedUpdate(); + + if (rotationAnchorType == NewObjectManipulator.RotateAnchorType.RotateAboutObjectCenter) + { + // We can't guarantee that the attach transform stays locked in the same place due to the object itself rotating + // Just check that it's rotation matches that of the hand + Quaternion objectRotation = NewObjectManipulator.transform.rotation; + TestUtilities.AssertAboutEqual(Quaternion.AngleAxis(degreeStep * i, Vector3.up), objectRotation, $"Rotation incorrect using {rotationAnchorType}"); + + // Also check that the object stays approximately infront of the hand + Assert.IsTrue(NewObjectManipulator.firstInteractorSelecting.transform.InverseTransformPoint(NewObjectManipulator.transform.position).z > 0); + } + else + { + // make sure that the grab point has not changed relative to the object while rotating + Vector3 grabPoint = NewObjectManipulator.firstInteractorSelecting.GetAttachTransform(NewObjectManipulator).position; + Vector3 grabPointRotated = testObject.transform.InverseTransformPoint(grabPoint); + TestUtilities.AssertAboutEqual(grabPointRotated, initialGrabPointInObject, $"Grab point on object changed during rotation using {rotationAnchorType}"); + } + } + + // Move the object forward and back + yield return hand.MoveTo(initialHandPosition + Vector3.forward); + // make sure that the offset between grab and object centre has grown + Vector3 currentInteractorPosition = NewObjectManipulator.firstInteractorSelecting.transform.position; + Vector3 currentOffset = currentInteractorPosition - testObject.transform.position; + Assert.IsTrue(currentOffset.magnitude > initialGrabOffset.magnitude, $"Object did not move farther away when moving forward while doing gaze manipulation"); + + yield return hand.MoveTo(initialHandPosition + Vector3.back * 0.2f); + // make sure that the offset between grab and object centre has shrunk as it moves closer to the camera + currentInteractorPosition = NewObjectManipulator.firstInteractorSelecting.transform.position; + currentOffset = currentInteractorPosition - testObject.transform.position; + Assert.IsTrue(currentOffset.magnitude < initialGrabOffset.magnitude, $"Object did not move closer when moving backwards while doing gaze manipulation"); + + yield return hand.MoveTo(initialHandPosition); + yield return hand.RotateTo(initialHandRotation); + yield return RuntimeTestUtilities.WaitForUpdates(); + + yield return hand.SetHandshape(HandshapeId.Open); + yield return hand.Hide(); + + // There seems to be some sort of deficiency with the object manipulator where the object does not return exactly to it's original position + // TODO: Fix or log a bug + // TestUtilities.AssertAboutEqual(testObject.transform.position, initialObjectPosition, "object has shifted significantly"); + testObject.transform.position = initialObjectPosition; + } + } + } + + + // + /// This tests that the gaze pointer can be used to directly invoke the manipulation logic via simulated pointer events, used + /// for scenarios like voice-driven movement using the gaze pointer. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorOneHandMoveGaze() + { + // Enable gaze interactions for this unit test; + InputTestUtilities.EnableGaze(); + + // Set up cube with NewObjectManipulator + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + Vector3 initialObjectPosition = InputTestUtilities.InFrontOfUser(1f); + testObject.transform.position = initialObjectPosition; + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + yield return new WaitForFixedUpdate(); + yield return null; + + const int numCircleSteps = 10; + + Vector3 initialHandPosition = InputTestUtilities.InFrontOfUser(0.5f); // Hand hovers in the center of the fov, but the hand ray misses the cube + Quaternion initialHandRotation = Quaternion.identity; + TestHand hand = new TestHand(Handedness.Right); + + Vector3[] objectScales = new Vector3[] { Vector3.one * 0.1f, new Vector3(0.1f, 0.05f, 0.08f) }; + + foreach (var objectScale in objectScales) + { + testObject.transform.localScale = objectScale; + yield return RuntimeTestUtilities.WaitForUpdates(); + + // do this test for every one hand rotation mode + foreach (NewObjectManipulator.RotateAnchorType rotationAnchorType in Enum.GetValues(typeof(NewObjectManipulator.RotateAnchorType))) + { + NewObjectManipulator.RotationAnchorNear = rotationAnchorType; + + InputTestUtilities.InitializeCameraToOriginAndForward(); + + yield return hand.Show(initialHandPosition); + yield return hand.RotateTo(initialHandRotation); + + Vector3 initialPosition = testObject.transform.position; + + yield return hand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Ensure the object didn't move after pinching if using object centered rotation + if (rotationAnchorType == NewObjectManipulator.RotateAnchorType.RotateAboutObjectCenter) + { + TestUtilities.AssertAboutEqual(initialPosition, testObject.transform.position, "object shifted during pinch", 0.01f); + } + // we do this because even though the interactor's position doesn't shift during the pinch + // what the hand considers to be the 'controller position' seems to shift slightly when performing the pinch gesture + yield return hand.MoveTo(initialHandPosition); + yield return hand.RotateTo(initialHandRotation); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // save relative pos grab point to object + // The firstInteractorSelecting is the one that is currently grabbing the object + Vector3 initialGrabPoint = NewObjectManipulator.firstInteractorSelecting.transform.position; + Vector3 initialGrabPointInObject = testObject.transform.InverseTransformPoint(initialGrabPoint); + Vector3 initialGrabOffset = NewObjectManipulator.firstInteractorSelecting.transform.position - testObject.transform.position; + + // full circle + const int degreeStep = 360 / numCircleSteps; + + // rotating the pointer in a circle around "the user" + for (int i = 1; i <= numCircleSteps; ++i) + { + // rotate main camera (user) + Vector3 rotationDelta = degreeStep * Vector3.up; + InputTestUtilities.RotateCamera(rotationDelta); + yield return new WaitForFixedUpdate(); + + // move hand with the camera + Vector3 newHandPosition = Quaternion.AngleAxis(degreeStep * i, Vector3.up) * initialHandPosition; + yield return hand.MoveTo(newHandPosition); + yield return hand.RotateTo(Quaternion.AngleAxis(degreeStep * i, Vector3.up) * initialHandRotation); + yield return new WaitForFixedUpdate(); + + // The exact position where a gaze pinched object ends up as it's manipulated by the hand is a bit unclear at the moment + // For now, use the following rough check to see that the object is rotating and is staying in front of the interactor + + // We can't guarantee that the attach transform stays locked in the same place due to the object itself rotating + // Just check that it's rotation matches that of the hand + Quaternion objectRotation = NewObjectManipulator.transform.rotation; + TestUtilities.AssertAboutEqual(Quaternion.AngleAxis(degreeStep * i, Vector3.up).normalized, objectRotation.normalized, $"Rotation incorrect using {rotationAnchorType}"); + + // Also check that the object stays approximately in front of the hand + Assert.IsTrue(NewObjectManipulator.firstInteractorSelecting.transform.InverseTransformPoint(NewObjectManipulator.transform.position).z > 0); + } + + // Move the object forward and back + yield return hand.MoveTo(initialHandPosition + Vector3.forward); + // make sure that the offset between grab and object centre hasn't changed while rotating + Vector3 currentGrabPoint = NewObjectManipulator.firstInteractorSelecting.transform.position; + Vector3 currentOffset = currentGrabPoint - testObject.transform.position; + Assert.IsTrue(currentOffset.magnitude > initialGrabOffset.magnitude, $"Object did not move farther away when moving forward while doing gaze manipulation"); + + yield return hand.MoveTo(initialHandPosition + Vector3.back * 0.2f); + // make sure that the offset between grab and object centre hasn't changed while rotating + currentGrabPoint = NewObjectManipulator.firstInteractorSelecting.transform.position; + currentOffset = currentGrabPoint - testObject.transform.position; + Assert.IsTrue(currentOffset.magnitude < initialGrabOffset.magnitude, $"Object did not move closer when moving backwards while doing gaze manipulation"); + + yield return hand.MoveTo(initialHandPosition); + yield return hand.RotateTo(initialHandRotation); + yield return RuntimeTestUtilities.WaitForUpdates(); + + yield return hand.SetHandshape(HandshapeId.Open); + yield return hand.Hide(); + + // There seems to be some sort of deficiency with the object manipulator where the object does not return exactly to it's original position + // TODO: Fix or log a bug + // TestUtilities.AssertAboutEqual(testObject.transform.position, initialObjectPosition, "object has shifted significantly"); + testObject.transform.position = initialObjectPosition; + } + } + } + + #endregion + + #region Two Handed Manipulation Tests + + // This test is not yet working due to some confusion as to how the centroid math works with the current object manipulator + + /* + /// + /// Test that the grab centroid is calculated correctly while rotating + /// the hands during a two-hand near interaction grab. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorTwoHandedCentroid() + { + InputTestUtilities.DisableGaze(); + + InputTestUtilities.InitializeCameraToOriginAndForward(); + + // Set up cube with NewObjectManipulator + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.5f; + Vector3 initialObjectPosition = new Vector3(0f, 0f, 1f); + Quaternion initialObjectRotation = testObject.transform.rotation; + testObject.transform.position = initialObjectPosition; + + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.HostTransform = testObject.transform; + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + // Configuring for two-handed interaction + NewObjectManipulator.selectMode = UnityEngine.XR.Interaction.Toolkit.InteractableSelectMode.Multiple; + + TestHand rightHand = new TestHand(Handedness.Right); + TestHand leftHand = new TestHand(Handedness.Left); + + yield return rightHand.Show(Vector3.zero); + yield return leftHand.Show(Vector3.zero); + + yield return rightHand.MoveTo(new Vector3(0.1f, -0.1f, 0.8f)); + yield return leftHand.MoveTo(new Vector3(-0.1f, -0.1f, 0.8f)); + yield return null; + + // Only testing move/rotate centroid position + NewObjectManipulator.AllowedManipulations = TransformFlags.Move | TransformFlags.Scale; + + // Only testing near manipulation + NewObjectManipulator.AllowedInteractionTypes = InteractionFlags.Near; + + int manipulationStartedCount = 0; + int manipulationEndedCount = 0; + NewObjectManipulator.selectEntered.AddListener((med) => manipulationStartedCount++); + NewObjectManipulator.selectExited.AddListener((med) => manipulationEndedCount++); + + // Grab the box. + yield return rightHand.SetGesture(GestureId.Pinch); + yield return leftHand.SetGesture(GestureId.Pinch); + + // Previously we checked that we didn't move after two pinches, however, due to the hand position shifting slighting on pinch, this is not applicable + // TODO, address in the future? + // Should not have moved (yet!) + // TestUtilities.AssertAboutEqual(testObject.transform.position, initialObjectPosition, $"Object moved when it shouldn't have! Position: {testObject.transform.position:F5}", 0.00001f); + + // The NewObjectManipulator should recognize that we've begun manipulation. + Assert.IsTrue(manipulationStartedCount > 0); + + yield return RuntimeTestUtilities.WaitForEnterKey(); + + // Move both hands outwards; the object may be scaled but the position should remain the same. + yield return rightHand.MoveTo(new Vector3(0.2f, -0.1f, 0.8f)); + yield return leftHand.MoveTo(new Vector3(-0.2f, -0.1f, 0.8f)); + + + yield return RuntimeTestUtilities.WaitForEnterKey(); + + // Should *still* not have moved! + // TestUtilities.AssertAboutEqual(testObject.transform.position, initialObjectPosition, $"Object moved when it shouldn't have! Position: {testObject.transform.position:F5}", 0.00001f); + + // Manipulation should not yet have ended. + Assert.IsTrue(manipulationEndedCount == 0); + + // Get the grab points before we rotate the hands. + // the left and right grab interactors should be the only interactors allowed to select this object manipulator + var leftGrabPoint = NewObjectManipulator.interactorsSelecting[0].transform.position; + var rightGrabPoint = NewObjectManipulator.interactorsSelecting[1].transform.position; + var originalCentroid = (leftGrabPoint + rightGrabPoint) / 2.0f; + + // List of test conditions for test fuzzing. + // Uses ValueTuple with layout (position, rotation) + List<(Vector3, Vector3)> testConditions = new List<(Vector3, Vector3)> + { + (new Vector3(0, 90, 0), new Vector3(0.2f, -0.1f, 0.8f)), + (new Vector3(25, 30, 45), new Vector3(0.3f, -0.2f, 0.7f)), + (new Vector3(75, 140, 0), new Vector3(0.1f, -0.1f, 0.8f)), + (new Vector3(10, 90, 20), new Vector3(0.5f, -0.2f, 0.5f)), + (new Vector3(45, 110, 0), new Vector3(0.3f, -0.1f, 0.8f)) + }; + + // Fuzz test. + foreach (var testCondition in testConditions) + { + yield return MoveHandsAndCheckCentroid(testCondition.Item1, testCondition.Item2, leftHand, rightHand, NewObjectManipulator, initialObjectPosition, originalCentroid, testObject.transform); + } + + yield return rightHand.SetGesture(GestureId.Open); + yield return leftHand.SetGesture(GestureId.Open); + } + + /// + /// Helper function for NewObjectManipulatorTwoHandedCentroid. Will mirror desired handRotation + /// and handPosition across the two hands, and verify that the centroid was still respected + /// by the manipulated object. + /// + private IEnumerator MoveHandsAndCheckCentroid(Vector3 handRotationEuler, Vector3 handPosition, + TestHand leftHand, TestHand rightHand, + NewObjectManipulator om, + Vector3 originalObjectPosition, Vector3 originalGrabCentroid, + Transform testObject) + { + // Rotate the hands. + yield return rightHand.RotateTo(Quaternion.Euler(handRotationEuler.x, handRotationEuler.y, handRotationEuler.z)); + yield return leftHand.RotateTo(Quaternion.Euler(handRotationEuler.x, -handRotationEuler.y, -handRotationEuler.z)); + + // Move the hands. + yield return rightHand.MoveTo(new Vector3(handPosition.x, handPosition.y, handPosition.z)); + yield return leftHand.MoveTo(new Vector3(-handPosition.x, handPosition.y, handPosition.z)); + + // Recalculate the new grab centroid. + var leftGrabPoint = om.interactorsSelecting[0].transform.position; + var rightGrabPoint = om.interactorsSelecting[0].transform.position; + var centroid = (leftGrabPoint + rightGrabPoint) / 2.0f; + + // Compute delta between original grab centroid and the new centroid. + var centroidDelta = centroid - originalGrabCentroid; + + // Ensure grab consistency. + TestUtilities.AssertAboutEqual(testObject.transform.position, originalObjectPosition + centroidDelta, + $"Object moved did not move according to the delta! Actual position: {testObject.transform.position:F5}, should be {originalObjectPosition + centroidDelta}", 0.00001f); + } + */ + + #endregion + + #region Physics Interaction Tests + + /// + /// Test that objects with both NewObjectManipulator and Rigidbody respond + /// correctly to static colliders. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorStaticCollision() + { + InputTestUtilities.InitializeCameraToOriginAndForward(); + + // set up cube with manipulation handler + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.5f; + testObject.transform.position = InputTestUtilities.InFrontOfUser(1f); + + var rigidbody = testObject.AddComponent(); + rigidbody.useGravity = false; + + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + var collisionListener = testObject.AddComponent(); + + // set up static cube to collide with + var backgroundObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + backgroundObject.transform.localScale = Vector3.one; + backgroundObject.transform.position = InputTestUtilities.InFrontOfUser(2f); + backgroundObject.GetComponent().material.color = Color.green; + + TestHand hand = new TestHand(Handedness.Right); + yield return hand.Show(testObject.transform.position); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Grab the cube and move towards the collider + yield return hand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + yield return hand.Move(Vector3.forward * 3f); + yield return RuntimeTestUtilities.WaitForFixedUpdates(); + + Assert.Less(testObject.transform.position.z, backgroundObject.transform.position.z); + Assert.AreEqual(1, collisionListener.CollisionCount); + } + + /// + /// Test that objects with both NewObjectManipulator and Rigidbody respond + /// correctly to rigidbody colliders. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorRigidbodyCollision() + { + InputTestUtilities.InitializeCameraToOriginAndForward(); + + // set up cube with manipulation handler + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.5f; + testObject.transform.position = InputTestUtilities.InFrontOfUser(1f); + + var rigidbody = testObject.AddComponent(); + rigidbody.useGravity = false; + + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + var collisionListener = testObject.AddComponent(); + + // set up static cube to collide with + var backgroundObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + backgroundObject.transform.localScale = Vector3.one; + backgroundObject.transform.position = InputTestUtilities.InFrontOfUser(2f); + backgroundObject.GetComponent().material.color = Color.green; + var backgroundRigidbody = backgroundObject.AddComponent(); + backgroundRigidbody.useGravity = false; + + TestHand hand = new TestHand(Handedness.Right); + yield return hand.Show(testObject.transform.position); + yield return RuntimeTestUtilities.WaitForUpdates(); + + // Grab the cube and move towards the collider + yield return hand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + + yield return hand.Move(Vector3.forward * 3f); + yield return RuntimeTestUtilities.WaitForFixedUpdates(); + + Assert.AreNotEqual(Vector3.zero, backgroundRigidbody.velocity); + Assert.AreEqual(1, collisionListener.CollisionCount); + } + + class TestCollisionListener : MonoBehaviour + { + public int CollisionCount { get; private set; } + + private void OnCollisionEnter(Collision collision) + { + CollisionCount++; + } + } + + #endregion + + /************** To be added in the future ***************** + /// + /// Test validates throw behavior on manipulation handler. Box with disabled gravity should travel a + /// certain distance when being released from grab during hand movement. Specifically for near interactions, + /// where we expect the thrown object to match the controllers velocities. + /// + [UnityTest] + public IEnumerator NewObjectManipulatorNearThrow() + { + // set up cube with manipulation handler + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.2f; + Vector3 initialObjectPosition = new Vector3(0f, 0f, 1f); + testObject.transform.position = initialObjectPosition; + + var rigidBody = testObject.AddComponent(); + rigidBody.useGravity = false; + + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.HostTransform = testObject.transform; + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + yield return new WaitForFixedUpdate(); + yield return null; + + TestHand hand = new TestHand(Handedness.Right); + + Vector3 handOffset = new Vector3(0, 0, 0.1f); + Vector3 initialHandPosition = new Vector3(0, 0, 0.5f); + Vector3 rightPosition = new Vector3(1f, 0f, 1f); + + yield return hand.Show(initialHandPosition); + yield return hand.MoveTo(initialObjectPosition); + yield return RuntimeInputTestUtils.WaitForEnterKey(); + + + // Note: don't wait for a physics update after releasing, because it would recompute + // the velocity of the hand and make it deviate from the rigid body velocity! + yield return hand.GrabAndThrowAt(rightPosition, false); + + // yield return RuntimeInputTestUtils.WaitForEnterKey(); + + + // With simulated hand angular velocity would not be equal to 0, because of how simulation + // moves hand when releasing the Pitch. Even though it doesn't directly follow from hand movement, there will always be some rotation. + // Assert.NotZero(rigidBody.angularVelocity.magnitude, "NewObjectManipulator should apply angular velocity to rigidBody upon release."); + Assert.AreEqual(hand.GetVelocity(), rigidBody.velocity, "NewObjectManipulator should apply hand velocity to rigidBody upon release."); + + // This is just for debugging purposes, so object's movement after release can be seen. + yield return hand.MoveTo(initialHandPosition); + yield return hand.Hide(); + + GameObject.Destroy(testObject); + yield return null; + } + + + /// + /// Test validates throw behavior on manipulation handler. Box with disabled gravity should travel a + /// certain distance when being released from grab during hand movement. Specifically for far interactions, + /// where we expect the thrown object to maintain it's velocities after being thrown + /// + [UnityTest] + public IEnumerator NewObjectManipulatorFarThrow() + { + // set up cube with manipulation handler + var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); + testObject.transform.localScale = Vector3.one * 0.2f; + Vector3 initialObjectPosition = new Vector3(-0.637f, -0.679f, 1.13f); + testObject.transform.position = initialObjectPosition; + + var rigidBody = testObject.AddComponent(); + rigidBody.useGravity = false; + + var NewObjectManipulator = testObject.AddComponent(); + NewObjectManipulator.HostTransform = testObject.transform; + NewObjectManipulator.SmoothingFar = false; + NewObjectManipulator.SmoothingNear = false; + + yield return new WaitForFixedUpdate(); + yield return null; + + TestHand hand = new TestHand(Handedness.Right); + + Vector3 handOffset = new Vector3(0, 0, 0.1f); + Vector3 initialHandPosition = Vector3.zero; + Vector3 rightPosition = new Vector3(1f, 0f, 1f); + + yield return hand.Show(initialHandPosition); + yield return RuntimeInputTestUtils.WaitForEnterKey(); + // Note: don't wait for a physics update after releasing, because it would recompute + // the velocity of the hand and make it deviate from the rigid body velocity! + yield return hand.GrabAndThrowAt(rightPosition, false); + + // With simulated hand angular velocity would not be equal to 0, because of how simulation + // moves hand when releasing the Pitch. Even though it doesn't directly follow from hand movement, there will always be some rotation. + Assert.Zero(rigidBody.angularVelocity.magnitude, "Object should have maintained its angular velocity of zero being released."); + + // Assert.IsTrue(rigidBody.velocity != hand.GetVelocity() && rigidBody.velocity.magnitude > 0.0f, "NewObjectManipulator should not dampen it's velocity to match the hand's upon release."); + yield return RuntimeInputTestUtils.WaitForEnterKey(); + // This is just for debugging purposes, so object's movement after release can be seen. + yield return hand.MoveTo(initialHandPosition); + yield return hand.Hide(); + + GameObject.Destroy(testObject); + yield return null; + } + */ + } +} + diff --git a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs.meta b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs.meta new file mode 100644 index 00000000000..87e7c3231bd --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 6cdc0f18b18695a43a9760457e9bc2ea +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From 68a4bc65edc8e9264fbd1a5916447d7c51fd725c Mon Sep 17 00:00:00 2001 From: RogPodge Date: Mon, 26 Dec 2022 21:27:48 -0800 Subject: [PATCH 02/10] fixed bugged object manipulator test --- .../Tests/Runtime/ObjectManipulatorTests.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/ObjectManipulatorTests.cs b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/ObjectManipulatorTests.cs index aca37fd1680..4f5f144f511 100644 --- a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/ObjectManipulatorTests.cs +++ b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/ObjectManipulatorTests.cs @@ -124,7 +124,10 @@ public IEnumerator TestObjManipSmoothingDrift() yield return RuntimeTestUtilities.WaitForUpdates(); ObjectManipulator objManip = cube.GetComponent(); - objManip.SmoothingNear = false; + + // Enable smoothing for near interaction. + objManip.SmoothingNear = true; + objManip.AllowedManipulations = TransformFlags.Move; var rightHand = new TestHand(Handedness.Right); yield return rightHand.Show(InputTestUtilities.InFrontOfUser(0.5f)); @@ -137,12 +140,10 @@ public IEnumerator TestObjManipSmoothingDrift() yield return rightHand.SetHandshape(HandshapeId.Pinch); yield return RuntimeTestUtilities.WaitForUpdates(); + yield return new WaitForSeconds(2.0f); Assert.IsTrue(objManip.IsGrabSelected, "ObjManip didn't report IsGrabSelected"); - // Enable smoothing for near interaction. - objManip.SmoothingNear = true; - // Move the hand to the right. Vector3 originalPosition = cube.transform.position; Vector3 attachTransform = objManip.firstInteractorSelecting.GetAttachTransform(objManip).position; @@ -168,6 +169,12 @@ public IEnumerator TestObjManipSmoothingDrift() // Disable smoothing, to check that it properly sticks to the hand once disabled. objManip.SmoothingNear = false; + yield return rightHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + originalAttachOffset = attachTransform - cube.transform.position; + newPosition = originalPosition - Vector3.right * 1.5f; yield return rightHand.MoveTo(newPosition); yield return RuntimeTestUtilities.WaitForUpdates(); From c18c3d530f6d6cc8f942b414d8448ee2a92c004d Mon Sep 17 00:00:00 2001 From: RogPodge Date: Mon, 26 Dec 2022 21:28:18 -0800 Subject: [PATCH 03/10] fixing NewObjectManipulator and tests to pass --- .../ObjectManipulator/NewObjectManipulator.cs | 2 -- .../PlacementHub.cs | 9 +++++++ .../Runtime/NewObjectManipulatorTests.cs | 24 ++++++++++--------- 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs index 4b37159a6e0..95d17bbf1bb 100644 --- a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs @@ -438,7 +438,6 @@ public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase up { rotateManipulation.interactorsSelecting = interactorsSelecting; rotateManipulation.useCenteredAnchor = useCenteredAnchor; - //targetTransform.Rotation = ManipulationLogic.rotateLogic.Update(interactorsSelecting, this, targetTransform, useCenteredAnchor); } } @@ -448,7 +447,6 @@ public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase up { moveManipulation.interactorsSelecting = interactorsSelecting; moveManipulation.useCenteredAnchor = useCenteredAnchor; - //targetTransform.Position = ManipulationLogic.moveLogic.Update(interactorsSelecting, this, targetTransform, useCenteredAnchor); } } } diff --git a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs index 8cffedc0c06..3b5489b73f0 100644 --- a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs +++ b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs @@ -58,6 +58,15 @@ void Update() [SerializeField] private bool useForces; + + // A little unsure of how the placement hub should manage UseForces, since + // its rigidbody can be set to IsKinematic during manipulation. + internal bool UseForces + { + get { return useForces; } + set { useForces = value; } + } + private Rigidbody rigidBody => transform.GetComponent(); private void FixedUpdate() diff --git a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs index cfe2b2da303..7c42802e918 100644 --- a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs +++ b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs @@ -25,7 +25,7 @@ public class NewObjectManipulatorTests : BaseRuntimeInputTests /// respects the various interactor filtering/interaction type rules. /// [UnityTest] - public IEnumerator TestObjManipInteractorRules() + public IEnumerator TestNewObjectManipulatorInteractorRules() { GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); cube.AddComponent(); @@ -34,9 +34,6 @@ public IEnumerator TestObjManipInteractorRules() yield return RuntimeTestUtilities.WaitForUpdates(); - // Verify that a ConstraintManager was automatically added. - Assert.IsTrue(cube.GetComponent() != null, "Runtime-spawned ObjManip didn't also spawn ConstraintManager"); - NewObjectManipulator objManip = cube.GetComponent(); Assert.IsTrue(objManip.AllowedManipulations == (TransformFlags.Move | TransformFlags.Rotate | TransformFlags.Scale), @@ -113,7 +110,7 @@ public IEnumerator TestObjManipInteractorRules() /// Verifies that an NewObjectManipulator created at runtime has proper smoothing characteristics. /// [UnityTest] - public IEnumerator TestObjManipSmoothingDrift() + public IEnumerator TestNewObjectManipulatorSmoothingDrift() { GameObject cube = GameObject.CreatePrimitive(PrimitiveType.Cube); NewObjectManipulator objManip = cube.AddComponent(); @@ -136,6 +133,7 @@ public IEnumerator TestObjManipSmoothingDrift() yield return rightHand.SetHandshape(HandshapeId.Pinch); yield return RuntimeTestUtilities.WaitForUpdates(); + yield return new WaitForSeconds(2.0f); Assert.IsTrue(objManip.IsGrabSelected, "ObjManip didn't report IsGrabSelected"); @@ -158,14 +156,18 @@ public IEnumerator TestObjManipSmoothingDrift() yield return new WaitForSeconds(6.0f); attachTransform = objManip.firstInteractorSelecting.GetAttachTransform(objManip).position; attachOffset = attachTransform - cube.transform.position; - Debug.Log(attachTransform.ToString("F3")); - Debug.Log(originalAttachOffset.ToString("F3")); Assert.IsTrue((attachOffset - originalAttachOffset).magnitude < 0.001f, "Cube didn't catch up with the hand after waiting for a bit. Magnitude: " + (attachOffset - originalAttachOffset).magnitude.ToString("F4")); // Disable smoothing, to check that it properly sticks to the hand once disabled. objManip.SmoothingNear = false; + yield return rightHand.SetHandshape(HandshapeId.Open); + yield return RuntimeTestUtilities.WaitForUpdates(); + yield return rightHand.SetHandshape(HandshapeId.Pinch); + yield return RuntimeTestUtilities.WaitForUpdates(); + originalAttachOffset = attachTransform - cube.transform.position; + newPosition = originalPosition - Vector3.right * 1.5f; yield return rightHand.MoveTo(newPosition); yield return RuntimeTestUtilities.WaitForUpdates(); @@ -187,12 +189,12 @@ public IEnumerator NewObjectManipulatorInstantiate() var testObject = GameObject.CreatePrimitive(PrimitiveType.Cube); testObject.transform.localScale = Vector3.one * 0.2f; - var NewObjectManipulator = testObject.AddComponent(); + testObject.AddComponent(); // Wait for two frames to make sure we don't get null pointer exception. yield return null; yield return null; - GameObject.Destroy(testObject); + UnityEngine.Object.Destroy(testObject); // Wait for a frame to give Unity a change to actually destroy the object yield return null; } @@ -798,6 +800,8 @@ public IEnumerator NewObjectManipulatorStaticCollision() var NewObjectManipulator = testObject.AddComponent(); NewObjectManipulator.SmoothingFar = false; NewObjectManipulator.SmoothingNear = false; + var placementHub = testObject.GetComponent(); + placementHub.UseForces = true; var collisionListener = testObject.AddComponent(); @@ -814,10 +818,8 @@ public IEnumerator NewObjectManipulatorStaticCollision() // Grab the cube and move towards the collider yield return hand.SetHandshape(HandshapeId.Pinch); yield return RuntimeTestUtilities.WaitForUpdates(); - yield return hand.Move(Vector3.forward * 3f); yield return RuntimeTestUtilities.WaitForFixedUpdates(); - Assert.Less(testObject.transform.position.z, backgroundObject.transform.position.z); Assert.AreEqual(1, collisionListener.CollisionCount); } From d6346bbed3f2ba935c4e8c40af7f3a9e4b385a1d Mon Sep 17 00:00:00 2001 From: RogPodge Date: Tue, 27 Dec 2022 19:53:33 -0800 Subject: [PATCH 04/10] Fixing unit tests --- .../Tests/Runtime/NewObjectManipulatorTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs index 7c42802e918..ced53930021 100644 --- a/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs +++ b/com.microsoft.mrtk.spatialmanipulation/Tests/Runtime/NewObjectManipulatorTests.cs @@ -800,6 +800,7 @@ public IEnumerator NewObjectManipulatorStaticCollision() var NewObjectManipulator = testObject.AddComponent(); NewObjectManipulator.SmoothingFar = false; NewObjectManipulator.SmoothingNear = false; + var placementHub = testObject.GetComponent(); placementHub.UseForces = true; @@ -845,6 +846,9 @@ public IEnumerator NewObjectManipulatorRigidbodyCollision() NewObjectManipulator.SmoothingFar = false; NewObjectManipulator.SmoothingNear = false; + var placementHub = testObject.GetComponent(); + placementHub.UseForces = true; + var collisionListener = testObject.AddComponent(); // set up static cube to collide with @@ -864,7 +868,7 @@ public IEnumerator NewObjectManipulatorRigidbodyCollision() yield return RuntimeTestUtilities.WaitForUpdates(); yield return hand.Move(Vector3.forward * 3f); - yield return RuntimeTestUtilities.WaitForFixedUpdates(); + yield return new WaitForSeconds(1.0f); Assert.AreNotEqual(Vector3.zero, backgroundRigidbody.velocity); Assert.AreEqual(1, collisionListener.CollisionCount); From 5860c89f8dc2d1c2e2eab68bdbf02735d55c5cbc Mon Sep 17 00:00:00 2001 From: RogPodge <39840334+RogPodge@users.noreply.github.com> Date: Wed, 4 Jan 2023 10:21:38 -0800 Subject: [PATCH 05/10] Update com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs Co-authored-by: Kurtis --- .../ObjectManipulator/ObjectManipulator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs index 052ed77e9fa..0e864c80537 100644 --- a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/ObjectManipulator.cs @@ -922,7 +922,7 @@ controllerInteractor.xrController is ActionBasedController abController && } } - #region ReleaseBehaviorEnum Extenions + #region ReleaseBehaviorEnum Extensions /// /// Extension methods specific to the enum. /// From 6c90dc79f4df10e3d68857918a8e411fff9d77e5 Mon Sep 17 00:00:00 2001 From: RogPodge Date: Wed, 4 Jan 2023 12:24:30 -0800 Subject: [PATCH 06/10] addressing PR asks --- .../ObjectManipulator/NewObjectManipulator.cs | 12 ++--- .../PlacementHub.cs | 53 ++++--------------- .../Transformations.meta | 8 +++ .../Transformations/ITransformation.cs | 18 +++++++ .../Transformations/ITransformation.cs.meta | 11 ++++ .../MinMaxConstraintTransformation.cs | 30 +++++++++++ .../MinMaxConstraintTransformation.cs.meta | 11 ++++ 7 files changed, 94 insertions(+), 49 deletions(-) create mode 100644 com.microsoft.mrtk.spatialmanipulation/Transformations.meta create mode 100644 com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs create mode 100644 com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs.meta create mode 100644 com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs create mode 100644 com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs.meta diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs index 95d17bbf1bb..c973c9d7690 100644 --- a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs @@ -364,9 +364,9 @@ protected override void OnSelectEntered(SelectEnterEventArgs args) rotateManipulation.logic.Setup(interactorsSelecting, this, initialTransform); scaleManipulation.logic.Setup(interactorsSelecting, this, initialTransform); - placementHub.transformations.Add(scaleManipulation); - placementHub.transformations.Add(rotateManipulation); - placementHub.transformations.Add(moveManipulation); + placementHub.Transformations.Add(scaleManipulation); + placementHub.Transformations.Add(rotateManipulation); + placementHub.Transformations.Add(moveManipulation); } } @@ -389,9 +389,9 @@ protected override void OnSelectExited(SelectExitEventArgs args) ReleaseRigidBody(rigidBody.velocity, rigidBody.angularVelocity); } - placementHub.transformations.Remove(moveManipulation); - placementHub.transformations.Remove(rotateManipulation); - placementHub.transformations.Remove(scaleManipulation); + placementHub.Transformations.Remove(moveManipulation); + placementHub.Transformations.Remove(rotateManipulation); + placementHub.Transformations.Remove(scaleManipulation); } } diff --git a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs index 3b5489b73f0..575717e7085 100644 --- a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs +++ b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs @@ -1,7 +1,7 @@ -using Microsoft.MixedReality.Toolkit; -using Microsoft.MixedReality.Toolkit.SpatialManipulation; +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + using System; -using System.Collections; using System.Collections.Generic; using UnityEngine; @@ -11,9 +11,14 @@ public class PlacementHub : MonoBehaviour { [SerializeReference] [InterfaceSelector] - public List transformations =new List(); + private List transformations = new List(); - (Vector3, Quaternion, Vector3) targetPose; + internal List Transformations => transformations; + + /// + /// The target pose + /// + private (Vector3, Quaternion, Vector3) targetPose; // Make this it's own class so we can potentially allow for alternate implementations which "blend" transformations or // use a transformation's execution order differently. @@ -367,41 +372,3 @@ private void ApplyForcesToRigidbody() #endregion } } - -/// -/// Interface which describes a transformation that is applied to MixedRealityTransform -/// -public interface ITransformation -{ - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale); - - /// - /// Execution order priority of this constraint. Lower numbers will be executed before higher numbers. - /// - public int ExecutionPriority { get; } -} - -/// -/// A transformation which restricts the scale of of the MixedRealityTransform -/// -public class MinMaxConstraintTransformation: ITransformation -{ - public Vector3 minScale = Vector3.one * 0.2f; - - public Vector3 maxScale = Vector3.one * 2.0f; - - protected const int scale_priority = -1000; - protected const int rotation_priority = -1000; - protected const int position_priority = -1000; - protected const int constraint_priority_modifier = 1; - - public int ExecutionPriority => throw new NotImplementedException(); - - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) - { - initialLocalScale.x = Mathf.Clamp(initialLocalScale.x, minScale.x, maxScale.x); - initialLocalScale.y = Mathf.Clamp(initialLocalScale.y, minScale.y, maxScale.y); - initialLocalScale.z = Mathf.Clamp(initialLocalScale.z, minScale.z, maxScale.z); - return (initialPosition, initialRotation, initialLocalScale); - } -} diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations.meta b/com.microsoft.mrtk.spatialmanipulation/Transformations.meta new file mode 100644 index 00000000000..c99ea9a5e63 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: a87b6c4bbf39bc744b68022807cb8e33 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs b/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs new file mode 100644 index 00000000000..b6573c3a0d0 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs @@ -0,0 +1,18 @@ +using System; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.SpatialManipulation +{ + /// + /// Interface which describes a transformation that is applied to MixedRealityTransform + /// + public interface ITransformation + { + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale); + + /// + /// Execution order priority of this constraint. Lower numbers will be executed before higher numbers. + /// + public int ExecutionPriority { get; } + } +} diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs.meta b/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs.meta new file mode 100644 index 00000000000..dc340e77b9e --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 35919b876a9374741bf9babaede7ffdd +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs b/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs new file mode 100644 index 00000000000..2e306fe4708 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs @@ -0,0 +1,30 @@ +using System; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.SpatialManipulation +{ + /// + /// A transformation which restricts the scale of of the MixedRealityTransform + /// + public class MinMaxConstraintTransformation : ITransformation + { + public Vector3 minScale = Vector3.one * 0.2f; + + public Vector3 maxScale = Vector3.one * 2.0f; + + protected const int scale_priority = -1000; + protected const int rotation_priority = -1000; + protected const int position_priority = -1000; + protected const int constraint_priority_modifier = 1; + + public int ExecutionPriority => throw new NotImplementedException(); + + public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + { + initialLocalScale.x = Mathf.Clamp(initialLocalScale.x, minScale.x, maxScale.x); + initialLocalScale.y = Mathf.Clamp(initialLocalScale.y, minScale.y, maxScale.y); + initialLocalScale.z = Mathf.Clamp(initialLocalScale.z, minScale.z, maxScale.z); + return (initialPosition, initialRotation, initialLocalScale); + } + } +} diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs.meta b/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs.meta new file mode 100644 index 00000000000..0b4f5d6316d --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdde46f9660b4d744936a19cb3fc28a9 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From c4a12ae354662784809a3b8f7d2de48314cbd1c2 Mon Sep 17 00:00:00 2001 From: Roger Liu Date: Mon, 9 Jan 2023 14:37:08 -0800 Subject: [PATCH 07/10] Refactoring the placementhub to improve usability --- .../PropertyDrawers/DrawIfPropertyDrawer.cs | 4 +- .../PlacementHub.cs | 181 ++++++++++-------- 2 files changed, 104 insertions(+), 81 deletions(-) diff --git a/com.microsoft.mrtk.core/Editor/Inspectors/PropertyDrawers/DrawIfPropertyDrawer.cs b/com.microsoft.mrtk.core/Editor/Inspectors/PropertyDrawers/DrawIfPropertyDrawer.cs index 90ab04d5321..3aa273df17c 100644 --- a/com.microsoft.mrtk.core/Editor/Inspectors/PropertyDrawers/DrawIfPropertyDrawer.cs +++ b/com.microsoft.mrtk.core/Editor/Inspectors/PropertyDrawers/DrawIfPropertyDrawer.cs @@ -21,7 +21,7 @@ public override void OnGUI(Rect position, SerializedProperty property, GUIConten { if (ShouldShow(property)) { - EditorGUI.PropertyField(position, property, label); + EditorGUI.PropertyField(position, property, label, true); } } @@ -33,7 +33,7 @@ public override float GetPropertyHeight(SerializedProperty property, GUIContent return 0f; } - return base.GetPropertyHeight(property, label); + return EditorGUI.GetPropertyHeight(property, true); } private bool ShouldShow(SerializedProperty property) diff --git a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs index 575717e7085..95292fadb67 100644 --- a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs +++ b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.SpatialManipulation @@ -61,22 +62,11 @@ void Update() public MixedRealityTransform targetTransform; public bool isManipulated; - [SerializeField] - private bool useForces; - - // A little unsure of how the placement hub should manage UseForces, since - // its rigidbody can be set to IsKinematic during manipulation. - internal bool UseForces - { - get { return useForces; } - set { useForces = value; } - } - private Rigidbody rigidBody => transform.GetComponent(); private void FixedUpdate() { - if (useForces && rigidBody != null && GetTargetTransform()) + if (UseForces && rigidBody != null && GetTargetTransform()) { ApplyForcesToRigidbody(); } @@ -103,7 +93,7 @@ private void ApplyTargetTransform() else { // There is a Rigidbody. Potential different paths for near vs far manipulation - if (!useForces) + if (!UseForces) { rigidBody.MovePosition(targetTransform.Position); rigidBody.MoveRotation(targetTransform.Rotation); @@ -123,50 +113,60 @@ public bool UseSmoothing set { useSmoothing = value; } } - [SerializeReference, InterfaceSelector(false)] - [DrawIf("shouldSmooth")] - private ITransformSmoothingLogic smoothingLogic = new DefaultTransformSmoothingLogic(); - [SerializeField] - [Range(0, 1)] - [Tooltip("Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value.")] - private float moveLerpTime = 0.001f; + [DrawIf("useSmoothing")] + private SmoothingSettings smoothingSettings; + + [Serializable] + private struct SmoothingSettings + { + [SerializeReference, InterfaceSelector(false)] + internal ITransformSmoothingLogic smoothingLogic; + + [SerializeField] + [Range(0, 1)] + [DefaultValue("0.001f")] + [Tooltip("Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value.")] + internal float moveLerpTime; + + [SerializeField] + [Range(0, 1)] + [DefaultValue("0.001f")] + [Tooltip("Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value.")] + internal float rotateLerpTime; + + [SerializeField] + [Range(0, 1)] + [DefaultValue("0.001f")] + [Tooltip("Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value.")] + internal float scaleLerpTime; + } /// /// Enter amount representing amount of smoothing to apply to the movement. Smoothing of 0 means no smoothing. Max value means no change to value. /// public float MoveLerpTime { - get => moveLerpTime; - set => moveLerpTime = value; + get => smoothingSettings.moveLerpTime; + set => smoothingSettings.moveLerpTime = value; } - [SerializeField] - [Range(0, 1)] - [Tooltip("Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value.")] - private float rotateLerpTime = 0.001f; - /// /// Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value. /// public float RotateLerpTime { - get => rotateLerpTime; - set => rotateLerpTime = value; + get => smoothingSettings.rotateLerpTime; + set => smoothingSettings.rotateLerpTime = value; } - [SerializeField] - [Range(0, 1)] - [Tooltip("Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value.")] - private float scaleLerpTime = 0.001f; - /// /// Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value. /// public float ScaleLerpTime { - get => scaleLerpTime; - set => scaleLerpTime = value; + get => smoothingSettings.scaleLerpTime; + set => smoothingSettings.scaleLerpTime = value; } /// @@ -184,84 +184,107 @@ protected virtual void SmoothTargetPose(ref MixedRealityTransform targetPose, re { // TODO: Elastics. Compute elastics here and apply to modifiedTransformFlags. - bool applySmoothing = UseSmoothing && smoothingLogic != null; + bool applySmoothing = UseSmoothing && smoothingSettings.smoothingLogic != null; - targetPose.Position = (applySmoothing && !useForces) ? smoothingLogic.SmoothPosition(transform.position, targetPose.Position, moveLerpTime, Time.deltaTime) : targetPose.Position; - targetPose.Rotation = (applySmoothing && !useForces) ? smoothingLogic.SmoothRotation(transform.rotation, targetPose.Rotation, rotateLerpTime, Time.deltaTime) : targetPose.Rotation; - targetPose.Scale = applySmoothing ? smoothingLogic.SmoothScale(transform.localScale, targetPose.Scale, scaleLerpTime, Time.deltaTime) : targetPose.Scale; + targetPose.Position = (applySmoothing && !UseForces) ? smoothingSettings.smoothingLogic.SmoothPosition(transform.position, targetPose.Position, MoveLerpTime, Time.deltaTime) : targetPose.Position; + targetPose.Rotation = (applySmoothing && !UseForces) ? smoothingSettings.smoothingLogic.SmoothRotation(transform.rotation, targetPose.Rotation, RotateLerpTime, Time.deltaTime) : targetPose.Rotation; + targetPose.Scale = applySmoothing ? smoothingSettings.smoothingLogic.SmoothScale(transform.localScale, targetPose.Scale, ScaleLerpTime, Time.deltaTime) : targetPose.Scale; } #endregion #region rigidbodies [SerializeField] - [Tooltip( - "Apply torque to control orientation of the body")] - private bool applyTorque = true; + private bool useForces; - /// - /// Apply torque to control orientation of the body - /// - public bool ApplyTorque + // A little unsure of how the placement hub should manage UseForces, since + // its rigidbody can be set to IsKinematic during manipulation. + internal bool UseForces { - get => applyTorque; - set => applyTorque = value; + get { return useForces; } + set { useForces = value; } } [SerializeField] - [Range(0.001f, 2.0f)] - [Tooltip("The time scale at which a Rigidbody reacts to input movement defined as oscillation period of the dampened spring force.")] - private float springForceSoftness = 0.1f; + [DrawIf("useForces")] + private PhysicsSettings physicsSettings; + + [Serializable] + private struct PhysicsSettings + { + [SerializeField] + [Range(0.001f, 2.0f)] + [DefaultValue("0.1f")] + [Tooltip("The time scale at which a Rigidbody reacts to input movement defined as oscillation period of the dampened spring force.")] + internal float springForceSoftness; + + [SerializeField] + [DefaultValue(true)] + [Tooltip("Apply torque to control orientation of the body")] + internal bool applyTorque; + + [SerializeField] + [Range(0.001f, 2.0f)] + [DefaultValue(0.1f)] + [Tooltip("The time scale at which a Rigidbody reacts to input rotation defined as oscillation period of the dampened spring torque.")] + internal float springTorqueSoftness; + + [SerializeField] + [Range(0, 2.0f)] + [DefaultValue(1.0f)] + [Tooltip("The damping of the spring force&torque: 1.0f corresponds to critical damping, lower values lead to underdamping (i.e. oscillation).")] + internal float springDamping; + + [SerializeField] + [Range(0, 10000f)] + [DefaultValue(100.0f)] + [Tooltip("The maximum acceleration applied by the spring force to avoid trembling when pushing a body against a static object.")] + internal float springForceLimit; + } /// /// The time scale at which a Rigidbody reacts to input movement defined as oscillation period of the dampened spring force. /// public float SpringForceSoftness { - get => springForceSoftness; - set => springForceSoftness = value; + get => physicsSettings.springForceSoftness; + set => physicsSettings.springForceSoftness = value; } - [SerializeField] - [Range(0.001f, 2.0f)] - [Tooltip("The time scale at which a Rigidbody reacts to input rotation defined as oscillation period of the dampened spring torque.")] - private float springTorqueSoftness = 0.1f; + /// + /// Apply torque to control orientation of the body + /// + public bool ApplyTorque + { + get => physicsSettings.applyTorque; + set => physicsSettings.applyTorque = value; + } /// /// The time scale at which a Rigidbody reacts to input rotation defined as oscillation period of the dampened angular spring force. /// public float SpringTorqueSoftness { - get => springTorqueSoftness; - set => springTorqueSoftness = value; + get => physicsSettings.springTorqueSoftness; + set => physicsSettings.springTorqueSoftness = value; } - [SerializeField] - [Range(0, 2.0f)] - [Tooltip("The damping of the spring force&torque: 1.0f corresponds to critical damping, lower values lead to underdamping (i.e. oscillation).")] - private float springDamping = 1.0f; - /// /// The damping of the spring force&torque: 1.0f corresponds to critical damping, lower values lead to underdamping (i.e. oscillation). /// public float SpringDamping { - get => springDamping; - set => springDamping = value; + get => physicsSettings.springDamping; + set => physicsSettings.springDamping = value; } - [SerializeField] - [Range(0, 10000f)] - [Tooltip("The maximum acceleration applied by the spring force to avoid trembling when pushing a body against a static object.")] - private float springForceLimit = 100.0f; - /// /// The maximum acceleration applied by the spring force to avoid trembling when pushing a body against a static object. /// public float SpringForceLimit { - get => springForceLimit; - set => springForceLimit = value; + get => physicsSettings.springForceLimit; + set => physicsSettings.springForceLimit = value; } /// @@ -297,7 +320,7 @@ private void ApplyForcesToRigidbody() } // implement critically dampened spring force, scaled to mass-independent frequency - float omega = Mathf.PI / springForceSoftness; // angular frequency, sqrt(k/m) + float omega = Mathf.PI / SpringForceSoftness; // angular frequency, sqrt(k/m) Vector3 distance = transform.position - targetTransform.Position; @@ -312,7 +335,7 @@ private void ApplyForcesToRigidbody() // apply springForceLimit only for slow-moving body (e.g. pressed against wall) // when body is already moving fast, also allow strong acceleration - var maxAcceleration = Mathf.Max(springForceLimit, 10 * velocity.magnitude / Time.fixedDeltaTime); + var maxAcceleration = Mathf.Max(SpringForceLimit, 10 * velocity.magnitude / Time.fixedDeltaTime); if (accelerationMagnitude > maxAcceleration) { @@ -323,7 +346,7 @@ private void ApplyForcesToRigidbody() // To compensate for the finite time step, this is split in two equal factors, // one applied before, the other after the spring force // equivalent with applying damping as well as spring force continuously - float halfDampingFactor = Mathf.Exp(-springDamping * omega * Time.fixedDeltaTime); + float halfDampingFactor = Mathf.Exp(-SpringDamping * omega * Time.fixedDeltaTime); velocity -= referenceFrameVelocity; // change to the player's frame of reference before damping @@ -335,13 +358,13 @@ private void ApplyForcesToRigidbody() rigidBody.velocity = velocity; - if (applyTorque) + if (ApplyTorque) { // Torque calculations: same calculation & parameters as for linear velocity // skipping referenceFrameVelocity and springForceLimit which do not exactly apply here // implement critically dampened spring force, scaled to mass-independent frequency - float angularOmega = Mathf.PI / springTorqueSoftness; // angular frequency, sqrt(k/m) + float angularOmega = Mathf.PI / SpringTorqueSoftness; // angular frequency, sqrt(k/m) var angularDistance = transform.rotation * Quaternion.Inverse(targetTransform.Rotation); angularDistance.ToAngleAxis(out float angle, out Vector3 axis); From ec8d0a28a12049e0379ab3a4e7d934bdd44edf31 Mon Sep 17 00:00:00 2001 From: RogPodge Date: Tue, 10 Jan 2023 11:30:38 -0800 Subject: [PATCH 08/10] Adjusted ITransformation to use MixedRealityTransform --- .../ObjectManipulator/NewObjectManipulator.cs | 27 +++++++++---------- .../PlacementHub.cs | 13 +++------ .../Transformations/ITransformation.cs | 2 +- .../MinMaxConstraintTransformation.cs | 15 +++++++---- 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs index c973c9d7690..87f457a191f 100644 --- a/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs +++ b/com.microsoft.mrtk.spatialmanipulation/ObjectManipulator/NewObjectManipulator.cs @@ -503,7 +503,7 @@ controllerInteractor.xrController is ActionBasedController abController && } - public class MoveTransformation : ITransformation + protected class MoveTransformation : ITransformation { private IXRSelectInteractable interactable; @@ -518,15 +518,14 @@ public MoveTransformation(IXRSelectInteractable affectedInteractable) public int ExecutionPriority => throw new NotImplementedException(); - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + public MixedRealityTransform ApplyTransformation(MixedRealityTransform initialTransform) { - var targetTransform = new MixedRealityTransform(initialPosition, initialRotation, initialLocalScale); - Vector3 newPosition = logic.Update(interactorsSelecting, interactable, targetTransform, useCenteredAnchor); - return (newPosition, initialRotation, initialLocalScale); + initialTransform.Position = logic.Update(interactorsSelecting, interactable, initialTransform, useCenteredAnchor); + return initialTransform; } } - public class RotateTransformation : ITransformation + protected class RotateTransformation : ITransformation { private IXRSelectInteractable interactable; @@ -541,15 +540,14 @@ public RotateTransformation(IXRSelectInteractable affectedInteractable) public int ExecutionPriority => throw new NotImplementedException(); - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + public MixedRealityTransform ApplyTransformation(MixedRealityTransform initialTransform) { - var targetTransform = new MixedRealityTransform(initialPosition, initialRotation, initialLocalScale); - Quaternion newRotation = logic.Update(interactorsSelecting, interactable, targetTransform, useCenteredAnchor); - return (initialPosition, newRotation, initialLocalScale); + initialTransform.Rotation = logic.Update(interactorsSelecting, interactable, initialTransform, useCenteredAnchor); + return initialTransform; } } - public class ScaleTransformation : ITransformation + protected class ScaleTransformation : ITransformation { private IXRSelectInteractable interactable; @@ -564,11 +562,10 @@ public ScaleTransformation(IXRSelectInteractable affectedInteractable) public int ExecutionPriority => throw new NotImplementedException(); - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + public MixedRealityTransform ApplyTransformation(MixedRealityTransform initialTransform) { - var targetTransform = new MixedRealityTransform(initialPosition, initialRotation, initialLocalScale); - Vector3 newScale = logic.Update(interactorsSelecting, interactable, targetTransform, useCenteredAnchor); - return (initialPosition, initialRotation, newScale); + initialTransform.Scale = logic.Update(interactorsSelecting, interactable, initialTransform, useCenteredAnchor); + return initialTransform; } } } diff --git a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs index 95292fadb67..1e9d3c90bda 100644 --- a/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs +++ b/com.microsoft.mrtk.spatialmanipulation/PlacementHub.cs @@ -19,7 +19,7 @@ public class PlacementHub : MonoBehaviour /// /// The target pose /// - private (Vector3, Quaternion, Vector3) targetPose; + private MixedRealityTransform targetTransform; // Make this it's own class so we can potentially allow for alternate implementations which "blend" transformations or // use a transformation's execution order differently. @@ -30,17 +30,15 @@ private bool GetTargetTransform() return false; } - Vector3 currentPosition = transform.position; - Quaternion currentRotation = transform.rotation; - Vector3 currentLocalScale = transform.localScale; + MixedRealityTransform localTargetTransform = new MixedRealityTransform(transform); for (int i = 0; i < transformations.Count; i++) { var t = transformations[i]; - (currentPosition, currentRotation, currentLocalScale) = t.ApplyTransformation(currentPosition, currentRotation, currentLocalScale); + localTargetTransform = t.ApplyTransformation(localTargetTransform); } - targetPose = (currentPosition, currentRotation, currentLocalScale); + targetTransform = localTargetTransform; return true; } @@ -59,7 +57,6 @@ void Update() private Transform referenceFrameTransform = null; private bool referenceFrameHasLastPos = false; private Vector3 referenceFrameLastPos; - public MixedRealityTransform targetTransform; public bool isManipulated; private Rigidbody rigidBody => transform.GetComponent(); @@ -79,8 +76,6 @@ private void FixedUpdate() /// private void ApplyTargetTransform() { - targetTransform = new MixedRealityTransform(targetPose.Item1, targetPose.Item2, targetPose.Item3); - // modifiedTransformFlags currently unused. TransformFlags modifiedTransformFlags = TransformFlags.None; SmoothTargetPose(ref targetTransform, ref modifiedTransformFlags); diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs b/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs index b6573c3a0d0..d8300b02f1a 100644 --- a/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations/ITransformation.cs @@ -8,7 +8,7 @@ namespace Microsoft.MixedReality.Toolkit.SpatialManipulation /// public interface ITransformation { - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale); + public MixedRealityTransform ApplyTransformation(MixedRealityTransform initialTransform); /// /// Execution order priority of this constraint. Lower numbers will be executed before higher numbers. diff --git a/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs b/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs index 2e306fe4708..3a8044b64ff 100644 --- a/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs +++ b/com.microsoft.mrtk.spatialmanipulation/Transformations/MinMaxConstraintTransformation.cs @@ -19,12 +19,17 @@ public class MinMaxConstraintTransformation : ITransformation public int ExecutionPriority => throw new NotImplementedException(); - public (Vector3, Quaternion, Vector3) ApplyTransformation(Vector3 initialPosition, Quaternion initialRotation, Vector3 initialLocalScale) + public MixedRealityTransform ApplyTransformation(MixedRealityTransform initialTransform) { - initialLocalScale.x = Mathf.Clamp(initialLocalScale.x, minScale.x, maxScale.x); - initialLocalScale.y = Mathf.Clamp(initialLocalScale.y, minScale.y, maxScale.y); - initialLocalScale.z = Mathf.Clamp(initialLocalScale.z, minScale.z, maxScale.z); - return (initialPosition, initialRotation, initialLocalScale); + Vector3 newScale = new Vector3() + { + x = Mathf.Clamp(initialTransform.Scale.x, minScale.x, maxScale.x), + y = Mathf.Clamp(initialTransform.Scale.y, minScale.y, maxScale.y), + z = Mathf.Clamp(initialTransform.Scale.z, minScale.z, maxScale.z) + }; + + initialTransform.Scale = newScale; + return initialTransform; } } } From f49d7fbc4b3b6080103ceac4a98bcc4513131297 Mon Sep 17 00:00:00 2001 From: RogPodge Date: Tue, 10 Jan 2023 16:37:35 -0800 Subject: [PATCH 09/10] revamping the new Object Manipulator Editor --- .../Editor/BaseInteractableEditor.cs | 4 +- .../NewObjectManipulatorInspector.cs | 137 ++++++++++++++++++ .../NewObjectManipulatorInspector.cs.meta | 11 ++ 3 files changed, 150 insertions(+), 2 deletions(-) create mode 100644 com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs create mode 100644 com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs.meta diff --git a/com.microsoft.mrtk.core/Editor/BaseInteractableEditor.cs b/com.microsoft.mrtk.core/Editor/BaseInteractableEditor.cs index e32dd13e4ef..99fb44b65ab 100644 --- a/com.microsoft.mrtk.core/Editor/BaseInteractableEditor.cs +++ b/com.microsoft.mrtk.core/Editor/BaseInteractableEditor.cs @@ -80,6 +80,8 @@ protected override List GetDerivedSerializedPropertyNames() protected override void DrawProperties() { + EditorGUILayout.PropertyField(disabledInteractorTypes); + xriBaseFoldout = EditorGUILayout.Foldout(xriBaseFoldout, EditorGUIUtility.TrTempContent("Base XRI Settings"), true, EditorStyles.foldoutHeader); if (xriBaseFoldout) { @@ -92,8 +94,6 @@ protected override void DrawProperties() protected override void DrawInteractableEvents() { - EditorGUILayout.PropertyField(disabledInteractorTypes); - mrtkExpanded = EditorGUILayout.Foldout(mrtkExpanded, EditorGUIUtility.TrTempContent("MRTK Events"), true); if (mrtkExpanded) diff --git a/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs new file mode 100644 index 00000000000..1778d71b001 --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.MixedReality.Toolkit.Editor; +using UnityEditor; +using UnityEngine; + +namespace Microsoft.MixedReality.Toolkit.SpatialManipulation.Editor +{ + /// + /// A custom inspector for ObjectManipulator used to separate + /// ObjectManipulator options into distinct foldout panels. + /// + [CustomEditor(typeof(NewObjectManipulator))] + [CanEditMultipleObjects] + public class NewObjectManipulatorInspector : StatefulInteractableInspector + { + private NewObjectManipulator instance; + private SerializedProperty allowedManipulations; + private SerializedProperty rotationAnchorNear; + private SerializedProperty rotationAnchorFar; + private SerializedProperty manipulationLogicTypes; + private SerializedProperty selectMode; + + private SerializedProperty releaseBehavior; + + private SerializedProperty smoothingFar; + private SerializedProperty smoothingNear; + + // These alias to the XRI First/Last Select Entered/Exited events + private SerializedProperty manipulationStarted; + private SerializedProperty manipulationEnded; + + protected override void OnEnable() + { + base.OnEnable(); + instance = target as NewObjectManipulator; + allowedManipulations = SetUpProperty(nameof(allowedManipulations)); + + // Rotation anchor settings + rotationAnchorNear = SetUpProperty(nameof(rotationAnchorNear)); + rotationAnchorFar = SetUpProperty(nameof(rotationAnchorFar)); + + // Manipulation logic + manipulationLogicTypes = SetUpProperty(nameof(manipulationLogicTypes)); + + // Physics + releaseBehavior = SetUpProperty(nameof(releaseBehavior)); + + //Smoothing + smoothingFar = SetUpProperty(nameof(smoothingFar)); + smoothingNear = SetUpProperty(nameof(smoothingNear)); + + // Mirroring base XRI settings for easy access + selectMode = serializedObject.FindProperty("m_SelectMode"); + manipulationStarted = serializedObject.FindProperty("m_FirstSelectEntered"); + manipulationEnded = serializedObject.FindProperty("m_LastSelectExited"); + } + + static bool baseInteractableFoldout = false; + static bool advancedSettingFoldout = false; + static bool physicsFoldout = false; + static bool smoothingFoldout = false; + protected override void DrawProperties() + { + EditorGUILayout.Space(); + EditorGUILayout.LabelField("Object Manipulator Settings", EditorStyles.boldLabel); + using (new EditorGUI.IndentLevelScope()) + { + EditorGUILayout.PropertyField(allowedManipulations); + + // This is just the XRI SelectMode property, but renamed/aliased to avoid confusion. + EditorGUILayout.PropertyField(selectMode, new GUIContent("Multiselect Mode", "Can the object can be grabbed by one interactor or multiple at a time?")); + + if (advancedSettingFoldout = EditorGUILayout.Foldout(advancedSettingFoldout, "Advanced Object Manipulator Settings", true, EditorStyles.foldoutHeader)) + { + using (new EditorGUI.IndentLevelScope()) + { + EditorGUILayout.PropertyField(rotationAnchorNear); + EditorGUILayout.PropertyField(rotationAnchorFar); + EditorGUILayout.PropertyField(manipulationLogicTypes); + + Rigidbody rb = instance.GetComponent(); + if (physicsFoldout = EditorGUILayout.Foldout(physicsFoldout, "Physics", true)) + { + using (new EditorGUI.IndentLevelScope()) + { + if (rb != null && !rb.isKinematic) + { + EditorGUILayout.PropertyField(releaseBehavior); + } + else + { + EditorGUILayout.HelpBox("Physics options disabled. If you wish to enable physics options, add a Rigidbody component to this object.", MessageType.Info); + } + } + } + + smoothingFoldout = EditorGUILayout.Foldout(smoothingFoldout, "Smoothing", true); + if (smoothingFoldout) + { + using (new EditorGUI.IndentLevelScope()) + { + EditorGUILayout.PropertyField(smoothingFar); + EditorGUILayout.PropertyField(smoothingNear); + } + } + } + } + } + + if (baseInteractableFoldout = EditorGUILayout.Foldout(baseInteractableFoldout, "Stateful Interactable Settings", true, EditorStyles.foldoutHeader)) + { + using (new EditorGUI.IndentLevelScope()) + { + base.DrawProperties(); + } + } + serializedObject.ApplyModifiedProperties(); + } + + static bool manipulationEventsFoldout = false; + protected override void DrawInteractableEvents() + { + if (manipulationEventsFoldout = EditorGUILayout.Foldout(manipulationEventsFoldout, "Manipulation Events", true)) + { + // These events just alias to the existing XRI FirstSelectEntered/LastSelectExited events, + // but are mirrored here for clarity + to be explicit that they can be used for manip start/end. + EditorGUILayout.PropertyField(manipulationStarted, new GUIContent("Manipulation Started [FirstSelectEntered]", "Fired when manipulation starts.")); + EditorGUILayout.PropertyField(manipulationEnded, new GUIContent("Manipulation Ended [LastSelectExited]", "Fired when manipulation ends.")); + } + + base.DrawInteractableEvents(); + serializedObject.ApplyModifiedProperties(); + } + } +} diff --git a/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs.meta b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs.meta new file mode 100644 index 00000000000..49d13e6cf1a --- /dev/null +++ b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: ae0a8e6fa17c50146ab03b9202418193 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: From c5dfe636ccc6e65ce0332d742bc536b0a66d35c1 Mon Sep 17 00:00:00 2001 From: Roger Liu Date: Tue, 17 Jan 2023 14:33:28 -0800 Subject: [PATCH 10/10] renaming the inspector to editor --- ...tManipulatorInspector.cs => NewObjectManipulatorEditor.cs} | 4 ++-- ...orInspector.cs.meta => NewObjectManipulatorEditor.cs.meta} | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/{NewObjectManipulatorInspector.cs => NewObjectManipulatorEditor.cs} (98%) rename com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/{NewObjectManipulatorInspector.cs.meta => NewObjectManipulatorEditor.cs.meta} (100%) diff --git a/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorEditor.cs similarity index 98% rename from com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs rename to com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorEditor.cs index 1778d71b001..043014d190f 100644 --- a/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs +++ b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorEditor.cs @@ -13,7 +13,7 @@ namespace Microsoft.MixedReality.Toolkit.SpatialManipulation.Editor /// [CustomEditor(typeof(NewObjectManipulator))] [CanEditMultipleObjects] - public class NewObjectManipulatorInspector : StatefulInteractableInspector + public class NewObjectManipulatorEditor : StatefulInteractableEditor { private NewObjectManipulator instance; private SerializedProperty allowedManipulations; @@ -47,7 +47,7 @@ protected override void OnEnable() // Physics releaseBehavior = SetUpProperty(nameof(releaseBehavior)); - //Smoothing + // Smoothing smoothingFar = SetUpProperty(nameof(smoothingFar)); smoothingNear = SetUpProperty(nameof(smoothingNear)); diff --git a/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs.meta b/com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorEditor.cs.meta similarity index 100% rename from com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorInspector.cs.meta rename to com.microsoft.mrtk.spatialmanipulation/Editor/ObjectManipulator/NewObjectManipulatorEditor.cs.meta