using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.IO
{
///
/// SVG import and export. Unlike a DXF, an SVG's numbers are not drawing units, so both
/// directions take the of the drawing; see docs/svg-import-export.md.
///
public static class Svg
{
internal const string PlateLayerName = "PLATE";
private static readonly XNamespace Ns = "http://www.w3.org/2000/svg";
#region Import
public static bool IsSvg(string path) =>
Path.GetExtension(path).Equals(".svg", StringComparison.OrdinalIgnoreCase);
///
/// Imports an SVG in . Throws
/// when the file does not state its real-world size
/// and is not set: a guessed scale
/// would silently produce wrong-size parts.
///
public static DxfImportResult Import(string path, CadImportOptions options) =>
SvgReader.Read(path, options);
///
/// The file's width and height in its own numbers when it does not state its
/// real-world size, or null when it does and no scale is needed.
///
public static (double Width, double Height)? UndeclaredSize(string path) =>
SvgReader.UndeclaredSize(path);
#endregion
#region Export
public static void ExportProgram(Program program, string path, Units units)
{
using var stream = File.Create(path);
ExportProgram(program, stream, units);
}
public static void ExportProgram(Program program, Stream stream, Units units) =>
ExportEntities(ConvertProgram.ToGeometry(program), stream, units);
public static void ExportPlate(Plate plate, string path, Units units)
{
using var stream = File.Create(path);
ExportPlate(plate, stream, units);
}
public static void ExportPlate(Plate plate, Stream stream, Units units)
{
var entities = PlateOutline(plate);
foreach (var part in plate.Parts)
{
var geometry = ConvertProgram.ToGeometry(part.Program);
foreach (var entity in geometry)
entity.Offset(part.Location);
entities.AddRange(geometry);
}
ExportEntities(entities, stream, units);
}
///
/// Writes entities as one group per layer, with the document's physical size stated in
/// so one user unit is exactly one drawing unit.
///
public static void ExportEntities(IEnumerable entities, string path, Units units)
{
using var stream = File.Create(path);
ExportEntities(entities, stream, units);
}
public static void ExportEntities(IEnumerable entities, Stream stream, Units units)
{
var list = entities.ToList();
var bounds = list.Count > 0 ? list.GetBoundingBox() : new Box(0, 0, 1, 1);
// Half of each stroke falls outside the outermost geometry; keep it on the page.
var margin = System.Math.Max(System.Math.Max(bounds.Length, bounds.Width), 1) * 0.01;
var left = bounds.Left - margin;
var top = -bounds.Top - margin;
var width = bounds.Length + margin * 2;
var height = bounds.Width + margin * 2;
var unit = UnitsHelper.GetShortString(units);
var root = new XElement(
Ns + "svg",
new XAttribute("version", "1.1"),
new XAttribute("width", Number(width) + unit),
new XAttribute("height", Number(height) + unit),
new XAttribute(
"viewBox",
$"{Number(left)} {Number(top)} {Number(width)} {Number(height)}"
),
new XAttribute("fill", "none")
);
foreach (var layer in list.GroupBy(e => e.Layer?.Name ?? Layer.Default.Name))
root.Add(LayerGroup(layer.Key, layer.ToList()));
new XDocument(new XDeclaration("1.0", "utf-8", null), root).Save(stream);
}
private static XElement LayerGroup(string name, List entities)
{
var group = new XElement(
Ns + "g",
new XAttribute("data-layer", name),
new XAttribute("stroke", StrokeOf(name)),
new XAttribute("stroke-width", "1"),
new XAttribute("vector-effect", "non-scaling-stroke")
);
var data = new StringBuilder();
var position = Vector.Invalid;
foreach (var entity in entities)
{
switch (entity)
{
case Line line:
MoveTo(data, line.StartPoint, ref position);
data.Append($"L{Point(line.EndPoint)}");
position = line.EndPoint;
break;
case Arc arc when arc.StartPoint().DistanceTo(arc.EndPoint()) < Tolerance.Epsilon:
group.Add(CircleElement(arc.Center, arc.Radius));
break;
case Arc arc:
MoveTo(data, arc.StartPoint(), ref position);
var large = arc.SweepAngle() > System.Math.PI ? 1 : 0;
// The Y flip keeps how an arc looks, and SVG's positive sweep is clockwise.
var sweep = arc.IsReversed ? 1 : 0;
var radius = Number(arc.Radius);
data.Append($"A{radius} {radius} 0 {large} {sweep} {Point(arc.EndPoint())}");
position = arc.EndPoint();
break;
case Circle circle:
group.Add(CircleElement(circle.Center, circle.Radius));
break;
}
}
if (data.Length > 0)
group.Add(new XElement(Ns + "path", new XAttribute("d", data.ToString())));
return group;
}
private static void MoveTo(StringBuilder data, Vector point, ref Vector position)
{
if (!position.IsValid() || position.DistanceTo(point) >= Tolerance.Epsilon)
data.Append($"M{Point(point)}");
position = point;
}
private static XElement CircleElement(Vector center, double radius) =>
new XElement(
Ns + "circle",
new XAttribute("cx", Number(center.X)),
new XAttribute("cy", Number(-center.Y)),
new XAttribute("r", Number(radius))
);
private static string StrokeOf(string layer)
{
if (string.Equals(layer, SpecialLayers.Rapid.Name, StringComparison.OrdinalIgnoreCase))
return "#808080";
if (string.Equals(layer, SpecialLayers.Scribe.Name, StringComparison.OrdinalIgnoreCase))
return "#c000c0";
if (string.Equals(layer, PlateLayerName, StringComparison.OrdinalIgnoreCase))
return "#0080c0";
return "#000000";
}
private static string Point(Vector point) => $"{Number(point.X)} {Number(-point.Y)}";
private static string Number(double value) =>
value.ToString("R", CultureInfo.InvariantCulture);
private static List PlateOutline(Plate plate)
{
var entities = new List();
if (plate.Quadrant < 1 || plate.Quadrant > 4)
return entities;
var signX = plate.Quadrant is 1 or 4 ? 1 : -1;
var signY = plate.Quadrant is 1 or 2 ? 1 : -1;
var length = plate.Size.Length * signX;
var width = plate.Size.Width * signY;
var layer = new Layer(PlateLayerName) { IsVisible = true };
void Rectangle(double x1, double y1, double x2, double y2)
{
var corners = new[]
{
new Vector(x1, y1),
new Vector(x1, y2),
new Vector(x2, y2),
new Vector(x2, y1),
};
for (var i = 0; i < corners.Length; i++)
entities.Add(
new Line(corners[i], corners[(i + 1) % corners.Length]) { Layer = layer }
);
}
Rectangle(0, 0, length, width);
Rectangle(
plate.EdgeSpacing.Left,
plate.EdgeSpacing.Bottom,
length - plate.EdgeSpacing.Right,
width - plate.EdgeSpacing.Top
);
return entities;
}
#endregion
}
}