1585 lines
60 KiB
C#
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
|
|
}
|
|
}
|