mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-02 10:11:35 -05:00
Customers and design tools hand over SVGs, and getting them in meant a trip through another CAD tool. CadImporter now reads them like any other drawing, and plates, programs and converter entities can be written out. An SVG's numbers are not drawing units, so import converts to the nest's units and never guesses: only an absolute width or height counts as a stated size, since px means 1/96 in to browsers but 1/72 in to Illustrator. Without one the desktop asks, showing the size each choice gives, and headless callers must pass the scale or the import fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
138 lines
4.7 KiB
C#
138 lines
4.7 KiB
C#
using System;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using OpenNest.Geometry;
|
|
|
|
namespace OpenNest.IO
|
|
{
|
|
/// <summary>An SVG affine matrix: x' = A*x + C*y + E, y' = B*x + D*y + F.</summary>
|
|
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; }
|
|
|
|
/// <summary>True when circles stay circles.</summary>
|
|
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);
|
|
|
|
/// <summary>Applies the linear part only, for directions rather than positions.</summary>
|
|
public Vector ApplyLinear(Vector v) => new Vector(A * v.X + C * v.Y, B * v.X + D * v.Y);
|
|
|
|
/// <summary>This transform applied after <paramref name="inner"/>.</summary>
|
|
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);
|
|
|
|
/// <summary>Parses a transform list, applied left to right as nested coordinate systems.</summary>
|
|
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);
|
|
}
|
|
}
|