/// Credit jack.sydorenko, firagon
/// Sourced from - http://forum.unity3d.com/threads/new-ui-and-line-drawing.253772/
/// Updated/Refactored from - http://forum.unity3d.com/threads/new-ui-and-line-drawing.253772/#post-2528050
using System.Collections.Generic;
namespace UnityEngine.UI.Extensions
{
[AddComponentMenu("UI/Extensions/Primitives/UILineRenderer")]
[RequireComponent(typeof(RectTransform))]
public class UILineRenderer : UIPrimitiveBase
{
private enum SegmentType
{
Start,
Middle,
End,
Full,
}
public enum JoinType
{
Bevel,
Miter
}
public enum BezierType
{
None,
Quick,
Basic,
Improved,
Catenary,
}
private const float MIN_MITER_JOIN = 15 * Mathf.Deg2Rad;
// A bevel 'nice' join displaces the vertices of the line segment instead of simply rendering a
// quad to connect the endpoints. This improves the look of textured and transparent lines, since
// there is no overlapping.
private const float MIN_BEVEL_NICE_JOIN = 30 * Mathf.Deg2Rad;
private static Vector2 UV_TOP_LEFT, UV_BOTTOM_LEFT, UV_TOP_CENTER_LEFT, UV_TOP_CENTER_RIGHT, UV_BOTTOM_CENTER_LEFT, UV_BOTTOM_CENTER_RIGHT, UV_TOP_RIGHT, UV_BOTTOM_RIGHT;
private static Vector2[] startUvs, middleUvs, endUvs, fullUvs;
[SerializeField, Tooltip("Points to draw lines between\n Can be improved using the Resolution Option")]
internal Vector2[] m_points;
[SerializeField, Tooltip("Thickness of the line")]
internal float lineThickness = 2;
[SerializeField, Tooltip("Use the relative bounds of the Rect Transform (0,0 -> 0,1) or screen space coordinates")]
internal bool relativeSize;
[SerializeField, Tooltip("Do the points identify a single line or split pairs of lines")]
internal bool lineList;
[SerializeField, Tooltip("Add end caps to each line\nMultiple caps when used with Line List")]
internal bool lineCaps;
[SerializeField, Tooltip("Resolution of the Bezier curve, different to line Resolution")]
internal int bezierSegmentsPerCurve = 10;
public float LineThickness
{
get { return lineThickness; }
set { lineThickness = value; SetAllDirty(); }
}
public bool RelativeSize
{
get { return relativeSize; }
set { relativeSize = value; SetAllDirty(); }
}
public bool LineList
{
get { return lineList; }
set { lineList = value; SetAllDirty(); }
}
public bool LineCaps
{
get { return lineCaps; }
set { lineCaps = value; SetAllDirty(); }
}
[Tooltip("The type of Join used between lines, Square/Mitre or Curved/Bevel")]
public JoinType LineJoins = JoinType.Bevel;
[Tooltip("Bezier method to apply to line, see docs for options\nCan't be used in conjunction with Resolution as Bezier already changes the resolution")]
public BezierType BezierMode = BezierType.None;
public int BezierSegmentsPerCurve
{
get { return bezierSegmentsPerCurve; }
set { bezierSegmentsPerCurve = value; }
}
[HideInInspector]
public bool drivenExternally = false;
///
/// Points to be drawn in the line.
///
public Vector2[] Points
{
get
{
return m_points;
}
set
{
if (m_points == value)
return;
m_points = value;
SetAllDirty();
}
}
protected override void OnPopulateMesh(VertexHelper vh)
{
if (m_points == null)
return;
GeneratedUVs();
Vector2[] pointsToDraw = m_points;
//If Bezier is desired, pick the implementation
if (BezierMode != BezierType.None && BezierMode != BezierType.Catenary && m_points.Length > 3)
{
BezierPath bezierPath = new BezierPath();
bezierPath.SetControlPoints(pointsToDraw);
bezierPath.SegmentsPerCurve = bezierSegmentsPerCurve;
List drawingPoints;
switch (BezierMode)
{
case BezierType.Basic:
drawingPoints = bezierPath.GetDrawingPoints0();
break;
case BezierType.Improved:
drawingPoints = bezierPath.GetDrawingPoints1();
break;
default:
drawingPoints = bezierPath.GetDrawingPoints2();
break;
}
pointsToDraw = drawingPoints.ToArray();
}
if (BezierMode == BezierType.Catenary && m_points.Length == 2)
{
CableCurve cable = new CableCurve(pointsToDraw);
cable.slack = Resoloution;
cable.steps = BezierSegmentsPerCurve;
pointsToDraw = cable.Points();
}
if (ImproveResolution != ResolutionMode.None)
{
pointsToDraw = IncreaseResolution(pointsToDraw);
}
// scale based on the size of the rect or use absolute, this is switchable
var sizeX = !relativeSize ? 1 : rectTransform.rect.width;
var sizeY = !relativeSize ? 1 : rectTransform.rect.height;
var offsetX = -rectTransform.pivot.x * sizeX;
var offsetY = -rectTransform.pivot.y * sizeY;
vh.Clear();
// Generate the quads that make up the wide line
var segments = new List();
if (lineList)
{
for (var i = 1; i < pointsToDraw.Length; i += 2)
{
var start = pointsToDraw[i - 1];
var end = pointsToDraw[i];
start = new Vector2(start.x * sizeX + offsetX, start.y * sizeY + offsetY);
end = new Vector2(end.x * sizeX + offsetX, end.y * sizeY + offsetY);
if (lineCaps)
{
segments.Add(CreateLineCap(start, end, SegmentType.Start));
}
//segments.Add(CreateLineSegment(start, end, SegmentType.Full));
segments.Add(CreateLineSegment(start, end, SegmentType.Middle));
if (lineCaps)
{
segments.Add(CreateLineCap(start, end, SegmentType.End));
}
}
}
else
{
for (var i = 1; i < pointsToDraw.Length; i++)
{
var start = pointsToDraw[i - 1];
var end = pointsToDraw[i];
start = new Vector2(start.x * sizeX + offsetX, start.y * sizeY + offsetY);
end = new Vector2(end.x * sizeX + offsetX, end.y * sizeY + offsetY);
if (lineCaps && i == 1)
{
segments.Add(CreateLineCap(start, end, SegmentType.Start));
}
segments.Add(CreateLineSegment(start, end, SegmentType.Middle));
//segments.Add(CreateLineSegment(start, end, SegmentType.Full));
if (lineCaps && i == pointsToDraw.Length - 1)
{
segments.Add(CreateLineCap(start, end, SegmentType.End));
}
}
}
// Add the line segments to the vertex helper, creating any joins as needed
for (var i = 0; i < segments.Count; i++)
{
if (!lineList && i < segments.Count - 1)
{
var vec1 = segments[i][1].position - segments[i][2].position;
var vec2 = segments[i + 1][2].position - segments[i + 1][1].position;
var angle = Vector2.Angle(vec1, vec2) * Mathf.Deg2Rad;
// Positive sign means the line is turning in a 'clockwise' direction
var sign = Mathf.Sign(Vector3.Cross(vec1.normalized, vec2.normalized).z);
// Calculate the miter point
var miterDistance = lineThickness / (2 * Mathf.Tan(angle / 2));
var miterPointA = segments[i][2].position - vec1.normalized * miterDistance * sign;
var miterPointB = segments[i][3].position + vec1.normalized * miterDistance * sign;
var joinType = LineJoins;
if (joinType == JoinType.Miter)
{
// Make sure we can make a miter join without too many artifacts.
if (miterDistance < vec1.magnitude / 2 && miterDistance < vec2.magnitude / 2 && angle > MIN_MITER_JOIN)
{
segments[i][2].position = miterPointA;
segments[i][3].position = miterPointB;
segments[i + 1][0].position = miterPointB;
segments[i + 1][1].position = miterPointA;
}
else
{
joinType = JoinType.Bevel;
}
}
if (joinType == JoinType.Bevel)
{
if (miterDistance < vec1.magnitude / 2 && miterDistance < vec2.magnitude / 2 && angle > MIN_BEVEL_NICE_JOIN)
{
if (sign < 0)
{
segments[i][2].position = miterPointA;
segments[i + 1][1].position = miterPointA;
}
else
{
segments[i][3].position = miterPointB;
segments[i + 1][0].position = miterPointB;
}
}
var join = new UIVertex[] { segments[i][2], segments[i][3], segments[i + 1][0], segments[i + 1][1] };
vh.AddUIVertexQuad(join);
}
}
vh.AddUIVertexQuad(segments[i]);
}
if (vh.currentVertCount > 64000)
{
Debug.LogError("Max Verticies size is 64000, current mesh vertcies count is [" + vh.currentVertCount + "] - Cannot Draw");
vh.Clear();
return;
}
}
private UIVertex[] CreateLineCap(Vector2 start, Vector2 end, SegmentType type)
{
if (type == SegmentType.Start)
{
var capStart = start - ((end - start).normalized * lineThickness / 2);
return CreateLineSegment(capStart, start, SegmentType.Start);
}
else if (type == SegmentType.End)
{
var capEnd = end + ((end - start).normalized * lineThickness / 2);
return CreateLineSegment(end, capEnd, SegmentType.End);
}
Debug.LogError("Bad SegmentType passed in to CreateLineCap. Must be SegmentType.Start or SegmentType.End");
return null;
}
private UIVertex[] CreateLineSegment(Vector2 start, Vector2 end, SegmentType type)
{
Vector2 offset = new Vector2((start.y - end.y), end.x - start.x).normalized * lineThickness / 2;
var v1 = start - offset;
var v2 = start + offset;
var v3 = end + offset;
var v4 = end - offset;
//Return the VDO with the correct uvs
switch (type)
{
case SegmentType.Start:
return SetVbo(new[] { v1, v2, v3, v4 }, startUvs);
case SegmentType.End:
return SetVbo(new[] { v1, v2, v3, v4 }, endUvs);
case SegmentType.Full:
return SetVbo(new[] { v1, v2, v3, v4 }, fullUvs);
default:
return SetVbo(new[] { v1, v2, v3, v4 }, middleUvs);
}
}
protected override void GeneratedUVs()
{
if (activeSprite != null)
{
var outer = Sprites.DataUtility.GetOuterUV(activeSprite);
var inner = Sprites.DataUtility.GetInnerUV(activeSprite);
UV_TOP_LEFT = new Vector2(outer.x, outer.y);
UV_BOTTOM_LEFT = new Vector2(outer.x, outer.w);
UV_TOP_CENTER_LEFT = new Vector2(inner.x, inner.y);
UV_TOP_CENTER_RIGHT = new Vector2(inner.z, inner.y);
UV_BOTTOM_CENTER_LEFT = new Vector2(inner.x, inner.w);
UV_BOTTOM_CENTER_RIGHT = new Vector2(inner.z, inner.w);
UV_TOP_RIGHT = new Vector2(outer.z, outer.y);
UV_BOTTOM_RIGHT = new Vector2(outer.z, outer.w);
}
else
{
UV_TOP_LEFT = Vector2.zero;
UV_BOTTOM_LEFT = new Vector2(0, 1);
UV_TOP_CENTER_LEFT = new Vector2(0.5f, 0);
UV_TOP_CENTER_RIGHT = new Vector2(0.5f, 0);
UV_BOTTOM_CENTER_LEFT = new Vector2(0.5f, 1);
UV_BOTTOM_CENTER_RIGHT = new Vector2(0.5f, 1);
UV_TOP_RIGHT = new Vector2(1, 0);
UV_BOTTOM_RIGHT = Vector2.one;
}
startUvs = new[] { UV_TOP_LEFT, UV_BOTTOM_LEFT, UV_BOTTOM_CENTER_LEFT, UV_TOP_CENTER_LEFT };
middleUvs = new[] { UV_TOP_CENTER_LEFT, UV_BOTTOM_CENTER_LEFT, UV_BOTTOM_CENTER_RIGHT, UV_TOP_CENTER_RIGHT };
endUvs = new[] { UV_TOP_CENTER_RIGHT, UV_BOTTOM_CENTER_RIGHT, UV_BOTTOM_RIGHT, UV_TOP_RIGHT };
fullUvs = new[] { UV_TOP_LEFT, UV_BOTTOM_LEFT, UV_BOTTOM_RIGHT, UV_TOP_RIGHT };
}
protected override void ResolutionToNativeSize(float distance)
{
if (UseNativeSize)
{
m_Resolution = distance / (activeSprite.rect.width / pixelsPerUnit);
lineThickness = activeSprite.rect.height / pixelsPerUnit;
}
}
}
}