/// <summary> /// Initializes a new instance of the <see cref="NGraphics.Custom.Models.Pen"/> class. /// </summary> /// <param name="color">Color.</param> /// <param name="width">Width.</param> public Pen(Color color, double width = 1.0) { Color = color; Width = width; LineJoin = SvgStrokeLineJoin.Miter; LineCap = SvgStrokeLineCap.Square; }
/// <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(SvgPathSegmentList path, double width, SvgStrokeLineCap lineCap = SvgStrokeLineCap.Butt, SvgStrokeLineJoin lineJoin = SvgStrokeLineJoin.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, SvgFillRule.NonZero)); }
/// <summary> /// Initializes a new instance of the <see cref="NGraphics.Custom.Models.Pen"/> class. /// </summary> public Pen() { Color = Colors.Black; Width = 1; LineJoin = SvgStrokeLineJoin.Miter; LineCap = SvgStrokeLineCap.Square; }
public static IEnumerable <Curve> ConvertToFill(CurveData data, double halfWidth, SvgStrokeLineCap lineCap, SvgStrokeLineJoin 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 = GenerateLineJoins(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 JointStyle AsJointStyle(this SvgStrokeLineJoin join) { switch (join) { case SvgStrokeLineJoin.Miter: return(JointStyle.Miter); case SvgStrokeLineJoin.Round: return(JointStyle.Round); case SvgStrokeLineJoin.Bevel: return(JointStyle.Square); case SvgStrokeLineJoin.Inherit: default: return(JointStyle.Miter); } }
public static LineJoin ConvertToDynamicPdf(this SvgStrokeLineJoin strokeLineJoin) { switch (strokeLineJoin) { case SvgStrokeLineJoin.Miter: return(LineJoin.Miter); case SvgStrokeLineJoin.Round: return(LineJoin.Round); case SvgStrokeLineJoin.Bevel: return(LineJoin.Bevel); case SvgStrokeLineJoin.Inherit: throw new Exception("Inherited value can not be resolved"); default: throw new ArgumentOutOfRangeException(nameof(strokeLineJoin), strokeLineJoin, null); } }
internal static XPen Stroke2XPen(SvgPaintServer stroke, SvgUnit strokeWidth, SvgStrokeLineCap strokeLineCap, SvgStrokeLineJoin strokeLineJoin) { var pen = stroke is SvgColourServer stroke1 ? new XPen(Color2XColor(stroke1), strokeWidth.Value) : new XPen(XColors.Black, strokeWidth.Value); switch (strokeLineCap) { case SvgStrokeLineCap.Inherit: pen.LineCap = XLineCap.Flat; break; case SvgStrokeLineCap.Butt: break; case SvgStrokeLineCap.Round: pen.LineCap = XLineCap.Round; break; case SvgStrokeLineCap.Square: pen.LineCap = XLineCap.Square; break; default: throw new ArgumentOutOfRangeException(); } switch (strokeLineJoin) { case SvgStrokeLineJoin.Inherit: break; case SvgStrokeLineJoin.Miter: pen.LineJoin = XLineJoin.Miter; break; case SvgStrokeLineJoin.Round: pen.LineJoin = XLineJoin.Round; break; case SvgStrokeLineJoin.Bevel: pen.LineJoin = XLineJoin.Bevel; break; default: throw new ArgumentOutOfRangeException(nameof(strokeLineJoin), strokeLineJoin, null); } return(pen); }
private static IEnumerable <Curve> GenerateLineJoins(Curve prevCurve, Curve nextCurve, double halfWidth, SvgStrokeLineJoin lineJoin, double miterLimit) { // First, calculate the cross-product between the tangents var exitTangent = prevCurve.ExitTangent; var entryTangent = nextCurve.EntryTangent; var diff = entryTangent.Cross(exitTangent); var sd = diff < 0 ? -1 : 1; // Account for half-turns here too // The common point and the offset vectors var p = (prevCurve.At(1) + nextCurve.At(0)) / 2; var entryOffset = sd * halfWidth * entryTangent.CCWPerpendicular; var exitOffset = sd * halfWidth * exitTangent.CCWPerpendicular; // Now, create the next triangles if necessary switch (lineJoin) { case SvgStrokeLineJoin.Bevel: // Just the bevel line yield return(Curve.Line(p + exitOffset, p + entryOffset)); break; case SvgStrokeLineJoin.Miter: case SvgStrokeLineJoin.MiterClip: { // Calculate the bisector and miter length var cos = exitOffset.Dot(entryOffset) / Math.Sqrt(exitOffset.LengthSquared * entryOffset.LengthSquared); var miter = halfWidth / Math.Sqrt(0.5 + 0.5 * cos); // Check the conditions for the miter (only clip if miter-clip is explicity selected) if (lineJoin == SvgStrokeLineJoin.Miter && miter >= halfWidth * miterLimit) { goto case SvgStrokeLineJoin.Bevel; } // Generate the miter var miterWidth = halfWidth * miterLimit; var bisectorOffset = miter * (entryOffset + exitOffset).Normalized; if (miter < miterWidth) { yield return(Curve.Line(p + exitOffset, p + bisectorOffset)); yield return(Curve.Line(p + bisectorOffset, p + entryOffset)); } else { // Clip the miter Double2 p1, p2; // Account for the special case of a 180 degree turn if (double.IsInfinity(miter)) { var outwardOffset = entryOffset.Normalized.CCWPerpendicular; p1 = entryOffset + miterWidth * outwardOffset; p2 = exitOffset + miterWidth * outwardOffset; } else { p1 = entryOffset + miterWidth * (bisectorOffset - entryOffset) / miter; p2 = exitOffset + miterWidth * (bisectorOffset - exitOffset) / miter; } yield return(Curve.Line(p + exitOffset, p + p2)); yield return(Curve.Line(p + p2, p + p1)); yield return(Curve.Line(p + p1, p + entryOffset)); } break; } case SvgStrokeLineJoin.Round: // Generate the circle yield return(Curve.Circle(p, halfWidth, exitOffset, entryOffset, sd < 0)); break; case SvgStrokeLineJoin.Arcs: { // Compute the curvatures of the curves var exitKappa = prevCurve.ExitCurvature; var entryKappa = nextCurve.EntryCurvature; // If one of the curvatures is too large, fall back to round if (Math.Abs(exitKappa) * halfWidth >= 1 || Math.Abs(entryKappa) * halfWidth >= 1) { goto case SvgStrokeLineJoin.Round; } // If both of them are zero, fall back to miter if (RoughlyZero(exitKappa) && RoughlyZero(entryKappa)) { goto case SvgStrokeLineJoin.MiterClip; } // If one (or both) are nonzero, build the possible circles else { // The "arcs" must use a different sign convention var sign = diff > 0 ? 1 : -1; var exitRadius = 1 / exitKappa - sign * halfWidth; var entryRadius = 1 / entryKappa - sign * halfWidth; var exitCenter = exitTangent.CCWPerpendicular / exitKappa; var entryCenter = entryTangent.CCWPerpendicular / entryKappa; // Find the intersection points Double2[] points; if (!RoughlyZero(exitKappa) && !RoughlyZero(entryKappa)) { points = GeometricUtils.CircleIntersection(exitCenter, exitRadius, entryCenter, entryRadius); } else if (!RoughlyZero(exitKappa)) { points = GeometricUtils.CircleLineIntersection(exitCenter, exitRadius, entryOffset, entryTangent); } else { points = GeometricUtils.CircleLineIntersection(entryCenter, entryRadius, exitOffset, exitTangent); } // If there are no intersections, adjust the curves if (points.Length == 0) { if (!RoughlyZero(exitKappa) && !RoughlyZero(entryKappa)) { points = AdjustCircles(ref exitCenter, ref exitRadius, exitTangent.CCWPerpendicular, ref entryCenter, ref entryRadius, entryTangent.CCWPerpendicular); } else if (!RoughlyZero(exitKappa)) { points = AdjustCircleLine(ref exitCenter, ref exitRadius, exitTangent.CCWPerpendicular, entryOffset, entryTangent); } else { points = AdjustCircleLine(ref entryCenter, ref entryRadius, entryTangent.CCWPerpendicular, exitOffset, exitTangent); } } // If still no solutions are found, go to the miter-clip case if (points.Length == 0) { goto case SvgStrokeLineJoin.MiterClip; } // Check both which point have less travelled distance double PointParameter(Double2 pt) { if (RoughlyZero(exitKappa)) { var d = exitTangent.Dot(pt - exitOffset); return(d < 0 ? double.PositiveInfinity : d); } return((Math.Sign(exitKappa) * (exitOffset - exitCenter).AngleBetween(pt - exitCenter)).WrapAngle360()); } var angles = points.Select(PointParameter).ToArray(); // Pick the point with the least travelled angle var point = angles[0] <= angles[1] ? points[0] : points[1]; // Generate the arcs to be rendered if (!RoughlyZero(exitKappa)) { yield return(Curve.Circle(p + exitCenter, Math.Abs(exitRadius), exitOffset - exitCenter, point - exitCenter, exitKappa > 0)); } else { yield return(Curve.Line(p + exitOffset, p + point)); } if (!RoughlyZero(entryKappa)) { yield return(Curve.Circle(p + entryCenter, Math.Abs(entryRadius), point - entryCenter, entryOffset - entryCenter, entryKappa > 0)); } else { yield return(Curve.Line(p + point, p + entryOffset)); } } } break; default: throw new ArgumentException("Unrecognized lineJoin", nameof(lineJoin)); } }
private Tuple <string, ICssValue> ParseValue(string name, string value, string priority, bool presentation) { var important = priority == "important"; if (!presentation) { name = name.ToLower(); } ICssValue parsedValue = null; switch (name) { case "fill": case "stroke": parsedValue = new SvgPaint(value); break; case "stroke-width": parsedValue = SvgLength.Parse(value, presentation); if (((SvgLength)parsedValue).Value < 0.0F) { return(null); } break; case "stroke-linecap": parsedValue = new SvgStrokeLineCap(value); break; case "stroke-linejoin": parsedValue = new SvgStrokeLineJoin(value); break; case "stroke-miterlimit": parsedValue = SvgNumber.Parse(value); break; case "color": parsedValue = new SvgColor(value); break; case "stop-color": parsedValue = new SvgStopColor(value); break; case "fill-opacity": case "stroke-opacity": case "stop-opacity": case "opacity": parsedValue = SvgNumber.Parse(value, 0.0F, 1.0F); break; case "clip-path": parsedValue = new SvgIri(value); break; case "fill-rule": case "clip-rule": parsedValue = new SvgFillRule(presentation ? value : value.ToLower()); break; } if (!this._cache.ContainsKey(name)) { var result = Tuple.Create(value, parsedValue); this._cache.Add(name, result); return(result); } else if (important) { var result = Tuple.Create(value, parsedValue); this._cache[name] = result; return(result); } return(null); }