AR Design
UBC EML collab with UBC SALA - visualizing IoT data in AR
ObjectCollection.cs
Go to the documentation of this file.
1 // Copyright (c) Microsoft Corporation. All rights reserved.
2 // Licensed under the MIT License. See LICENSE in the project root for license information.
3 
4 using System;
5 using System.Collections.Generic;
6 using UnityEngine;
7 
8 namespace HoloToolkit.Unity.Collections
9 {
15  public class ObjectCollection : MonoBehaviour
16  {
17  #region public members
18  public Action<ObjectCollection> OnCollectionUpdated;
22 
26  [SerializeField]
27  public List<CollectionNode> NodeList = new List<CollectionNode>();
28 
32  [Tooltip("Type of surface to map the collection to")]
33  public SurfaceTypeEnum SurfaceType = SurfaceTypeEnum.Plane;
34 
38  [Tooltip("Type of sorting to use")]
39  public SortTypeEnum SortType = SortTypeEnum.None;
40 
44  [Tooltip("Should the objects in the collection be rotated / how should they be rotated")]
45  public OrientTypeEnum OrientType = OrientTypeEnum.FaceOrigin;
46 
50  [Tooltip("Whether to sort objects by row first or by column first")]
51  public LayoutTypeEnum LayoutType = LayoutTypeEnum.ColumnThenRow;
52 
56  public bool IgnoreInactiveTransforms = true;
57 
61  [Range(0.05f, 5.0f)]
62  [Tooltip("Radius for the sphere or cylinder")]
63  public float Radius = 2f;
64 
65  [SerializeField]
66  [Tooltip("Radial range for radial layout")]
67  [Range(5f, 360f)]
68  private float radialRange = 180f;
69 
73  public float RadialRange
74  {
75  get { return radialRange; }
76  set { radialRange = value; }
77  }
78 
82  [Tooltip("Number of rows per column")]
83  public int Rows = 3;
84 
88  [Tooltip("Width of cell per object")]
89  public float CellWidth = 0.5f;
90 
94  [Tooltip("Height of cell per object")]
95  public float CellHeight = 0.5f;
96 
97  [SerializeField]
98  [Tooltip("Margin between objects horizontally")]
99  private float horizontalMargin = 0.2f;
100 
104  public float HorizontalMargin
105  {
106  get { return horizontalMargin; }
107  set { horizontalMargin = value; }
108  }
109 
110  [SerializeField]
111  [Tooltip("Margin between objects vertically")]
112  private float verticalMargin = 0.2f;
113 
117  public float VerticalMargin
118  {
119  get { return verticalMargin; }
120  set { verticalMargin = value; }
121  }
122 
123  [SerializeField]
124  [Tooltip("Margin between objects in depth")]
125  private float depthMargin = 0.2f;
126 
130  public float DepthMargin
131  {
132  get { return depthMargin; }
133  set { depthMargin = value; }
134  }
135 
139  [HideInInspector]
140  public Mesh SphereMesh;
141 
145  [HideInInspector]
146  public Mesh CylinderMesh;
147 
148  public float Width { get; private set; }
149 
150  public float Height { get; private set; }
151  #endregion
152 
153  #region private variables
154  private int _columns;
155  private float _circumference;
156  private float _radialCellAngle;
157  private Vector2 _halfCell;
158  #endregion
159 
164  public void UpdateCollection()
165  {
166  // Check for empty nodes and remove them
167  List<CollectionNode> emptyNodes = new List<CollectionNode>();
168 
169  for (int i = 0; i < NodeList.Count; i++)
170  {
171  if (NodeList[i].transform == null || (IgnoreInactiveTransforms && !NodeList[i].transform.gameObject.activeSelf) || NodeList[i].transform.parent==null || !(NodeList[i].transform.parent.gameObject==this.gameObject))
172  {
173  emptyNodes.Add(NodeList[i]);
174  }
175  }
176 
177  // Now delete the empty nodes
178  for (int i = 0; i < emptyNodes.Count; i++)
179  {
180  NodeList.Remove(emptyNodes[i]);
181  }
182 
183  emptyNodes.Clear();
184 
185  // Check when children change and adjust
186  for (int i = 0; i < this.transform.childCount; i++)
187  {
188  Transform child = this.transform.GetChild(i);
189 
190  if (!ContainsNode(child) && (child.gameObject.activeSelf || !IgnoreInactiveTransforms))
191  {
192  CollectionNode node = new CollectionNode();
193 
194  node.Name = child.name;
195  node.transform = child;
196  NodeList.Add(node);
197  }
198  }
199 
200  switch (SortType)
201  {
202  case SortTypeEnum.None:
203  break;
204 
205  case SortTypeEnum.Transform:
206  NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.transform.GetSiblingIndex().CompareTo(c2.transform.GetSiblingIndex()); });
207  break;
208 
209  case SortTypeEnum.Alphabetical:
210  NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.Name.CompareTo(c2.Name); });
211  break;
212 
213  case SortTypeEnum.AlphabeticalReversed:
214  NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.Name.CompareTo(c2.Name); });
215  NodeList.Reverse();
216  break;
217 
218  case SortTypeEnum.TransformReversed:
219  NodeList.Sort(delegate (CollectionNode c1, CollectionNode c2) { return c1.transform.GetSiblingIndex().CompareTo(c2.transform.GetSiblingIndex()); });
220  NodeList.Reverse();
221  break;
222  }
223 
224  _columns = Mathf.CeilToInt((float)NodeList.Count / Rows);
225  Width = _columns * CellWidth;
226  Height = Rows * CellHeight;
227  _halfCell = new Vector2(CellWidth * 0.5f, CellHeight * 0.5f);
228  _circumference = 2f * Mathf.PI * Radius;
229  _radialCellAngle = RadialRange / _columns;
230 
231  LayoutChildren();
232 
233  if (OnCollectionUpdated != null)
234  {
235  OnCollectionUpdated.Invoke(this);
236  }
237  }
238 
242  private void LayoutChildren() {
243 
244  int cellCounter = 0;
245  float startOffsetX;
246  float startOffsetY;
247 
248  Vector3[] nodeGrid = new Vector3[NodeList.Count];
249  Vector3 newPos = Vector3.zero;
250 
251  // Now lets lay out the grid
252  startOffsetX = (_columns * 0.5f) * CellWidth;
253  startOffsetY = (Rows * 0.5f) * CellHeight;
254 
255  cellCounter = 0;
256 
257  // First start with a grid then project onto surface
258  switch (LayoutType)
259  {
260  case LayoutTypeEnum.ColumnThenRow:
261  default:
262  for (int c = 0; c < _columns; c++)
263  {
264  for (int r = 0; r < Rows; r++)
265  {
266  if (cellCounter < NodeList.Count)
267  {
268  nodeGrid[cellCounter] = new Vector3((c * CellWidth) - startOffsetX + _halfCell.x, -(r * CellHeight) + startOffsetY - _halfCell.y, 0f) + (Vector3)((NodeList[cellCounter])).Offset;
269  }
270  cellCounter++;
271  }
272  }
273  break;
274 
275  case LayoutTypeEnum.RowThenColumn:
276  for (int r = 0; r < Rows; r++)
277  {
278  for (int c = 0; c < _columns; c++)
279  {
280  if (cellCounter < NodeList.Count)
281  {
282  nodeGrid[cellCounter] = new Vector3((c * CellWidth) - startOffsetX + _halfCell.x, -(r * CellHeight) + startOffsetY - _halfCell.y, 0f) + (Vector3)((NodeList[cellCounter])).Offset;
283  }
284  cellCounter++;
285  }
286  }
287  break;
288 
289  }
290 
291  switch (SurfaceType) {
292  case SurfaceTypeEnum.Plane:
293  for (int i = 0; i < NodeList.Count; i++)
294  {
295  newPos = nodeGrid[i];
296  NodeList[i].transform.localPosition = newPos;
297  UpdateNodeFacing(NodeList[i], OrientType, newPos);
298  }
299  break;
300  case SurfaceTypeEnum.Cylinder:
301  for (int i = 0; i < NodeList.Count; i++)
302  {
303  newPos = CylindricalMapping(nodeGrid[i], Radius);
304  NodeList[i].transform.localPosition = newPos;
305  UpdateNodeFacing(NodeList[i], OrientType, newPos);
306  }
307  break;
308  case SurfaceTypeEnum.Sphere:
309 
310  for (int i = 0; i < NodeList.Count; i++)
311  {
312  newPos = SphericalMapping(nodeGrid[i], Radius);
313  NodeList[i].transform.localPosition = newPos;
314  UpdateNodeFacing(NodeList[i], OrientType, newPos);
315  }
316  break;
317  case SurfaceTypeEnum.Radial:
318  int curColumn = 0;
319  int curRow = 1;
320 
321  for (int i = 0; i < NodeList.Count; i++)
322  {
323  newPos = RadialMapping(nodeGrid[i], Radius, curRow, curColumn);
324  if (curColumn == (_columns - 1))
325  {
326  curColumn = 0;
327  ++curRow;
328  }
329  else
330  {
331  ++curColumn;
332  }
333 
334  NodeList[i].transform.localPosition = newPos;
335  UpdateNodeFacing(NodeList[i], OrientType, newPos);
336  }
337  break;
338  case SurfaceTypeEnum.Scatter:
339  // Get randomized planar mapping
340  // Calculate radius of each node while we're here
341  // Then use the packer function to shift them into place
342  for (int i = 0; i < NodeList.Count; i++)
343  {
344  newPos = ScatterMapping (nodeGrid[i], Radius);
345  Collider nodeCollider = NodeList[i].transform.GetComponentInChildren<Collider>();
346  if (nodeCollider != null)
347  {
348  // Make the radius the largest of the object's dimensions to avoid overlap
349  Bounds bounds = nodeCollider.bounds;
350  NodeList[i].Radius = Mathf.Max (Mathf.Max(bounds.size.x, bounds.size.y), bounds.size.z) * 0.5f;
351  }
352  else
353  {
354  // Make the radius a default value
355  // TODO move this into a public field ?
356  NodeList[i].Radius = 1f;
357  }
358  NodeList[i].transform.localPosition = newPos;
359  UpdateNodeFacing(NodeList[i], OrientType, newPos);
360  }
361 
362  // Iterate [x] times
363  // TODO move center, iterations and padding into a public field
364  for (int i = 0; i < 100; i++)
365  {
366  IterateScatterPacking (NodeList, Radius);
367  }
368  break;
369  }
370  }
371 
378  private void UpdateNodeFacing(CollectionNode node, OrientTypeEnum orientType, Vector3 newPos = default(Vector3))
379  {
380  Vector3 centerAxis;
381  Vector3 pointOnAxisNearestNode;
382  switch (OrientType)
383  {
384  case OrientTypeEnum.FaceOrigin:
385  node.transform.rotation = Quaternion.LookRotation(node.transform.position - this.transform.position, this.transform.up);
386  break;
387 
388  case OrientTypeEnum.FaceOriginReversed:
389  node.transform.rotation = Quaternion.LookRotation(this.transform.position - node.transform.position, this.transform.up);
390  break;
391 
392  case OrientTypeEnum.FaceCenterAxis:
393  centerAxis = Vector3.Project(node.transform.position - this.transform.position, this.transform.up);
394  pointOnAxisNearestNode = this.transform.position + centerAxis;
395  node.transform.rotation = Quaternion.LookRotation(node.transform.position - pointOnAxisNearestNode, this.transform.up);
396  break;
397 
398  case OrientTypeEnum.FaceCenterAxisReversed:
399  centerAxis = Vector3.Project(node.transform.position - this.transform.position, this.transform.up);
400  pointOnAxisNearestNode = this.transform.position + centerAxis;
401  node.transform.rotation = Quaternion.LookRotation(pointOnAxisNearestNode - node.transform.position, this.transform.up);
402  break;
403 
404  case OrientTypeEnum.FaceFoward:
405  node.transform.forward = transform.rotation * Vector3.forward;
406  break;
407 
408  case OrientTypeEnum.FaceForwardReversed:
409  node.transform.forward = transform.rotation * Vector3.back;
410  break;
411 
412  case OrientTypeEnum.FaceParentUp:
413  node.transform.forward = transform.rotation * Vector3.up;
414  break;
415 
416  case OrientTypeEnum.FaceParentDown:
417  node.transform.forward = transform.rotation * Vector3.down;
418  break;
419 
420  case OrientTypeEnum.None:
421  break;
422 
423  default:
424  throw new ArgumentOutOfRangeException();
425  }
426  }
427 
428 
435  private Vector3 SphericalMapping(Vector3 source, float radius)
436  {
437  Radius = radius >= 0 ? Radius : radius;
438  Vector3 newPos = new Vector3(0f, 0f, Radius);
439 
440  float xAngle = (source.x / _circumference) * 360f;
441  float yAngle = -(source.y / _circumference) * 360f;
442 
443  Quaternion rot = Quaternion.Euler(yAngle, xAngle, 0.0f);
444  newPos = rot * newPos;
445 
446  return newPos;
447  }
448 
455  private Vector3 CylindricalMapping(Vector3 source, float radius)
456  {
457  Radius = radius >= 0 ? Radius : radius;
458  Vector3 newPos = new Vector3(0f, source.y, Radius);
459 
460  float xAngle = (source.x / _circumference) * 360f;
461 
462  Quaternion rot = Quaternion.Euler(0.0f, xAngle, 0.0f);
463  newPos = rot * newPos;
464 
465  return newPos;
466  }
467 
476  private Vector3 RadialMapping(Vector3 source, float radius, int row, int column)
477  {
478  Radius = radius >= 0 ? Radius : radius;
479 
480  source.x = 0f;
481  source.y = 0f;
482  source.z = (Radius / Rows) * row;
483 
484  float yAngle = _radialCellAngle * (column - (_columns * 0.5f)) + (_radialCellAngle * .5f);
485 
486  Quaternion rot = Quaternion.Euler(0.0f, yAngle, 0.0f);
487  source = rot * source;
488 
489  return source;
490  }
491 
497  private bool ContainsNode(Transform node)
498  {
499  for (int i = 0; i < NodeList.Count; i++)
500  {
501  if (NodeList[i] != null)
502  {
503  if (NodeList[i].transform == node)
504  {
505  return true;
506  }
507  }
508  }
509  return false;
510  }
511 
518  private Vector3 ScatterMapping(Vector3 source, float radius)
519  {
520  source.x = UnityEngine.Random.Range(-radius, radius);
521  source.y = UnityEngine.Random.Range(-radius, radius);
522  return source;
523  }
524 
525 
531  private void IterateScatterPacking(List<CollectionNode> nodes, float radiusPadding)
532  {
533  // Sort by closest to center (don't worry about z axis)
534  // Use the position of the collection as the packing center
535  nodes.Sort(delegate (CollectionNode circle1, CollectionNode circle2) {
536  float distance1 = (circle1.transform.localPosition).sqrMagnitude;
537  float distance2 = (circle2.transform.localPosition).sqrMagnitude;
538  return distance1.CompareTo(distance2);
539  });
540 
541  Vector3 difference;
542  Vector2 difference2D;
543 
544  // Move them closer together
545  float radiusPaddingSquared = Mathf.Pow(radiusPadding, 2f);
546 
547  for (int i = 0; i < nodes.Count - 1; i++)
548  {
549  for (int j = i + 1; j < nodes.Count; j++)
550  {
551  if (i != j)
552  {
553  difference = nodes[j].transform.localPosition - nodes[i].transform.localPosition;
554  // Ignore Z axis
555  difference2D.x = difference.x;
556  difference2D.y = difference.y;
557  float combinedRadius = nodes[i].Radius + nodes[j].Radius;
558  float distance = difference2D.SqrMagnitude() - radiusPaddingSquared;
559  float minSeparation = Mathf.Min(distance, radiusPaddingSquared);
560  distance -= minSeparation;
561 
562  if (distance < (Mathf.Pow(combinedRadius, 2)))
563  {
564  difference2D.Normalize();
565  difference *= ((combinedRadius - Mathf.Sqrt(distance)) * 0.5f);
566  nodes[j].transform.localPosition += difference;
567  nodes[i].transform.localPosition -= difference;
568  }
569  }
570  }
571  }
572  }
573 
577  protected virtual void OnDrawGizmosSelected()
578  {
579  Vector3 scale = (2f * Radius) * Vector3.one;
580  switch (SurfaceType)
581  {
582  case SurfaceTypeEnum.Plane:
583  break;
584  case SurfaceTypeEnum.Cylinder:
585  Gizmos.color = Color.green;
586  Gizmos.DrawWireMesh(CylinderMesh, transform.position, transform.rotation, scale);
587  break;
588  case SurfaceTypeEnum.Sphere:
589  Gizmos.color = Color.green;
590  Gizmos.DrawWireMesh(SphereMesh, transform.position, transform.rotation, scale);
591  break;
592  }
593  }
594  }
595 }
SortTypeEnum
Sorting type for collections
Definition: SortTypeEnum.cs:9
Mesh CylinderMesh
Reference mesh to use for rendering the cylinder layout
An Object Collection is simply a set of child objects organized with some layout parameters. The object collection can be used to quickly create control panels or sets of prefab/objects.
SurfaceTypeEnum
The type of surface to map the collect to.
Collection node is a data storage class for individual data about an object in the collection...
void UpdateCollection()
Update collection is called from the editor button on the inspector. This function rebuilds / updates...
Mesh SphereMesh
Reference mesh to use for rendering the sphere layout
LayoutTypeEnum
Collection layout type enum
virtual void OnDrawGizmosSelected()
Gizmos to draw when the Collection is selected.
OrientTypeEnum
Orientation type enum for collections