using System; using System.Globalization; using System.IO; using System.Linq; using System.Text.RegularExpressions; using OpenNest.Geometry; namespace OpenNest.IO { /// An SVG affine matrix: x' = A*x + C*y + E, y' = B*x + D*y + F. internal readonly struct SvgTransform { private const double SimilarityTolerance = 1e-9; private static readonly Regex Function = new Regex( @"([a-zA-Z]+)\s*\(([^)]*)\)", RegexOptions.CultureInvariant, TimeSpan.FromSeconds(1) ); public static readonly SvgTransform Identity = new SvgTransform(1, 0, 0, 1, 0, 0); public SvgTransform(double a, double b, double c, double d, double e, double f) { A = a; B = b; C = c; D = d; E = e; F = f; } public double A { get; } public double B { get; } public double C { get; } public double D { get; } public double E { get; } public double F { get; } /// True when circles stay circles. public bool IsSimilarity { get { var lengthX = A * A + B * B; var lengthY = C * C + D * D; var size = System.Math.Max(lengthX, lengthY); return System.Math.Abs(lengthX - lengthY) <= SimilarityTolerance * size && System.Math.Abs(A * C + B * D) <= SimilarityTolerance * size; } } public double ScaleFactor => System.Math.Sqrt(A * A + B * B); public Vector Apply(Vector p) => new Vector(A * p.X + C * p.Y + E, B * p.X + D * p.Y + F); /// Applies the linear part only, for directions rather than positions. public Vector ApplyLinear(Vector v) => new Vector(A * v.X + C * v.Y, B * v.X + D * v.Y); /// This transform applied after . public SvgTransform Then(SvgTransform inner) => new SvgTransform( A * inner.A + C * inner.B, B * inner.A + D * inner.B, A * inner.C + C * inner.D, B * inner.C + D * inner.D, A * inner.E + C * inner.F + E, B * inner.E + D * inner.F + F ); public static SvgTransform Translate(double x, double y) => new SvgTransform(1, 0, 0, 1, x, y); public static SvgTransform Scale(double x, double y) => new SvgTransform(x, 0, 0, y, 0, 0); /// Parses a transform list, applied left to right as nested coordinate systems. public static SvgTransform Parse(string text) { var result = Identity; if (string.IsNullOrWhiteSpace(text)) return result; foreach (Match match in Function.Matches(text)) { var values = SvgReader.ParseNumbers(match.Groups[2].Value); result = result.Then(ParseFunction(match.Groups[1].Value, values, text)); } return result; } private static SvgTransform ParseFunction(string name, double[] v, string text) { switch (name) { case "matrix" when v.Length == 6: return new SvgTransform(v[0], v[1], v[2], v[3], v[4], v[5]); case "translate" when v.Length is 1 or 2: return Translate(v[0], v.Length == 2 ? v[1] : 0); case "scale" when v.Length is 1 or 2: return Scale(v[0], v.Length == 2 ? v[1] : v[0]); case "rotate" when v.Length == 1: return Rotation(v[0]); case "rotate" when v.Length == 3: return Translate(v[1], v[2]) .Then(Rotation(v[0])) .Then(Translate(-v[1], -v[2])); case "skewX" when v.Length == 1: return new SvgTransform(1, 0, Tan(v[0]), 1, 0, 0); case "skewY" when v.Length == 1: return new SvgTransform(1, Tan(v[0]), 0, 1, 0, 0); default: throw new InvalidDataException($"Unsupported SVG transform \"{text}\"."); } } private static SvgTransform Rotation(double degrees) { var radians = degrees * System.Math.PI / 180; var cos = System.Math.Cos(radians); var sin = System.Math.Sin(radians); return new SvgTransform(cos, sin, -sin, cos, 0, 0); } private static double Tan(double degrees) => System.Math.Tan(degrees * System.Math.PI / 180); } }