// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Input; using Microsoft.MixedReality.Toolkit.UI.BoundsControlTypes; using System.Collections.Generic; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.UI.BoundsControl { /// /// Handles for that are used for manipulating the /// Gameobject BoundsControl is attached to with near or far interaction /// public abstract class PerAxisHandles : HandlesBase { /// /// Configuration defining the handle behavior. /// protected override HandlesBaseConfiguration BaseConfig => config; protected PerAxisHandlesConfiguration config; /// /// Defines the axes the handles are assigned to. /// There needs to be an entry for each of the created handles. /// Predefined arrays for Edges/Links and /// Faces can be passed from VisualUtils /// internal abstract CardinalAxisType[] handleAxes { get; } /// /// This description is used as the name (followed by an index) for the handle gameobject. /// Can be used to search the rigroot tree to find a specific handle by name /// protected virtual string HandlePositionDescription => "handle"; private int NumHandles => handleAxes.Length; /// /// Cached handle positions - we keep track of handle positions in this array /// in case we have to reload the handles due to configuration changes. /// protected Vector3[] HandlePositions { get; private set; } /// /// Defines the positions of the handles. Has to be provided in specific handle class. /// internal abstract void CalculateHandlePositions(ref Vector3[] boundsCorners); /// /// Provide the rotation alignment for a handle. This method will be called when creating the handles. /// /// Index of the handle the rotation alignment is provided for. protected abstract Quaternion GetRotationRealignment(int handleIndex); internal PerAxisHandles(PerAxisHandlesConfiguration configuration) { HandlePositions = new Vector3[NumHandles]; Debug.Assert(configuration != null, "Can't create " + ToString() + " without valid configuration"); config = configuration; config.handlesChanged.AddListener(HandlesChanged); config.colliderTypeChanged.AddListener(UpdateColliderType); } ~PerAxisHandles() { config.handlesChanged.RemoveListener(HandlesChanged); config.colliderTypeChanged.RemoveListener(UpdateColliderType); } private void UpdateColliderType() { foreach (var handle in handles) { // remove old colliders bool shouldCreateNewCollider = false; var oldBoxCollider = handle.GetComponent(); // Caution, Destroy() will destroy one frame later. // Do not check later for presence this frame! if (oldBoxCollider != null && config.HandlePrefabColliderType == HandlePrefabCollider.Sphere) { shouldCreateNewCollider = true; Object.Destroy(oldBoxCollider); } var oldSphereCollider = handle.GetComponent(); if (oldSphereCollider != null && config.HandlePrefabColliderType == HandlePrefabCollider.Box) { shouldCreateNewCollider = true; Object.Destroy(oldSphereCollider); } if (shouldCreateNewCollider) { // attach new collider var handleBounds = VisualUtils.GetMaxBounds(GetVisual(handle).gameObject); float maxDim = VisualUtils.GetMaxComponent(handleBounds.size); float invScale = maxDim == 0.0f ? 0.0f : config.HandleSize / maxDim; Vector3 colliderSizeScaled = handleBounds.size * invScale; Vector3 colliderCenterScaled = handleBounds.center * invScale; if (config.HandlePrefabColliderType == HandlePrefabCollider.Box) { BoxCollider collider = handle.gameObject.AddComponent(); collider.size = colliderSizeScaled; collider.center = colliderCenterScaled; collider.size += config.ColliderPadding; } else { SphereCollider sphere = handle.gameObject.AddComponent(); sphere.center = colliderCenterScaled; sphere.radius = VisualUtils.GetMaxComponent(colliderSizeScaled) * 0.5f; sphere.radius += VisualUtils.GetMaxComponent(config.ColliderPadding); } } } } internal int GetHandleIndex(Transform handle) { for (int i = 0; i < handles.Count; ++i) { if (handle == handles[i]) { return i; } } return handles.Count; } internal Vector3 GetHandlePosition(int index) { Debug.Assert(index >= 0 && index < NumHandles, "Handle position index out of bounds"); return HandlePositions[index]; } internal CardinalAxisType GetAxisType(int index) { Debug.Assert(index >= 0 && index < NumHandles, "Edge axes index out of bounds"); return handleAxes[index]; } internal CardinalAxisType GetAxisType(Transform handle) { int index = GetHandleIndex(handle); return GetAxisType(index); } protected void UpdateHandles() { for (int i = 0; i < handles.Count; ++i) { handles[i].position = GetHandlePosition(i); } } internal void Reset(bool areHandlesActive, FlattenModeType flattenAxis) { IsActive = areHandlesActive; ResetHandles(); if (IsActive && handleAxes.Length == handles.Count) { List flattenedHandles = VisualUtils.GetFlattenedIndices(flattenAxis, handleAxes); if (flattenedHandles != null) { for (int i = 0; i < flattenedHandles.Count; ++i) { handles[flattenedHandles[i]].gameObject.SetActive(false); } } } } internal void Create(ref Vector3[] boundsCorners, Transform parent) { CalculateHandlePositions(ref boundsCorners); CreateHandles(parent); } private void CreateHandles(Transform parent) { for (int i = 0; i < HandlePositions.Length; ++i) { GameObject handle = new GameObject(); handle.name = HandlePositionDescription + "_" + i.ToString(); handle.transform.position = HandlePositions[i]; handle.transform.parent = parent; Bounds handleVisualBounds = CreateVisual(i, handle); float maxDim = VisualUtils.GetMaxComponent(handleVisualBounds.size); float invScale = maxDim == 0.0f ? 0.0f : config.HandleSize / maxDim; // TODO: Some subclasses of PerAxisHandles shouldn't use CursorContextInfo.CursorAction.Rotate VisualUtils.AddComponentsToAffordance(handle, new Bounds(handleVisualBounds.center * invScale, handleVisualBounds.size * invScale), config.HandlePrefabColliderType, CursorContextInfo.CursorAction.Rotate, config.ColliderPadding, parent, config.DrawTetherWhenManipulating); handles.Add(handle.transform); } VisualUtils.HandleIgnoreCollider(config.HandlesIgnoreCollider, handles); objectsChangedEvent.Invoke(this); } protected override void RecreateVisuals() { for (int i = 0; i < handles.Count; ++i) { // get parent of visual Transform obsoleteChild = handles[i].Find(visualsName); if (obsoleteChild) { // get old child and remove it obsoleteChild.parent = null; // Caution, Destroy() will destroy one frame later. // Do not check later for presence this frame! Object.Destroy(obsoleteChild.gameObject); } else { Debug.LogError("Couldn't find handle visual on recreating visuals"); } // create new visual Bounds visualBounds = CreateVisual(i, handles[i].gameObject); // update handle collider bounds UpdateColliderBounds(handles[i], visualBounds.size); } objectsChangedEvent.Invoke(this); } protected override void UpdateColliderBounds(Transform handle, Vector3 visualSize) { float maxDim = VisualUtils.GetMaxComponent(visualSize); float invScale = maxDim == 0.0f ? 0.0f : config.HandleSize / maxDim; GetVisual(handle).transform.localScale = new Vector3(invScale, invScale, invScale); Vector3 colliderSizeScaled = visualSize * invScale; if (config.HandlePrefabColliderType == HandlePrefabCollider.Box) { BoxCollider collider = handle.gameObject.GetComponent(); collider.size = colliderSizeScaled; collider.size += BaseConfig.ColliderPadding; } else { SphereCollider collider = handle.gameObject.GetComponent(); collider.radius = VisualUtils.GetMaxComponent(colliderSizeScaled) * 0.5f; collider.radius += VisualUtils.GetMaxComponent(config.ColliderPadding); } } private Bounds CreateVisual(int handleIndex, GameObject parent) { GameObject handleVisual; GameObject prefabType = config.HandlePrefab; if (prefabType != null) { handleVisual = Object.Instantiate(prefabType); } else { handleVisual = GameObject.CreatePrimitive(PrimitiveType.Sphere); // We only want the Primitive sphere mesh, but CreatePrimitive will // give us a sphere collider too. Remove the sphere collider here // so we can manually add our own properly configured collider later. var collider = handleVisual.GetComponent(); collider.enabled = false; // Caution, Destroy() will destroy one frame later. // Do not check later for presence this frame! Object.Destroy(collider); } // handleVisualBounds are returned in handleVisual-local space. Bounds handleVisualBounds = VisualUtils.GetMaxBounds(handleVisual); float maxDim = VisualUtils.GetMaxComponent(handleVisualBounds.size); float invScale = maxDim == 0.0f ? 0.0f : config.HandleSize / maxDim; handleVisual.name = visualsName; handleVisual.transform.parent = parent.transform; handleVisual.transform.localScale = new Vector3(invScale, invScale, invScale); handleVisual.transform.localPosition = Vector3.zero; handleVisual.transform.localRotation = Quaternion.identity; Quaternion realignment = GetRotationRealignment(handleIndex); parent.transform.localRotation = realignment; if (config.HandleMaterial != null) { VisualUtils.ApplyMaterialToAllRenderers(handleVisual, config.HandleMaterial); } return handleVisualBounds; } #region BoundsControlHandlerBase internal override bool IsVisible(Transform handle) { if (!IsActive) { return false; } else { CardinalAxisType axisType = GetAxisType(handle); switch (axisType) { case CardinalAxisType.X: return config.ShowHandleForX; case CardinalAxisType.Y: return config.ShowHandleForY; case CardinalAxisType.Z: return config.ShowHandleForZ; } return false; } } protected override Transform GetVisual(Transform handle) { // visual is first child Transform childTransform = handle.GetChild(0); if (childTransform != null && childTransform.name == visualsName) { return childTransform; } return null; } #endregion BoundsControlHandlerBase #region IProximityScaleObjectProvider public override bool IsActive { get { return (config.ShowHandleForX || config.ShowHandleForY || config.ShowHandleForZ) && base.IsActive; } } #endregion IProximityScaleObjectProvider } }