862 lines
42 KiB
C#
862 lines
42 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace ToolBuddy.ThirdParty.VectorGraphics
|
|
{
|
|
public static partial class VectorUtils
|
|
{
|
|
/// <summary>
|
|
/// Structure to store the tessellation options.
|
|
/// </summary>
|
|
public struct TessellationOptions
|
|
{
|
|
private float m_MaxCordDev, m_MaxCordDevSq, m_MaxTanAngleDev, m_MaxTanAngleDevCosine, m_StepSize;
|
|
|
|
/// <summary>
|
|
/// The uniform tessellation step distance.
|
|
/// </summary>
|
|
public float StepDistance { get; set; } // A split to happen uniformly at fixed distances
|
|
|
|
/// <summary>
|
|
/// The maximum distance on the cord to a straight line between to points after which more tessellation will be generated.
|
|
/// To disable, specify float.MaxValue.
|
|
/// </summary>
|
|
public float MaxCordDeviation // Maximum distance allowed between a cord and its line projection
|
|
{
|
|
get { return m_MaxCordDev; }
|
|
set
|
|
{
|
|
m_MaxCordDev = Mathf.Max(value, 0.0f);
|
|
m_MaxCordDevSq = (m_MaxCordDev == float.MaxValue) ? float.MaxValue : m_MaxCordDev * m_MaxCordDev;
|
|
}
|
|
}
|
|
internal float MaxCordDeviationSquared { get { return m_MaxCordDevSq; } }
|
|
|
|
/// <summary>
|
|
/// The maximum angle (in degrees) between the curve tangent and the next point after which more tessellation will be generated.
|
|
/// To disable, specify float.MaxValue.
|
|
/// </summary>
|
|
public float MaxTanAngleDeviation // The maximum angle allowed (in radians) between tangents before a split happens
|
|
{
|
|
get { return m_MaxTanAngleDev; }
|
|
set
|
|
{
|
|
m_MaxTanAngleDev = Mathf.Clamp(value, VectorUtils.Epsilon, Mathf.PI * 0.5f);
|
|
m_MaxTanAngleDevCosine = Mathf.Cos(m_MaxTanAngleDev);
|
|
}
|
|
}
|
|
internal float MaxTanAngleDeviationCosine { get { return m_MaxTanAngleDevCosine; } } // Cosine of the maximum angle allowed between tangents before a split happens
|
|
|
|
/// <summary>
|
|
/// The number of samples used internally to evaluate the curves. More samples = higher quality.
|
|
/// Should be between 0 and 1 (inclusive).
|
|
/// </summary>
|
|
public float SamplingStepSize
|
|
{
|
|
get { return m_StepSize; }
|
|
set { m_StepSize = Mathf.Clamp(value, Epsilon, 1.0f); }
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Tessellates a path.
|
|
/// </summary>
|
|
/// <param name="contour">The path to tessellate</param>
|
|
/// <param name="pathProps">The path properties</param>
|
|
/// <param name="tessellateOptions">The tessellation options</param>
|
|
/// <param name="vertices">The resulting vertices</param>
|
|
/// <param name="indices">The resulting triangles</param>
|
|
/// <remarks>
|
|
/// The individual line segments generated during tessellation are made out of a set of ordered vertices. It is important
|
|
/// to honor this ordering so joining and and capping connect properly with the existing vertices without generating dupes.
|
|
/// The ordering assumed is as follows:
|
|
/// The last two vertices of a piece must be such that the first is generated at the end with a positive half-thickness
|
|
/// while the second vertex is at the end too but at a negative half-thickness.
|
|
/// No assumptions are enforced for other vertices before the two last vertices.
|
|
/// </remarks>
|
|
public static void TessellatePath(BezierContour contour, PathProperties pathProps, TessellationOptions tessellateOptions, out Vector2[] vertices, out UInt16[] indices)
|
|
{
|
|
if (tessellateOptions.StepDistance < Epsilon)
|
|
throw new Exception("stepDistance too small");
|
|
|
|
if (contour.Segments.Length < 2)
|
|
{
|
|
vertices = new Vector2[0];
|
|
indices = new UInt16[0];
|
|
return;
|
|
}
|
|
|
|
tessellateOptions.MaxCordDeviation = Mathf.Max(0.0001f, tessellateOptions.MaxCordDeviation);
|
|
tessellateOptions.MaxTanAngleDeviation = Mathf.Max(0.0001f, tessellateOptions.MaxTanAngleDeviation);
|
|
|
|
UnityEngine.Profiling.Profiler.BeginSample("TessellatePath");
|
|
|
|
float[] segmentLengths = VectorUtils.SegmentsLengths(contour.Segments, contour.Closed);
|
|
|
|
// Approximate the number of vertices/indices we need to store the results so we reduce memory reallocations during work
|
|
float approxTotalLength = 0.0f;
|
|
foreach (var s in segmentLengths)
|
|
approxTotalLength += s;
|
|
|
|
int approxStepCount = Math.Max((int)(approxTotalLength / tessellateOptions.StepDistance + 0.5f), 2);
|
|
if (pathProps.Stroke.Pattern != null)
|
|
approxStepCount += pathProps.Stroke.Pattern.Length * 2;
|
|
|
|
List<Vector2> verts = new List<Vector2>(approxStepCount * 2 + 32); // A little bit possibly for the endings
|
|
List<UInt16> inds = new List<UInt16>((int)(verts.Capacity * 1.5f)); // Usually every 4 verts represent a quad that uses 6 indices
|
|
|
|
var patternIt = new PathPatternIterator(pathProps.Stroke.Pattern, pathProps.Stroke.PatternOffset);
|
|
var pathIt = new PathDistanceForwardIterator(contour.Segments, contour.Closed, tessellateOptions.MaxCordDeviationSquared, tessellateOptions.MaxTanAngleDeviationCosine, tessellateOptions.SamplingStepSize);
|
|
|
|
JoiningInfo[] joiningInfo = new JoiningInfo[2];
|
|
HandleNewSegmentJoining(pathIt, patternIt, joiningInfo, pathProps.Stroke.HalfThickness, segmentLengths);
|
|
|
|
int rangeIndex = 0;
|
|
while (!pathIt.Ended)
|
|
{
|
|
if (patternIt.IsSolid)
|
|
TessellateRange(patternIt.SegmentLength, pathIt, patternIt, pathProps, tessellateOptions, joiningInfo, segmentLengths, approxTotalLength, rangeIndex++, verts, inds);
|
|
else
|
|
SkipRange(patternIt.SegmentLength, pathIt, patternIt, pathProps, joiningInfo, segmentLengths);
|
|
patternIt.Advance();
|
|
}
|
|
|
|
vertices = verts.ToArray();
|
|
indices = inds.ToArray();
|
|
|
|
UnityEngine.Profiling.Profiler.EndSample();
|
|
}
|
|
|
|
static Vector2[] TraceShape(BezierContour contour, Stroke stroke, TessellationOptions tessellateOptions)
|
|
{
|
|
if (tessellateOptions.StepDistance < Epsilon)
|
|
throw new Exception("stepDistance too small");
|
|
|
|
if (contour.Segments.Length < 2)
|
|
return new Vector2[0];
|
|
|
|
float[] segmentLengths = VectorUtils.SegmentsLengths(contour.Segments, contour.Closed);
|
|
|
|
// Approximate the number of vertices/indices we need to store the results so we reduce memory reallocations during work
|
|
float approxTotalLength = 0.0f;
|
|
foreach (var s in segmentLengths)
|
|
approxTotalLength += s;
|
|
|
|
int approxStepCount = Math.Max((int)(approxTotalLength / tessellateOptions.StepDistance + 0.5f), 2);
|
|
var strokePattern = stroke != null ? stroke.Pattern : null;
|
|
var strokePatternOffset = stroke != null ? stroke.PatternOffset : 0.0f;
|
|
if (strokePattern != null)
|
|
approxStepCount += strokePattern.Length * 2;
|
|
|
|
List<Vector2> verts = new List<Vector2>(approxStepCount); // A little bit possibly for the endings
|
|
|
|
var patternIt = new PathPatternIterator(strokePattern, strokePatternOffset);
|
|
var pathIt = new PathDistanceForwardIterator(contour.Segments, true, tessellateOptions.MaxCordDeviationSquared, tessellateOptions.MaxTanAngleDeviationCosine, tessellateOptions.SamplingStepSize);
|
|
verts.Add(pathIt.EvalCurrent());
|
|
|
|
while (!pathIt.Ended)
|
|
{
|
|
float distance = patternIt.SegmentLength;
|
|
float startingLength = pathIt.LengthSoFar;
|
|
float unitsRemaining = Mathf.Min(tessellateOptions.StepDistance, distance);
|
|
bool endedEntirePath = false;
|
|
for (;;)
|
|
{
|
|
var result = pathIt.AdvanceBy(unitsRemaining, out unitsRemaining);
|
|
if (result == PathDistanceForwardIterator.Result.Ended)
|
|
{
|
|
endedEntirePath = true;
|
|
break;
|
|
}
|
|
else if (result == PathDistanceForwardIterator.Result.NewSegment)
|
|
verts.Add(pathIt.EvalCurrent());
|
|
|
|
if ((unitsRemaining <= Epsilon) &&
|
|
!TryGetMoreRemainingUnits(ref unitsRemaining, pathIt, startingLength, distance, tessellateOptions.StepDistance))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (result == PathDistanceForwardIterator.Result.Stepped)
|
|
verts.Add(pathIt.EvalCurrent());
|
|
}
|
|
|
|
// Ending
|
|
if (endedEntirePath)
|
|
break;
|
|
else verts.Add(pathIt.EvalCurrent());
|
|
patternIt.Advance();
|
|
}
|
|
|
|
if ((verts[0] - verts[verts.Count - 1]).sqrMagnitude < Epsilon)
|
|
verts.RemoveAt(verts.Count - 1);
|
|
return verts.ToArray(); // Why not return verts itself?
|
|
}
|
|
|
|
static bool TryGetMoreRemainingUnits(ref float unitsRemaining, PathDistanceForwardIterator pathIt, float startingLength, float distance, float stepDistance)
|
|
{
|
|
float distanceCrossedSoFar = pathIt.LengthSoFar - startingLength;
|
|
float epsilon = Math.Max(Epsilon, distance * Epsilon * 100.0f);
|
|
if ((distance - distanceCrossedSoFar) <= epsilon)
|
|
return false;
|
|
if (distanceCrossedSoFar + stepDistance > distance)
|
|
unitsRemaining = distance - distanceCrossedSoFar;
|
|
else unitsRemaining = stepDistance;
|
|
return true;
|
|
}
|
|
|
|
static void HandleNewSegmentJoining(PathDistanceForwardIterator pathIt, PathPatternIterator patternIt, JoiningInfo[] joiningInfo, float halfThickness, float[] segmentLengths)
|
|
{
|
|
joiningInfo[0] = joiningInfo[1];
|
|
joiningInfo[1] = null;
|
|
|
|
if (!patternIt.IsSolidAt(pathIt.LengthSoFar + segmentLengths[pathIt.CurrentSegment]))
|
|
return; // The joining center falls outside the pattern, so don't join... period
|
|
|
|
if (pathIt.Closed && pathIt.Segments.Count <= 2)
|
|
return; // Not enough segments to do proper closing
|
|
|
|
if (pathIt.Closed)
|
|
{
|
|
JoiningInfo closing;
|
|
if ((pathIt.CurrentSegment == 0) || (pathIt.CurrentSegment == pathIt.Segments.Count - 2))
|
|
{
|
|
closing = ForeseeJoining(
|
|
VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.Segments.Count - 2),
|
|
VectorUtils.PathSegmentAtIndex(pathIt.Segments, 0),
|
|
halfThickness, segmentLengths[pathIt.Segments.Count - 2]);
|
|
|
|
if (pathIt.CurrentSegment == 0)
|
|
joiningInfo[0] = closing;
|
|
else
|
|
{
|
|
joiningInfo[1] = closing;
|
|
return;
|
|
}
|
|
}
|
|
else if (pathIt.CurrentSegment > pathIt.Segments.Count - 2)
|
|
return;
|
|
}
|
|
else if (pathIt.CurrentSegment >= pathIt.Segments.Count - 2)
|
|
return;
|
|
|
|
joiningInfo[1] = ForeseeJoining(
|
|
VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment),
|
|
VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment + 1),
|
|
halfThickness, segmentLengths[pathIt.CurrentSegment]);
|
|
}
|
|
|
|
static void SkipRange(
|
|
float distance, PathDistanceForwardIterator pathIt, PathPatternIterator patternIt,
|
|
PathProperties pathProps, JoiningInfo[] joiningInfo, float[] segmentLengths)
|
|
{
|
|
float unitsRemaining = distance;
|
|
while (unitsRemaining > Epsilon)
|
|
{
|
|
var result = pathIt.AdvanceBy(unitsRemaining, out unitsRemaining);
|
|
switch (result)
|
|
{
|
|
case PathDistanceForwardIterator.Result.Ended:
|
|
return;
|
|
case PathDistanceForwardIterator.Result.Stepped:
|
|
if (unitsRemaining < Epsilon)
|
|
return;
|
|
break;
|
|
case PathDistanceForwardIterator.Result.NewSegment:
|
|
HandleNewSegmentJoining(pathIt, patternIt, joiningInfo, pathProps.Stroke.HalfThickness, segmentLengths);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void TessellateRange(
|
|
float distance, PathDistanceForwardIterator pathIt, PathPatternIterator patternIt, PathProperties pathProps,
|
|
TessellationOptions tessellateOptions, JoiningInfo[] joiningInfo, float[] segmentLengths, float totalLength, int rangeIndex, List<Vector2> verts, List<UInt16> inds)
|
|
{
|
|
bool startOfLoop = pathIt.Closed && (pathIt.CurrentSegment == 0) && (pathIt.CurrentT == 0.0f);
|
|
if (startOfLoop && (joiningInfo[0] != null))
|
|
{
|
|
GenerateJoining(joiningInfo[0], pathProps.Corners, pathProps.Stroke.HalfThickness, pathProps.Stroke.TippedCornerLimit, tessellateOptions, verts, inds);
|
|
}
|
|
else
|
|
{
|
|
var pathEnding = pathProps.Head;
|
|
|
|
// If pattern at the end will overlap with beginning, use a chopped ending to allow merging
|
|
if (pathIt.Closed && rangeIndex == 0 && patternIt.IsSolidAt(pathIt.CurrentT) && patternIt.IsSolidAt(totalLength))
|
|
pathEnding = PathEnding.Chop;
|
|
|
|
GenerateTip(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), true, pathIt.CurrentT, pathEnding, pathProps.Stroke.HalfThickness, tessellateOptions, verts, inds);
|
|
}
|
|
|
|
float startingLength = pathIt.LengthSoFar;
|
|
float unitsRemaining = Mathf.Min(tessellateOptions.StepDistance, distance);
|
|
bool endedEntirePath = false;
|
|
for (;;)
|
|
{
|
|
var result = pathIt.AdvanceBy(unitsRemaining, out unitsRemaining);
|
|
if (result == PathDistanceForwardIterator.Result.Ended)
|
|
{
|
|
endedEntirePath = true;
|
|
break;
|
|
}
|
|
else if (result == PathDistanceForwardIterator.Result.NewSegment)
|
|
{
|
|
if (joiningInfo[1] != null)
|
|
GenerateJoining(joiningInfo[1], pathProps.Corners, pathProps.Stroke.HalfThickness, pathProps.Stroke.TippedCornerLimit, tessellateOptions, verts, inds);
|
|
else AddSegment(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), pathIt.CurrentT, pathProps.Stroke.HalfThickness, null, pathIt.SegmentLengthSoFar, verts, inds);
|
|
HandleNewSegmentJoining(pathIt, patternIt, joiningInfo, pathProps.Stroke.HalfThickness, segmentLengths);
|
|
}
|
|
|
|
if ((unitsRemaining <= Epsilon) &&
|
|
!TryGetMoreRemainingUnits(ref unitsRemaining, pathIt, startingLength, distance, tessellateOptions.StepDistance))
|
|
{
|
|
break;
|
|
}
|
|
|
|
if (result == PathDistanceForwardIterator.Result.Stepped)
|
|
AddSegment(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), pathIt.CurrentT, pathProps.Stroke.HalfThickness, joiningInfo, pathIt.SegmentLengthSoFar, verts, inds);
|
|
}
|
|
|
|
// Ending
|
|
if (endedEntirePath && pathIt.Closed)
|
|
{
|
|
// No joining needed, the start and end of the path should just connect
|
|
inds.Add(0);
|
|
inds.Add(1);
|
|
inds.Add((UInt16)(verts.Count - 2));
|
|
inds.Add((UInt16)(verts.Count - 1));
|
|
inds.Add((UInt16)(verts.Count - 2));
|
|
inds.Add(1);
|
|
}
|
|
else
|
|
{
|
|
AddSegment(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), pathIt.CurrentT, pathProps.Stroke.HalfThickness, joiningInfo, pathIt.SegmentLengthSoFar, verts, inds);
|
|
GenerateTip(VectorUtils.PathSegmentAtIndex(pathIt.Segments, pathIt.CurrentSegment), false, pathIt.CurrentT, pathProps.Tail, pathProps.Stroke.HalfThickness, tessellateOptions, verts, inds);
|
|
}
|
|
}
|
|
|
|
static void AddSegment(BezierSegment segment, float toT, float halfThickness, JoiningInfo[] joinInfo, float segmentLengthSoFar, List<Vector2> verts, List<UInt16> inds)
|
|
{
|
|
Vector2 tanTo, normTo;
|
|
Vector2 posTo = VectorUtils.EvalFull(segment, toT, out tanTo, out normTo);
|
|
|
|
Vector2 posThickness = posTo + normTo * halfThickness;
|
|
Vector2 negThickness = posTo + normTo * -halfThickness;
|
|
|
|
if (joinInfo != null)
|
|
{
|
|
if ((joinInfo[0] != null) && (segmentLengthSoFar < joinInfo[0].InnerCornerDistFromStart))
|
|
{
|
|
if (joinInfo[0].RoundPosThickness)
|
|
negThickness = joinInfo[0].InnerCornerVertex;
|
|
else posThickness = joinInfo[0].InnerCornerVertex;
|
|
}
|
|
|
|
if ((joinInfo[1] != null) && (segmentLengthSoFar > joinInfo[1].InnerCornerDistToEnd))
|
|
{
|
|
if (joinInfo[1].RoundPosThickness)
|
|
negThickness = joinInfo[1].InnerCornerVertex;
|
|
else posThickness = joinInfo[1].InnerCornerVertex;
|
|
}
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert(verts.Count >= 2);
|
|
int indexStart = verts.Count - 2;
|
|
verts.Add(posThickness);
|
|
verts.Add(negThickness);
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
inds.Add((UInt16)(indexStart + 1));
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
}
|
|
|
|
class JoiningInfo
|
|
{
|
|
public Vector2 JoinPos;
|
|
public Vector2 TanAtEnd, TanAtStart;
|
|
public Vector2 NormAtEnd, NormAtStart;
|
|
public Vector2 PosThicknessStart, NegThicknessStart;
|
|
public Vector2 PosThicknessEnd, NegThicknessEnd;
|
|
public Vector2 PosThicknessClosingPoint, NegThicknessClosingPoint;
|
|
public bool RoundPosThickness;
|
|
public bool SimpleJoin;
|
|
public Vector2 InnerCornerVertex;
|
|
public float InnerCornerDistToEnd, InnerCornerDistFromStart;
|
|
}
|
|
|
|
static JoiningInfo ForeseeJoining(BezierSegment end, BezierSegment start, float halfThickness, float endSegmentLength)
|
|
{
|
|
JoiningInfo joinInfo = new JoiningInfo();
|
|
|
|
// The joining generates the vertices at both ends as well as the joining itself
|
|
joinInfo.JoinPos = end.P3;
|
|
joinInfo.TanAtEnd = VectorUtils.EvalTangent(end, 1.0f);
|
|
joinInfo.NormAtEnd = Vector2.Perpendicular(joinInfo.TanAtEnd);
|
|
joinInfo.TanAtStart = VectorUtils.EvalTangent(start, 0.0f);
|
|
joinInfo.NormAtStart = Vector2.Perpendicular(joinInfo.TanAtStart);
|
|
|
|
// If the tangents are continuous at the join location, we don't have
|
|
// to generate a corner, we do a "simple" join by just connecting the vertices
|
|
// from the two segments directly
|
|
float cosAngleBetweenTans = Vector2.Dot(joinInfo.TanAtEnd, joinInfo.TanAtStart);
|
|
joinInfo.SimpleJoin = Mathf.Approximately(Mathf.Abs(cosAngleBetweenTans), 1.0f);
|
|
if (joinInfo.SimpleJoin)
|
|
return null;
|
|
|
|
joinInfo.PosThicknessEnd = joinInfo.JoinPos + joinInfo.NormAtEnd * halfThickness;
|
|
joinInfo.NegThicknessEnd = joinInfo.JoinPos - joinInfo.NormAtEnd * halfThickness;
|
|
joinInfo.PosThicknessStart = joinInfo.JoinPos + joinInfo.NormAtStart * halfThickness;
|
|
joinInfo.NegThicknessStart = joinInfo.JoinPos - joinInfo.NormAtStart * halfThickness;
|
|
|
|
if (joinInfo.SimpleJoin)
|
|
{
|
|
joinInfo.PosThicknessClosingPoint = Vector2.LerpUnclamped(joinInfo.PosThicknessEnd, joinInfo.PosThicknessStart, 0.5f);
|
|
joinInfo.NegThicknessClosingPoint = Vector2.LerpUnclamped(joinInfo.NegThicknessEnd, joinInfo.NegThicknessStart, 0.5f);
|
|
}
|
|
else
|
|
{
|
|
joinInfo.PosThicknessClosingPoint = VectorUtils.IntersectLines(joinInfo.PosThicknessEnd, joinInfo.PosThicknessEnd + joinInfo.TanAtEnd, joinInfo.PosThicknessStart, joinInfo.PosThicknessStart + joinInfo.TanAtStart);
|
|
joinInfo.NegThicknessClosingPoint = VectorUtils.IntersectLines(joinInfo.NegThicknessEnd, joinInfo.NegThicknessEnd + joinInfo.TanAtEnd, joinInfo.NegThicknessStart, joinInfo.NegThicknessStart + joinInfo.TanAtStart);
|
|
|
|
if (float.IsInfinity(joinInfo.PosThicknessClosingPoint.x) || float.IsInfinity(joinInfo.PosThicknessClosingPoint.y))
|
|
joinInfo.PosThicknessClosingPoint = joinInfo.JoinPos;
|
|
if (float.IsInfinity(joinInfo.NegThicknessClosingPoint.x) || float.IsInfinity(joinInfo.NegThicknessClosingPoint.y))
|
|
joinInfo.NegThicknessClosingPoint = joinInfo.JoinPos;
|
|
}
|
|
|
|
// Should we round the positive thickness side or the negative thickness side?
|
|
joinInfo.RoundPosThickness = PointOnTheLeftOfLine(Vector2.zero, joinInfo.TanAtEnd, joinInfo.TanAtStart);
|
|
|
|
// Inner corner vertex should be calculated by intersection of the inner segments
|
|
Vector2[] startTrail = null, endTrail = null;
|
|
Vector2 intersectionOnStart = Vector2.zero, intersectionOnEnd = Vector2.zero;
|
|
if (!joinInfo.SimpleJoin)
|
|
{
|
|
BezierSegment endFlipped = VectorUtils.FlipSegment(end);
|
|
Vector2 thicknessClosingPoint = joinInfo.RoundPosThickness ? joinInfo.PosThicknessClosingPoint : joinInfo.NegThicknessClosingPoint;
|
|
Vector2 meetingPoint = end.P3;
|
|
Vector2 thicknessDiagonalEnd = meetingPoint + (thicknessClosingPoint - meetingPoint) * 10.0f;
|
|
startTrail = LineBezierThicknessIntersect(
|
|
start, joinInfo.RoundPosThickness ? -halfThickness : halfThickness, meetingPoint, thicknessDiagonalEnd,
|
|
out joinInfo.InnerCornerDistFromStart, out intersectionOnStart);
|
|
endTrail = LineBezierThicknessIntersect(
|
|
endFlipped, joinInfo.RoundPosThickness ? halfThickness : -halfThickness, meetingPoint, thicknessDiagonalEnd,
|
|
out joinInfo.InnerCornerDistToEnd, out intersectionOnEnd);
|
|
}
|
|
|
|
bool intersectionFound = false;
|
|
if ((startTrail != null) && (endTrail != null))
|
|
{
|
|
var intersect = VectorUtils.IntersectLines(startTrail[0], startTrail[1], endTrail[0], endTrail[1]);
|
|
var isOnStartTrail = PointOnLineIsWithinSegment(startTrail[0], startTrail[1], intersect);
|
|
var isOnEndTrail = PointOnLineIsWithinSegment(endTrail[0], endTrail[1], intersect);
|
|
if (!float.IsInfinity(intersect.x) && isOnStartTrail && isOnEndTrail)
|
|
{
|
|
var vStart = intersectionOnStart - intersect;
|
|
var vEnd = intersectionOnEnd - intersect;
|
|
joinInfo.InnerCornerDistFromStart += (vStart == Vector2.zero) ? 0.0f : vStart.magnitude;
|
|
joinInfo.InnerCornerDistToEnd += (vEnd == Vector2.zero) ? 0.0f : vEnd.magnitude;
|
|
joinInfo.InnerCornerDistToEnd = endSegmentLength - joinInfo.InnerCornerDistToEnd;
|
|
joinInfo.InnerCornerVertex = intersect; // Found it!
|
|
intersectionFound = true;
|
|
}
|
|
}
|
|
|
|
if (!intersectionFound)
|
|
{
|
|
joinInfo.InnerCornerVertex = joinInfo.JoinPos + ((joinInfo.TanAtStart - joinInfo.TanAtEnd) / 2.0f).normalized * halfThickness;
|
|
joinInfo.InnerCornerDistFromStart = 0;
|
|
joinInfo.InnerCornerDistToEnd = endSegmentLength;
|
|
}
|
|
return joinInfo;
|
|
}
|
|
|
|
static Vector2[] LineBezierThicknessIntersect(BezierSegment seg, float thickness, Vector2 lineFrom, Vector2 lineTo, out float distanceToIntersection, out Vector2 intersection)
|
|
{
|
|
Vector2 tan = VectorUtils.EvalTangent(seg, 0.0f);
|
|
Vector2 nrm = Vector2.Perpendicular(tan);
|
|
Vector2 lastPoint = seg.P0 + nrm * thickness;
|
|
distanceToIntersection = 0.0f;
|
|
intersection = new Vector2(float.PositiveInfinity, float.PositiveInfinity);
|
|
float stepT = 0.01f;
|
|
float t = 0;
|
|
while (t < 1.0f)
|
|
{
|
|
t += stepT;
|
|
var point = VectorUtils.EvalFull(seg, t, out tan, out nrm) + nrm * thickness;
|
|
intersection = VectorUtils.IntersectLines(lineFrom, lineTo, lastPoint, point);
|
|
if (PointOnLineIsWithinSegment(lastPoint, point, intersection))
|
|
{
|
|
distanceToIntersection += (lastPoint - intersection).magnitude;
|
|
return new Vector2[] { lastPoint, point };
|
|
}
|
|
distanceToIntersection += (lastPoint - point).magnitude;
|
|
lastPoint = point;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
static bool PointOnLineIsWithinSegment(Vector2 lineFrom, Vector2 lineTo, Vector2 point)
|
|
{
|
|
// Point is assumed to be already on the line, but we would like to know if it is within the segment specified
|
|
var v = (lineTo - lineFrom).normalized;
|
|
if (Vector2.Dot(point - lineFrom, v) < -Epsilon)
|
|
return false;
|
|
if (Vector2.Dot(point - lineTo, v) > Epsilon)
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
static void GenerateJoining(JoiningInfo joinInfo, PathCorner corner, float halfThickness, float tippedCornerLimit, TessellationOptions tessellateOptions, List<Vector2> verts, List<UInt16> inds)
|
|
{
|
|
// The joining generates the vertices at both ends as well as the joining itself
|
|
if (verts.Count == 0)
|
|
{
|
|
// Starting a path with a joining (meaning a loop)
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessEnd : joinInfo.InnerCornerVertex);
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessEnd);
|
|
}
|
|
|
|
System.Diagnostics.Debug.Assert(verts.Count >= 2);
|
|
int indexStart = verts.Count - 2; // Using the last two vertices
|
|
|
|
// Convert a tipped corner to a beveled one if tippedCornerLimit ratio is reached
|
|
if (corner == PathCorner.Tipped && tippedCornerLimit >= 1.0f)
|
|
{
|
|
var theta = Vector2.Angle(-joinInfo.TanAtEnd, joinInfo.TanAtStart) * Mathf.Deg2Rad;
|
|
var ratio = 1.0f / Mathf.Sin(theta / 2.0f);
|
|
if (ratio > tippedCornerLimit)
|
|
corner = PathCorner.Beveled;
|
|
}
|
|
|
|
if (joinInfo.SimpleJoin)
|
|
{
|
|
// TODO
|
|
}
|
|
else if (corner == PathCorner.Tipped)
|
|
{
|
|
verts.Add(joinInfo.PosThicknessClosingPoint);
|
|
verts.Add(joinInfo.NegThicknessClosingPoint);
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessStart : joinInfo.InnerCornerVertex);
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessStart);
|
|
|
|
// Ending to tip
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
inds.Add((UInt16)(indexStart + 1));
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
|
|
// Tip to starting
|
|
inds.Add((UInt16)(indexStart + 4));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 4));
|
|
inds.Add((UInt16)(indexStart + 5));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
|
|
return;
|
|
}
|
|
else if (corner == PathCorner.Beveled)
|
|
{
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessEnd : joinInfo.InnerCornerVertex); // 2
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessEnd); // 3
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.PosThicknessStart : joinInfo.InnerCornerVertex); // 4
|
|
verts.Add(joinInfo.RoundPosThickness ? joinInfo.InnerCornerVertex : joinInfo.NegThicknessStart); // 5
|
|
|
|
// Ending to tip
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 1));
|
|
inds.Add((UInt16)(indexStart + 1));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
|
|
// Bevel
|
|
if (joinInfo.RoundPosThickness)
|
|
{
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 4));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
}
|
|
else
|
|
{
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 5));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (corner == PathCorner.Round)
|
|
{
|
|
float sweepAngle = Mathf.Acos(Vector2.Dot(joinInfo.NormAtEnd, joinInfo.NormAtStart));
|
|
bool flipArc = false;
|
|
if (!PointOnTheLeftOfLine(Vector2.zero, joinInfo.NormAtEnd, joinInfo.NormAtStart))
|
|
{
|
|
sweepAngle = -sweepAngle;
|
|
flipArc = true;
|
|
}
|
|
|
|
UInt16 innerCornerVertexIndex = (UInt16)verts.Count;
|
|
verts.Add(joinInfo.InnerCornerVertex);
|
|
|
|
int arcSegments = CalculateArcSteps(halfThickness, 0, sweepAngle, tessellateOptions);
|
|
for (int i = 0; i <= arcSegments; i++)
|
|
{
|
|
float angle = sweepAngle * (i / (float)arcSegments);
|
|
Vector2 nrm = Matrix2D.RotateLH(angle) * joinInfo.NormAtEnd;
|
|
if (flipArc) nrm = -nrm;
|
|
verts.Add(nrm * halfThickness + joinInfo.JoinPos);
|
|
|
|
if (i == 0)
|
|
{
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
inds.Add((UInt16)(indexStart + (joinInfo.RoundPosThickness ? 2 : 1)));
|
|
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + (joinInfo.RoundPosThickness ? 1 : 3)));
|
|
}
|
|
else
|
|
{
|
|
if (joinInfo.RoundPosThickness)
|
|
{
|
|
inds.Add((UInt16)(indexStart + i + (flipArc ? 3 : 2)));
|
|
inds.Add((UInt16)(indexStart + i + (flipArc ? 2 : 3)));
|
|
inds.Add(innerCornerVertexIndex);
|
|
}
|
|
else
|
|
{
|
|
inds.Add((UInt16)(indexStart + i + (flipArc ? 3 : 2)));
|
|
inds.Add((UInt16)(indexStart + i + (flipArc ? 2 : 3)));
|
|
inds.Add(innerCornerVertexIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Manually add the last segment, maintain the expected vertex positioning
|
|
int endingVerticesIndex = verts.Count;
|
|
if (joinInfo.RoundPosThickness)
|
|
{
|
|
verts.Add(joinInfo.PosThicknessStart);
|
|
verts.Add(joinInfo.InnerCornerVertex);
|
|
}
|
|
else
|
|
{
|
|
verts.Add(joinInfo.InnerCornerVertex);
|
|
verts.Add(joinInfo.NegThicknessStart);
|
|
}
|
|
inds.Add((UInt16)(endingVerticesIndex - 1));
|
|
inds.Add((UInt16)(endingVerticesIndex + 0));
|
|
inds.Add(innerCornerVertexIndex);
|
|
}
|
|
}
|
|
|
|
static void GenerateTip(BezierSegment segment, bool atStart, float t, PathEnding ending, float halfThickness, TessellationOptions tessellateOptions, List<Vector2> verts, List<UInt16> inds)
|
|
{
|
|
// The tip includes the vertices at the end itself
|
|
Vector2 tan, nrm;
|
|
var pos = VectorUtils.EvalFull(segment, t, out tan, out nrm);
|
|
int indexStart = verts.Count;
|
|
|
|
switch (ending)
|
|
{
|
|
case PathEnding.Chop:
|
|
if (atStart)
|
|
{
|
|
verts.Add(pos + nrm * halfThickness);
|
|
verts.Add(pos - nrm * halfThickness);
|
|
}
|
|
else
|
|
{
|
|
// Not much, path segments are always expected to be generated perpendicular to the path
|
|
// at the segment point location, so we don't have to do anything for the ending
|
|
}
|
|
break;
|
|
|
|
case PathEnding.Square:
|
|
if (atStart)
|
|
{
|
|
verts.Add(pos + nrm * halfThickness - tan * halfThickness);
|
|
verts.Add(pos - nrm * halfThickness - tan * halfThickness);
|
|
verts.Add(pos + nrm * halfThickness);
|
|
verts.Add(pos - nrm * halfThickness);
|
|
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
inds.Add((UInt16)(indexStart + 1));
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
inds.Add((UInt16)(indexStart + 2));
|
|
inds.Add((UInt16)(indexStart + 3));
|
|
}
|
|
else
|
|
{
|
|
// Relying on the last two vertices, and just adding two of our own here
|
|
verts.Add(pos + nrm * halfThickness + tan * halfThickness);
|
|
verts.Add(pos - nrm * halfThickness + tan * halfThickness);
|
|
|
|
inds.Add((UInt16)(indexStart + 0 - 2));
|
|
inds.Add((UInt16)(indexStart + 3 - 2));
|
|
inds.Add((UInt16)(indexStart + 1 - 2));
|
|
inds.Add((UInt16)(indexStart + 0 - 2));
|
|
inds.Add((UInt16)(indexStart + 2 - 2));
|
|
inds.Add((UInt16)(indexStart + 3 - 2));
|
|
}
|
|
break;
|
|
|
|
case PathEnding.Round:
|
|
float arcSign = atStart ? -1 : 1;
|
|
int arcSegments = CalculateArcSteps(halfThickness, 0, Mathf.PI, tessellateOptions);
|
|
for (int i = 1; i < arcSegments; i++)
|
|
{
|
|
float angle = Mathf.PI * (i / (float)arcSegments);
|
|
verts.Add(pos + Matrix2D.RotateLH(angle) * nrm * halfThickness * arcSign);
|
|
}
|
|
|
|
if (atStart)
|
|
{
|
|
// Note how we maintain the last two vertices being setup for connection by the rest of the path vertices
|
|
int indexTipStart = verts.Count;
|
|
verts.Add(pos + nrm * halfThickness);
|
|
verts.Add(pos - nrm * halfThickness);
|
|
|
|
for (int i = 1; i < arcSegments; i++)
|
|
{
|
|
inds.Add((UInt16)(indexTipStart + 1));
|
|
inds.Add((UInt16)(indexStart + i - 1));
|
|
inds.Add((UInt16)(indexStart + i));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
inds.Add((UInt16)(indexStart - 1));
|
|
inds.Add((UInt16)(indexStart - 2));
|
|
inds.Add((UInt16)(indexStart + 0));
|
|
for (int i = 1; i < arcSegments - 1; i++)
|
|
{
|
|
inds.Add((UInt16)(indexStart - 1));
|
|
inds.Add((UInt16)(indexStart + i - 1));
|
|
inds.Add((UInt16)(indexStart + i));
|
|
}
|
|
}
|
|
break;
|
|
|
|
default:
|
|
System.Diagnostics.Debug.Assert(false); // Joining has its own function
|
|
break;
|
|
}
|
|
}
|
|
|
|
static int CalculateArcSteps(float radius, float fromAngle, float toAngle, TessellationOptions tessellateOptions)
|
|
{
|
|
float stepDivisor = float.MaxValue;
|
|
|
|
if (tessellateOptions.StepDistance != float.MaxValue)
|
|
stepDivisor = tessellateOptions.StepDistance / radius;
|
|
|
|
if (tessellateOptions.MaxCordDeviation != float.MaxValue)
|
|
{
|
|
float y = radius - tessellateOptions.MaxCordDeviation;
|
|
float cordHalfLength = Mathf.Sqrt(radius * radius - y * y);
|
|
float div = Mathf.Min(stepDivisor, Mathf.Asin(cordHalfLength / radius));
|
|
if (div > VectorUtils.Epsilon)
|
|
stepDivisor = div;
|
|
}
|
|
|
|
if (tessellateOptions.MaxTanAngleDeviation < Mathf.PI * 0.5f)
|
|
stepDivisor = Mathf.Min(stepDivisor, tessellateOptions.MaxTanAngleDeviation * 2.0f);
|
|
|
|
float stepsInFullCircle = (Mathf.PI * 2.0f) / stepDivisor;
|
|
float arcPercentage = Mathf.Abs(fromAngle - toAngle) / (Mathf.PI * 2.0f);
|
|
return (int)Mathf.Max(stepsInFullCircle * arcPercentage + 0.5f, 3); // Never less than 3 segments
|
|
}
|
|
|
|
/// <summary>Tessellates a rectangle.</summary>
|
|
/// <param name="rect">Rectangle to tessellate</param>
|
|
/// <param name="vertices">The output vertices</param>
|
|
/// <param name="indices">The output triangles</param>
|
|
public static void TessellateRect(Rect rect, out Vector2[] vertices, out UInt16[] indices)
|
|
{
|
|
vertices = new Vector2[] {
|
|
new Vector2(rect.xMin, rect.yMin),
|
|
new Vector2(rect.xMax, rect.yMin),
|
|
new Vector2(rect.xMax, rect.yMax),
|
|
new Vector2(rect.xMin, rect.yMax)
|
|
};
|
|
indices = new UInt16[] {
|
|
1, 0, 2, 2, 0, 3
|
|
};
|
|
}
|
|
|
|
/// <summary>Tessellates a rectangle border.</summary>
|
|
/// <param name="rect">Rectangle to tessellate</param>
|
|
/// <param name="halfThickness">The half-thickness of the border</param>
|
|
/// <param name="vertices">The output vertices</param>
|
|
/// <param name="indices">The output triangles</param>
|
|
public static void TessellateRectBorder(Rect rect, float halfThickness, out Vector2[] vertices, out UInt16[] indices)
|
|
{
|
|
var verts = new List<Vector2>(16);
|
|
var inds = new List<UInt16>(24);
|
|
|
|
// Left edge
|
|
var p0 = new Vector2(rect.x, rect.y + rect.height);
|
|
var p1 = new Vector2(rect.x, rect.y);
|
|
|
|
var q0 = p0 + new Vector2(-halfThickness, halfThickness);
|
|
var q1 = p1 + new Vector2(-halfThickness, -halfThickness);
|
|
var q2 = p1 + new Vector2(halfThickness, halfThickness);
|
|
var q3 = p0 + new Vector2(halfThickness, -halfThickness);
|
|
|
|
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
|
inds.Add(0); inds.Add(3); inds.Add(2); inds.Add(2); inds.Add(1); inds.Add(0);
|
|
|
|
// Top edge
|
|
p0 = new Vector2(rect.x, rect.y);
|
|
p1 = new Vector2(rect.x + rect.width, rect.y);
|
|
|
|
q0 = p0 + new Vector2(-halfThickness, -halfThickness);
|
|
q1 = p1 + new Vector2(halfThickness, -halfThickness);
|
|
q2 = p1 + new Vector2(-halfThickness, halfThickness);
|
|
q3 = p0 + new Vector2(halfThickness, halfThickness);
|
|
|
|
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
|
inds.Add(4); inds.Add(7); inds.Add(6); inds.Add(6); inds.Add(5); inds.Add(4);
|
|
|
|
// Right edge
|
|
p0 = new Vector2(rect.x + rect.width, rect.y);
|
|
p1 = new Vector2(rect.x + rect.width, rect.y + rect.height);
|
|
|
|
q0 = p0 + new Vector2(halfThickness, -halfThickness);
|
|
q1 = p1 + new Vector2(halfThickness, halfThickness);
|
|
q2 = p1 + new Vector2(-halfThickness, -halfThickness);
|
|
q3 = p0 + new Vector2(-halfThickness, halfThickness);
|
|
|
|
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
|
inds.Add(8); inds.Add(11); inds.Add(10); inds.Add(10); inds.Add(9); inds.Add(8);
|
|
|
|
// Bottom edge
|
|
p0 = new Vector2(rect.x + rect.width, rect.y + rect.height);
|
|
p1 = new Vector2(rect.x, rect.y + rect.height);
|
|
|
|
q0 = p0 + new Vector2(halfThickness, halfThickness);
|
|
q1 = p1 + new Vector2(-halfThickness, halfThickness);
|
|
q2 = p1 + new Vector2(halfThickness, -halfThickness);
|
|
q3 = p0 + new Vector2(-halfThickness, -halfThickness);
|
|
|
|
verts.Add(q0); verts.Add(q1); verts.Add(q2); verts.Add(q3);
|
|
inds.Add(12); inds.Add(15); inds.Add(14); inds.Add(14); inds.Add(13); inds.Add(12);
|
|
|
|
vertices = verts.ToArray();
|
|
indices = inds.ToArray();
|
|
}
|
|
}
|
|
}
|