using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEngine; namespace ToolBuddy.ThirdParty.VectorGraphics { /// /// Provides various tools to work with vector graphics. /// public static partial class VectorUtils { /// A small value used everywhere by the vector graphics package. public static readonly float Epsilon = 0.000001f; /// Convert a segments into a path. /// The BezierSegment /// An array of two path segments /// The second path segment will hold the ending position of the curve. public static BezierPathSegment[] BezierSegmentToPath(BezierSegment segment) { return new BezierPathSegment[] { new BezierPathSegment() { P0 = segment.P0, P1 = segment.P1, P2 = segment.P2 }, new BezierPathSegment() { P0 = segment.P3 } }; } /// Converts an array of BezierSegments into a connected path. /// An array of BezierSegment /// An array of path segments /// If two consecutive segments are disconnected, a straight line will be added between the two endpoints. public static BezierPathSegment[] BezierSegmentsToPath(BezierSegment[] segments) { if (segments.Count() == 0) return new BezierPathSegment[0]; int segmentCount = segments.Length; var path = new List(segments.Length*2 + 1); for (int i = 0; i < segmentCount; ++i) { var seg = segments[i]; path.Add(new BezierPathSegment() { P0 = seg.P0, P1 = seg.P1, P2 = seg.P2 }); if (i == (segmentCount-1)) { // Last segment, close the path path.Add(new BezierPathSegment() { P0 = seg.P3 }); } else { // Check for connectivity, insert path to connect the endpoints when needed var nextSeg = segments[i+1]; if (seg.P3 != nextSeg.P0) { var line = VectorUtils.MakeLine(seg.P3, nextSeg.P0); path.Add(new BezierPathSegment() { P0 = line.P0, P1 = line.P1, P2 = line.P2 }); } } } return path.ToArray(); } /// /// Computes the BezierSegment at a given index from a list of BezierPathSegments. /// /// The chain of BezierPathSegments /// The segment index /// The BezierSegment at the given index public static BezierSegment PathSegmentAtIndex(IList path, int index) { if (index < 0 || index >= (path.Count-1)) throw new IndexOutOfRangeException("Invalid index passed to PathSegmentAtIndex"); return new BezierSegment() { P0 = path[index].P0, P1 = path[index].P1, P2 = path[index].P2, P3 = path[index + 1].P0 }; } /// /// Checks if the two ends of a BezierPathSegment chain are at the same location. /// /// The chain of BezierPathSegments /// True if the two ends of the chain are at the same location, false otherwise public static bool PathEndsPerfectlyMatch(IList path) { if (path.Count < 2) return false; if ((path[0].P0 - path[path.Count - 1].P0).sqrMagnitude > Epsilon) return false; return true; } /// Builds a rectangle shape. /// The shape object that will be filled with a rectangle. /// The position and dimensions of the rectangle. public static void MakeRectangleShape(Shape rectShape, Rect rect) { MakeRectangleShape(rectShape, rect, Vector2.zero, Vector2.zero, Vector2.zero, Vector2.zero); } /// Builds a rectangle shape. /// The shape object that will be filled with a rectangle. /// The position and dimensions of the rectangle. /// The top-left radius of the rectangle /// The top-right radius of the rectangle /// The bottom-right radius of the rectangle /// The bottom-left radius of the rectangle public static void MakeRectangleShape(Shape rectShape, Rect rect, Vector2 radiusTL, Vector2 radiusTR, Vector2 radiusBR, Vector2 radiusBL) { var contour = BuildRectangleContour(rect, radiusTL, radiusTR, radiusBR, radiusBL); if (rectShape.Contours == null || rectShape.Contours.Length != 1) rectShape.Contours = new BezierContour[1]; rectShape.Contours[0] = contour; rectShape.IsConvex = true; } /// Builds an ellipse shape. /// The shape object that will be filled with an ellipse. /// The position of the circle, relative to its center. /// The x component of the radius of the circle. /// The y component of the radius of the circle. public static void MakeEllipseShape(Shape ellipseShape, Vector2 pos, float radiusX, float radiusY) { var rect = new Rect(pos.x-radiusX, pos.y-radiusY, radiusX+radiusX, radiusY+radiusY); var rad = new Vector2(radiusX, radiusY); MakeRectangleShape(ellipseShape, rect, rad, rad, rad, rad); } /// Builds a circle shape. /// The shape object that will be filled with a circle. /// The position of the circle, relative to its center. /// The radius of the circle. public static void MakeCircleShape(Shape circleShape, Vector2 pos, float radius) { MakeEllipseShape(circleShape, pos, radius, radius); } /// Computes the bounds of a bezier path. /// The path to compute the bounds from /// A Rect containing the axis-aligned bounding-box of the contour public static Rect Bounds(BezierPathSegment[] path) { var min = new Vector2(float.MaxValue, float.MaxValue); var max = new Vector2(-float.MaxValue, -float.MaxValue); foreach (var s in VectorUtils.SegmentsInPath(path)) { Vector2 segMin, segMax; Bounds(s, out segMin, out segMax); min = Vector2.Min(min, segMin); max = Vector2.Max(max, segMax); } return (min.x != float.MaxValue) ? new Rect(min, max - min) : Rect.zero; } /// Computes the bounds of a list of vertices. /// The list of vertices to compute the bounds from /// A Rect containing the axis-aligned bounding-box of the vertices public static Rect Bounds(IEnumerable vertices) { var min = new Vector2(float.MaxValue, float.MaxValue); var max = new Vector2(-float.MaxValue, -float.MaxValue); foreach (var v in vertices) { min = Vector2.Min(min, v); max = Vector2.Max(max, v); } return (min.x != float.MaxValue) ? new Rect(min, max - min) : Rect.zero; } /// Builds a line segment. /// The starting position of the line segment /// The ending position of the line segment /// A straight line BezierSegment /// The control points are spaced out equally to maintain a constant speed on t public static BezierSegment MakeLine(Vector2 from, Vector2 to) { return new BezierSegment() { P0 = from, P1 = (to - from) / 3.0f + from, P2 = (to - from) * 2.0f / 3.0f + from, P3 = to }; } /// Converts a quadratic bezier to a cubic bezier /// The starting position of the quadratic segment /// The control position of the quadratic segment /// The ending position of the quadratic segment /// The resulting BezierSegment public static BezierSegment QuadraticToCubic(Vector2 p0, Vector2 p1, Vector2 p2) { var p = p1; var t = 2.0f / 3.0f; return new BezierSegment() { P0 = p0, P1 = p0 + t * (p - p0), P2 = p2 + t * (p - p2), P3 = p2, }; } /// Builds a line path segment. /// The starting position of the line segment /// The ending position of the line segment /// A BezierPathSegment array of two elements, configured in a straight line /// The control points are spaced out equally to maintain a constant speed on t public static BezierPathSegment[] MakePathLine(Vector2 from, Vector2 to) { return new BezierPathSegment[] { new BezierPathSegment() { P0 = from, P1 = (to - from) / 3.0f + from, P2 = (to - from) * 2.0f / 3.0f + from }, new BezierPathSegment() { P0 = to } }; } internal static BezierSegment MakeArcQuarter(Vector2 center, float startAngleRads, float sweepAngleRads) { // Approximation adapted from http://spencermortensen.com/articles/bezier-circle/ float s = Mathf.Sin(sweepAngleRads); float c = Mathf.Cos(sweepAngleRads); Matrix2D m = Matrix2D.RotateLH(startAngleRads); m.m02 = center.x; m.m12 = center.y; float f = 0.551915024494f; return new BezierSegment() { P0 = m * new Vector2(1, 0), P1 = m * new Vector2(1, f), P2 = m * new Vector2(c + f * s, s), P3 = m * new Vector2(c, s) }; } /// Approximates a circle arc with up to 4 segments. /// The center of the arc /// The starting angle of the arc, in radians /// The "length" of the arc, in radians /// The radius of the arc /// An array of up to four BezierSegments holding the arc public static BezierPathSegment[] MakeArc(Vector2 center, float startAngleRads, float sweepAngleRads, float radius) { bool shouldFlip = false; if (sweepAngleRads < 0.0f) { startAngleRads += sweepAngleRads; sweepAngleRads = -sweepAngleRads; shouldFlip = true; } sweepAngleRads = Mathf.Min(sweepAngleRads, Mathf.PI * 2); BezierSegment subSeg1; BezierSegment subSeg2; var segments = new List(); int endQuadrant = QuadrantAtAngle(sweepAngleRads); for (int quadrant = 0; quadrant <= endQuadrant; ++quadrant) { var seg = ArcSegmentForQuadrant(quadrant); // Check if we need to split the segment var p0 = Vector2.zero; var p1 = new Vector2(2.0f, 0.0f); var intersects = FindBezierLineIntersections(seg, p0, p1); if (quadrant != 3 && intersects.Length > 0) { VectorUtils.SplitSegment(seg, intersects[0], out subSeg1, out subSeg2); seg = subSeg2; } p1 = new Vector2(Mathf.Cos(sweepAngleRads), Mathf.Sin(sweepAngleRads)) * 2.0f; intersects = FindBezierLineIntersections(seg, p0, p1); if (intersects.Length > 0) { VectorUtils.SplitSegment(seg, intersects[0], out subSeg1, out subSeg2); seg = subSeg1; } if (!VectorUtils.IsEmptySegment(seg)) segments.Add(seg); } for (int i = 0; i < segments.Count; ++i) segments[i] = TransformSegment(segments[i], center, -startAngleRads, Vector2.one * radius); if (shouldFlip) { // Path is reversed, so we should flip it now for (int i = 0; i < segments.Count / 2; ++i) { int j = segments.Count - i - 1; var seg0 = VectorUtils.FlipSegment(segments[i]); var seg1 = VectorUtils.FlipSegment(segments[j]); segments[i] = seg1; segments[j] = seg0; } if ((segments.Count % 2) == 1) { int i = segments.Count / 2; segments[i] = VectorUtils.FlipSegment(segments[i]); } } return VectorUtils.BezierSegmentsToPath(segments.ToArray()); } internal static int QuadrantAtAngle(float angle) { angle = angle % (Mathf.PI * 2); if (angle < 0.0f) angle = Mathf.PI * 2 + angle; if (angle <= Mathf.PI / 2.0f) return 0; else if (angle <= Mathf.PI) return 1; else if (angle <= Mathf.PI / 2.0f * 3.0f) return 2; else return 3; } internal static BezierSegment ArcSegmentForQuadrant(int quadrant) { switch (quadrant) { case 0: return VectorUtils.MakeArcQuarter(Vector2.zero, 0.0f, Mathf.PI / 2.0f); case 1: return VectorUtils.MakeArcQuarter(Vector2.zero, -Mathf.PI / 2.0f, Mathf.PI / 2.0f); case 2: return VectorUtils.MakeArcQuarter(Vector2.zero, -Mathf.PI, Mathf.PI / 2.0f); case 3: return VectorUtils.MakeArcQuarter(Vector2.zero, -Mathf.PI / 2.0f * 3.0f, Mathf.PI / 2.0f); default: return new BezierSegment(); } } /// Flips a segment direction. /// The segment to flip /// The flipped segment public static BezierSegment FlipSegment(BezierSegment segment) { var s = segment; var tmp = s.P0; s.P0 = s.P3; s.P3 = tmp; tmp = s.P1; s.P1 = s.P2; s.P2 = tmp; return s; } /// Computes the bounds of a segment. /// The segment to flip /// The output min value of the segment /// The output max value of the segment public static void Bounds(BezierSegment segment, out Vector2 min, out Vector2 max) { min = Vector2.Min(segment.P0, segment.P3); max = Vector2.Max(segment.P0, segment.P3); Vector2 a = 3.0f * segment.P3 - 9.0f * segment.P2 + 9.0f * segment.P1 - 3.0f * segment.P0; Vector2 b = 6.0f * segment.P2 - 12.0f * segment.P1 + 6.0f * segment.P0; Vector2 c = 3.0f * segment.P1 - 3.0f * segment.P0; float[] solutions = new float[4]; SolveQuadratic(a.x, b.x, c.x, out solutions[0], out solutions[1]); SolveQuadratic(a.y, b.y, c.y, out solutions[2], out solutions[3]); foreach (var s in solutions) { if (float.IsNaN(s) || (s < 0.0f) || (s > 1.0f)) continue; Vector2 v = Eval(segment, s); min = Vector2.Min(min, v); max = Vector2.Max(max, v); } } /// Evaluates the position on a curve segment. /// The curve segment on which to evaluate the position /// The parametric location on the curve /// The position on the curve at parametric location "t" public static Vector2 Eval(BezierSegment segment, float t) { float t2 = t * t; float t3 = t2 * t; return (segment.P3 - 3.0f * segment.P2 + 3.0f * segment.P1 - segment.P0) * t3 + (3.0f * segment.P2 - 6.0f * segment.P1 + 3.0f * segment.P0) * t2 + (3.0f * segment.P1 - 3.0f * segment.P0) * t + segment.P0; } /// Evaluates the tangent on a curve segment. /// The curve segment on which to evaluate the tangent /// The parametric location on the curve /// The tangent of the curve at parametric location "t" public static Vector2 EvalTangent(BezierSegment segment, float t) { var tan = (segment.P3 - 3.0f * segment.P2 + 3.0f * segment.P1 - segment.P0) * 3.0f * t * t + (3.0f * segment.P2 - 6.0f * segment.P1 + 3.0f * segment.P0) * 2.0f * t + (3.0f * segment.P1 - 3.0f * segment.P0); // If the result is a zero vector (happens at coincident p0 and p1 or p2 and p3) try again by manual stepping if (tan.sqrMagnitude < Epsilon) { if (t > 0.5f) tan = Eval(segment, t) - Eval(segment, t - 0.01f); else tan = Eval(segment, t + 0.01f) - Eval(segment, t); } return tan.normalized; } /// Evalutes the normal on a curve segment. /// The curve segment on which to evaluate the normal /// The parametric location on the curve /// The normal of the curve at parametric location "t" /// /// A positive normal at a point on the bezier curve is always on the /// right side of the forward direction (tangent) of the curve at that point. /// public static Vector2 EvalNormal(BezierSegment segment, float t) { return Vector2.Perpendicular(EvalTangent(segment, t)); } /// Evalutes both the position and tangent on a curve segment. /// The curve segment on which to evaluate the normal /// The parametric location on the curve /// The output tangent at parametric location "t" /// The position on the curve at parametric location "t" /// /// This is more efficient than calling "Eval" and "EvalTangent" successively. /// public static Vector2 EvalFull(BezierSegment segment, float t, out Vector2 tangent) { float t2 = t * t; float t3 = t2 * t; Vector2 C1 = segment.P3 - 3.0f * segment.P2 + 3.0f * segment.P1 - segment.P0; Vector2 C2 = 3.0f * segment.P2 - 6.0f * segment.P1 + 3.0f * segment.P0; Vector2 C3 = 3.0f * segment.P1 - 3.0f * segment.P0; Vector2 C4 = segment.P0; var pos = C1 * t3 + C2 * t2 + C3 * t + C4; tangent = ((3.0f * C1 * t2) + (2.0f * C2 * t) + C3); // If the result is a zero vector (happens at coincident p0 and p1 or p2 and p3) try again by manual stepping if (tangent.sqrMagnitude < Epsilon) { if (t > 0.5f) tangent = pos - Eval(segment, t - 0.01f); else tangent = Eval(segment, t + 0.01f) - pos; } tangent = tangent.normalized; return pos; } /// Evalutes the position, tangent and normal on a curve segment. /// The curve segment on which to evaluate the normal /// The parametric location on the curve /// The output tangent at parametric location "t" /// The output normal at parametric location "t" /// The position on the curve at parametric location "t" /// /// This is more efficient than calling "Eval", "EvalTangent" and "EvalNormal" successively. /// public static Vector2 EvalFull(BezierSegment segment, float t, out Vector2 tangent, out Vector2 normal) { Vector2 pos = EvalFull(segment, t, out tangent); normal = Vector2.Perpendicular(tangent); return pos; } /// Computes the individual lengths of a segment chain. /// The segments on which to compute the lengths /// A boolean indicating if the length of the segment joining the first and last points should be computed /// The precision of the lengths computation /// An array containing the lenghts of the segments public static float[] SegmentsLengths(IList segments, bool closed, float precision = 0.001f) { float[] segmentLengths = new float[segments.Count - 1 + (closed ? 1 : 0)]; int i = 0; foreach (var segment in SegmentsInPath(segments, closed)) segmentLengths[i++] = SegmentLength(segment, precision); return segmentLengths; } /// Computes the combined length of a segment chain. /// The curve segments on which to evaluate the length /// A boolean indicating if the length of the segment joining the first and last points should be computed /// The precision of the length computation /// The combined length of the segment chain public static float SegmentsLength(IList segments, bool closed, float precision = 0.001f) { if (segments.Count < 2) return 0.0f; float length = 0.0f; foreach (var segment in SegmentsInPath(segments)) length += SegmentLength(segment, precision); if (closed) length += (segments[segments.Count - 1].P0 - segments[0].P0).magnitude; return length; } /// Computes the length of a single curve segment. /// The curve segment on which to evaluate the length /// The precision of the length computation /// The length of the segment public static float SegmentLength(BezierSegment segment, float precision = 0.001f) { // This adaptive algorithm doesn't behave well at the limit of float precision, // so we revert to a dummy iterative approach in this case if (VectorUtils.HasLargeCoordinates(segment)) { int steps = Math.Min(100, (int)(1.0f/precision)); return SegmentLengthIterative(segment, steps); } float tmax = 0.0f; float length = 0.0f; while ((tmax = AdaptiveQuadraticApproxSplitPoint(segment, precision)) < 1.0f) { BezierSegment b1, b2; SplitSegment(segment, tmax, out b1, out b2); float midPointLength = MidPointQuadraticApproxLength(b1); if (float.IsNaN(midPointLength)) // Could happen because of float precision issues midPointLength = SegmentLengthIterative(b1); length += midPointLength; segment = b2; } length += MidPointQuadraticApproxLength(segment); return length; } internal static float SegmentLengthIterative(BezierSegment segment, int steps = 10) { if (steps <= 2) return (segment.P3 - segment.P0).magnitude; float length = 0.0f; var p = segment.P0; for (int i = 1; i <= steps; ++i) { float t = (float)i/steps; var q = VectorUtils.Eval(segment, t); length += (q-p).magnitude; p = q; } return length; } internal static bool HasLargeCoordinates(BezierSegment segment) { const float kMaxCoord = 10000.0f; return segment.P0.x > kMaxCoord || segment.P0.y > kMaxCoord || segment.P1.x > kMaxCoord || segment.P1.y > kMaxCoord || segment.P2.x > kMaxCoord || segment.P2.y > kMaxCoord || segment.P3.x > kMaxCoord || segment.P3.y > kMaxCoord; } static float AdaptiveQuadraticApproxSplitPoint(BezierSegment segment, float precision) { float quadraticApproxDist = (segment.P3 - 3.0f * segment.P2 + 3.0f * segment.P1 - segment.P0).magnitude * 0.5f; return Mathf.Pow((18.0f / Mathf.Sqrt(3.0f)) * precision / quadraticApproxDist, 1.0f / 3.0f); } static float MidPointQuadraticApproxLength(BezierSegment segment) { var A = segment.P0; var B = (3.0f * segment.P2 - segment.P3 + 3.0f * segment.P1 - segment.P0) / 4.0f; var C = segment.P3; if (A == C) return (A == B) ? 0.0f : (A - B).magnitude; if (B == A || B == C) return (A - C).magnitude; var A0 = B - A; var A1 = A - 2.0f * B + C; if (A1 != Vector2.zero) { double c = 4.0f * Vector2.Dot(A1, A1); double b = 8.0f * Vector2.Dot(A0, A1); double a = 4.0f * Vector2.Dot(A0, A0); double q = 4.0f * a * c - b * b; double twoCpB = 2.0f * c + b; double sumCBA = c + b + a; var l0 = (0.25f / c) * (twoCpB * Math.Sqrt(sumCBA) - b * Math.Sqrt(a)); if (Math.Abs(q) <= VectorUtils.Epsilon) return (float)l0; var l1 = (q / (8.0f * Math.Pow(c, 1.5f))) * (Math.Log(2.0f * Math.Sqrt(c * sumCBA) + twoCpB) - Math.Log(2.0f * Math.Sqrt(c * a) + b)); return (float)(l0 + l1); } else return 2.0f * A0.magnitude; } /// Splits a curve segment at a given parametric location. /// The curve segment to split /// The parametric location at which the segment will be split /// The output of the first segment /// The output of the second segment public static void SplitSegment(BezierSegment segment, float t, out BezierSegment b1, out BezierSegment b2) { var a = Vector2.LerpUnclamped(segment.P0, segment.P1, t); var b = Vector2.LerpUnclamped(segment.P1, segment.P2, t); var c = Vector2.LerpUnclamped(segment.P2, segment.P3, t); var m = Vector2.LerpUnclamped(a, b, t); var n = Vector2.LerpUnclamped(b, c, t); var p = Eval(segment, t); b1 = new BezierSegment() { P0 = segment.P0, P1 = a, P2 = m, P3 = p }; b2 = new BezierSegment() { P0 = p, P1 = n, P2 = c, P3 = segment.P3 }; } /// Transforms a curve segment by a translation, rotation and scaling. /// The curve segment to transform /// The translation to apply on the curve segment /// The rotation to apply on the curve segment /// The scaling to apply on the curve segment /// The transformed curve segment public static BezierSegment TransformSegment(BezierSegment segment, Vector2 translation, float rotation, Vector2 scaling) { var m = Matrix2D.RotateLH(rotation); var newSeg = new BezierSegment() { P0 = m * Vector2.Scale(segment.P0, scaling) + translation, P1 = m * Vector2.Scale(segment.P1, scaling) + translation, P2 = m * Vector2.Scale(segment.P2, scaling) + translation, P3 = m * Vector2.Scale(segment.P3, scaling) + translation }; return newSeg; } /// Transforms a curve segment by a transformation matrix. /// The curve segment to transform /// The transformation matrix to apply on the curve segment /// The transformed curve segment public static BezierSegment TransformSegment(BezierSegment segment, Matrix2D matrix) { var newSeg = new BezierSegment() { P0 = matrix * segment.P0, P1 = matrix * segment.P1, P2 = matrix * segment.P2, P3 = matrix * segment.P3 }; return newSeg; } /// Transforms a path by a transformation matrix. /// The path to transform /// The translation to apply /// The rotation to apply, in radians /// The scaling to apply /// The transformed path public static BezierPathSegment[] TransformBezierPath(BezierPathSegment[] path, Vector2 translation, float rotation, Vector2 scaling) { var m = Matrix2D.RotateLH(rotation); var newPath = new BezierPathSegment[path.Length]; for (int i = 0; i < newPath.Length; ++i) { var seg = path[i]; newPath[i] = new BezierPathSegment() { P0 = m * Vector2.Scale(seg.P0, scaling) + translation, P1 = m * Vector2.Scale(seg.P1, scaling) + translation, P2 = m * Vector2.Scale(seg.P2, scaling) + translation }; } return newPath; } /// Transforms a path by a transformation matrix. /// The path to transform /// The transformation matrix to apply on the curve segment /// The transformed path public static BezierPathSegment[] TransformBezierPath(BezierPathSegment[] path, Matrix2D matrix) { var newPath = new BezierPathSegment[path.Length]; for (int i = 0; i < newPath.Length; ++i) { var seg = path[i]; newPath[i] = new BezierPathSegment() { P0 = matrix * seg.P0, P1 = matrix * seg.P1, P2 = matrix * seg.P2 }; } return newPath; } /// Lists every nodes under a root node. /// The root node /// The enumerable listing every nodes under "root", including the root itself. public static IEnumerable SceneNodes(SceneNode root) { yield return root; if (root.Children != null) { foreach (var c in root.Children) { foreach (var n in SceneNodes(c)) yield return n; } } } /// Structure holding the SceneNode computed transforms, opacities and enumeration path. /// This helper structure is used by the WorldTransformedSceneNodes method. public struct SceneNodeWorldTransform { /// The node we are currently visiting. public SceneNode Node; /// The parent of the node we are currently visiting. public SceneNode Parent; /// The accumulated world transform of this node. public Matrix2D WorldTransform; /// The accumulated world opacity of this node. public float WorldOpacity; } static IEnumerable WorldTransformedSceneNodes(SceneNode child, Dictionary nodeOpacities, SceneNodeWorldTransform parent) { var childOpacity = 1.0f; if (nodeOpacities == null || !nodeOpacities.TryGetValue(child, out childOpacity)) childOpacity = 1.0f; var childWorldTransform = new SceneNodeWorldTransform() { Node = child, WorldTransform = parent.WorldTransform * child.Transform, WorldOpacity = parent.WorldOpacity * childOpacity, Parent = parent.Node }; yield return childWorldTransform; if (child.Children != null) { foreach (var c in child.Children) { foreach (var n in WorldTransformedSceneNodes(c, nodeOpacities, childWorldTransform)) yield return n; } } } /// Iterates through every nodes under a root with computed transform and opacities. /// The starting node of the hierarchy /// Storage for the resulting node opacities, may be null /// An enumeration of every node with their pre-computed world transforms, opacities and paths. public static IEnumerable WorldTransformedSceneNodes(SceneNode root, Dictionary nodeOpacities) { var rootNodeWorldTransform = new SceneNodeWorldTransform() { Node = root, WorldTransform = Matrix2D.identity, WorldOpacity = 1, Parent = null }; return WorldTransformedSceneNodes(root, nodeOpacities, rootNodeWorldTransform); } /// Realigns the vertices (in-place) inside their axis-aligned bounding-box. /// The vertices to realign /// The bounds into which the vertices will be realigned /// A boolean indicating whether to flip the coordinates on the Y axis public static void RealignVerticesInBounds(IList vertices, Rect bounds, bool flip) { var p = bounds.position; var h = bounds.height; for (int i = 0; i < vertices.Count; ++i) { var v = vertices[i]; v -= p; if (flip) v.y = h - v.y; vertices[i] = v; } } /// Flip the vertices (in-place) inside their axis-aligned bounding-box. /// The vertices to realign /// The bounds into which the vertices will be realigned public static void FlipVerticesInBounds(IList vertices, Rect bounds) { var h = bounds.height; for (int i = 0; i < vertices.Count; ++i) { var v = vertices[i]; v.y = h - v.y; vertices[i] = v; } } internal static void ClampVerticesInBounds(IList vertices, Rect bounds) { for (int i = 0; i < vertices.Count; ++i) vertices[i] = Vector2.Max(bounds.min, Vector2.Min(bounds.max, vertices[i])); } /// Iterates through every segment in a list of path segments. /// The path segments to iterate from /// Whether to return the segment connecting the last point to the beginning of the path /// An enumerable of every segments in the path public static IEnumerable SegmentsInPath(IEnumerable segments, bool closed = false) { var e = segments.GetEnumerator(); if (!e.MoveNext()) yield break; var s1 = e.Current; if (!e.MoveNext()) yield break; do { var s2 = e.Current; yield return new BezierSegment { P0 = s1.P0, P1 = s1.P1, P2 = s1.P2, P3 = s2.P0 }; s1 = s2; } while (e.MoveNext()); if (closed) yield return new BezierSegment { P0 = s1.P0, P1 = s1.P1, P2 = s1.P2, P3 = segments.First().P0 }; } static void SolveQuadratic(float a, float b, float c, out float s1, out float s2) { float det = b * b - 4.0f * a * c; if (det < 0.0f) { s1 = s2 = float.NaN; return; } float detSqrt = Mathf.Sqrt(det); s1 = (-b + detSqrt) / 2.0f * a; if (Mathf.Abs(a) > float.Epsilon) s2 = (-b - detSqrt) / 2.0f * a; else s2 = float.NaN; } /// Finds the intersection between two infinite lines /// The first point of the first line /// The second point of the first line /// The first point of the second line /// The second point of the second line /// The intersection point, or (float.PositiveInfinity, float.PositiveInfinity) if the lines are parallel public static Vector2 IntersectLines(Vector2 line1Pt1, Vector2 line1Pt2, Vector2 line2Pt1, Vector2 line2Pt2) { var a1 = line1Pt2.y - line1Pt1.y; var b1 = line1Pt1.x - line1Pt2.x; var a2 = line2Pt2.y - line2Pt1.y; var b2 = line2Pt1.x - line2Pt2.x; var det = a1 * b2 - a2 * b1; if (Mathf.Abs(det) <= Epsilon) return new Vector2(float.PositiveInfinity, float.PositiveInfinity); // Parallel, no intersection var c1 = a1 * line1Pt1.x + b1 * line1Pt1.y; var c2 = a2 * line2Pt1.x + b2 * line2Pt1.y; var detInv = 1.0f / det; return new Vector2((b2 * c1 - b1 * c2) * detInv, (a1 * c2 - a2 * c1) * detInv); } /// Finds the intersection between two line segments /// The first point of the first line /// The second point of the first line /// The first point of the second line /// The second point of the second line /// The intersection point, or (float.PositiveInfinity, float.PositiveInfinity) if the lines are parallel public static Vector2 IntersectLineSegments(Vector2 line1Pt1, Vector2 line1Pt2, Vector2 line2Pt1, Vector2 line2Pt2) { var a1 = (line1Pt1.x - line2Pt2.x) * (line1Pt2.y - line2Pt2.y) - (line1Pt1.y - line2Pt2.y) * (line1Pt2.x - line2Pt2.x); var a2 = (line1Pt1.x - line2Pt1.x) * (line1Pt2.y - line2Pt1.y) - (line1Pt1.y - line2Pt1.y) * (line1Pt2.x - line2Pt1.x); if (a1 * a2 <= 0.0f) { var a3 = (line2Pt1.x - line1Pt1.x) * (line2Pt2.y - line1Pt1.y) - (line2Pt1.y - line1Pt1.y) * (line2Pt2.x - line1Pt1.x); var a4 = a3 + a2 - a1; if (a3 * a4 <= 0.0f) { float t = a3 / (a3 - a4); var p = line1Pt1 + t * (line1Pt2 - line1Pt1); return p; } } return new Vector2(float.PositiveInfinity, float.PositiveInfinity); } static bool PointOnTheLeftOfLine(Vector2 lineFrom, Vector2 lineTo, Vector2 point) { return ((lineFrom.x - lineTo.x) * (point.y - lineTo.y) - (lineFrom.y - lineTo.y) * (point.x - lineTo.x)) > 0; } /// Find the intersections (up to three) between a line and a curve segment. /// The curve segment /// The first point /// The second point /// Returns the Bezier's 't' parametric values where the line p0-p1 intersects the segment, up to 3 values public static float[] FindBezierLineIntersections(BezierSegment segment, Vector2 p0, Vector2 p1) { var A = p1.y - p0.y; var B = p0.x - p1.x; var C = p0.x * (p0.y - p1.y) + p0.y * (p1.x - p0.x); var coeffs = BezierCoefficients(segment); var P = new float[4]; P[0] = A * coeffs[0].x + B * coeffs[0].y; P[1] = A * coeffs[1].x + B * coeffs[1].y; P[2] = A * coeffs[2].x + B * coeffs[2].y; P[3] = A * coeffs[3].x + B * coeffs[3].y + C; var roots = CubicRoots(P[0], P[1], P[2], P[3]); var validRoots = new List(roots.Length); foreach (var t in roots) { var t2 = t * t; var t3 = t2 * t; var p = coeffs[0] * t3 + coeffs[1] * t2 + coeffs[2] * t + coeffs[3]; var s = 0.0f; if (Mathf.Abs(p1.x - p0.x) > VectorUtils.Epsilon) s = (p.x - p0.x) / (p1.x - p0.x); else s = (p.y - p0.y) / (p1.y - p0.y); if (t >= 0.0f && t <= 1.0f && s >= 0.0f && s <= 1.0f) validRoots.Add(t); } return validRoots.ToArray(); } private static float[] CubicRoots(double a, double b, double c, double d) { var A = b / a; var B = c / a; var C = d / a; var Q = (3 * B - Math.Pow(A, 2)) / 9; var R = (9 * A * B - 27 * C - 2 * Math.Pow(A, 3)) / 54; var D = Math.Pow(Q, 3) + Math.Pow(R, 2); var Im = 0.0; var t = new List(3); t.AddRange(new double[] { -1.0, -1.0, -1.0 }); if (D >= 0) { var sqrtD = Math.Sqrt(D); var S = Math.Sign(R + sqrtD) * Math.Pow(Math.Abs(R + sqrtD), 1.0 / 3.0); var T = Math.Sign(R - sqrtD) * Math.Pow(Math.Abs(R - sqrtD), 1.0 / 3.0); t[0] = -A / 3 + (S + T); t[1] = -A / 3 - (S + T) / 2; t[2] = t[1]; Im = Math.Abs(Math.Sqrt(3.0) * (S - T) / 2); if (Math.Abs(Im) > VectorUtils.Epsilon) { t[1] = -1; t[2] = -1; } } else { var th = Math.Acos(R / Math.Sqrt(-Math.Pow(Q, 3))); var sqrtMinusQ = Math.Sqrt(-Q); t[0] = 2 * sqrtMinusQ * Math.Cos(th / 3) - A / 3; t[1] = 2 * sqrtMinusQ * Math.Cos((th + 2 * Math.PI) / 3) - A / 3; t[2] = 2 * sqrtMinusQ * Math.Cos((th + 4 * Math.PI) / 3) - A / 3; } for (int i = 0; i < 3; ++i) { if (t[i] < 0.0 || t[i] > 1.0) t[i] = -1; } // Remove -1 values which means no root was found t.RemoveAll(x => Math.Abs(x + 1.0) < (double)Epsilon); return t.Select(x => (float)x).ToArray(); } private static Vector2[] BezierCoefficients(BezierSegment segment) { var coeffs = new Vector2[4]; coeffs[0] = -segment.P0 + 3 * segment.P1 + -3 * segment.P2 + segment.P3; coeffs[1] = 3 * segment.P0 - 6 * segment.P1 + 3 * segment.P2; coeffs[2] = -3 * segment.P0 + 3 * segment.P1; coeffs[3] = segment.P0; return coeffs; } /// Computes a pretty accurate approximation of the scene bounds. /// The root node of the hierarchy to computes the bounds from /// An approximation of the node hierarchy axis-aligned bounding-box /// /// This will properly evaluate the bounds of the paths and shapes, but will ignore the paths stroke widths. /// #pragma warning disable 612, 618 // Silence use of deprecated IDrawable public static Rect SceneNodeBounds(SceneNode root) { var min = new Vector2(float.MaxValue, float.MaxValue); var max = new Vector2(-float.MaxValue, -float.MaxValue); foreach (var tnode in WorldTransformedSceneNodes(root, null)) { var shapeMin = new Vector2(float.MaxValue, float.MaxValue); var shapeMax = new Vector2(-float.MaxValue, -float.MaxValue); if (tnode.Node.Shapes != null) { foreach (var shape in tnode.Node.Shapes) { foreach (var contour in shape.Contours) { var bbox = Bounds(TransformBezierPath(contour.Segments, tnode.WorldTransform)); shapeMin = Vector2.Min(shapeMin, bbox.min); shapeMax = Vector2.Max(shapeMax, bbox.max); } } } if (shapeMin.x != float.MaxValue) { min = Vector2.Min(min, shapeMin); max = Vector2.Max(max, shapeMax); } } return (min.x != float.MaxValue) ? new Rect(min, max - min) : Rect.zero; } /// Computes a rough approximation of the node hierarchy bounds. /// The root node of the hierarchy to computes the bounds from /// An approximation of the root hierarchy axis-aligned bounding-box /// /// This will use the control point positions as a rough estimate of the bounds for the paths and shapes. /// public static Rect ApproximateSceneNodeBounds(SceneNode root) { var vertices = new List(100); foreach (var tnode in WorldTransformedSceneNodes(root, null)) { if (tnode.Node.Shapes != null) { foreach (var shape in tnode.Node.Shapes) { foreach (var contour in shape.Contours) { foreach (var seg in TransformBezierPath(contour.Segments, tnode.WorldTransform)) { vertices.Add(seg.P0); vertices.Add(seg.P1); vertices.Add(seg.P2); } } } } } return Bounds(vertices); } #pragma warning restore 612, 618 internal static bool IsEmptySegment(BezierSegment bs) { return (bs.P0 - bs.P1).sqrMagnitude <= Epsilon && (bs.P0 - bs.P2).sqrMagnitude <= Epsilon && (bs.P0 - bs.P3).sqrMagnitude <= Epsilon; } } // VectorUtils class internal class PathDistanceForwardIterator { class BezierLoop : IList { IList OpenPath; public BezierLoop(IList openPath) { this.OpenPath = openPath; } public BezierPathSegment this[int index] { get { if (index == OpenPath.Count) return OpenPath[0]; return OpenPath[index]; } set { throw new NotSupportedException(); } } public int Count { get { return OpenPath.Count + 1; } } public bool IsReadOnly { get { return true; } } public void Add(BezierPathSegment item) { throw new NotSupportedException(); } public void Clear() {} public bool Contains(BezierPathSegment item) { throw new NotImplementedException(); } public void CopyTo(BezierPathSegment[] array, int arrayIndex) { throw new NotImplementedException(); } public IEnumerator GetEnumerator() { throw new NotImplementedException(); } public int IndexOf(BezierPathSegment item) { throw new NotImplementedException(); } public void Insert(int index, BezierPathSegment item) { throw new NotSupportedException(); } public bool Remove(BezierPathSegment item) { throw new NotSupportedException(); } public void RemoveAt(int index) { throw new NotSupportedException(); } IEnumerator IEnumerable.GetEnumerator() { throw new NotImplementedException(); } } public enum Result { Stepped, NewSegment, Ended }; public PathDistanceForwardIterator(IList pathSegments, bool closed, float maxCordDeviationSq, float maxTanAngleDevCosine, float stepSizeT) { if (pathSegments.Count < 2) throw new Exception("Cannot iterate a path with no segments in it"); Segments = closed && !VectorUtils.PathEndsPerfectlyMatch(pathSegments) ? new BezierLoop(pathSegments) : pathSegments; this.closed = closed; this.needTangentsDuringEval = maxTanAngleDevCosine < 1.0f; this.maxCordDeviationSq = maxCordDeviationSq; this.maxTanAngleDevCosine = maxTanAngleDevCosine; this.stepSizeT = stepSizeT; currentBezSeg = new BezierSegment() { P0 = pathSegments[0].P0, P1 = pathSegments[0].P1, P2 = pathSegments[0].P2, P3 = pathSegments[1].P0 }; lastPointEval = pathSegments[0].P0; currentTTangent = needTangentsDuringEval ? VectorUtils.EvalTangent(currentBezSeg, 0.0f) : Vector2.zero; } float PointToLineDistanceSq(Vector2 point, Vector2 lineStart, Vector2 lineEnd) { float lineMagSq = (lineEnd - lineStart).sqrMagnitude; if (lineMagSq < VectorUtils.Epsilon) return (point - lineStart).sqrMagnitude; float num = (lineEnd.y - lineStart.y) * point.x - (lineEnd.x - lineStart.x) * point.y + lineEnd.x * lineStart.y - lineEnd.y * lineStart.x; return (num * num) / lineMagSq; } public Result AdvanceBy(float units, out float unitsRemaining) { unitsRemaining = units; if (Ended) return Result.Ended; // Reached the end float t = currentT; Vector2 currentTPosition = lastPointEval; for (;;) { float nextT = Mathf.Min(t + stepSizeT, 1.0f); Vector2 tangent = Vector2.zero; Vector2 point = needTangentsDuringEval ? VectorUtils.EvalFull(currentBezSeg, nextT, out tangent) : VectorUtils.Eval(currentBezSeg, nextT); bool generateStepHere = false; if (needTangentsDuringEval) { float tangentDiffCosAngle = Vector2.Dot(tangent, currentTTangent); generateStepHere = tangentDiffCosAngle < this.maxTanAngleDevCosine; } if (!generateStepHere && (maxCordDeviationSq != float.MaxValue)) { Vector2 firstPoint = currentTPosition; float distPtToFirstSq = (point - firstPoint).sqrMagnitude; if (distPtToFirstSq > VectorUtils.Epsilon) { Vector2 secondPoint = VectorUtils.Eval(currentBezSeg, Mathf.Min((nextT - currentT) * 2.0f + currentT, 1.0f)); float midPointDistSq = PointToLineDistanceSq(point, firstPoint, secondPoint); generateStepHere = midPointDistSq >= maxCordDeviationSq; } } float dist = (point - lastPointEval).magnitude; if (dist > unitsRemaining) { nextT = t + stepSizeT * (unitsRemaining / dist); // A linear approximation, not too bad for small step sizes dist = unitsRemaining; point = VectorUtils.Eval(currentBezSeg, nextT); } segmentLengthSoFar += dist; lengthSoFar += dist; unitsRemaining -= dist; lastPointEval = point; t = nextT; if (nextT < 1.0f) { if ((unitsRemaining > 0) && !generateStepHere) continue; currentT = nextT; currentTTangent = tangent; return Result.Stepped; } // Crossing to a new segment if (currentSegment + 1 == Segments.Count - 1) { currentT = 1.0f; return Result.Ended; // Reached the end } currentSegment++; currentBezSeg = new BezierSegment() { P0 = Segments[currentSegment].P0, P1 = Segments[currentSegment].P1, P2 = Segments[currentSegment].P2, P3 = Segments[currentSegment + 1].P0 }; segmentLengthSoFar = 0.0f; currentT = 0.0f; currentTTangent = tangent; lastPointEval = currentBezSeg.P0; return Result.NewSegment; } } public IList Segments { get; } public bool Closed { get { return closed; } } public int CurrentSegment { get { return currentSegment; } } public float CurrentT { get { return currentT; } } public float LengthSoFar { get { return lengthSoFar; } } public float SegmentLengthSoFar { get { return segmentLengthSoFar; } } public bool Ended { get { return (currentT == 1.0f) && (currentSegment + 1 == Segments.Count - 1); } } public Vector2 EvalCurrent() { return VectorUtils.Eval(currentBezSeg, currentT); } // Path data and settings readonly bool closed, needTangentsDuringEval; readonly float maxCordDeviationSq, maxTanAngleDevCosine, stepSizeT; // Quality control variables // State int currentSegment; float currentT; float segmentLengthSoFar; // For user's tracking purposes, not really used in our calculations float lengthSoFar; // For user's tracking purposes, not really used in our calculations Vector2 lastPointEval, currentTTangent; BezierSegment currentBezSeg; } internal class PathPatternIterator { public PathPatternIterator(float[] pattern, float patternOffset = 0.0f) { if (pattern != null) { foreach (var l in pattern) patternLength += l; } if (patternLength < VectorUtils.Epsilon) { segmentLength = float.MaxValue; return; } this.pattern = pattern; this.patternOffset = patternOffset; if (patternOffset == 0.0f) segmentLength = pattern[0]; else this.solid = IsSolidAt(0.0f, out currentSegment, out segmentLength); } public void Advance() { if (pattern == null) return; currentSegment++; if (currentSegment >= pattern.Length) currentSegment = 0; solid = !solid; segmentLength = pattern[currentSegment]; } public bool IsSolidAt(float unitsFromPathStart) { int patternSegmentIndex; float patternSegmentLength; return IsSolidAt(unitsFromPathStart, out patternSegmentIndex, out patternSegmentLength); } public bool IsSolidAt(float unitsFromPathStart, out int patternSegmentIndex, out float patternSegmentLength) { patternSegmentIndex = 0; patternSegmentLength = 0; if (pattern == null) return true; bool isSolid = true; unitsFromPathStart += patternOffset; int hops = (int)(Mathf.Abs(unitsFromPathStart) / patternLength); if (unitsFromPathStart < 0.0f) { unitsFromPathStart = patternLength - ((-unitsFromPathStart) % patternLength); if ((pattern.Length & 1) == 1) isSolid = (hops & 1) == 0; } else { unitsFromPathStart = unitsFromPathStart % patternLength; if ((pattern.Length & 1) == 1) isSolid = (hops & 1) == 1; } while (unitsFromPathStart > pattern[patternSegmentIndex]) { unitsFromPathStart -= pattern[patternSegmentIndex++]; isSolid = !isSolid; } patternSegmentLength = pattern[patternSegmentIndex] - unitsFromPathStart; return isSolid; } public float SegmentLength { get { return segmentLength; } } public bool IsSolid { get { return solid; } } float[] pattern; int currentSegment; bool solid = true; float segmentLength; float patternLength; float patternOffset; } }