584 lines
21 KiB
C#
584 lines
21 KiB
C#
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Microsoft.MixedReality.Toolkit.Utilities
|
|
{
|
|
/// <summary>
|
|
/// A Grid Object Collection is simply a set of child objects organized with some
|
|
/// layout parameters. The collection can be used to quickly create
|
|
/// control panels or sets of prefab/objects.
|
|
/// </summary>
|
|
[HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/ux-building-blocks/object-collection")]
|
|
[AddComponentMenu("Scripts/MRTK/SDK/GridObjectCollection")]
|
|
[ExecuteAlways]
|
|
public partial class GridObjectCollection : BaseObjectCollection
|
|
{
|
|
[Tooltip("Type of surface to map the collection to")]
|
|
[SerializeField]
|
|
private ObjectOrientationSurfaceType surfaceType = ObjectOrientationSurfaceType.Plane;
|
|
|
|
/// <summary>
|
|
/// Type of surface to map the collection to.
|
|
/// </summary>
|
|
public ObjectOrientationSurfaceType SurfaceType
|
|
{
|
|
get { return surfaceType; }
|
|
set { surfaceType = value; }
|
|
}
|
|
|
|
[Tooltip("Should the objects in the collection be rotated / how should they be rotated")]
|
|
[SerializeField]
|
|
private OrientationType orientType = OrientationType.None;
|
|
|
|
/// <summary>
|
|
/// Should the objects in the collection face the origin of the collection
|
|
/// </summary>
|
|
public OrientationType OrientType
|
|
{
|
|
get { return orientType; }
|
|
set { orientType = value; }
|
|
}
|
|
|
|
[Tooltip("Specify direction in which children are laid out")]
|
|
[SerializeField]
|
|
private LayoutOrder layout = LayoutOrder.RowThenColumn;
|
|
|
|
/// <summary>
|
|
/// Specify direction in which children are laid out
|
|
/// </summary>
|
|
public LayoutOrder Layout
|
|
{
|
|
get { return layout; }
|
|
set { layout = value; }
|
|
}
|
|
|
|
|
|
[SerializeField, Tooltip("Where the grid is anchored relative to local origin")]
|
|
private LayoutAnchor anchor = LayoutAnchor.MiddleCenter;
|
|
|
|
/// <summary>
|
|
/// Where the grid is anchored relative to local origin
|
|
/// </summary>
|
|
public LayoutAnchor Anchor
|
|
{
|
|
get { return anchor; }
|
|
set { anchor = value; }
|
|
}
|
|
|
|
[SerializeField, Tooltip("Whether anchoring occurs along an objects axis or not")]
|
|
private bool anchorAlongAxis = false;
|
|
|
|
/// <summary>
|
|
/// Whether anchoring occurs along an objects axis or not
|
|
/// </summary>
|
|
public bool AnchorAlongAxis
|
|
{
|
|
get { return anchorAlongAxis; }
|
|
set { anchorAlongAxis = value; }
|
|
}
|
|
|
|
|
|
[SerializeField, Tooltip("How the columns are aligned in the grid")]
|
|
private LayoutHorizontalAlignment columnAlignment = LayoutHorizontalAlignment.Left;
|
|
|
|
/// <summary>
|
|
/// How the columns are aligned in the grid
|
|
/// </summary>
|
|
public LayoutHorizontalAlignment ColumnAlignment
|
|
{
|
|
get { return columnAlignment; }
|
|
set { columnAlignment = value; }
|
|
}
|
|
|
|
[SerializeField, Tooltip("How the rows are aligned in the grid")]
|
|
private LayoutVerticalAlignment rowAlignment = LayoutVerticalAlignment.Top;
|
|
|
|
/// <summary>
|
|
/// How the rows are aligned in the grid
|
|
/// </summary>
|
|
public LayoutVerticalAlignment RowAlignment
|
|
{
|
|
get { return rowAlignment; }
|
|
set { rowAlignment = value; }
|
|
}
|
|
|
|
[Range(0.05f, 100.0f)]
|
|
[Tooltip("Radius for the sphere or cylinder")]
|
|
[SerializeField]
|
|
private float radius = 2f;
|
|
|
|
/// <summary>
|
|
/// This is the radius of either the Cylinder or Sphere mapping and is ignored when using the plane mapping.
|
|
/// </summary>
|
|
public float Radius
|
|
{
|
|
get { return radius; }
|
|
set { radius = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Radial range for radial layout")]
|
|
[Range(5f, 360f)]
|
|
private float radialRange = 180f;
|
|
|
|
/// <summary>
|
|
/// This is the radial range for creating a radial fan layout.
|
|
/// </summary>
|
|
public float RadialRange
|
|
{
|
|
get { return radialRange; }
|
|
set { radialRange = value; }
|
|
}
|
|
|
|
[SerializeField]
|
|
[Tooltip("Distance for plane layout")]
|
|
[Range(0f, 100f)]
|
|
private float distance = 0f;
|
|
|
|
/// <summary>
|
|
/// This is the Distance for an offset for the Plane mapping and is ignored for the other mappings.
|
|
/// </summary>
|
|
public float Distance
|
|
{
|
|
get { return distance; }
|
|
set { distance = value; }
|
|
}
|
|
|
|
private const int DefaultValueRowsCols = 3;
|
|
|
|
[Tooltip("Number of rows per column")]
|
|
[SerializeField]
|
|
private int rows = DefaultValueRowsCols;
|
|
|
|
/// <summary>
|
|
/// Number of rows per column. Can only be assigned when layout type is
|
|
/// RowsThenColumns
|
|
/// </summary>
|
|
public int Rows
|
|
{
|
|
get { return rows; }
|
|
set
|
|
{
|
|
if (Layout == LayoutOrder.ColumnThenRow)
|
|
{
|
|
Debug.LogError("When using ColumnThenRow layout, assign Columns instead of Rows.");
|
|
return;
|
|
}
|
|
rows = value;
|
|
}
|
|
}
|
|
|
|
[Tooltip("Number of columns per row")]
|
|
[SerializeField]
|
|
private int columns = DefaultValueRowsCols;
|
|
|
|
/// <summary>
|
|
/// Number of columns per row. Can only be assigned when layout type is
|
|
/// ColumnsThenRows
|
|
/// </summary>
|
|
public int Columns
|
|
{
|
|
get { return columns; }
|
|
set
|
|
{
|
|
if (Layout == LayoutOrder.RowThenColumn)
|
|
{
|
|
Debug.LogError("When using RowThenColumn layout, assign Rows instead of Columns.");
|
|
return;
|
|
}
|
|
columns = value;
|
|
}
|
|
}
|
|
|
|
[Tooltip("Width of cell per object")]
|
|
[SerializeField]
|
|
private float cellWidth = 0.5f;
|
|
|
|
/// <summary>
|
|
/// Width of the cell per object in the collection.
|
|
/// </summary>
|
|
public float CellWidth
|
|
{
|
|
get { return cellWidth; }
|
|
set { cellWidth = value; }
|
|
}
|
|
|
|
[Tooltip("Height of cell per object")]
|
|
[SerializeField]
|
|
private float cellHeight = 0.5f;
|
|
|
|
/// <summary>
|
|
/// Height of the cell per object in the collection.
|
|
/// </summary>
|
|
public float CellHeight
|
|
{
|
|
get { return cellHeight; }
|
|
set { cellHeight = value; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Total Width of collection
|
|
/// </summary>
|
|
public float Width => Columns * CellWidth;
|
|
|
|
/// <summary>
|
|
/// Total Height of collection
|
|
/// </summary>
|
|
public float Height => rows * CellHeight;
|
|
|
|
/// <summary>
|
|
/// Reference mesh to use for rendering the sphere layout
|
|
/// </summary>
|
|
public Mesh SphereMesh { get; set; }
|
|
|
|
/// <summary>
|
|
/// Reference mesh to use for rendering the cylinder layout
|
|
/// </summary>
|
|
public Mesh CylinderMesh { get; set; }
|
|
|
|
protected Vector2 HalfCell;
|
|
|
|
/// <summary>
|
|
/// Overriding base function for laying out all the children when UpdateCollection is called.
|
|
/// </summary>
|
|
protected override void LayoutChildren()
|
|
{
|
|
var nodeGrid = new Vector3[NodeList.Count];
|
|
Vector3 newPos;
|
|
|
|
// Now lets lay out the grid
|
|
if (Layout == LayoutOrder.RowThenColumn)
|
|
{
|
|
columns = Mathf.CeilToInt((float)NodeList.Count / rows);
|
|
}
|
|
else if (Layout == LayoutOrder.ColumnThenRow)
|
|
{
|
|
rows = Mathf.CeilToInt((float)NodeList.Count / columns);
|
|
}
|
|
HalfCell = new Vector2(CellWidth * 0.5f, CellHeight * 0.5f);
|
|
|
|
// First start with a grid then project onto surface
|
|
ResolveGridLayout(nodeGrid, layout);
|
|
|
|
switch (SurfaceType)
|
|
{
|
|
case ObjectOrientationSurfaceType.Plane:
|
|
for (int i = 0; i < NodeList.Count; i++)
|
|
{
|
|
ObjectCollectionNode node = NodeList[i];
|
|
newPos = nodeGrid[i];
|
|
newPos.z = distance;
|
|
node.Transform.localPosition = newPos;
|
|
UpdateNodeFacing(node);
|
|
NodeList[i] = node;
|
|
}
|
|
break;
|
|
|
|
case ObjectOrientationSurfaceType.Cylinder:
|
|
for (int i = 0; i < NodeList.Count; i++)
|
|
{
|
|
ObjectCollectionNode node = NodeList[i];
|
|
newPos = VectorExtensions.CylindricalMapping(nodeGrid[i], radius);
|
|
node.Transform.localPosition = newPos;
|
|
UpdateNodeFacing(node);
|
|
NodeList[i] = node;
|
|
}
|
|
break;
|
|
|
|
case ObjectOrientationSurfaceType.Sphere:
|
|
|
|
for (int i = 0; i < NodeList.Count; i++)
|
|
{
|
|
ObjectCollectionNode node = NodeList[i];
|
|
newPos = VectorExtensions.SphericalMapping(nodeGrid[i], radius);
|
|
node.Transform.localPosition = newPos;
|
|
UpdateNodeFacing(node);
|
|
NodeList[i] = node;
|
|
}
|
|
break;
|
|
|
|
case ObjectOrientationSurfaceType.Radial:
|
|
int curColumn = 0;
|
|
int curRow = 1;
|
|
|
|
for (int i = 0; i < NodeList.Count; i++)
|
|
{
|
|
ObjectCollectionNode node = NodeList[i];
|
|
newPos = VectorExtensions.RadialMapping(nodeGrid[i], radialRange, radius, curRow, rows, curColumn, Columns);
|
|
|
|
if (curColumn == (Columns - 1))
|
|
{
|
|
curColumn = 0;
|
|
++curRow;
|
|
}
|
|
else
|
|
{
|
|
++curColumn;
|
|
}
|
|
|
|
node.Transform.localPosition = newPos;
|
|
UpdateNodeFacing(node);
|
|
NodeList[i] = node;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected void ResolveGridLayout(Vector3[] grid, LayoutOrder order)
|
|
{
|
|
int cellCounter = 0;
|
|
int xMax, yMax;
|
|
|
|
switch (order)
|
|
{
|
|
case LayoutOrder.RowThenColumn:
|
|
xMax = Columns;
|
|
yMax = Rows;
|
|
break;
|
|
case LayoutOrder.ColumnThenRow:
|
|
xMax = Columns;
|
|
yMax = Rows;
|
|
break;
|
|
case LayoutOrder.Vertical:
|
|
xMax = 1;
|
|
yMax = NodeList.Count;
|
|
break;
|
|
case LayoutOrder.Horizontal:
|
|
xMax = NodeList.Count;
|
|
yMax = 1;
|
|
break;
|
|
default:
|
|
xMax = Mathf.CeilToInt((float)NodeList.Count / rows);
|
|
yMax = rows;
|
|
break;
|
|
}
|
|
|
|
float startOffsetX = (xMax * 0.5f) * CellWidth;
|
|
if (anchor == LayoutAnchor.BottomLeft || anchor == LayoutAnchor.UpperLeft || anchor == LayoutAnchor.MiddleLeft)
|
|
{
|
|
startOffsetX = anchorAlongAxis ? 0.5f * CellWidth : 0;
|
|
}
|
|
else if (anchor == LayoutAnchor.BottomRight || anchor == LayoutAnchor.UpperRight || anchor == LayoutAnchor.MiddleRight)
|
|
{
|
|
startOffsetX = anchorAlongAxis ? (xMax - 0.5f) * CellWidth : xMax * CellWidth;
|
|
}
|
|
|
|
float startOffsetY = (yMax * 0.5f) * CellHeight;
|
|
if (anchor == LayoutAnchor.UpperLeft || anchor == LayoutAnchor.UpperCenter || anchor == LayoutAnchor.UpperRight)
|
|
{
|
|
startOffsetY = anchorAlongAxis ? 0.5f * CellHeight : 0;
|
|
}
|
|
else if (anchor == LayoutAnchor.BottomLeft || anchor == LayoutAnchor.BottomCenter || anchor == LayoutAnchor.BottomRight)
|
|
{
|
|
startOffsetY = anchorAlongAxis ? (yMax - 0.5f) * CellHeight : yMax * CellHeight;
|
|
}
|
|
float alignmentOffsetX = 0;
|
|
float alignmentOffsetY = 0;
|
|
|
|
if (layout == LayoutOrder.ColumnThenRow)
|
|
{
|
|
for (int y = 0; y < yMax; y++)
|
|
{
|
|
for (int x = 0; x < xMax; x++)
|
|
{
|
|
if (y == yMax - 1)
|
|
{
|
|
switch (ColumnAlignment)
|
|
{
|
|
case LayoutHorizontalAlignment.Left:
|
|
alignmentOffsetX = 0;
|
|
break;
|
|
case LayoutHorizontalAlignment.Center:
|
|
alignmentOffsetX = CellWidth * ((xMax - (NodeList.Count % xMax)) % xMax) * 0.5f;
|
|
break;
|
|
case LayoutHorizontalAlignment.Right:
|
|
alignmentOffsetX = CellWidth * ((xMax - (NodeList.Count % xMax)) % xMax);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cellCounter < NodeList.Count)
|
|
{
|
|
grid[cellCounter].Set((-startOffsetX + (x * CellWidth) + HalfCell.x) + NodeList[cellCounter].Offset.x + alignmentOffsetX,
|
|
(startOffsetY - (y * CellHeight) - HalfCell.y) + NodeList[cellCounter].Offset.y + alignmentOffsetY,
|
|
0.0f);
|
|
}
|
|
cellCounter++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
for (int x = 0; x < xMax; x++)
|
|
{
|
|
for (int y = 0; y < yMax; y++)
|
|
{
|
|
if (x == xMax - 1)
|
|
{
|
|
switch (RowAlignment)
|
|
{
|
|
case LayoutVerticalAlignment.Top:
|
|
alignmentOffsetY = 0;
|
|
break;
|
|
case LayoutVerticalAlignment.Middle:
|
|
alignmentOffsetY = -CellHeight * ((yMax - (NodeList.Count % yMax)) % yMax) * 0.5f;
|
|
break;
|
|
case LayoutVerticalAlignment.Bottom:
|
|
alignmentOffsetY = -CellHeight * ((yMax - (NodeList.Count % yMax)) % yMax);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (cellCounter < NodeList.Count)
|
|
{
|
|
grid[cellCounter].Set((-startOffsetX + (x * CellWidth) + HalfCell.x) + NodeList[cellCounter].Offset.x + alignmentOffsetX,
|
|
(startOffsetY - (y * CellHeight) - HalfCell.y) + NodeList[cellCounter].Offset.y + alignmentOffsetY,
|
|
0.0f);
|
|
}
|
|
cellCounter++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the facing of a node given the nodes new position for facing origin with node and orientation type
|
|
/// </summary>
|
|
protected void UpdateNodeFacing(ObjectCollectionNode node)
|
|
{
|
|
Vector3 centerAxis;
|
|
Vector3 pointOnAxisNearestNode;
|
|
switch (OrientType)
|
|
{
|
|
case OrientationType.FaceOrigin:
|
|
node.Transform.rotation = Quaternion.LookRotation(node.Transform.position - transform.position, transform.up);
|
|
break;
|
|
|
|
case OrientationType.FaceOriginReversed:
|
|
node.Transform.rotation = Quaternion.LookRotation(transform.position - node.Transform.position, transform.up);
|
|
break;
|
|
|
|
case OrientationType.FaceCenterAxis:
|
|
centerAxis = Vector3.Project(node.Transform.position - transform.position, transform.up);
|
|
pointOnAxisNearestNode = transform.position + centerAxis;
|
|
node.Transform.rotation = Quaternion.LookRotation(node.Transform.position - pointOnAxisNearestNode, transform.up);
|
|
break;
|
|
|
|
case OrientationType.FaceCenterAxisReversed:
|
|
centerAxis = Vector3.Project(node.Transform.position - transform.position, transform.up);
|
|
pointOnAxisNearestNode = transform.position + centerAxis;
|
|
node.Transform.rotation = Quaternion.LookRotation(pointOnAxisNearestNode - node.Transform.position, transform.up);
|
|
break;
|
|
|
|
case OrientationType.FaceParentFoward:
|
|
node.Transform.forward = transform.rotation * Vector3.forward;
|
|
break;
|
|
|
|
case OrientationType.FaceParentForwardReversed:
|
|
node.Transform.forward = transform.rotation * Vector3.back;
|
|
break;
|
|
|
|
case OrientationType.FaceParentUp:
|
|
node.Transform.forward = transform.rotation * Vector3.up;
|
|
break;
|
|
|
|
case OrientationType.FaceParentDown:
|
|
node.Transform.forward = transform.rotation * Vector3.down;
|
|
break;
|
|
|
|
case OrientationType.None:
|
|
break;
|
|
|
|
default:
|
|
Debug.LogWarning("OrientationType out of range");
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Gizmos to draw when the Collection is selected.
|
|
protected virtual void OnDrawGizmosSelected()
|
|
{
|
|
Vector3 scale = (2f * radius) * Vector3.one;
|
|
switch (surfaceType)
|
|
{
|
|
case ObjectOrientationSurfaceType.Plane:
|
|
break;
|
|
case ObjectOrientationSurfaceType.Cylinder:
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawWireMesh(CylinderMesh, transform.position, transform.rotation, scale);
|
|
break;
|
|
case ObjectOrientationSurfaceType.Sphere:
|
|
Gizmos.color = Color.green;
|
|
Gizmos.DrawWireMesh(SphereMesh, transform.position, transform.rotation, scale);
|
|
break;
|
|
}
|
|
}
|
|
|
|
#if UNITY_EDITOR
|
|
private void Awake()
|
|
{
|
|
if (!EditorApplication.isPlaying)
|
|
{
|
|
if (assetVersion != CurrentAssetVersion)
|
|
{
|
|
Undo.RecordObject(this, "version patching");
|
|
PerformVersionPatching();
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
#region asset version migration
|
|
private const int CurrentAssetVersion = 1;
|
|
|
|
[SerializeField]
|
|
[HideInInspector]
|
|
private int assetVersion = 0;
|
|
|
|
private void PerformVersionPatching()
|
|
{
|
|
if (assetVersion == 0)
|
|
{
|
|
string friendlyName = GetUserFriendlyName();
|
|
|
|
// Migrate from version 0 to version 1
|
|
UpgradeAssetToVersion1();
|
|
assetVersion = 1;
|
|
}
|
|
assetVersion = CurrentAssetVersion;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Version 1 of GridObjectCollection introduced in MRTK 2.2 when
|
|
/// incorrect semantics of "ColumnsThenRows" layout was fixed.
|
|
/// See https://github.com/microsoft/MixedRealityToolkit-Unity/issues/6773#issuecomment-561918891
|
|
/// for details.
|
|
/// </summary>
|
|
private void UpgradeAssetToVersion1()
|
|
{
|
|
if (Layout == LayoutOrder.ColumnThenRow)
|
|
{
|
|
Layout = LayoutOrder.RowThenColumn;
|
|
var friendlyName = GetUserFriendlyName();
|
|
Debug.Log($"[MRTK 2.2 asset upgrade] Changing LayoutOrder for {friendlyName} from ColumnThenRow to RowThenColumn. See https://github.com/microsoft/MixedRealityToolkit-Unity/issues/6773#issuecomment-561918891 for details.");
|
|
}
|
|
}
|
|
|
|
private string GetUserFriendlyName()
|
|
{
|
|
string objectName = gameObject.name;
|
|
if (gameObject.transform.parent != null)
|
|
{
|
|
objectName += " (parent " + transform.parent.gameObject.name + ")";
|
|
}
|
|
|
|
return objectName;
|
|
}
|
|
#endregion
|
|
|
|
}
|
|
}
|