using System; using System.Collections.Generic; using UnityEngine; namespace ToolBuddy.ThirdParty.VectorGraphics { public static partial class VectorUtils { /// /// Structure to store the tessellation options. /// public struct TessellationOptions { private float m_MaxCordDev, m_MaxCordDevSq, m_MaxTanAngleDev, m_MaxTanAngleDevCosine, m_StepSize; /// /// The uniform tessellation step distance. /// public float StepDistance { get; set; } // A split to happen uniformly at fixed distances /// /// 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. /// 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; } } /// /// 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. /// 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 /// /// The number of samples used internally to evaluate the curves. More samples = higher quality. /// Should be between 0 and 1 (inclusive). /// public float SamplingStepSize { get { return m_StepSize; } set { m_StepSize = Mathf.Clamp(value, Epsilon, 1.0f); } } } /// /// Tessellates a path. /// /// The path to tessellate /// The path properties /// The tessellation options /// The resulting vertices /// The resulting triangles /// /// 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. /// 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 verts = new List(approxStepCount * 2 + 32); // A little bit possibly for the endings List inds = new List((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 verts = new List(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 verts, List 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 verts, List 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 verts, List 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 verts, List 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 } /// Tessellates a rectangle. /// Rectangle to tessellate /// The output vertices /// The output triangles 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 }; } /// Tessellates a rectangle border. /// Rectangle to tessellate /// The half-thickness of the border /// The output vertices /// The output triangles public static void TessellateRectBorder(Rect rect, float halfThickness, out Vector2[] vertices, out UInt16[] indices) { var verts = new List(16); var inds = new List(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(); } } }