/// <summary> /// Compiles the stroke to the necessary triangles to draw it. /// </summary> /// <param name="path">The path to be compiled.</param> /// <param name="width">The width of the stroke.</param> /// <param name="lineCap">The line cap method for the stroke's ends.</param> /// <param name="lineJoin">The line join method for the stroke's midpoints</param> /// <param name="miterLimit">The miter limit value.</param> public static CompiledDrawing CompileStroke(Path path, double width, StrokeLineCap lineCap = StrokeLineCap.Butt, StrokeLineJoin lineJoin = StrokeLineJoin.Bevel, double miterLimit = double.PositiveInfinity) { // Return empty if stroke width == 0 if (width == 0) { return(CompiledDrawing.Empty); } // Divide the width by 2 to cope with the SVG documentation var halfWidth = width / 2; var curves = new List <Curve>(); // Convert each split path to a fill foreach (var data in path.SplitCurves()) { curves.AddRange(StrokeUtils.ConvertToFill(data, halfWidth, lineCap, lineJoin, miterLimit)); } // And compile return(CompileCurves(curves, FillRule.Nonzero)); }
/// <summary> /// Sets the current <see cref="StrokeLineCap"/>. /// </summary> /// <param name="cap">A <see cref="StrokeLineCap"/> value.</param> public async ValueTask SetLineCapAsync(StrokeLineCap cap) { if (_disposed) { throw new ObjectDisposedException(nameof(ImageEditorService)); } if (!_moduleTask.IsValueCreated && cap == StrokeLineCap.butt) { return; } var module = await _moduleTask.Value.ConfigureAwait(false); await module.InvokeVoidAsync("setLineCap", cap.ToString()) .ConfigureAwait(false); }
public PathStyle(Dictionary <string, string> properties, PathStyle?parent) { // Fill in the default values according to the default values on https://svgwg.org/svg-next/painting.htm // Special handling for paint servers (which now only accept colors) FillColor = CSSColor.Parse(properties.GetOrDefault("fill")) ?? (parent.HasValue ? parent.Value.FillColor : Color.Black); StrokeColor = CSSColor.Parse(properties.GetOrDefault("stroke")) ?? parent?.StrokeColor; FillRule = CSSEnumPicker <FillRule> .Get(properties.GetOrDefault("fill-rule")) ?? parent?.FillRule ?? FillRule.Nonzero; StrokeWidth = DoubleUtils.TryParse(properties.GetOrDefault("stroke-width")) ?? parent?.StrokeWidth ?? 1; StrokeLineCap = CSSEnumPicker <StrokeLineCap> .Get(properties.GetOrDefault("stroke-linecap")) ?? parent?.StrokeLineCap ?? StrokeLineCap.Butt; StrokeLineJoin = CSSEnumPicker <StrokeLineJoin> .Get(properties.GetOrDefault("stroke-linejoin")) ?? parent?.StrokeLineJoin ?? StrokeLineJoin.Miter; MiterLimit = DoubleUtils.TryParse(properties.GetOrDefault("stroke-miterlimit")) ?? parent?.StrokeWidth ?? 4; }
public static List<Vector2> Stroke(StrokeSegment[] segments, float thickness, Color32 color, StrokeLineJoin lineJoin, StrokeLineCap lineCap, float miterLimit = 4f, ClosePathRule closeLine = ClosePathRule.NEVER, float roundQuality = 10f) { if(segments == null || segments.Length == 0) return null; if(segments.Length == 1) { closeLine = ClosePathRule.NEVER; } else if (closeLine == ClosePathRule.AUTO) { if(segments[0].startPoint == segments[segments.Length-1].endPoint) { closeLine = ClosePathRule.ALWAYS; } else { closeLine = ClosePathRule.NEVER; } } if(segments[0].startPoint == segments[segments.Length-1].endPoint) { List<StrokeSegment> tempSegments = new List<StrokeSegment>(segments); tempSegments.RemoveAt(tempSegments.Count - 1); segments = tempSegments.ToArray(); } List<Vector2> innerPoints = new List<Vector2>(); List<Vector2> outerPoints = new List<Vector2>(); if(closeLine == ClosePathRule.ALWAYS) { List<StrokeSegment> tempSegments = new List<StrokeSegment>(segments); tempSegments.Add(new StrokeSegment(segments[segments.Length -1].endPoint, segments[0].startPoint)); tempSegments.Add(new StrokeSegment(segments[0].startPoint, segments[0].endPoint)); segments = tempSegments.ToArray(); } miterLimit = (miterLimit - 1f) * thickness * 2f; if(miterLimit < 1f) miterLimit = 1f; int i, i1, j, segmentsLength = segments.Length; float segmentsAngle, segmentsAngleRotated, halfWidth = thickness * 0.5f, angleProgress, radAngle = 0f, miterClipHalf = miterLimit * 0.5f, miterClipHalfDouble = miterClipHalf * miterClipHalf; Vector2 segmentLeftStartA, segmentLeftEndA = Vector2.zero, segmentRightStartA, segmentRightEndA = Vector2.zero, segmentLeftEndB, segmentRightEndB, intersectionLeft, intersectionRight, segmentStartCenter; Matrix4x4 rotationMatrix = Matrix4x4.TRS(Vector2.zero, Quaternion.Euler(0f, 0f, 90f), Vector2.one); if(lineCap == StrokeLineCap.butt || closeLine == ClosePathRule.ALWAYS) { innerPoints.AddRange(new Vector2[]{ segments[0].startPoint - segments[0].directionNormalizedRotated * halfWidth, segments[0].startPoint + segments[0].directionNormalizedRotated * halfWidth, }); } else if(lineCap == StrokeLineCap.round) { segmentStartCenter = Vector2.Lerp(segments[0].startPoint - segments[0].directionNormalizedRotated * halfWidth, segments[0].startPoint + segments[0].directionNormalizedRotated * halfWidth, 0.5f); radAngle = Mathf.Atan2(segments[0].directionNormalizedRotated.y, segments[0].directionNormalizedRotated.x); float roundSegmentsfAlt = roundQuality * thickness; float roundSegmentsfAltMinusOne = roundSegmentsfAlt - 1; if(roundSegmentsfAltMinusOne > 0) { for(j = 0; j <= roundSegmentsfAlt; j++) { angleProgress = 1f - Mathf.Clamp01(j / roundSegmentsfAltMinusOne); innerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle + angleProgress * Mathf.PI) * halfWidth, Mathf.Sin(radAngle + angleProgress * Mathf.PI) * halfWidth)); } } innerPoints.AddRange(new Vector2[]{ segments[0].startPoint + segments[0].directionNormalizedRotated * halfWidth, }); outerPoints.AddRange(new Vector2[]{ segments[0].startPoint - segments[0].directionNormalizedRotated * halfWidth, }); } else if(lineCap == StrokeLineCap.square) { innerPoints.AddRange(new Vector2[]{ segments[0].startPoint - segments[0].directionNormalized * halfWidth - segments[0].directionNormalizedRotated * halfWidth, segments[0].startPoint - segments[0].directionNormalized * halfWidth + segments[0].directionNormalizedRotated * halfWidth, }); } if(segmentsLength > 1) { for(i = 1; i < segmentsLength; i++) { i1 = i - 1; /* if(segments[i1].length == 0f || segments[i].length == 0) { continue; } */ segmentsAngle = Vector2.Dot(segments[i].directionNormalized, segments[i1].directionNormalized); segmentsAngleRotated = Vector2.Dot(segments[i].directionNormalized, segments[i1].directionNormalizedRotated); float miterLength = (1f / Mathf.Sin((Mathf.PI - Mathf.Acos(segmentsAngle)) * 0.5f)) * thickness; float miterLengthHalf = miterLength * 0.5f; Vector2 miterVector = Vector2.Lerp(segments[i1].directionNormalizedRotated, segments[i].directionNormalizedRotated, 0.5f).normalized; Vector2 miterVectorLengthHalf = miterVector * miterLengthHalf; Vector2 miterVectorRotated = rotationMatrix.MultiplyVector(miterVector); segmentLeftStartA = segments[i].startPoint - segments[i].directionNormalizedRotated * halfWidth; segmentLeftEndA = segments[i].endPoint - segments[i].directionNormalizedRotated * halfWidth; segmentRightStartA = segments[i].startPoint + segments[i].directionNormalizedRotated * halfWidth; segmentRightEndA = segments[i].endPoint + segments[i].directionNormalizedRotated * halfWidth; //segmentLeftStartB = segments[i1].startPoint - segments[i1].directionNormalizedRotated * halfWidth; segmentLeftEndB = segments[i1].endPoint - segments[i1].directionNormalizedRotated * halfWidth; //segmentRightStartB = segments[i1].startPoint + segments[i1].directionNormalizedRotated * halfWidth; segmentRightEndB = segments[i1].endPoint + segments[i1].directionNormalizedRotated * halfWidth; if(lineJoin == StrokeLineJoin.miter) { if(miterLimit < miterLength) lineJoin = StrokeLineJoin.bevel; } if(lineJoin == StrokeLineJoin.miter || lineJoin == StrokeLineJoin.miterClip) { if(segmentsAngle == 1f || segmentsAngle == -1f) { innerPoints.AddRange(new Vector2[]{ segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, segmentLeftStartA, }); } else { if(segmentsAngleRotated < 0) { if(miterLimit <= miterLength) { Vector2 a = segments[i1].endPoint + miterVector * miterClipHalf; Vector2 b = segments[i1].endPoint + miterVectorLengthHalf; Vector2 c = a + miterVectorRotated; SVGMath.LineLineIntersection(out intersectionLeft, b, segmentRightEndB, a, c); SVGMath.LineLineIntersection(out intersectionRight, b, segmentRightStartA, a, c); if(miterClipHalfDouble <= (Vector2.Lerp(segmentRightEndB, segmentRightStartA, 0.5f) - segments[i1].endPoint).sqrMagnitude) { intersectionLeft = segmentRightEndB; intersectionRight = segmentRightStartA; } innerPoints.AddRange(new Vector2[]{ intersectionLeft, intersectionRight, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, segmentLeftStartA, }); } else { intersectionRight = segments[i1].endPoint + miterVectorLengthHalf; innerPoints.AddRange(new Vector2[]{ intersectionRight, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, segmentLeftStartA, }); } } else { if(miterLimit <= miterLength) { Vector2 a = segments[i1].endPoint - miterVector * miterClipHalf; Vector2 b = segments[i1].endPoint - miterVectorLengthHalf; Vector2 c = a + miterVectorRotated; SVGMath.LineLineIntersection(out intersectionLeft, b, segmentLeftStartA, a, c); SVGMath.LineLineIntersection(out intersectionRight, b, segmentLeftEndB, a, c); if(miterClipHalfDouble <= (Vector2.Lerp(segmentLeftStartA, segmentLeftEndB, 0.5f) - segments[i1].endPoint).sqrMagnitude) { intersectionLeft = segmentLeftStartA; intersectionRight = segmentLeftEndB; } outerPoints.AddRange(new Vector2[]{ intersectionRight, intersectionLeft, }); innerPoints.AddRange(new Vector2[]{ segmentRightEndB, segmentRightStartA, }); } else { intersectionLeft = segments[i1].endPoint - miterVectorLengthHalf; outerPoints.AddRange(new Vector2[]{ intersectionLeft, }); innerPoints.AddRange(new Vector2[]{ segmentRightEndB, segmentRightStartA, }); } } } } else if(lineJoin == StrokeLineJoin.bevel) { innerPoints.AddRange(new Vector2[]{ segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, segmentLeftStartA, }); } else if(lineJoin == StrokeLineJoin.round) { if(segmentsAngle == 1f) { innerPoints.AddRange(new Vector2[]{ segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, segmentLeftStartA, }); } else { if(segmentsAngleRotated < 0) { innerPoints.AddRange(new Vector2[]{ segmentRightEndB, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, segmentLeftStartA, }); segmentStartCenter = segments[i].startPoint; Vector2 dir = segments[i1].directionNormalizedRotated; float angle = Mathf.Acos(Vector2.Dot(segments[i1].directionNormalized, segments[i].directionNormalized)); radAngle = Mathf.Atan2(dir.y, dir.x); float roundSegmentsfAlt = roundQuality * thickness * (Mathf.Acos(segmentsAngle)/ Mathf.PI); if(roundSegmentsfAlt < 1) roundSegmentsfAlt = 1f; float roundSegmentsfAltMinusOne = roundSegmentsfAlt; if(roundSegmentsfAltMinusOne > 0) { for(j = 0; j < roundSegmentsfAlt; j++) { angleProgress = Mathf.Clamp01(j / roundSegmentsfAltMinusOne); innerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle - angleProgress * angle) * halfWidth, Mathf.Sin(radAngle - angleProgress * angle) * halfWidth)); } } innerPoints.AddRange(new Vector2[]{ segmentRightStartA, }); } else { innerPoints.AddRange(new Vector2[]{ segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndB, }); segmentStartCenter = segments[i].startPoint; Vector2 dir = -segments[i].directionNormalizedRotated; float angle = Mathf.Acos(Vector2.Dot(segments[i1].directionNormalized, segments[i].directionNormalized)); radAngle = Mathf.Atan2(dir.y, dir.x); float roundSegmentsfAlt = roundQuality * thickness * (Mathf.Acos(segmentsAngle)/ Mathf.PI); if(roundSegmentsfAlt < 1) roundSegmentsfAlt = 1f; float roundSegmentsfAltMinusOne = roundSegmentsfAlt; if(roundSegmentsfAltMinusOne > 0) { for(j = 0; j < roundSegmentsfAlt; j++) { angleProgress = Mathf.Clamp01(1f -(j / roundSegmentsfAltMinusOne)); outerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle - angleProgress * angle) * halfWidth, Mathf.Sin(radAngle - angleProgress * angle) * halfWidth)); } } outerPoints.AddRange(new Vector2[]{ segmentLeftStartA, }); } } } } } int lastSegmentIndex = segments.Length - 1; segmentLeftStartA = segments[lastSegmentIndex].startPoint - segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; segmentLeftEndA = segments[lastSegmentIndex].endPoint - segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; segmentRightStartA = segments[lastSegmentIndex].startPoint + segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; segmentRightEndA = segments[lastSegmentIndex].endPoint + segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; if(closeLine == ClosePathRule.NEVER) { if(lineCap == StrokeLineCap.butt) { innerPoints.AddRange(new Vector2[]{ segmentRightEndA, segmentLeftEndA, }); } else if(lineCap == StrokeLineCap.round) { innerPoints.AddRange(new Vector2[]{ segmentRightEndA }); outerPoints.AddRange(new Vector2[]{ segmentLeftEndA, }); segmentStartCenter = Vector2.Lerp(segmentLeftEndA, segmentRightEndA, 0.5f); radAngle = Mathf.Atan2(-segments[lastSegmentIndex].directionNormalizedRotated.y, -segments[lastSegmentIndex].directionNormalizedRotated.x); float roundSegmentsfAlt = roundQuality * thickness; float roundSegmentsfAltMinusOne = roundSegmentsfAlt - 1; if(roundSegmentsfAltMinusOne > 0) { for(j = 0; j <= roundSegmentsfAlt; j++) { angleProgress = 1f - Mathf.Clamp01(j / roundSegmentsfAltMinusOne); innerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle + angleProgress * Mathf.PI) * halfWidth, Mathf.Sin(radAngle + angleProgress * Mathf.PI) * halfWidth)); } } } else if(lineCap == StrokeLineCap.square) { Vector2 lastSegmentOffset = segments[lastSegmentIndex].directionNormalized * halfWidth; innerPoints.AddRange(new Vector2[]{ segmentRightEndA + lastSegmentOffset, segmentLeftEndA + lastSegmentOffset, }); } } if(closeLine == ClosePathRule.ALWAYS && lineJoin == StrokeLineJoin.miter || lineJoin == StrokeLineJoin.miterClip) { innerPoints.AddRange(new Vector2[]{ segmentRightEndA, segmentLeftEndA, }); } outerPoints.Reverse(); innerPoints.AddRange(outerPoints); return innerPoints; }
public static Mesh StrokeMesh(List<StrokeSegment[]> segments, float thickness, Color32 color, StrokeLineJoin lineJoin, StrokeLineCap lineCap, float miterLimit = 4f, float[] dashArray = null, float dashOffset = 0f, ClosePathRule closeLine = ClosePathRule.NEVER, float roundQuality = 10f) { List<List<Vector2>> finalPoints = StrokeShape(segments, thickness, color, lineJoin, lineCap, miterLimit, dashArray, dashOffset, closeLine, roundQuality); return TessellateStroke(finalPoints, color); }
public static List<List<Vector2>> StrokeShape(List<StrokeSegment[]> segments, float thickness, Color32 color, StrokeLineJoin lineJoin, StrokeLineCap lineCap, float miterLimit = 4f, float[] dashArray = null, float dashOffset = 0f, ClosePathRule closeLine = ClosePathRule.NEVER, float roundQuality = 10f) { if(segments == null || segments.Count == 0) return null; float totalCurveLength = 0f; int i, j; for(i = 0; i < segments.Count; i++) { if(segments[i] == null) continue; for(j = 0; j < segments[i].Length; j++) { totalCurveLength += segments[i][j].length; } } if(totalCurveLength == 0f) return null; bool useDash; ProcessDashArray(ref dashArray, out useDash); ClosePathRule closeSegments = closeLine; List<StrokeSegment[]> finalSegments = new List<StrokeSegment[]>(); for(i = 0; i < segments.Count; i++) { if(segments[i] == null || segments[i].Length == 0) continue; if(!useDash) { finalSegments.Add(segments[i]); } else { finalSegments.AddRange(CreateDashedStroke(segments[i], dashArray, dashOffset, ref closeSegments)); } } if(finalSegments.Count > 0) { List<List<Vector2>> finalPoints = new List<List<Vector2>>(); for(i = 0; i < finalSegments.Count; i++) { List<Vector2> points = Stroke(finalSegments[i], thickness, color, lineJoin, lineCap, miterLimit, closeSegments, roundQuality); if(points == null || points.Count < 2) continue; finalPoints.Add(points); } return finalPoints; } else { return null; } }
private static IEnumerable <Curve> GenerateLineCaps(Curve curve, bool atEnd, double halfWidth, StrokeLineCap lineCap) { // First, get the offset of the curve var tangent = atEnd ? curve.ExitTangent : curve.EntryTangent; var p = curve.At(atEnd ? 1 : 0); // Now, get the offset, properly accounting for the direction var offset = halfWidth * tangent.CCWPerpendicular; // Finally, generate the line cap switch (lineCap) { case StrokeLineCap.Butt: yield return(Curve.Line(p + offset, p - offset)); break; case StrokeLineCap.Round: yield return(Curve.Circle(p, halfWidth, offset, -offset, !atEnd)); break; case StrokeLineCap.Square: { // Generate a half-square var ext = offset.CCWPerpendicular; if (atEnd) { ext = -ext; } yield return(Curve.Line(p + offset, p + offset + ext)); yield return(Curve.Line(p + offset + ext, p - offset + ext)); yield return(Curve.Line(p - offset + ext, p - offset)); } break; default: throw new ArgumentException("Unrecognized lineCap", nameof(lineCap)); } }
public static IEnumerable <Curve> ConvertToFill(CurveData data, double halfWidth, StrokeLineCap lineCap, StrokeLineJoin lineJoin, double miterLimit) { // We are going to generate a "stroke fill" var leftCurves = new List <Curve>(); var rightCurves = new List <Curve>(); // Offset each curve for (int i = 0; i < data.Curves.Length; i++) { leftCurves.AddRange(OffsetCurve(data.Curves[i], halfWidth)); rightCurves.AddRange(OffsetCurve(data.Curves[i], -halfWidth)); // Detect where the line join should be added if (data.IsClosed || i < data.Curves.Length - 1) { var ik = (i + 1) % data.Curves.Length; var prevTangent = data.Curves[i].ExitTangent; var nextTangent = data.Curves[ik].EntryTangent; // If the angles are roughly equal, no line join if (RoughlyEquals(prevTangent, nextTangent)) { continue; } else { // Else, add to the appropriate list var lineJoint = GenerateLineJoints(data.Curves[i], data.Curves[ik], halfWidth, lineJoin, miterLimit); (prevTangent.Cross(nextTangent) > 0 ? rightCurves : leftCurves).AddRange(lineJoint); } } } // If the curve is open, add the line caps if (!data.IsClosed) { rightCurves.InsertRange(0, GenerateLineCaps(data.Curves[0], false, halfWidth, lineCap)); leftCurves.AddRange(GenerateLineCaps(data.Curves[data.Curves.Length - 1], true, halfWidth, lineCap)); } // Reverse the inner list and each curve var reverseRightCurves = rightCurves.Select(c => c.Reverse).Reverse(); // Generate the compiled fill return(leftCurves.Concat(reverseRightCurves).SelectMany(c => c.Simplify(true))); }
public static List <Vector2> Stroke(StrokeSegment[] segments, float thickness, Color32 color, StrokeLineJoin lineJoin, StrokeLineCap lineCap, float miterLimit = 4f, ClosePathRule closeLine = ClosePathRule.NEVER, float roundQuality = 10f) { if (segments == null || segments.Length == 0) { return(null); } if (segments.Length == 1) { closeLine = ClosePathRule.NEVER; } else if (closeLine == ClosePathRule.AUTO) { if (segments[0].startPoint == segments[segments.Length - 1].endPoint) { closeLine = ClosePathRule.ALWAYS; } else { closeLine = ClosePathRule.NEVER; } } if (segments[0].startPoint == segments[segments.Length - 1].endPoint) { List <StrokeSegment> tempSegments = new List <StrokeSegment>(segments); tempSegments.RemoveAt(tempSegments.Count - 1); segments = tempSegments.ToArray(); } List <Vector2> innerPoints = new List <Vector2>(); List <Vector2> outerPoints = new List <Vector2>(); if (closeLine == ClosePathRule.ALWAYS) { List <StrokeSegment> tempSegments = new List <StrokeSegment>(segments); tempSegments.Add(new StrokeSegment(segments[segments.Length - 1].endPoint, segments[0].startPoint)); tempSegments.Add(new StrokeSegment(segments[0].startPoint, segments[0].endPoint)); segments = tempSegments.ToArray(); } miterLimit = (miterLimit - 1f) * thickness * 2f; if (miterLimit < 1f) { miterLimit = 1f; } int i, i1, j, segmentsLength = segments.Length; float segmentsAngle, segmentsAngleRotated, halfWidth = thickness * 0.5f, angleProgress, radAngle = 0f, miterClipHalf = miterLimit * 0.5f, miterClipHalfDouble = miterClipHalf * miterClipHalf; Vector2 segmentLeftStartA, segmentLeftEndA = Vector2.zero, segmentRightStartA, segmentRightEndA = Vector2.zero, segmentLeftEndB, segmentRightEndB, intersectionLeft, intersectionRight, segmentStartCenter; Matrix4x4 rotationMatrix = Matrix4x4.TRS(Vector2.zero, Quaternion.Euler(0f, 0f, 90f), Vector2.one); if (lineCap == StrokeLineCap.butt || closeLine == ClosePathRule.ALWAYS) { innerPoints.AddRange(new Vector2[] { segments[0].startPoint - segments[0].directionNormalizedRotated * halfWidth, segments[0].startPoint + segments[0].directionNormalizedRotated * halfWidth, }); } else if (lineCap == StrokeLineCap.round) { segmentStartCenter = Vector2.Lerp(segments[0].startPoint - segments[0].directionNormalizedRotated * halfWidth, segments[0].startPoint + segments[0].directionNormalizedRotated * halfWidth, 0.5f); radAngle = Mathf.Atan2(segments[0].directionNormalizedRotated.y, segments[0].directionNormalizedRotated.x); float roundSegmentsfAlt = roundQuality * thickness; float roundSegmentsfAltMinusOne = roundSegmentsfAlt - 1; if (roundSegmentsfAltMinusOne > 0) { for (j = 0; j <= roundSegmentsfAlt; j++) { angleProgress = 1f - Mathf.Clamp01(j / roundSegmentsfAltMinusOne); innerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle + angleProgress * Mathf.PI) * halfWidth, Mathf.Sin(radAngle + angleProgress * Mathf.PI) * halfWidth)); } } innerPoints.AddRange(new Vector2[] { segments[0].startPoint + segments[0].directionNormalizedRotated * halfWidth, }); outerPoints.AddRange(new Vector2[] { segments[0].startPoint - segments[0].directionNormalizedRotated * halfWidth, }); } else if (lineCap == StrokeLineCap.square) { innerPoints.AddRange(new Vector2[] { segments[0].startPoint - segments[0].directionNormalized * halfWidth - segments[0].directionNormalizedRotated * halfWidth, segments[0].startPoint - segments[0].directionNormalized * halfWidth + segments[0].directionNormalizedRotated * halfWidth, }); } if (segmentsLength > 1) { for (i = 1; i < segmentsLength; i++) { i1 = i - 1; /* * if(segments[i1].length == 0f || segments[i].length == 0) * { * continue; * } */ segmentsAngle = Vector2.Dot(segments[i].directionNormalized, segments[i1].directionNormalized); segmentsAngleRotated = Vector2.Dot(segments[i].directionNormalized, segments[i1].directionNormalizedRotated); float miterLength = (1f / Mathf.Sin((Mathf.PI - Mathf.Acos(segmentsAngle)) * 0.5f)) * thickness; float miterLengthHalf = miterLength * 0.5f; Vector2 miterVector = Vector2.Lerp(segments[i1].directionNormalizedRotated, segments[i].directionNormalizedRotated, 0.5f).normalized; Vector2 miterVectorLengthHalf = miterVector * miterLengthHalf; Vector2 miterVectorRotated = rotationMatrix.MultiplyVector(miterVector); segmentLeftStartA = segments[i].startPoint - segments[i].directionNormalizedRotated * halfWidth; segmentLeftEndA = segments[i].endPoint - segments[i].directionNormalizedRotated * halfWidth; segmentRightStartA = segments[i].startPoint + segments[i].directionNormalizedRotated * halfWidth; segmentRightEndA = segments[i].endPoint + segments[i].directionNormalizedRotated * halfWidth; //segmentLeftStartB = segments[i1].startPoint - segments[i1].directionNormalizedRotated * halfWidth; segmentLeftEndB = segments[i1].endPoint - segments[i1].directionNormalizedRotated * halfWidth; //segmentRightStartB = segments[i1].startPoint + segments[i1].directionNormalizedRotated * halfWidth; segmentRightEndB = segments[i1].endPoint + segments[i1].directionNormalizedRotated * halfWidth; if (lineJoin == StrokeLineJoin.miter) { if (miterLimit < miterLength) { lineJoin = StrokeLineJoin.bevel; } } if (lineJoin == StrokeLineJoin.miter || lineJoin == StrokeLineJoin.miterClip) { if (segmentsAngle == 1f || segmentsAngle == -1f) { innerPoints.AddRange(new Vector2[] { segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, segmentLeftStartA, }); } else { if (segmentsAngleRotated < 0) { if (miterLimit <= miterLength) { Vector2 a = segments[i1].endPoint + miterVector * miterClipHalf; Vector2 b = segments[i1].endPoint + miterVectorLengthHalf; Vector2 c = a + miterVectorRotated; SVGMath.LineLineIntersection(out intersectionLeft, b, segmentRightEndB, a, c); SVGMath.LineLineIntersection(out intersectionRight, b, segmentRightStartA, a, c); if (miterClipHalfDouble <= (Vector2.Lerp(segmentRightEndB, segmentRightStartA, 0.5f) - segments[i1].endPoint).sqrMagnitude) { intersectionLeft = segmentRightEndB; intersectionRight = segmentRightStartA; } innerPoints.AddRange(new Vector2[] { intersectionLeft, intersectionRight, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, segmentLeftStartA, }); } else { intersectionRight = segments[i1].endPoint + miterVectorLengthHalf; innerPoints.AddRange(new Vector2[] { intersectionRight, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, segmentLeftStartA, }); } } else { if (miterLimit <= miterLength) { Vector2 a = segments[i1].endPoint - miterVector * miterClipHalf; Vector2 b = segments[i1].endPoint - miterVectorLengthHalf; Vector2 c = a + miterVectorRotated; SVGMath.LineLineIntersection(out intersectionLeft, b, segmentLeftStartA, a, c); SVGMath.LineLineIntersection(out intersectionRight, b, segmentLeftEndB, a, c); if (miterClipHalfDouble <= (Vector2.Lerp(segmentLeftStartA, segmentLeftEndB, 0.5f) - segments[i1].endPoint).sqrMagnitude) { intersectionLeft = segmentLeftStartA; intersectionRight = segmentLeftEndB; } outerPoints.AddRange(new Vector2[] { intersectionRight, intersectionLeft, }); innerPoints.AddRange(new Vector2[] { segmentRightEndB, segmentRightStartA, }); } else { intersectionLeft = segments[i1].endPoint - miterVectorLengthHalf; outerPoints.AddRange(new Vector2[] { intersectionLeft, }); innerPoints.AddRange(new Vector2[] { segmentRightEndB, segmentRightStartA, }); } } } } else if (lineJoin == StrokeLineJoin.bevel) { innerPoints.AddRange(new Vector2[] { segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, segmentLeftStartA, }); } else if (lineJoin == StrokeLineJoin.round) { if (segmentsAngle == 1f) { innerPoints.AddRange(new Vector2[] { segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, segmentLeftStartA, }); } else { if (segmentsAngleRotated < 0) { innerPoints.AddRange(new Vector2[] { segmentRightEndB, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, segmentLeftStartA, }); segmentStartCenter = segments[i].startPoint; Vector2 dir = segments[i1].directionNormalizedRotated; float angle = Mathf.Acos(Vector2.Dot(segments[i1].directionNormalized, segments[i].directionNormalized)); radAngle = Mathf.Atan2(dir.y, dir.x); float roundSegmentsfAlt = roundQuality * thickness * (Mathf.Acos(segmentsAngle) / Mathf.PI); if (roundSegmentsfAlt < 1) { roundSegmentsfAlt = 1f; } float roundSegmentsfAltMinusOne = roundSegmentsfAlt; if (roundSegmentsfAltMinusOne > 0) { for (j = 0; j < roundSegmentsfAlt; j++) { angleProgress = Mathf.Clamp01(j / roundSegmentsfAltMinusOne); innerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle - angleProgress * angle) * halfWidth, Mathf.Sin(radAngle - angleProgress * angle) * halfWidth)); } } innerPoints.AddRange(new Vector2[] { segmentRightStartA, }); } else { innerPoints.AddRange(new Vector2[] { segmentRightEndB, segmentRightStartA, }); outerPoints.AddRange(new Vector2[] { segmentLeftEndB, }); segmentStartCenter = segments[i].startPoint; Vector2 dir = -segments[i].directionNormalizedRotated; float angle = Mathf.Acos(Vector2.Dot(segments[i1].directionNormalized, segments[i].directionNormalized)); radAngle = Mathf.Atan2(dir.y, dir.x); float roundSegmentsfAlt = roundQuality * thickness * (Mathf.Acos(segmentsAngle) / Mathf.PI); if (roundSegmentsfAlt < 1) { roundSegmentsfAlt = 1f; } float roundSegmentsfAltMinusOne = roundSegmentsfAlt; if (roundSegmentsfAltMinusOne > 0) { for (j = 0; j < roundSegmentsfAlt; j++) { angleProgress = Mathf.Clamp01(1f - (j / roundSegmentsfAltMinusOne)); outerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle - angleProgress * angle) * halfWidth, Mathf.Sin(radAngle - angleProgress * angle) * halfWidth)); } } outerPoints.AddRange(new Vector2[] { segmentLeftStartA, }); } } } } } int lastSegmentIndex = segments.Length - 1; segmentLeftStartA = segments[lastSegmentIndex].startPoint - segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; segmentLeftEndA = segments[lastSegmentIndex].endPoint - segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; segmentRightStartA = segments[lastSegmentIndex].startPoint + segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; segmentRightEndA = segments[lastSegmentIndex].endPoint + segments[lastSegmentIndex].directionNormalizedRotated * halfWidth; if (closeLine == ClosePathRule.NEVER) { if (lineCap == StrokeLineCap.butt) { innerPoints.AddRange(new Vector2[] { segmentRightEndA, segmentLeftEndA, }); } else if (lineCap == StrokeLineCap.round) { innerPoints.AddRange(new Vector2[] { segmentRightEndA }); outerPoints.AddRange(new Vector2[] { segmentLeftEndA, }); segmentStartCenter = Vector2.Lerp(segmentLeftEndA, segmentRightEndA, 0.5f); radAngle = Mathf.Atan2(-segments[lastSegmentIndex].directionNormalizedRotated.y, -segments[lastSegmentIndex].directionNormalizedRotated.x); float roundSegmentsfAlt = roundQuality * thickness; float roundSegmentsfAltMinusOne = roundSegmentsfAlt - 1; if (roundSegmentsfAltMinusOne > 0) { for (j = 0; j <= roundSegmentsfAlt; j++) { angleProgress = 1f - Mathf.Clamp01(j / roundSegmentsfAltMinusOne); innerPoints.Add(segmentStartCenter + new Vector2(Mathf.Cos(radAngle + angleProgress * Mathf.PI) * halfWidth, Mathf.Sin(radAngle + angleProgress * Mathf.PI) * halfWidth)); } } } else if (lineCap == StrokeLineCap.square) { Vector2 lastSegmentOffset = segments[lastSegmentIndex].directionNormalized * halfWidth; innerPoints.AddRange(new Vector2[] { segmentRightEndA + lastSegmentOffset, segmentLeftEndA + lastSegmentOffset, }); } } if (closeLine == ClosePathRule.ALWAYS && lineJoin == StrokeLineJoin.miter || lineJoin == StrokeLineJoin.miterClip) { innerPoints.AddRange(new Vector2[] { segmentRightEndA, segmentLeftEndA, }); } outerPoints.Reverse(); innerPoints.AddRange(outerPoints); return(innerPoints); }
public static List <List <Vector2> > StrokeShape(List <StrokeSegment[]> segments, float thickness, Color32 color, StrokeLineJoin lineJoin, StrokeLineCap lineCap, float miterLimit = 4f, float[] dashArray = null, float dashOffset = 0f, ClosePathRule closeLine = ClosePathRule.NEVER, float roundQuality = 10f) { if (segments == null || segments.Count == 0) { return(null); } float totalCurveLength = 0f; int i, j; for (i = 0; i < segments.Count; i++) { if (segments[i] == null) { continue; } for (j = 0; j < segments[i].Length; j++) { totalCurveLength += segments[i][j].length; } } if (totalCurveLength == 0f) { return(null); } bool useDash; ProcessDashArray(ref dashArray, out useDash); ClosePathRule closeSegments = closeLine; List <StrokeSegment[]> finalSegments = new List <StrokeSegment[]>(); for (i = 0; i < segments.Count; i++) { if (segments[i] == null || segments[i].Length == 0) { continue; } if (!useDash) { finalSegments.Add(segments[i]); } else { finalSegments.AddRange(CreateDashedStroke(segments[i], dashArray, dashOffset, ref closeSegments)); } } if (finalSegments.Count > 0) { List <List <Vector2> > finalPoints = new List <List <Vector2> >(); for (i = 0; i < finalSegments.Count; i++) { List <Vector2> points = Stroke(finalSegments[i], thickness, color, lineJoin, lineCap, miterLimit, closeSegments, roundQuality); if (points == null || points.Count < 2) { continue; } List <List <Vector2> > simplifiedShape = SVGGeom.SimplifyPolygon(points); for (j = 0; j < simplifiedShape.Count; j++) { if (simplifiedShape[j] == null || simplifiedShape[j].Count == 0) { continue; } finalPoints.Add(simplifiedShape[j]); } } return(finalPoints); } else { return(null); } }
public static Mesh StrokeMesh(StrokeSegment[] segments, float thickness, Color32 color, StrokeLineJoin lineJoin, StrokeLineCap lineCap, float miterLimit = 4f, float[] dashArray = null, float dashOffset = 0f, ClosePathRule closeLine = ClosePathRule.NEVER, float roundQuality = 10f) { if (segments == null || segments.Length == 0) { return(null); } List <List <Vector2> > finalPoints = StrokeShape(new List <StrokeSegment[]>() { segments }, thickness, color, lineJoin, lineCap, miterLimit, dashArray, dashOffset, closeLine, roundQuality); return(TessellateStroke(finalPoints, color)); }
private static void GenerateLineCaps(List <Triangle> triangles, List <CurveTriangle> curveTriangles, Curve curve, bool atEnd, double width, StrokeLineCap lineCap) { throw new NotImplementedException(); }
public static CompiledStroke FromSegment(Curve[] curves, bool closed, double halfWidth, StrokeLineCap lineCap, StrokeLineJoin lineJoin, double miterLimit) { var triangles = new List <Triangle>(); var curveTriangles = new List <CurveTriangle>(); // Generate the main lines for (int i = 0; i < curves.Length; i++) { var prevAngle = curves[(i + curves.Length - 1) % curves.Length].ExitAngle; var nextAngle = curves[(i + 1) % curves.Length].EntryAngle; if (!closed && i == 0) { prevAngle = curves[0].EntryAngle; } if (!closed && i == curves.Length - 1) { nextAngle = curves[curves.Length - 1].ExitAngle; } // Generate the right triangles for the right curve types if (curves[i].Type == CurveType.Line) { GenerateLineTriangles(triangles, curves[i], halfWidth, prevAngle, nextAngle); } else { GenerateCurveTriangles(curveTriangles, curves[i], halfWidth, prevAngle, nextAngle); } } // Generate the line joins for (int i = 1; i < curves.Length; i++) { GenerateLineJoints(triangles, curveTriangles, curves[i - 1], curves[i], halfWidth, lineJoin, miterLimit); } if (closed) { GenerateLineJoints(triangles, curveTriangles, curves[curves.Length - 1], curves[0], halfWidth, lineJoin, miterLimit); } else { // Generate the line caps GenerateLineCaps(triangles, curveTriangles, curves[0], false, halfWidth, lineCap); GenerateLineCaps(triangles, curveTriangles, curves[curves.Length - 1], true, halfWidth, lineCap); } // Generate the new compiled stroke return(new CompiledStroke() { Triangles = triangles.ToArray(), CurveTriangles = curveTriangles.ToArray() }); }
/// <summary> /// Sets the current <see cref="Blazor.ImageEditor.StrokeLineCap"/>. /// </summary> /// <param name="cap">A <see cref="Blazor.ImageEditor.StrokeLineCap"/> value.</param> public async Task SetLineCapAsync(StrokeLineCap cap) { StrokeLineCap = cap; await Service.SetLineCapAsync(cap).ConfigureAwait(false); }