// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
namespace Microsoft.MixedReality.GraphicsTools.Editor
{
///
/// General purpose tool for viewing metric sizes of gameObjects.
///
public class MeasureTool
{
private MeasureToolSettings settings;
private List selectedObjects;
public List SelectedObjects { get => selectedObjects; set => selectedObjects = value; }
public MeasureTool(MeasureToolSettings toolSettings)
{
settings = toolSettings;
selectedObjects = new List();
}
///
/// Method calls the proper Draw method for GameObjects based on the current ToolMode setting.
///
/// An array of selected GameObjects to be measured
public void DrawMeasurement(GameObject[] gameObjects)
{
if (gameObjects.Length == 0) { return; }
switch (settings.Mode)
{
case MeasureToolSettings.ToolMode.Auto:
foreach (GameObject gameObject in gameObjects)
{
DrawAuto(gameObject);
}
break;
case MeasureToolSettings.ToolMode.Rect:
foreach (GameObject gameObject in gameObjects)
{
DrawRectMeasurement(gameObject);
}
break;
case MeasureToolSettings.ToolMode.Collider:
foreach (GameObject gameObject in gameObjects)
{
DrawColliderMeasurement(gameObject);
}
break;
case MeasureToolSettings.ToolMode.Renderer:
foreach (GameObject gameObject in gameObjects)
{
DrawRendererMeasurement(gameObject);
}
break;
case MeasureToolSettings.ToolMode.BetweenObjects:
DrawDistanceBetweenObjects(selectedObjects);
break;
}
}
///
/// The default mode of the tool, automatically detects the proper mode based on selected GameObject.
/// Using RectTransforms for 2D GameObjects and prefering Renderers to Colliders for 3D GameObjects
///
/// GameObject to be measured
private void DrawAuto(GameObject gameObject)
{
if (gameObject.GetComponent() != null)
{
DrawRectMeasurement(gameObject);
}
else if (gameObject.GetComponent() != null)
{
DrawRendererMeasurement(gameObject);
}
else
{
DrawColliderMeasurement(gameObject);
}
}
///
/// Gets 4 Vector3s used as corners of the box.
/// Calculates distance between 2 horizontal vectors and 2 vertical vectors.
/// Draws a box around the GameObject from an array of Vector3.
/// Draws handles and text around the box with measurement displayed.
///
/// GameObject to be measured, requires a RectTransform attached
private void DrawRectMeasurement(GameObject gameObject)
{
RectTransform rt = gameObject.GetComponent();
if (rt == null) return;
Vector3[] v = new Vector3[4];
switch (settings.Scale)
{
case MeasureToolSettings.ToolScale.Local:
rt.GetLocalCorners(v);
TransformPoints(gameObject, v);
break;
case MeasureToolSettings.ToolScale.World:
default:
rt.GetWorldCorners(v);
break;
}
Vector3 verticalOffset = gameObject.transform.right * settings.Offset;
Vector3 horizontalOffset = gameObject.transform.up * settings.Offset;
Handles.color = settings.LineColor;
DrawRectFromVectorArray(v);
DrawHandle(v[0], v[3], -horizontalOffset, DistanceInUnits(Vector3.Distance(v[0], v[3])), Color.red);
DrawHandle(v[3], v[2], verticalOffset, DistanceInUnits(Vector3.Distance(v[3], v[2])), Color.green);
}
///
/// Gets bounds from attached 3D collider and draws a box based on those bounds.
/// Draws handles and text around the box with measurement displayed.
///
/// GameObject to be measured, requires a 3D collider attached
private void DrawColliderMeasurement(GameObject gameObject)
{
Collider col = gameObject.GetComponent();
if (col == null) return;
Vector3[] array = VerticesFromBounds(col.bounds);
if (settings.Scale == MeasureToolSettings.ToolScale.Local)
{
if (col.GetType() == typeof(SphereCollider))
{
SphereCollider sphereCollider = (SphereCollider)col;
array = VerticesFromSize(sphereCollider.center, Vector3.one * sphereCollider.radius * 2);
TransformPoints(gameObject, array);
}
else if (col.GetType() == typeof(BoxCollider))
{
BoxCollider boxCol = (BoxCollider)col;
array = VerticesFromSize(boxCol.center, boxCol.size);
TransformPoints(gameObject, array);
}
else if (col.GetType() == typeof(CapsuleCollider))
{
CapsuleCollider capsuleCol = (CapsuleCollider)col;
array = VerticesFromSize(capsuleCol.center, new Vector3(capsuleCol.radius * 2f, capsuleCol.height, capsuleCol.radius * 2f));
TransformPoints(gameObject, array);
}
else if (col.GetType() == typeof(MeshCollider))
{
MeshCollider meshCol = (MeshCollider)col;
array = VerticesFromBounds(meshCol.sharedMesh.bounds);
TransformPoints(gameObject, array);
}
DrawCubeFromVectorArray(array, gameObject.transform);
return;
}
DrawCubeFromVectorArray(array, null);
}
///
///Gets bounds from attached Renderer and draws a box based on those bounds.
///Draws handles and text around the box with measurement displayed.
///
/// GameObject to be measured, requires a Renderer attached
private void DrawRendererMeasurement(GameObject gameObject)
{
Renderer rend = gameObject.GetComponent();
if (rend == null) return;
Handles.color = settings.LineColor;
#if UNITY_2021_2_OR_NEWER
Vector3[] array = VerticesFromBounds(rend.localBounds);
#else
UnityEngine.Mesh mesh = rend.GetComponent().sharedMesh;
Vector3[] array = VerticesFromBounds(mesh.bounds);
#endif
if (settings.Scale == MeasureToolSettings.ToolScale.Local)
{
TransformPoints(gameObject, array);
DrawCubeFromVectorArray(array, gameObject.transform);
}
else
{
array = VerticesFromBounds(rend.bounds);
DrawCubeFromVectorArray(array, null);
}
}
///
/// Utility method to transform a Vector3 array from local space to world space
///
///
///
private void TransformPoints(GameObject gameObject, Vector3[] array)
{
for (int i = 0; i < array.Length; i++)
{
array[i] = gameObject.transform.TransformPoint(array[i]);
}
}
///
/// Utility method for drawing a cube and handles from a Vector3[8]
/// Used instead of Gizmos.DrawWireCube for Oriented Bounding Box support
///
/// Vector3[8] containing all corner positions of the cube
/// Transform required to ensure handles rotate with cube
private void DrawCubeFromVectorArray(Vector3[] array, Transform attachedTransform)
{
Vector3 zOffset = Vector3.forward * settings.Offset * 10f;
Vector3 xOffset = Vector3.right * settings.Offset * 10f;
Vector3 diagonalOffset = (Vector3.right + Vector3.forward).normalized * settings.Offset * 10f;
DrawLine(array[5], array[1]);
DrawLine(array[1], array[7]);
DrawLine(array[7], array[3]);
DrawLine(array[3], array[5]);
DrawLine(array[2], array[6]);
DrawLine(array[6], array[4]);
DrawLine(array[4], array[0]);
DrawLine(array[0], array[2]);
DrawLine(array[5], array[2]);
DrawLine(array[1], array[6]);
DrawLine(array[7], array[4]);
DrawLine(array[3], array[0]);
if (attachedTransform != null)
{
zOffset = attachedTransform.forward * settings.Offset * 10f;
xOffset = attachedTransform.right * settings.Offset * 10f;
diagonalOffset = (attachedTransform.right + attachedTransform.forward).normalized * settings.Offset * 10f;
}
DrawHandle(array[4], array[0], -zOffset, DistanceInUnits(Vector3.Distance(array[4], array[0])), Color.red);
DrawHandle(array[0], array[2], -xOffset, DistanceInUnits(Vector3.Distance(array[0], array[2])), Color.blue);
DrawHandle(array[3], array[0], -diagonalOffset, DistanceInUnits(Vector3.Distance(array[3], array[0])), Color.green);
}
///
/// Utility method for drawing a rectangle from a Vector3[4]
///
/// Vector3[4] containing all corner positions of the rectangle
private void DrawRectFromVectorArray(Vector3[] array)
{
DrawLine(array[0], array[1]);
DrawLine(array[1], array[2]);
DrawLine(array[2], array[3]);
DrawLine(array[3], array[0]);
}
///
/// Draws a line and measurement between GameObjects
///
/// List of GameObjects to be measured
private void DrawDistanceBetweenObjects(List gameObjects)
{
for (int i = 1; i < gameObjects.Count; i++)
{
var pos1 = gameObjects[i].transform.position;
var pos2 = gameObjects[i - 1].transform.position;
Handles.color = settings.LineColor;
Handles.DrawAAPolyLine(EditorGUIUtility.whiteTexture, settings.LineThickness, pos1, pos2);
GUIStyle style = GetTextStyle();
Handles.Label(Vector3.Lerp(pos1, pos2, 0.5f), DistanceInUnits(Vector3.Distance(pos1, pos2)), style);
}
}
///
/// Utility method that returns a Vector3[8] from a Bounds
///
///
///
private Vector3[] VerticesFromBounds(Bounds bounds)
{
var vertices = new Vector3[8];
vertices[0] = bounds.min;
vertices[1] = bounds.max;
vertices[2] = new Vector3(vertices[0].x, vertices[0].y, vertices[1].z);
vertices[3] = new Vector3(vertices[0].x, vertices[1].y, vertices[0].z);
vertices[4] = new Vector3(vertices[1].x, vertices[0].y, vertices[0].z);
vertices[5] = new Vector3(vertices[0].x, vertices[1].y, vertices[1].z);
vertices[6] = new Vector3(vertices[1].x, vertices[0].y, vertices[1].z);
vertices[7] = new Vector3(vertices[1].x, vertices[1].y, vertices[0].z);
return vertices;
}
///
/// Utility method that given a position and a size, returns a Vector3[8] of points around the position
///
/// Position, used as the center of the cube
///
///
private Vector3[] VerticesFromSize(Vector3 pos, Vector3 size)
{
size = size * 0.5f;
size = pos + size;
var vertices = new Vector3[8];
vertices[0] = new Vector3(-size.x, -size.y, -size.z);
vertices[1] = new Vector3(size.x, size.y, size.z);
vertices[2] = new Vector3(-size.x, -size.y, size.z);
vertices[3] = new Vector3(-size.x, size.y, -size.z);
vertices[4] = new Vector3(size.x, -size.y, -size.z);
vertices[5] = new Vector3(-size.x, size.y, size.z);
vertices[6] = new Vector3(size.x, -size.y, size.z);
vertices[7] = new Vector3(size.x, size.y, -size.z);
return vertices;
}
///
/// Utility method to define a consistent Text style
///
/// Method returns a GUIStyle
private GUIStyle GetTextStyle()
{
GUIStyle style = new GUIStyle();
style.normal.textColor = settings.TextColor;
style.alignment = TextAnchor.MiddleCenter;
style.fontSize = (int)settings.TextSize;
style.fontStyle = FontStyle.Bold;
return style;
}
///
/// Utility method to format a float value as a measurement including the units.
///
///
///
private string DistanceInUnits(float val)
{
switch (settings.Unit)
{
case MeasureToolSettings.ToolUnits.Millimeter:
return $"{(val * 1000).ToString("0.##")} mm";
case MeasureToolSettings.ToolUnits.Centimeter:
return $"{(val * 100).ToString("0.##")} cm";
case MeasureToolSettings.ToolUnits.Meter:
default:
return $"{val.ToString("0.##")} m";
}
}
///
/// Utility method to Draw a colored line between two Vector3 positions
///
/// Line start position
/// Line end position
/// Line color
private void DrawLine(Vector3 start, Vector3 end, Color color)
{
Handles.color = color;
Handles.DrawAAPolyLine(EditorGUIUtility.whiteTexture, settings.LineThickness, start, end);
}
///
/// Utility method to Draw a line between two Vector3 positions
///
/// Line start position
/// Line end position
private void DrawLine(Vector3 start, Vector3 end)
{
DrawLine(start, end, settings.LineColor);
}
///
/// Utility method to Draw a colored line and some text between two Vector3 positions
///
/// Line start position
/// Line end position
/// Handle offset
/// Text rendered on the line
/// Line color
private void DrawHandle(Vector3 start, Vector3 end, Vector3 offset, string text, Color color)
{
DrawLine(start + offset, end + offset, color);
Handles.DrawDottedLines(new Vector3[] { start, start + offset }, 4f);
Handles.DrawDottedLines(new Vector3[] { end, end + offset }, 4f);
Handles.Label(Vector3.Lerp(start, end, 0.5f) + offset, text, GetTextStyle());
}
public void OnSelectionChanged()
{
if (Selection.count == 0)
{
selectedObjects.Clear();
return;
}
if (Selection.count == 1)
{
selectedObjects.Clear();
selectedObjects.Add(Selection.activeGameObject);
return;
}
foreach (var item in Selection.gameObjects)
{
if (!selectedObjects.Contains(item))
{
selectedObjects.Add(item);
}
}
//check for removed items
List objectsToRemove = new List();
foreach (var item in selectedObjects)
{
if (!Selection.Contains(item))
{
objectsToRemove.Add(item);
}
}
foreach (var item in objectsToRemove)
{
selectedObjects.Remove(item);
}
}
}
}