using System; using UnityEngine; using System.Reflection; namespace UnityEditor.Rendering { /// /// Provide a gizmo/handle representing a box where all face can be moved independently. /// Also add a contained sub gizmo/handle box if contained is used at creation. /// /// /// /// class MyComponentEditor : Editor /// { /// static HierarchicalBox box; /// static HierarchicalBox containedBox; /// /// static MyComponentEditor() /// { /// Color[] handleColors = new Color[] /// { /// Color.red, /// Color.green, /// Color.Blue, /// new Color(0.5f, 0f, 0f, 1f), /// new Color(0f, 0.5f, 0f, 1f), /// new Color(0f, 0f, 0.5f, 1f) /// }; /// box = new HierarchicalBox(new Color(1f, 1f, 1f, 0.25), handleColors); /// containedBox = new HierarchicalBox(new Color(1f, 0f, 1f, 0.25), handleColors, container: box); /// } /// /// [DrawGizmo(GizmoType.Selected|GizmoType.Active)] /// void DrawGizmo(MyComponent comp, GizmoType gizmoType) /// { /// box.center = comp.transform.position; /// box.size = comp.transform.scale; /// box.DrawHull(gizmoType == GizmoType.Selected); /// /// containedBox.center = comp.innerposition; /// containedBox.size = comp.innerScale; /// containedBox.DrawHull(gizmoType == GizmoType.Selected); /// } /// /// void OnSceneGUI() /// { /// EditorGUI.BeginChangeCheck(); /// /// //container box must be also set for contained box for clamping /// box.center = comp.transform.position; /// box.size = comp.transform.scale; /// box.DrawHandle(); /// /// containedBox.DrawHandle(); /// containedBox.center = comp.innerposition; /// containedBox.size = comp.innerScale; /// /// if(EditorGUI.EndChangeCheck()) /// { /// comp.innerposition = containedBox.center; /// comp.innersize = containedBox.size; /// } /// } /// } /// /// public class HierarchicalBox { const float k_HandleSizeCoef = 0.05f; static Material k_Material_Cache; static Material k_Material => (k_Material_Cache == null || k_Material_Cache.Equals(null) ? (k_Material_Cache = new Material(Shader.Find("Hidden/UnlitTransparentColored"))) : k_Material_Cache); static Mesh k_MeshQuad_Cache; static Mesh k_MeshQuad => k_MeshQuad_Cache == null || k_MeshQuad_Cache.Equals(null) ? (k_MeshQuad_Cache = Resources.GetBuiltinResource("Quad.fbx")) : k_MeshQuad_Cache; enum NamedFace { Right, Top, Front, Left, Bottom, Back, None } Material m_Material; readonly Color[] m_PolychromeHandleColor; readonly HierarchicalBox m_Parent; Color m_MonochromeFillColor; Color m_MonochromeHandleColor; Color m_WireframeColor; Color m_WireframeColorBehind; int[] m_ControlIDs = new int[6] { 0, 0, 0, 0, 0, 0 }; bool m_MonoHandle = true; Material material { get { if (m_Material == null || m_Material.Equals(null)) m_Material = new Material(k_Material); //material can be lost when exiting play mode so gather the color again when reconstructing it m_Material.color = m_MonochromeFillColor; return m_Material; } } /// /// Allow to switch between the mode where all axis are controlled together or not /// Note that if there is several handles, they will use the polychrome colors. /// public bool monoHandle { get => m_MonoHandle; set => m_MonoHandle = value; } /// The position of the center of the box in Handle.matrix space. public Vector3 center { get; set; } /// The size of the box in Handle.matrix space. public Vector3 size { get; set; } /// The baseColor used to fill hull. All other colors are deduced from it except specific handle colors. public Color baseColor { get { return material.color; } set { value.a = 8f / 255; m_MonochromeFillColor = value; material.color = m_MonochromeFillColor; value.a = 1f; m_MonochromeHandleColor = value; value.a = 0.7f; m_WireframeColor = value; value.a = 0.2f; m_WireframeColorBehind = value; } } //Note: Handles.Slider not allow to use a specific ControlID. //Thus Slider1D is used (with reflection) static Type k_Slider1D = Type.GetType("UnityEditorInternal.Slider1D, UnityEditor"); static MethodInfo k_Slider1D_Do = k_Slider1D .GetMethod( "Do", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public, null, CallingConventions.Any, new[] { typeof(int), typeof(Vector3), typeof(Vector3), typeof(float), typeof(Handles.CapFunction), typeof(float) }, null); static void Slider1D(int controlID, ref Vector3 handlePosition, Vector3 handleOrientation, float snapScale, Color color) { using (new Handles.DrawingScope(color)) { handlePosition = (Vector3)k_Slider1D_Do.Invoke(null, new object[] { controlID, handlePosition, handleOrientation, HandleUtility.GetHandleSize(handlePosition) * k_HandleSizeCoef, new Handles.CapFunction(Handles.DotHandleCap), snapScale }); } } /// Constructor. Used to setup colors and also the container if any. /// The color of each face of the box. Other colors are deduced from it. /// The color of handle when they are separated. When they are grouped, they use a variation of the faceColor instead. /// The HierarchicalBox containing this box. If null, the box will not be limited in size. public HierarchicalBox(Color baseColor, Color[] polychromeHandleColors = null, HierarchicalBox parent = null) { if (polychromeHandleColors != null && polychromeHandleColors.Length != 6) throw new ArgumentException("polychromeHandleColors must be null or have a size of 6."); m_Parent = parent; m_Material = new Material(k_Material); this.baseColor = baseColor; m_PolychromeHandleColor = polychromeHandleColors ?? new Color[] { Handles.xAxisColor, Handles.yAxisColor, Handles.zAxisColor, Handles.xAxisColor, Handles.yAxisColor, Handles.zAxisColor }; } /// Draw the hull which means the boxes without the handles /// If true, also fill the faces of the hull public void DrawHull(bool filled) { Color previousColor = Handles.color; if (filled) { // Draw the hull var xSize = new Vector3(size.z, size.y, 1f); material.SetPass(0); Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.x * .5f * Vector3.left, Quaternion.FromToRotation(Vector3.forward, Vector3.left), xSize)); Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.x * .5f * Vector3.right, Quaternion.FromToRotation(Vector3.forward, Vector3.right), xSize)); var ySize = new Vector3(size.x, size.z, 1f); Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.y * .5f * Vector3.up, Quaternion.FromToRotation(Vector3.forward, Vector3.up), ySize)); Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.y * .5f * Vector3.down, Quaternion.FromToRotation(Vector3.forward, Vector3.down), ySize)); var zSize = new Vector3(size.x, size.y, 1f); Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.z * .5f * Vector3.forward, Quaternion.identity, zSize)); Graphics.DrawMeshNow(k_MeshQuad, Handles.matrix * Matrix4x4.TRS(center + size.z * .5f * Vector3.back, Quaternion.FromToRotation(Vector3.forward, Vector3.back), zSize)); //if as a parent, also draw handle distance to the parent if (m_Parent != null) { var centerDiff = center - m_Parent.center; var xRecal = centerDiff; var yRecal = centerDiff; var zRecal = centerDiff; xRecal.x = 0; yRecal.y = 0; zRecal.z = 0; Handles.color = GetHandleColor(NamedFace.Left); Handles.DrawLine(m_Parent.center + xRecal + m_Parent.size.x * .5f * Vector3.left, center + size.x * .5f * Vector3.left); Handles.color = GetHandleColor(NamedFace.Right); Handles.DrawLine(m_Parent.center + xRecal + m_Parent.size.x * .5f * Vector3.right, center + size.x * .5f * Vector3.right); Handles.color = GetHandleColor(NamedFace.Top); Handles.DrawLine(m_Parent.center + yRecal + m_Parent.size.y * .5f * Vector3.up, center + size.y * .5f * Vector3.up); Handles.color = GetHandleColor(NamedFace.Bottom); Handles.DrawLine(m_Parent.center + yRecal + m_Parent.size.y * .5f * Vector3.down, center + size.y * .5f * Vector3.down); Handles.color = GetHandleColor(NamedFace.Front); Handles.DrawLine(m_Parent.center + zRecal + m_Parent.size.z * .5f * Vector3.forward, center + size.z * .5f * Vector3.forward); Handles.color = GetHandleColor(NamedFace.Back); Handles.DrawLine(m_Parent.center + zRecal + m_Parent.size.z * .5f * Vector3.back, center + size.z * .5f * Vector3.back); } } Handles.color = m_WireframeColor; Handles.zTest = UnityEngine.Rendering.CompareFunction.LessEqual; Handles.DrawWireCube(center, size); Handles.color = m_WireframeColorBehind; Handles.zTest = UnityEngine.Rendering.CompareFunction.Greater; Handles.DrawWireCube(center, size); Handles.zTest = UnityEngine.Rendering.CompareFunction.Always; Handles.color = previousColor; } /// Draw the manipulable handles public void DrawHandle() { Event evt = Event.current; bool useHomothety = evt.shift; bool useSymetry = evt.alt || evt.command; // Note: snapping is handled natively on ctrl for each Slider1D for (int i = 0, count = m_ControlIDs.Length; i < count; ++i) m_ControlIDs[i] = GUIUtility.GetControlID("HierarchicalBox".GetHashCode() + i, FocusType.Passive); var leftPosition = center + size.x * .5f * Vector3.left; var rightPosition = center + size.x * .5f * Vector3.right; var topPosition = center + size.y * .5f * Vector3.up; var bottomPosition = center + size.y * .5f * Vector3.down; var frontPosition = center + size.z * .5f * Vector3.forward; var backPosition = center + size.z * .5f * Vector3.back; var theChangedFace = NamedFace.None; EditorGUI.BeginChangeCheck(); EditorGUI.BeginChangeCheck(); Slider1D(m_ControlIDs[(int)NamedFace.Left], ref leftPosition, Vector3.left, EditorSnapSettings.scale, GetHandleColor(NamedFace.Left)); if (EditorGUI.EndChangeCheck()) theChangedFace = NamedFace.Left; EditorGUI.BeginChangeCheck(); Slider1D(m_ControlIDs[(int)NamedFace.Right], ref rightPosition, Vector3.right, EditorSnapSettings.scale, GetHandleColor(NamedFace.Right)); if (EditorGUI.EndChangeCheck()) theChangedFace = NamedFace.Right; EditorGUI.BeginChangeCheck(); Slider1D(m_ControlIDs[(int)NamedFace.Top], ref topPosition, Vector3.up, EditorSnapSettings.scale, GetHandleColor(NamedFace.Top)); if (EditorGUI.EndChangeCheck()) theChangedFace = NamedFace.Top; EditorGUI.BeginChangeCheck(); Slider1D(m_ControlIDs[(int)NamedFace.Bottom], ref bottomPosition, Vector3.down, EditorSnapSettings.scale, GetHandleColor(NamedFace.Bottom)); if (EditorGUI.EndChangeCheck()) theChangedFace = NamedFace.Bottom; EditorGUI.BeginChangeCheck(); Slider1D(m_ControlIDs[(int)NamedFace.Front], ref frontPosition, Vector3.forward, EditorSnapSettings.scale, GetHandleColor(NamedFace.Front)); if (EditorGUI.EndChangeCheck()) theChangedFace = NamedFace.Front; EditorGUI.BeginChangeCheck(); Slider1D(m_ControlIDs[(int)NamedFace.Back], ref backPosition, Vector3.back, EditorSnapSettings.scale, GetHandleColor(NamedFace.Back)); if (EditorGUI.EndChangeCheck()) theChangedFace = NamedFace.Back; if (EditorGUI.EndChangeCheck()) { float delta = 0f; switch (theChangedFace) { case NamedFace.Left: delta = (leftPosition - center - size.x * .5f * Vector3.left).x; break; case NamedFace.Right: delta = -(rightPosition - center - size.x * .5f * Vector3.right).x; break; case NamedFace.Top: delta = -(topPosition - center - size.y * .5f * Vector3.up).y; break; case NamedFace.Bottom: delta = (bottomPosition - center - size.y * .5f * Vector3.down).y; break; case NamedFace.Front: delta = -(frontPosition - center - size.z * .5f * Vector3.forward).z; break; case NamedFace.Back: delta = (backPosition - center - size.z * .5f * Vector3.back).z; break; } if (monoHandle || useHomothety && useSymetry) { var tempSize = size - Vector3.one * delta; //ensure that the box face are still facing outside for (int axis = 0; axis < 3; ++axis) { if (tempSize[axis] < 0) { delta += tempSize[axis]; tempSize = size - Vector3.one * delta; } } //ensure containedBox do not exit container if (m_Parent != null) { for (int axis = 0; axis < 3; ++axis) { if (tempSize[axis] > m_Parent.size[axis]) tempSize[axis] = m_Parent.size[axis]; } } size = tempSize; } else { if (useSymetry) { switch (theChangedFace) { case NamedFace.Left: rightPosition.x -= delta; break; case NamedFace.Right: leftPosition.x += delta; break; case NamedFace.Top: bottomPosition.y += delta; break; case NamedFace.Bottom: topPosition.y -= delta; break; case NamedFace.Front: backPosition.z += delta; break; case NamedFace.Back: frontPosition.z -= delta; break; } //ensure that the box face are still facing outside switch (theChangedFace) { case NamedFace.Left: case NamedFace.Right: if (rightPosition.x < leftPosition.x) rightPosition.x = leftPosition.x = center.x; break; case NamedFace.Top: case NamedFace.Bottom: if (topPosition.y < bottomPosition.y) topPosition.y = bottomPosition.y = center.y; break; case NamedFace.Front: case NamedFace.Back: if (frontPosition.z < backPosition.z) frontPosition.z = backPosition.z = center.z; break; } } if (useHomothety) { float halfDelta = delta * 0.5f; switch (theChangedFace) { case NamedFace.Left: case NamedFace.Right: bottomPosition.y += halfDelta; topPosition.y -= halfDelta; backPosition.z += halfDelta; frontPosition.z -= halfDelta; break; case NamedFace.Top: case NamedFace.Bottom: rightPosition.x -= halfDelta; leftPosition.x += halfDelta; backPosition.z += halfDelta; frontPosition.z -= halfDelta; break; case NamedFace.Front: case NamedFace.Back: rightPosition.x -= halfDelta; leftPosition.x += halfDelta; bottomPosition.y += halfDelta; topPosition.y -= halfDelta; break; } //ensure that the box face are still facing outside switch (theChangedFace) { case NamedFace.Left: if (rightPosition.x < leftPosition.x) leftPosition.x = rightPosition.x; if (topPosition.y < bottomPosition.y) topPosition.y = bottomPosition.y = center.y; if (frontPosition.z < backPosition.z) frontPosition.z = backPosition.z = center.z; break; case NamedFace.Right: if (rightPosition.x < leftPosition.x) rightPosition.x = leftPosition.x; if (topPosition.y < bottomPosition.y) topPosition.y = bottomPosition.y = center.y; if (frontPosition.z < backPosition.z) frontPosition.z = backPosition.z = center.z; break; case NamedFace.Top: if (topPosition.y < bottomPosition.y) topPosition.y = bottomPosition.y; if (rightPosition.x < leftPosition.x) rightPosition.x = leftPosition.x = center.x; if (frontPosition.z < backPosition.z) frontPosition.z = backPosition.z = center.z; break; case NamedFace.Bottom: if (topPosition.y < bottomPosition.y) bottomPosition.y = topPosition.y; if (rightPosition.x < leftPosition.x) rightPosition.x = leftPosition.x = center.x; if (frontPosition.z < backPosition.z) frontPosition.z = backPosition.z = center.z; break; case NamedFace.Front: if (frontPosition.z < backPosition.z) frontPosition.z = backPosition.z; if (rightPosition.x < leftPosition.x) rightPosition.x = leftPosition.x = center.x; if (topPosition.y < bottomPosition.y) topPosition.y = bottomPosition.y = center.y; break; case NamedFace.Back: if (frontPosition.z < backPosition.z) backPosition.z = frontPosition.z; if (rightPosition.x < leftPosition.x) rightPosition.x = leftPosition.x = center.x; if (topPosition.y < bottomPosition.y) topPosition.y = bottomPosition.y = center.y; break; } } var max = new Vector3(rightPosition.x, topPosition.y, frontPosition.z); var min = new Vector3(leftPosition.x, bottomPosition.y, backPosition.z); if (!useSymetry && !useHomothety) { //ensure that the box face are still facing outside for (int axis = 0; axis < 3; ++axis) { if (min[axis] > max[axis]) { // Control IDs in m_ControlIDs[0-3[ are for positive axes if (GUIUtility.hotControl == m_ControlIDs[axis]) max[axis] = min[axis]; else min[axis] = max[axis]; } } } //ensure containedBox do not exit container if (m_Parent != null) { for (int axis = 0; axis < 3; ++axis) { if (min[axis] < m_Parent.center[axis] - m_Parent.size[axis] * 0.5f) min[axis] = m_Parent.center[axis] - m_Parent.size[axis] * 0.5f; if (max[axis] > m_Parent.center[axis] + m_Parent.size[axis] * 0.5f) max[axis] = m_Parent.center[axis] + m_Parent.size[axis] * 0.5f; } } center = (max + min) * .5f; size = max - min; } } } Color GetHandleColor(NamedFace name) => monoHandle ? m_MonochromeHandleColor : m_PolychromeHandleColor[(int)name]; } }