using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Converters;
using OpenNest.Geometry;
namespace OpenNest.Support
{
///
/// The material of one placed part as it drops onto the support bed, in world coordinates:
/// its outline minus its holes, chorded at . A tabbed cutout's
/// slug stays attached while the sheet is cut (see ),
/// so it counts as part. Immutable; an unusable program gives an incomplete material.
///
public sealed class SupportMaterial
{
/// Chord tolerance for arcs, in model units.
public const double ChordTolerance = 0.001;
private readonly Vector[] outline;
private readonly Vector[][] holes;
private readonly Box bounds = new();
private SupportMaterial(Vector[] outline, Vector[][] holes, string reason)
{
this.outline = outline ?? Array.Empty();
this.holes = holes ?? Array.Empty();
Reason = reason;
if (reason != null)
return;
var minX = double.PositiveInfinity;
var minY = double.PositiveInfinity;
var maxX = double.NegativeInfinity;
var maxY = double.NegativeInfinity;
foreach (var point in this.outline)
{
minX = System.Math.Min(minX, point.X);
minY = System.Math.Min(minY, point.Y);
maxX = System.Math.Max(maxX, point.X);
maxY = System.Math.Max(maxY, point.Y);
}
bounds = new Box(minX, minY, maxX - minX, maxY - minY);
}
/// True when the material was captured; otherwise says why not.
public bool IsComplete => Reason == null;
public string Reason { get; }
/// The outline's vertices, without a repeated closing vertex.
public IReadOnlyList Outline => outline;
/// Each hole's vertices, without a repeated closing vertex.
public IReadOnlyList> Holes => holes;
/// A copy of the outline's bounding box.
public Box Bounds => new(bounds.X, bounds.Y, bounds.Length, bounds.Width);
///
/// Material from an outline and holes already in world coordinates. Fewer than three
/// vertices, or a nonfinite one, gives an incomplete material.
///
public static SupportMaterial FromPolygons(IEnumerable outline, IEnumerable> holes = null)
{
var ring = Ring(outline);
if (ring == null)
return Incomplete("The part outline is not a polygon.");
var cutouts = new List();
foreach (var hole in holes ?? Enumerable.Empty>())
{
var cutout = Ring(hole);
if (cutout == null)
return Incomplete("A part hole is not a polygon.");
cutouts.Add(cutout);
}
return new SupportMaterial(ring, cutouts.ToArray(), null);
}
/// The material of a placed part: its program at its location.
public static SupportMaterial FromPart(Part part)
{
ArgumentNullException.ThrowIfNull(part);
return FromProgram(part.Program, part.Location);
}
///
/// The material of (part coordinates, rotation applied) placed
/// at . Leads, rapids, scribes and break cuts are not material.
/// A perimeter left open by a tab still bounds its material, closed across the tab.
///
public static SupportMaterial FromProgram(Program program, Vector location)
{
ArgumentNullException.ThrowIfNull(program);
try
{
var entities = ConvertProgram.ToGeometry(program)
.Where(e => e.Layer != SpecialLayers.Leadin && e.Layer != SpecialLayers.Leadout)
.ForNesting();
if (entities.Count == 0)
return Incomplete("The part has no material contour.");
var profile = new ShapeProfile(entities);
var ring = Ring(Chord(profile.Perimeter, location));
if (ring == null)
return Incomplete("The part outline is not a polygon.");
var cutouts = new List();
foreach (var cutout in profile.Cutouts)
if (Ring(Chord(cutout, location)) is { } hole)
cutouts.Add(hole);
return new SupportMaterial(ring, cutouts.ToArray(), null);
}
catch (Exception ex) when (ex is ArgumentException or InvalidOperationException or NotSupportedException
or ArithmeticException or IndexOutOfRangeException)
{
return Incomplete(ex.Message);
}
}
private static SupportMaterial Incomplete(string reason) => new(null, null, reason);
private static IEnumerable Chord(Shape shape, Vector location) =>
shape.ToPolygonWithTolerance(ChordTolerance).Vertices.Select(v => v + location);
// The distinct vertices of a closed ring, or null when it cannot bound any area.
private static Vector[] Ring(IEnumerable vertices)
{
if (vertices == null)
return null;
var ring = new List();
foreach (var vertex in vertices)
{
if (!double.IsFinite(vertex.X) || !double.IsFinite(vertex.Y))
return null;
if (ring.Count == 0 || ring[^1] != vertex)
ring.Add(vertex);
}
if (ring.Count > 1 && ring[0] == ring[^1])
ring.RemoveAt(ring.Count - 1);
return ring.Count < 3 ? null : ring.ToArray();
}
///
/// The material's area and center of gravity (its area centroid: plate is of even
/// thickness), or false when the material is incomplete or encloses no area.
///
public bool TryGetCenterOfGravity(out Vector centerOfGravity, out double area)
{
centerOfGravity = Vector.Invalid;
area = 0;
if (!IsComplete || !PolygonAreaMoments.TryCompute(outline, out var whole))
return false;
var cutouts = new List();
foreach (var hole in holes)
if (PolygonAreaMoments.TryCompute(hole, out var moments))
cutouts.Add(moments);
if (!PolygonAreaMoments.TrySubtract(whole, cutouts, out var net))
return false;
centerOfGravity = net.Centroid;
area = net.Area;
return true;
}
///
/// The spans of the horizontal line at that lie in material, in
/// ascending order. A line along an edge or through a vertex counts that crossing once,
/// on the half-open rule, so touching is not crossing.
///
public IReadOnlyList<(double Start, double End)> SpansAtY(double y)
{
var crossings = new List();
if (!IsComplete || y < bounds.Bottom || y > bounds.Top)
return Array.Empty<(double, double)>();
AddCrossings(outline, y, crossings);
foreach (var hole in holes)
AddCrossings(hole, y, crossings);
crossings.Sort();
var spans = new List<(double, double)>(crossings.Count / 2);
for (var i = 0; i + 1 < crossings.Count; i += 2)
spans.Add((crossings[i], crossings[i + 1]));
return spans;
}
///
/// The spans of the vertical line at that lie in material, in
/// ascending order, on the same half-open rule as .
///
public IReadOnlyList<(double Start, double End)> SpansAtX(double x)
{
var crossings = new List();
if (!IsComplete || x < bounds.Left || x > bounds.Right)
return Array.Empty<(double, double)>();
AddCrossingsAtX(outline, x, crossings);
foreach (var hole in holes)
AddCrossingsAtX(hole, x, crossings);
crossings.Sort();
var spans = new List<(double, double)>(crossings.Count / 2);
for (var i = 0; i + 1 < crossings.Count; i += 2)
spans.Add((crossings[i], crossings[i + 1]));
return spans;
}
/// Whether lies in material (even-odd over outline and holes).
public bool Contains(Vector point)
{
if (!IsComplete)
return false;
var inside = Inside(outline, point);
foreach (var hole in holes)
if (Inside(hole, point))
inside = !inside;
return inside;
}
private static void AddCrossings(Vector[] ring, double y, List crossings)
{
for (int i = 0, j = ring.Length - 1; i < ring.Length; j = i++)
{
var a = ring[j];
var b = ring[i];
if ((a.Y > y) != (b.Y > y))
crossings.Add(a.X + (y - a.Y) * (b.X - a.X) / (b.Y - a.Y));
}
}
private static void AddCrossingsAtX(Vector[] ring, double x, List crossings)
{
for (int i = 0, j = ring.Length - 1; i < ring.Length; j = i++)
{
var a = ring[j];
var b = ring[i];
if ((a.X > x) != (b.X > x))
crossings.Add(a.Y + (x - a.X) * (b.Y - a.Y) / (b.X - a.X));
}
}
private static bool Inside(Vector[] ring, Vector point)
{
var inside = false;
for (int i = 0, j = ring.Length - 1; i < ring.Length; j = i++)
{
var a = ring[j];
var b = ring[i];
if ((a.Y > point.Y) != (b.Y > point.Y)
&& point.X < a.X + (point.Y - a.Y) * (b.X - a.X) / (b.Y - a.Y))
inside = !inside;
}
return inside;
}
}
}