mixedreality/com.microsoft.mixedreality..../SDK/Features/UX/Scripts/Collections/GridObjectCollection.cs

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
}
}