mixedreality/com.microsoft.mixedreality..../SDK/Features/UX/Scripts/BoundsControl/BoundsControl.cs

1585 lines
60 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using Microsoft.MixedReality.Toolkit.Experimental.Physics;
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.UI.BoundsControlTypes;
using Microsoft.MixedReality.Toolkit.Utilities;
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityPhysics = UnityEngine.Physics;
namespace Microsoft.MixedReality.Toolkit.UI.BoundsControl
{
/// <summary>
/// Bounds Control allows to transform objects (rotate and scale) and draws a cube around the object to visualize
/// the possibility of user triggered transform manipulation.
/// Bounds Control provides scale and rotation handles that can be used for far and near interaction manipulation
/// of the object. It further provides a proximity effect for scale and rotation handles that alters scaling and material.
/// </summary>
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/ux-building-blocks/bounds-control")]
[RequireComponent(typeof(ConstraintManager))]
[AddComponentMenu("Scripts/MRTK/SDK/BoundsControl")]
public class BoundsControl : MonoBehaviour,
IMixedRealitySourceStateHandler,
IMixedRealityFocusChangedHandler,
IMixedRealityFocusHandler,
IBoundsTargetProvider
{
#region Serialized Fields and Properties
[SerializeField]
[Tooltip("The object that the bounds control rig will be modifying.")]
private GameObject targetObject;
/// <summary>
/// The object that the bounds control rig will be modifying.
/// </summary>
public GameObject Target
{
get
{
if (targetObject == null)
{
targetObject = gameObject;
}
return targetObject;
}
set
{
if (targetObject != value)
{
targetObject = value;
isChildOfTarget = transform.IsChildOf(targetObject.transform);
// reparent rigroot
if (rigRoot != null)
{
rigRoot.parent = targetObject.transform;
UpdateBounds();
}
}
}
}
[Tooltip("For complex objects, automatic bounds calculation may not behave as expected. Use an existing Box Collider (even on a child object) to manually determine bounds of bounds control.")]
[SerializeField]
private BoxCollider boundsOverride = null;
/// <summary>
/// For complex objects, automatic bounds calculation may not behave as expected. Use an existing Box Collider (even on a child object) to manually determine bounds of bounds control.
/// </summary>
public BoxCollider BoundsOverride
{
get { return boundsOverride; }
set
{
if (boundsOverride != value)
{
boundsOverride = value;
if (boundsOverride == null)
{
prevBoundsOverride = new Bounds();
}
UpdateBounds();
}
}
}
[SerializeField]
[Tooltip("Defines the volume type and the priority for the bounds calculation")]
private BoundsCalculationMethod boundsCalculationMethod = BoundsCalculationMethod.RendererOverCollider;
/// <summary>
/// Defines the volume type and the priority for the bounds calculation
/// </summary>
public BoundsCalculationMethod CalculationMethod
{
get { return boundsCalculationMethod; }
set
{
if (boundsCalculationMethod != value)
{
boundsCalculationMethod = value;
UpdateBounds();
}
}
}
[SerializeField]
[Tooltip("Type of activation method for showing/hiding bounds control handles and controls")]
private BoundsControlActivationType activation = BoundsControlActivationType.ActivateOnStart;
/// <summary>
/// Type of activation method for showing/hiding bounds control handles and controls
/// </summary>
public BoundsControlActivationType BoundsControlActivation
{
get { return activation; }
set
{
if (activation != value)
{
activation = value;
SetActivationFlags();
ResetVisuals();
}
}
}
[SerializeField]
[Tooltip("Flatten bounds in the specified axis or flatten the smallest one if 'auto' is selected")]
private FlattenModeType flattenAxis = FlattenModeType.DoNotFlatten;
/// <summary>
/// Flatten bounds in the specified axis or flatten the smallest one if 'auto' is selected
/// </summary>
public FlattenModeType FlattenAxis
{
get { return flattenAxis; }
set
{
if (flattenAxis != value)
{
flattenAxis = value;
UpdateExtents();
UpdateVisuals();
ResetVisuals();
}
}
}
[SerializeField]
[Tooltip("Whether scale the flattened axis when uniform scale is used.")]
private bool uniformScaleOnFlattenedAxis = true;
/// <summary>
/// Whether scale the flattened axis when uniform scale is used.
/// </summary>
public bool UniformScaleOnFlattenedAxis
{
get => uniformScaleOnFlattenedAxis;
set => uniformScaleOnFlattenedAxis = value;
}
[SerializeField]
[Tooltip("Extra padding added to the actual Target bounds")]
private Vector3 boxPadding = Vector3.zero;
/// <summary>
/// Extra padding added to the actual Target bounds
/// </summary>
public Vector3 BoxPadding
{
get { return boxPadding; }
set
{
if (Vector3.Distance(boxPadding, value) > float.Epsilon)
{
boxPadding = value;
UpdateBounds();
}
}
}
[SerializeField]
[Tooltip("Bounds control box display configuration section.")]
private BoxDisplayConfiguration boxDisplayConfiguration;
/// <summary>
/// Bounds control box display configuration section.
/// </summary>
public BoxDisplayConfiguration BoxDisplayConfig
{
get => boxDisplayConfiguration;
set
{
boxDisplayConfiguration = value;
boxDisplay = new BoxDisplay(boxDisplayConfiguration);
CreateRig();
}
}
[SerializeField]
[Tooltip("This section defines the links / lines that are drawn between the corners of the control.")]
private LinksConfiguration linksConfiguration;
/// <summary>
/// This section defines the links / lines that are drawn between the corners of the control.
/// </summary>
public LinksConfiguration LinksConfig
{
get => linksConfiguration;
set
{
linksConfiguration = value;
links = new Links(linksConfiguration);
CreateRig();
}
}
[SerializeField]
[Tooltip("Configuration of the scale handles.")]
private ScaleHandlesConfiguration scaleHandlesConfiguration;
/// <summary>
/// Configuration of the scale handles.
/// </summary>
public ScaleHandlesConfiguration ScaleHandlesConfig
{
get => scaleHandlesConfiguration;
set
{
scaleHandlesConfiguration = value;
scaleHandles = scaleHandlesConfiguration.ConstructInstance();
CreateRig();
}
}
[SerializeField]
[Tooltip("Configuration of the rotation handles.")]
private RotationHandlesConfiguration rotationHandlesConfiguration;
/// <summary>
/// Configuration of the rotation handles.
/// </summary>
public RotationHandlesConfiguration RotationHandlesConfig
{
get => rotationHandlesConfiguration;
set
{
rotationHandlesConfiguration = value;
rotationHandles = rotationHandlesConfiguration.ConstructInstance();
CreateRig();
}
}
[SerializeField]
[Tooltip("Configuration of the translation handles.")]
private TranslationHandlesConfiguration translationHandlesConfiguration;
/// <summary>
/// Configuration of the translation handles.
/// </summary>
public TranslationHandlesConfiguration TranslationHandlesConfig
{
get => translationHandlesConfiguration;
set
{
translationHandlesConfiguration = value;
translationHandles = translationHandlesConfiguration.ConstructInstance();
CreateRig();
}
}
[SerializeField]
[Tooltip("Configuration for Proximity Effect to scale handles or change materials on proximity.")]
private ProximityEffectConfiguration handleProximityEffectConfiguration;
/// <summary>
/// Configuration for Proximity Effect to scale handles or change materials on proximity.
/// </summary>
public ProximityEffectConfiguration HandleProximityEffectConfig
{
get => handleProximityEffectConfiguration;
set
{
handleProximityEffectConfiguration = value;
proximityEffect = new ProximityEffect(handleProximityEffectConfiguration);
CreateRig();
}
}
[Tooltip("Debug only. Component used to display debug messages.")]
private TextMesh debugText;
/// <summary>
/// Component used to display debug messages.
/// </summary>
public TextMesh DebugText
{
get => debugText;
set => debugText = value;
}
[SerializeField]
[Tooltip("Determines whether to hide GameObjects (i.e handles, links etc) created and managed by this component in the editor")]
private bool hideElementsInInspector = true;
/// <summary>
/// Determines whether to hide GameObjects (i.e handles, links etc) created and managed by this component in the editor
/// </summary>
public bool HideElementsInInspector
{
get { return hideElementsInInspector; }
set
{
if (hideElementsInInspector != value)
{
hideElementsInInspector = value;
UpdateRigVisibilityInInspector();
}
}
}
[SerializeField]
[Tooltip("Check to enable frame-rate independent smoothing.")]
private bool smoothingActive = false;
/// <summary>
/// Check to enable frame-rate independent smoothing.
/// </summary>
public bool SmoothingActive
{
get => smoothingActive;
set => smoothingActive = 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;
/// <summary>
/// Enter amount representing amount of smoothing to apply to the rotation. Smoothing of 0 means no smoothing. Max value means no change to value.
/// </summary>
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;
/// <summary>
/// Enter amount representing amount of smoothing to apply to the scale. Smoothing of 0 means no smoothing. Max value means no change to value.
/// </summary>
public float ScaleLerpTime
{
get => scaleLerpTime;
set => scaleLerpTime = value;
}
[SerializeField]
[Range(0, 1)]
[Tooltip("Enter amount representing amount of smoothing to apply to the translation. " +
"Smoothing of 0 means no smoothing. Max value means no change to value.")]
private float translateLerpTime = 0.001f;
/// <summary>
/// Enter amount representing amount of smoothing to apply to the translation. Smoothing of 0
/// means no smoothing. Max value means no change to value.
/// </summary>
public float TranslateLerpTime
{
get => translateLerpTime;
set => translateLerpTime = value;
}
[SerializeField]
[Tooltip("Enable or disable constraint support of this component. When enabled, transform " +
"changes will be post processed by the linked constraint manager.")]
private bool enableConstraints = true;
/// <summary>
/// Enable or disable constraint support of this component. When enabled, transform
/// changes will be post processed by the linked constraint manager.
/// </summary>
public bool EnableConstraints
{
get => enableConstraints;
set => enableConstraints = value;
}
[SerializeField]
[Tooltip("Constraint manager slot to enable constraints when manipulating the object.")]
private ConstraintManager constraintsManager;
/// <summary>
/// Constraint manager slot to enable constraints when manipulating the object.
/// </summary>
public ConstraintManager ConstraintsManager
{
get => constraintsManager;
set => constraintsManager = value;
}
[SerializeField]
[Tooltip("Event that gets fired when interaction with a rotation handle starts.")]
private UnityEvent rotateStarted = new UnityEvent();
/// <summary>
/// Event that gets fired when interaction with a rotation handle starts.
/// </summary>
public UnityEvent RotateStarted
{
get => rotateStarted;
set => rotateStarted = value;
}
[SerializeField]
[Tooltip("Event that gets fired when interaction with a rotation handle stops.")]
private UnityEvent rotateStopped = new UnityEvent();
/// <summary>
/// Event that gets fired when interaction with a rotation handle stops.
/// </summary>
public UnityEvent RotateStopped
{
get => rotateStopped;
set => rotateStopped = value;
}
[SerializeField]
[Tooltip("Event that gets fired when interaction with a scale handle starts.")]
private UnityEvent scaleStarted = new UnityEvent();
/// <summary>
/// Event that gets fired when interaction with a scale handle starts.
/// </summary>
public UnityEvent ScaleStarted
{
get => scaleStarted;
set => scaleStarted = value;
}
[SerializeField]
[Tooltip("Event that gets fired when interaction with a scale handle stops.")]
private UnityEvent scaleStopped = new UnityEvent();
/// <summary>
/// Event that gets fired when interaction with a scale handle stops.
/// </summary>
public UnityEvent ScaleStopped
{
get => scaleStopped;
set => scaleStopped = value;
}
[SerializeField]
[Tooltip("Event that gets fired when interaction with a translation handle starts.")]
private UnityEvent translateStarted = new UnityEvent();
/// <summary>
/// Event that gets fired when interaction with a translation handle starts.
/// </summary>
public UnityEvent TranslateStarted
{
get => translateStarted;
set => translateStarted = value;
}
[SerializeField]
[Tooltip("Event that gets fired when interaction with a translation handle stops.")]
private UnityEvent translateStopped = new UnityEvent();
/// <summary>
/// Event that gets fired when interaction with a translation handle stops.
/// </summary>
public UnityEvent TranslateStopped
{
get => translateStopped;
set => translateStopped = value;
}
[SerializeField]
[Tooltip("Elastics Manager slot to enable elastics simulation when manipulating the object.")]
private ElasticsManager elasticsManager;
/// <summary>
/// Elastics Manager slot to enable elastics simulation when manipulating the object.
/// </summary>
public ElasticsManager ElasticsManager
{
get => elasticsManager;
set => elasticsManager = value;
}
#endregion Serialized Fields
#region Private Fields
// runtime instantiated visuals of bounding box
private Links links;
private ScaleHandles scaleHandles;
private RotationHandles rotationHandles;
private TranslationHandles translationHandles;
private BoxDisplay boxDisplay;
private ProximityEffect proximityEffect;
/// <summary>
/// Whether we should be displaying just the wireframe (if enabled) or the handles too
/// </summary>
public bool WireframeOnly { get => wireframeOnly; }
private bool wireframeOnly = false;
// Pointer that is being used to manipulate the bounds control
private IMixedRealityPointer currentPointer;
// parent/root game object for all bounding box visuals (like handles, edges, boxdisplay,..)
private Transform rigRoot;
// Half the size of the current bounds
private Vector3 currentBoundsExtents;
private readonly List<IMixedRealityInputSource> touchingSources = new List<IMixedRealityInputSource>();
private List<IMixedRealityController> sourcesDetected;
// Current axis of rotation about the center of the rig root
private Vector3 currentRotationAxis;
// Current axis of translation about the center of the rig root
private Vector3 currentTranslationAxis;
// Scale of the target at the beginning of the current manipulation
private Vector3 initialScaleOnGrabStart;
// Rotation of the target at the beginning of the current manipulation
private Quaternion initialRotationOnGrabStart;
// Position of the target at the beginning of the current manipulation
private Vector3 initialPositionOnGrabStart;
// Point that was initially grabbed in OnPointerDown()
private Vector3 initialGrabPoint;
// Current position of the grab point
private Vector3 currentGrabPoint;
// Grab point position in pointer space. Used to calculate the current grab point from the current pointer pose.
private Vector3 grabPointInPointer;
// Corner opposite to the grabbed one. Scaling will be relative to it.
private Vector3 oppositeCorner;
// Direction of the diagonal from the opposite corner to the grabbed one.
private Vector3 diagonalDir;
private HandleType currentHandleType;
// The size, position of boundsOverride object in the previous frame
// Used to determine if boundsOverride size has changed.
private Bounds prevBoundsOverride = new Bounds();
// Used to record the initial size of the bounds override, if it exists.
// Necessary because BoxPadding will destructively edit the size of the
// override BoxCollider, and repeated calls to BoxPadding will result
// in the override bounds growing continually larger/smaller.
private Vector3? initialBoundsOverrideSize = null;
// True if this game object is a child of the Target one
private bool isChildOfTarget = false;
private const string RigRootName = "rigRoot";
// Cache for the corner points of either renderers or colliders during the bounds calculation phase
private static readonly List<Vector3> TotalBoundsCorners = new List<Vector3>();
private Vector3[] boundsCorners = new Vector3[8];
/// <summary>
/// This property is unused and will be removed in a future release. It has not, and does not, return any information.
/// </summary>
[Obsolete("The BoundsCorners property is unused and will be removed in a future release. It has not, and does not, return any information.")]
public Vector3[] BoundsCorners { get; private set; }
// Current actual flattening axis, derived from FlattenAuto, if set
private FlattenModeType ActualFlattenAxis
{
get
{
if (FlattenAxis == FlattenModeType.FlattenAuto)
{
return VisualUtils.DetermineAxisToFlatten(TargetBounds.bounds.extents);
}
else
{
return FlattenAxis;
}
}
}
#endregion
#region public Properties
/// <summary>
/// The collider reference tracking the bounds utilized by this component during runtime
/// </summary>
public BoxCollider TargetBounds { get; private set; }
// TODO Review this, it feels like we should be using Behaviour.enabled instead.
private bool active = false;
/// <summary>
/// Whether the bounds control is currently active and will respond to input.
/// </summary>
/// <remarks>
/// Setting this property will also set the entire gameObject's active state, as well as
/// resetting the visuals and proximity scale effects.
/// </remarks>
public bool Active
{
get
{
return active;
}
set
{
if (active != value)
{
active = value;
if (rigRoot != null)
{
rigRoot.gameObject.SetActive(value);
}
ResetVisuals();
if (active)
{
proximityEffect?.ResetProximityScale();
}
}
}
}
#endregion Public Properties
#region Private Properties
private bool IsInitialized
{
get
{
return rotationHandles != null &&
scaleHandles != null &&
translationHandles != null &&
boxDisplay != null &&
links != null &&
proximityEffect != null;
}
}
#endregion
#region Public Methods
/// <summary>
/// Allows the manual enabling of the wireframe display of the bounds control.
/// This is useful if connected to the Manipulation events of a
/// <see cref="Microsoft.MixedReality.Toolkit.UI.ObjectManipulator"/>
/// when used in conjunction with this MonoBehavior.
/// </summary>
public void HighlightWires()
{
SetHighlighted(null);
}
/// <summary>
/// Allows the manual disabling of the wireframe display.
/// </summary>
public void UnhighlightWires()
{
ResetVisuals();
}
/// <summary>
/// Destroys and re-creates the rig around the bounds control
/// </summary>
public void CreateRig()
{
if (!IsInitialized)
{
return;
}
// Record what the initial size of the bounds override
// was when we constructed the rig, so we can restore
// it after we destructively edit the size with the
// BoxPadding (https://github.com/microsoft/MixedRealityToolkit-Unity/issues/7997)
if (boundsOverride != null)
{
initialBoundsOverrideSize = boundsOverride.size;
}
DestroyRig();
InitializeRigRoot();
InitializeDataStructures();
DetermineTargetBounds();
UpdateExtents();
CreateVisuals();
ResetVisuals();
rigRoot.gameObject.SetActive(active);
UpdateRigVisibilityInInspector();
}
/// <summary>
/// Update the bounds.
/// Call this function after modifying the transform of the target externally to make sure the bounds are also updated accordingly.
/// </summary>
public void UpdateBounds()
{
DetermineTargetBounds();
UpdateExtents();
UpdateVisuals();
}
#endregion
#region MonoBehaviour Methods
private void Awake()
{
if (targetObject == null)
targetObject = gameObject;
// ensure we have a default configuration in case there's none set by the user
scaleHandlesConfiguration = EnsureScriptable(scaleHandlesConfiguration);
rotationHandlesConfiguration = EnsureScriptable(rotationHandlesConfiguration);
translationHandlesConfiguration = EnsureScriptable(translationHandlesConfiguration);
boxDisplayConfiguration = EnsureScriptable(boxDisplayConfiguration);
linksConfiguration = EnsureScriptable(linksConfiguration);
handleProximityEffectConfiguration = EnsureScriptable(handleProximityEffectConfiguration);
// instantiate runtime classes for visuals
scaleHandles = scaleHandlesConfiguration.ConstructInstance();
rotationHandles = rotationHandlesConfiguration.ConstructInstance();
translationHandles = translationHandlesConfiguration.ConstructInstance();
boxDisplay = new BoxDisplay(boxDisplayConfiguration);
links = new Links(linksConfiguration);
proximityEffect = new ProximityEffect(handleProximityEffectConfiguration);
if (constraintsManager == null && EnableConstraints)
{
constraintsManager = gameObject.EnsureComponent<ConstraintManager>();
}
}
private static T EnsureScriptable<T>(T instance) where T : ScriptableObject
{
if (instance == null)
{
instance = ScriptableObject.CreateInstance<T>();
}
return instance;
}
private void OnEnable()
{
DetermineTargetBounds();
SetActivationFlags();
CreateRig();
CaptureInitialState();
}
private void SetActivationFlags()
{
wireframeOnly = false;
if (activation == BoundsControlActivationType.ActivateByProximityAndPointer ||
activation == BoundsControlActivationType.ActivateByProximity ||
activation == BoundsControlActivationType.ActivateByPointer)
{
Active = true;
if (currentPointer == null || !DoesActivationMatchPointer(currentPointer))
{
wireframeOnly = true;
}
}
else if (activation == BoundsControlActivationType.ActivateOnStart)
{
Active = true;
}
else if (activation == BoundsControlActivationType.ActivateManually)
{
Active = false;
}
}
private void OnDisable()
{
DestroyRig();
if (currentPointer != null)
{
DropController();
}
}
private void Update()
{
if (active)
{
if (currentPointer != null)
{
TransformTarget(currentHandleType);
UpdateExtents();
UpdateVisuals();
}
else if ((!isChildOfTarget && Target.transform.hasChanged)
|| (boundsOverride != null && HasBoundsOverrideChanged()))
{
UpdateExtents();
UpdateVisuals();
Target.transform.hasChanged = false;
}
// Only update proximity scaling of handles if they are visible which is when
// active is true and wireframeOnly is false
// also only use proximity effect if nothing is being dragged or grabbed
if (!wireframeOnly && currentPointer == null)
{
proximityEffect.UpdateScaling(TargetBounds.transform.TransformPoint(TargetBounds.center), currentBoundsExtents);
}
}
}
#endregion MonoBehaviour Methods
#region Private Methods
/// <summary>
/// Assumes that boundsOverride is not null
/// Returns true if the size / location of boundsOverride has changed.
/// If boundsOverride gets set to null, rig is re-created in BoundsOverride
/// property setter.
/// </summary>
private bool HasBoundsOverrideChanged()
{
Debug.Assert(boundsOverride != null, "HasBoundsOverrideChanged called but boundsOverride is null");
Bounds curBounds = boundsOverride.bounds;
bool result = curBounds != prevBoundsOverride;
prevBoundsOverride = curBounds;
return result;
}
private void DetermineTargetBounds()
{
// Make sure that the bounds of all child objects are up to date before we compute bounds
UnityPhysics.SyncTransforms();
if (boundsOverride != null)
{
TargetBounds = boundsOverride;
TargetBounds.transform.hasChanged = true;
}
else
{
TargetBounds = Target.EnsureComponent<BoxCollider>();
Bounds bounds = GetTargetBounds();
TargetBounds.center = bounds.center;
TargetBounds.size = bounds.size;
}
// add box padding
if (boxPadding == Vector3.zero) { return; }
Vector3 scale = TargetBounds.transform.lossyScale;
for (int i = 0; i < 3; i++)
{
if (scale[i] == 0f) { return; }
scale[i] = 1f / scale[i];
}
TargetBounds.size += Vector3.Scale(boxPadding, scale);
}
private readonly List<Transform> childTransforms = new List<Transform>();
private Bounds GetTargetBounds()
{
TotalBoundsCorners.Clear();
// Collect all Transforms except for the rigRoot(s) transform structure(s)
// Its possible we have two rigRoots here, the one about to be deleted and the new one
// Since those have the gizmo structure childed, be need to omit them completely in the calculation of the bounds
// This can only happen by name unless there is a better idea of tracking the rigRoot that needs destruction
childTransforms.Clear();
if (Target != gameObject)
{
childTransforms.Add(Target.transform);
}
foreach (Transform childTransform in Target.transform)
{
if (childTransform.name.Equals(RigRootName)) { continue; }
childTransforms.AddRange(childTransform.GetComponentsInChildren<Transform>());
}
// Iterate transforms and collect bound volumes
foreach (Transform childTransform in childTransforms)
{
Debug.Assert(childTransform != rigRoot);
ExtractBoundsCorners(childTransform, boundsCalculationMethod);
}
Transform targetTransform = Target.transform;
// In case we found nothing and this is the Target, we add its inevitable collider's bounds
if (TotalBoundsCorners.Count == 0 && Target == gameObject)
{
ExtractBoundsCorners(targetTransform, BoundsCalculationMethod.ColliderOnly);
}
Bounds finalBounds = new Bounds(targetTransform.InverseTransformPoint(TotalBoundsCorners[0]), Vector3.zero);
for (int i = 1; i < TotalBoundsCorners.Count; i++)
{
finalBounds.Encapsulate(targetTransform.InverseTransformPoint(TotalBoundsCorners[i]));
}
return finalBounds;
}
private void ExtractBoundsCorners(Transform childTransform, BoundsCalculationMethod boundsCalculationMethod)
{
KeyValuePair<Transform, Collider> colliderByTransform = default;
KeyValuePair<Transform, Bounds> rendererBoundsByTransform = default;
if (boundsCalculationMethod != BoundsCalculationMethod.RendererOnly)
{
Collider collider = childTransform.GetComponent<Collider>();
if (collider != null)
{
colliderByTransform = new KeyValuePair<Transform, Collider>(childTransform, collider);
}
else
{
colliderByTransform = new KeyValuePair<Transform, Collider>();
}
}
if (boundsCalculationMethod != BoundsCalculationMethod.ColliderOnly)
{
MeshFilter meshFilter = childTransform.GetComponent<MeshFilter>();
SkinnedMeshRenderer skinnedMeshRenderer = childTransform.GetComponent<SkinnedMeshRenderer>();
if (meshFilter != null && meshFilter.sharedMesh != null)
{
rendererBoundsByTransform = new KeyValuePair<Transform, Bounds>(childTransform, meshFilter.sharedMesh.bounds);
}
else if (skinnedMeshRenderer != null && skinnedMeshRenderer.sharedMesh != null)
{
rendererBoundsByTransform = new KeyValuePair<Transform, Bounds>(childTransform, skinnedMeshRenderer.sharedMesh.bounds);
}
else
{
rendererBoundsByTransform = new KeyValuePair<Transform, Bounds>();
}
}
// Encapsulate the collider bounds if criteria match
if (boundsCalculationMethod == BoundsCalculationMethod.ColliderOnly ||
boundsCalculationMethod == BoundsCalculationMethod.ColliderOverRenderer)
{
if (AddColliderBoundsCornersToTarget(colliderByTransform) && boundsCalculationMethod == BoundsCalculationMethod.ColliderOverRenderer ||
boundsCalculationMethod == BoundsCalculationMethod.ColliderOnly) { return; }
}
// Encapsulate the renderer bounds if criteria match
if (boundsCalculationMethod != BoundsCalculationMethod.ColliderOnly)
{
if (AddRendererBoundsCornersToTarget(rendererBoundsByTransform) && boundsCalculationMethod == BoundsCalculationMethod.RendererOverCollider ||
boundsCalculationMethod == BoundsCalculationMethod.RendererOnly) { return; }
}
// Do the collider for the one case that we chose RendererOverCollider and did not find a renderer
AddColliderBoundsCornersToTarget(colliderByTransform);
}
private bool AddRendererBoundsCornersToTarget(KeyValuePair<Transform, Bounds> rendererBoundsByTarget)
{
if (rendererBoundsByTarget.Key == null) { return false; }
Vector3[] cornersToWorld = null;
rendererBoundsByTarget.Value.GetCornerPositions(rendererBoundsByTarget.Key, ref cornersToWorld);
TotalBoundsCorners.AddRange(cornersToWorld);
return true;
}
private bool AddColliderBoundsCornersToTarget(KeyValuePair<Transform, Collider> colliderByTransform)
{
if (colliderByTransform.Key != null)
{
BoundsExtensions.GetColliderBoundsPoints(colliderByTransform.Value, TotalBoundsCorners, 0);
}
return colliderByTransform.Key != null;
}
private HandleType GetHandleType(Transform handle)
{
if (rotationHandles.IsHandleType(handle))
{
return rotationHandles.GetHandleType();
}
else if (scaleHandles.IsHandleType(handle))
{
return scaleHandles.GetHandleType();
}
else if (translationHandles.IsHandleType(handle))
{
return translationHandles.GetHandleType();
}
else
{
return HandleType.None;
}
}
private void CaptureInitialState()
{
if (Target != null)
{
isChildOfTarget = transform.IsChildOf(Target.transform);
}
}
private Vector3 CalculateBoundsExtents()
{
// Store current rotation then zero out the rotation so that the bounds
// are computed when the object is in its 'axis aligned orientation'.
Quaternion currentRotation = Target.transform.rotation;
Target.transform.rotation = Quaternion.identity;
UnityPhysics.SyncTransforms(); // Update collider bounds
Vector3 boundsExtents = TargetBounds.bounds.extents;
// After bounds are computed, restore rotation...
Target.transform.rotation = currentRotation;
UnityPhysics.SyncTransforms();
// apply flattening
return VisualUtils.FlattenBounds(boundsExtents, flattenAxis);
}
private void UpdateExtents()
{
if (TargetBounds != null)
{
Vector3 newExtents = CalculateBoundsExtents();
if (newExtents != Vector3.zero)
{
currentBoundsExtents = newExtents;
VisualUtils.GetCornerPositionsFromBounds(new Bounds(Vector3.zero, currentBoundsExtents * 2.0f), ref boundsCorners);
}
}
}
private bool DoesActivationMatchPointer(IMixedRealityPointer pointer)
{
switch (activation)
{
case BoundsControlActivationType.ActivateOnStart:
case BoundsControlActivationType.ActivateManually:
return false;
case BoundsControlActivationType.ActivateByProximity:
return pointer is IMixedRealityNearPointer;
case BoundsControlActivationType.ActivateByPointer:
return (pointer is IMixedRealityPointer && !(pointer is IMixedRealityNearPointer));
case BoundsControlActivationType.ActivateByProximityAndPointer:
return true;
default:
return false;
}
}
private void DropController()
{
HandleType lastHandleType = currentHandleType;
currentPointer = null;
currentHandleType = HandleType.None;
ResetVisuals();
if (lastHandleType == HandleType.Scale)
{
if (debugText != null) debugText.text = "DropController:ScaleStopped";
ScaleStopped?.Invoke();
}
else if (lastHandleType == HandleType.Rotation)
{
if (debugText != null) debugText.text = "DropController:RotateStopped";
RotateStopped?.Invoke();
}
else if (lastHandleType == HandleType.Translation)
{
if (debugText != null) debugText.text = "DropController:TranslateStopped";
TranslateStopped?.Invoke();
}
if (elasticsManager != null)
{
elasticsManager.EnableElasticsUpdate = true;
}
}
private void DestroyRig()
{
// If we have previously logged an initial bounds size,
// reset the boundsOverride BoxCollider to the initial size.
// This is because the CalculateBoxPadding
if (initialBoundsOverrideSize.HasValue)
{
boundsOverride.size = initialBoundsOverrideSize.Value;
}
// todo: move this out?
DestroyVisuals();
if (rigRoot != null)
{
Destroy(rigRoot.gameObject);
rigRoot = null;
}
}
private void UpdateRigVisibilityInInspector()
{
if (!IsInitialized)
{
return;
}
HideFlags desiredFlags = hideElementsInInspector ? HideFlags.HideInHierarchy | HideFlags.HideInInspector : HideFlags.None;
scaleHandles.UpdateVisibilityInInspector(desiredFlags);
links.UpdateVisibilityInInspector(desiredFlags);
boxDisplay.UpdateVisibilityInInspector(desiredFlags);
if (rigRoot != null)
{
rigRoot.hideFlags = desiredFlags;
}
}
private Vector3 GetRotationAxis(Transform handle)
{
CardinalAxisType axisType = rotationHandles.GetAxisType(handle);
if (axisType == CardinalAxisType.X)
{
return rigRoot.transform.right;
}
else if (axisType == CardinalAxisType.Y)
{
return rigRoot.transform.up;
}
else
{
return rigRoot.transform.forward;
}
}
private Vector3 GetTranslationAxis(Transform handle)
{
CardinalAxisType axisType = translationHandles.GetAxisType(handle);
if (axisType == CardinalAxisType.X)
{
return rigRoot.transform.right;
}
else if (axisType == CardinalAxisType.Y)
{
return rigRoot.transform.up;
}
else
{
return rigRoot.transform.forward;
}
}
private void InitializeRigRoot()
{
var rigRootObj = new GameObject(RigRootName);
rigRoot = rigRootObj.transform;
rigRoot.parent = Target.transform;
var pH = rigRootObj.AddComponent<PointerHandler>();
pH.OnPointerDown.AddListener(OnPointerDown);
pH.OnPointerDragged.AddListener(OnPointerDragged);
pH.OnPointerUp.AddListener(OnPointerUp);
}
private void InitializeDataStructures()
{
sourcesDetected = new List<IMixedRealityController>();
}
private void TransformTarget(HandleType transformType)
{
if (transformType != HandleType.None)
{
currentGrabPoint = (currentPointer.Rotation * grabPointInPointer) + currentPointer.Position;
bool isNear = currentPointer is IMixedRealityNearPointer;
TransformFlags transformUpdated = 0;
if (transformType == HandleType.Rotation)
{
Vector3 initDir = Vector3.ProjectOnPlane(initialGrabPoint - Target.transform.position, currentRotationAxis).normalized;
Vector3 currentDir = Vector3.ProjectOnPlane(currentGrabPoint - Target.transform.position, currentRotationAxis).normalized;
Quaternion goal = Quaternion.FromToRotation(initDir, currentDir) * initialRotationOnGrabStart;
MixedRealityTransform constraintRotation = MixedRealityTransform.NewRotate(goal);
if (EnableConstraints && constraintsManager != null)
{
constraintsManager.ApplyRotationConstraints(ref constraintRotation, true, isNear);
}
if (elasticsManager != null)
{
transformUpdated = elasticsManager.ApplyTargetTransform(constraintRotation, TransformFlags.Rotate);
}
if (!transformUpdated.IsMaskSet(TransformFlags.Rotate))
{
Target.transform.rotation = smoothingActive ?
Smoothing.SmoothTo(Target.transform.rotation, constraintRotation.Rotation, rotateLerpTime, Time.deltaTime) :
constraintRotation.Rotation;
}
}
else if (transformType == HandleType.Scale)
{
Vector3 scaleFactor = Target.transform.localScale;
if (ScaleHandlesConfig.ScaleBehavior == HandleScaleMode.Uniform)
{
float initialDist = Vector3.Dot(initialGrabPoint - oppositeCorner, diagonalDir);
float currentDist = Vector3.Dot(currentGrabPoint - oppositeCorner, diagonalDir);
float scaleFactorUniform = 1 + (currentDist - initialDist) / initialDist;
scaleFactor = new Vector3(scaleFactorUniform, scaleFactorUniform, scaleFactorUniform);
}
else // non-uniform scaling
{
// get diff from center point of box
Vector3 initialDist = Target.transform.InverseTransformVector(initialGrabPoint - oppositeCorner);
Vector3 currentDist = Target.transform.InverseTransformVector(currentGrabPoint - oppositeCorner);
Vector3 grabDiff = (currentDist - initialDist);
scaleFactor = Vector3.one + grabDiff.Div(initialDist);
}
// If non-uniform scaling or uniform scaling only on the non-flattened axes
if (ScaleHandlesConfig.ScaleBehavior != HandleScaleMode.Uniform || !UniformScaleOnFlattenedAxis)
{
var currentActualFlattenAxis = ActualFlattenAxis; // Calculate flatten axis once
if (currentActualFlattenAxis == FlattenModeType.FlattenX)
{
scaleFactor.x = 1;
}
else if (currentActualFlattenAxis == FlattenModeType.FlattenY)
{
scaleFactor.y = 1;
}
else if (currentActualFlattenAxis == FlattenModeType.FlattenZ)
{
scaleFactor.z = 1;
}
}
Vector3 newScale = initialScaleOnGrabStart.Mul(scaleFactor);
MixedRealityTransform clampedTransform = MixedRealityTransform.NewScale(newScale);
if (EnableConstraints && constraintsManager != null)
{
constraintsManager.ApplyScaleConstraints(ref clampedTransform, true, isNear);
}
if (elasticsManager != null)
{
transformUpdated = elasticsManager.ApplyTargetTransform(clampedTransform, TransformFlags.Scale);
}
if (!transformUpdated.IsMaskSet(TransformFlags.Scale))
{
Target.transform.localScale = smoothingActive ?
Smoothing.SmoothTo(Target.transform.localScale, clampedTransform.Scale, scaleLerpTime, Time.deltaTime) :
clampedTransform.Scale;
}
var originalRelativePosition = Target.transform.InverseTransformDirection(initialPositionOnGrabStart - oppositeCorner);
var newPosition = Target.transform.TransformDirection(originalRelativePosition.Mul(scaleFactor)) + oppositeCorner;
Target.transform.position = smoothingActive ? Smoothing.SmoothTo(Target.transform.position, newPosition, scaleLerpTime, Time.deltaTime) : newPosition;
}
else if (transformType == HandleType.Translation)
{
Vector3 translateVectorAlongAxis = Vector3.Project(currentGrabPoint - initialGrabPoint, currentTranslationAxis);
var goal = initialPositionOnGrabStart + translateVectorAlongAxis;
MixedRealityTransform constraintTranslate = MixedRealityTransform.NewTranslate(goal);
if (EnableConstraints && constraintsManager != null)
{
constraintsManager.ApplyTranslationConstraints(ref constraintTranslate, true, isNear);
}
if (elasticsManager != null)
{
transformUpdated = elasticsManager.ApplyTargetTransform(constraintTranslate, TransformFlags.Move);
}
if (!transformUpdated.IsMaskSet(TransformFlags.Move))
{
Target.transform.position = smoothingActive ?
Smoothing.SmoothTo(Target.transform.position, constraintTranslate.Position, translateLerpTime, Time.deltaTime) :
constraintTranslate.Position;
}
}
}
}
#endregion Private Methods
#region Used Event Handlers
/// <inheritdoc />
void IMixedRealityFocusChangedHandler.OnFocusChanged(FocusEventData eventData)
{
if (eventData.NewFocusedObject == null)
{
proximityEffect.ResetProximityScale();
}
if (activation == BoundsControlActivationType.ActivateManually || activation == BoundsControlActivationType.ActivateOnStart)
{
return;
}
if (!DoesActivationMatchPointer(eventData.Pointer))
{
return;
}
bool handInProximity = eventData.NewFocusedObject != null && eventData.NewFocusedObject.transform.IsChildOf(transform);
if (handInProximity == wireframeOnly)
{
wireframeOnly = !handInProximity;
// todo: move this out?
ResetVisuals();
}
}
/// <inheritdoc />
void IMixedRealityFocusHandler.OnFocusExit(FocusEventData eventData)
{
if (currentPointer != null && eventData.Pointer == currentPointer)
{
DropController();
}
}
/// <inheritdoc />
void IMixedRealityFocusHandler.OnFocusEnter(FocusEventData eventData) { }
private void OnPointerUp(MixedRealityPointerEventData eventData)
{
if (currentPointer != null && eventData.Pointer == currentPointer)
{
DropController();
eventData.Use();
}
}
private void OnPointerDown(MixedRealityPointerEventData eventData)
{
if (currentPointer == null && !eventData.used)
{
GameObject grabbedHandle = eventData.Pointer.Result.CurrentPointerTarget;
Transform grabbedHandleTransform = grabbedHandle.transform;
currentHandleType = GetHandleType(grabbedHandleTransform);
if (currentHandleType != HandleType.None)
{
currentPointer = eventData.Pointer;
initialGrabPoint = currentPointer.Result.Details.Point;
currentGrabPoint = initialGrabPoint;
initialScaleOnGrabStart = Target.transform.localScale;
initialRotationOnGrabStart = Target.transform.rotation;
initialPositionOnGrabStart = Target.transform.position;
grabPointInPointer = Quaternion.Inverse(eventData.Pointer.Rotation) * (initialGrabPoint - currentPointer.Position);
// todo: move this out?
SetHighlighted(grabbedHandleTransform, eventData.Pointer);
if (currentHandleType == HandleType.Scale)
{
// Will use this to scale the target relative to the opposite corner
oppositeCorner = rigRoot.transform.TransformPoint(-grabbedHandle.transform.localPosition);
diagonalDir = (grabbedHandle.transform.position - oppositeCorner).normalized;
ScaleStarted?.Invoke();
if (debugText != null)
{
debugText.text = "OnPointerDown:ScaleStarted";
}
}
else if (currentHandleType == HandleType.Rotation)
{
currentRotationAxis = GetRotationAxis(grabbedHandleTransform);
RotateStarted?.Invoke();
if (debugText != null)
{
debugText.text = "OnPointerDown:RotateStarted";
}
}
else if (currentHandleType == HandleType.Translation)
{
currentTranslationAxis = GetTranslationAxis(grabbedHandleTransform);
TranslateStarted?.Invoke();
if (debugText != null)
{
debugText.text = "OnPointerDown:TranslateStarted";
}
}
if (elasticsManager != null)
{
// Initialize elastic systems.
elasticsManager.InitializeElastics(Target.transform);
// disable auto update (elastics are going to be queried through applyTargetTransform when manipulating)
elasticsManager.EnableElasticsUpdate = false;
}
if (EnableConstraints && constraintsManager != null)
{
constraintsManager.Initialize(new MixedRealityTransform(Target.transform));
}
eventData.Use();
}
}
if (currentPointer != null)
{
// Always mark the pointer data as used to prevent any other behavior to handle pointer events
// as long as bounds control manipulation is active.
// This is due to us reacting to both "Select" and "Grip" events.
eventData.Use();
}
}
private void OnPointerDragged(MixedRealityPointerEventData eventData) { }
/// <inheritdoc />
public void OnSourceDetected(SourceStateEventData eventData)
{
if (eventData.Controller != null)
{
if (sourcesDetected.Count == 0 || sourcesDetected.Contains(eventData.Controller) == false)
{
sourcesDetected.Add(eventData.Controller);
}
}
}
/// <inheritdoc />
public void OnSourceLost(SourceStateEventData eventData)
{
sourcesDetected.Remove(eventData.Controller);
if (currentPointer != null && currentPointer.InputSourceParent.SourceId == eventData.SourceId)
{
DropController();
}
}
#endregion Used Event Handlers
#region Unused Event Handlers
/// <inheritdoc />
void IMixedRealityFocusChangedHandler.OnBeforeFocusChange(FocusEventData eventData) { }
#endregion Unused Event Handlers
#region BoundsControl Visuals Private Methods
private void SetHighlighted(Transform activeHandle, IMixedRealityPointer pointer = null)
{
scaleHandles.SetHighlighted(activeHandle, pointer);
rotationHandles.SetHighlighted(activeHandle, pointer);
translationHandles.SetHighlighted(activeHandle, pointer);
boxDisplay.SetHighlighted();
}
private void ResetVisuals()
{
if (currentPointer != null || !IsInitialized)
{
return;
}
// Cache computed flatten axis for subsequent calls to Reset()
var actualAxis = ActualFlattenAxis;
boxDisplay.Reset(active);
boxDisplay.UpdateFlattenAxis(actualAxis);
bool isVisible = (active == true && wireframeOnly == false);
rotationHandles.Reset(isVisible, actualAxis);
links.Reset(active, actualAxis);
scaleHandles.Reset(isVisible, actualAxis);
translationHandles.Reset(isVisible, actualAxis);
}
private void CreateVisuals()
{
// add corners
bool isFlattened = flattenAxis != FlattenModeType.DoNotFlatten;
// Add scale handles
scaleHandles.Create(ref boundsCorners, rigRoot, isFlattened);
proximityEffect.RegisterObjectProvider(scaleHandles);
// Add rotation handles
rotationHandles.Create(ref boundsCorners, rigRoot);
proximityEffect.RegisterObjectProvider(rotationHandles);
links.CreateLinks(ref boundsCorners, rigRoot, currentBoundsExtents);
// Add translation handles
translationHandles.Create(ref boundsCorners, rigRoot);
proximityEffect.RegisterObjectProvider(translationHandles);
// add box display
boxDisplay.AddBoxDisplay(rigRoot.transform, currentBoundsExtents, flattenAxis);
// update visuals
UpdateVisuals();
}
private void DestroyVisuals()
{
proximityEffect.ClearObjects();
links.Clear();
scaleHandles.DestroyHandles();
rotationHandles.DestroyHandles();
translationHandles.DestroyHandles();
}
private void UpdateVisuals()
{
if (rigRoot != null && Target != null && TargetBounds != null)
{
// We move the rigRoot to the scene root to ensure that non-uniform scaling performed
// anywhere above the rigRoot does not impact the position of rig corners / edges
rigRoot.parent = null;
rigRoot.rotation = Quaternion.identity;
rigRoot.position = Vector3.zero;
rigRoot.localScale = Vector3.one;
rotationHandles.CalculateHandlePositions(ref boundsCorners);
// Links depend on rotation handles for position calculations.
links.UpdateLinkPositions(ref boundsCorners);
links.UpdateLinkScales(currentBoundsExtents);
translationHandles.CalculateHandlePositions(ref boundsCorners);
scaleHandles.CalculateHandlePositions(ref boundsCorners);
boxDisplay.UpdateDisplay(currentBoundsExtents, ActualFlattenAxis);
// move rig into position and rotation
rigRoot.position = TargetBounds.bounds.center;
rigRoot.rotation = Target.transform.rotation;
rigRoot.parent = Target.transform;
}
}
#endregion BoundsControl Visuals Private Methods
}
}