using System;
using System.Collections.Generic;
using System.Linq;
using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
namespace OpenNest.Desktop.Rendering;
/// A program's display layers in its own frame, before placement.
///
/// Whether a cut contour is open, such as a cutout left attached by tabs. Filling
/// would close it with a chord; fill instead.
///
public sealed record ProgramLayers(
IReadOnlyList Cut,
IReadOnlyList Lead,
IReadOnlyList Etch,
bool HasOpenCuts);
///
/// A CNC program's moves as toolkit-neutral figures, routed by layer as the WinForms plate view
/// routes them (its GraphicsHelper): cuts, lead-ins/outs, etches and the filled material.
/// Suppressed moves are skipped, a tabbed cutout's tabs are filled but never drawn as cuts, and
/// break cuts are drawn but never bound the fill. Coordinates stay in the program frame: callers
/// place the figures at the part's location.
///
public static class ProgramGeometry
{
/// Cut strokes: everything but etches, leads and tabs (tabs are never cut).
public static bool IsCutLayer(LayerType layer) =>
layer is not (LayerType.Scribe or LayerType.Leadin or LayerType.Leadout or LayerType.Tab);
public static bool IsLeadLayer(LayerType layer) => layer is LayerType.Leadin or LayerType.Leadout;
public static bool IsEtchLayer(LayerType layer) => layer == LayerType.Scribe;
///
/// Material outline layers: everything but etches, leads and break cuts. Break cuts are open
/// lines through material: a fill would close each with a chord.
///
public static bool IsFillLayer(LayerType layer) =>
layer is not (LayerType.Scribe or LayerType.Leadin or LayerType.Leadout or LayerType.Break);
/// The cut, lead and etch figures of .
public static ProgramLayers Layers(Program program)
{
ArgumentNullException.ThrowIfNull(program);
var cut = Figures(program, IsCutLayer, bridgeTabs: false, out var hasOpenCuts);
var lead = Figures(program, IsLeadLayer, bridgeTabs: false, out _);
var etch = Figures(program, IsEtchLayer, bridgeTabs: false, out _);
return new ProgramLayers(cut, lead, etch, hasOpenCuts);
}
///
/// The closed material outlines to fill. A contour cut in pieces around tabs is rejoined
/// across its tab gaps, so it fills as one region rather than as chord-closed pieces.
///
public static IReadOnlyList Fill(Program program)
{
ArgumentNullException.ThrowIfNull(program);
return Figures(program, IsFillLayer, bridgeTabs: true, out _);
}
/// Whether or a sub-program has a break cut.
public static bool HasBreakCuts(Program program) =>
program.Codes.Any(code => code is LinearMove { Layer: LayerType.Break } or ArcMove { Layer: LayerType.Break }
|| code is SubProgramCall { Program: { } sub } && HasBreakCuts(sub));
///
/// The unsuppressed moves on layers accepts, one figure per
/// continuous run. A run ends at a skipped move, a rapid that moves, or a sub-program call.
/// With , open figures are chained end to start (see ).
///
public static List Figures(Program program, Func includeLayer, bool bridgeTabs,
out bool hasOpenFigures)
{
ArgumentNullException.ThrowIfNull(program);
ArgumentNullException.ThrowIfNull(includeLayer);
var figures = new List();
var position = Vector.Zero;
AddProgram(figures, program, ref position, includeLayer);
hasOpenFigures = figures.Exists(figure => !figure.IsClosed);
return Combine(figures, bridgeTabs);
}
///
/// With , each open figure is joined end-to-start with the open
/// figure starting nearest its end, until its own start is nearer, so the tab-separated pieces
/// of one contour fill as a single region. Closed figures pass through unchanged.
///
public static List Combine(IReadOnlyList figures, bool bridgeTabs)
{
var result = new List(figures.Count);
var pending = new List(figures);
while (pending.Count > 0)
{
var figure = pending[0];
pending.RemoveAt(0);
if (!bridgeTabs || figure.IsClosed)
{
result.Add(figure);
continue;
}
var segments = new List(figure.Segments);
var end = figure.End;
while (true)
{
PathFigure next = null;
var nextGap = end.DistanceTo(figure.Start);
foreach (var candidate in pending)
{
if (candidate.IsClosed)
continue;
var gap = end.DistanceTo(candidate.Start);
if (gap < nextGap)
{
next = candidate;
nextGap = gap;
}
}
if (next == null)
break;
pending.Remove(next);
// Bridge the tab gap with a line, as GDI+ AddPath(connect: true) does.
if (nextGap > 0)
segments.Add(new LineTo(next.Start));
segments.AddRange(next.Segments);
end = next.End;
}
result.Add(new PathFigure(figure.Start, segments));
}
return result;
}
private static void AddProgram(List figures, Program program, ref Vector position,
Func includeLayer)
{
// Sub-program offsets are relative to the frame origin at entry, not the tool position.
var frameOrigin = position;
var mode = program.Mode;
List current = null;
var figureStart = position;
// Call before moving past a skipped code so the figure records where its cut ended.
void Flush()
{
if (current != null)
{
figures.Add(new PathFigure(figureStart, current));
current = null;
}
}
void Begin(Vector start)
{
if (current == null)
{
current = new List();
figureStart = start;
}
}
for (var i = 0; i < program.Length; ++i)
{
switch (program[i])
{
case ArcMove arc:
{
if (!arc.Suppressed && includeLayer(arc.Layer))
{
Begin(position);
current.Add(Arc(arc, mode, position));
}
else
Flush();
position = mode == Mode.Incremental ? arc.EndPoint + position : arc.EndPoint;
break;
}
case LinearMove line:
{
var end = mode == Mode.Incremental ? line.EndPoint + position : line.EndPoint;
if (!line.Suppressed && includeLayer(line.Layer))
{
Begin(position);
current.Add(new LineTo(end));
}
else
Flush();
position = end;
break;
}
case RapidMove rapid:
{
var end = mode == Mode.Incremental ? rapid.EndPoint + position : rapid.EndPoint;
var dx = end.X - position.X;
var dy = end.Y - position.Y;
if (dx * dx + dy * dy > 0.001 * 0.001)
Flush();
position = end;
break;
}
case SubProgramCall call:
{
Flush();
if (call.Program != null)
{
position = new Vector(frameOrigin.X + call.Offset.X, frameOrigin.Y + call.Offset.Y);
AddProgram(figures, call.Program, ref position, includeLayer);
}
break;
}
}
}
Flush();
}
/// An arc move from , as GraphicsHelper.AddArc draws it.
private static ArcTo Arc(ArcMove arc, Mode mode, Vector position)
{
var end = arc.EndPoint;
var center = arc.CenterPoint;
if (mode == Mode.Incremental)
{
end += position;
center += position;
}
var startAngle = Angle.NormalizeDeg(Angle.ToDegrees(System.Math.Atan2(position.Y - center.Y, position.X - center.X)));
var endAngle = Angle.NormalizeDeg(Angle.ToDegrees(System.Math.Atan2(end.Y - center.Y, end.X - center.X)));
var radius = center.DistanceTo(end);
if (startAngle.IsEqualTo(endAngle))
return new ArcTo(end, center, radius, startAngle, arc.Rotation == RotationType.CW ? -360.0 : 360.0);
if (arc.Rotation == RotationType.CCW && endAngle < startAngle)
endAngle += 360.0;
else if (arc.Rotation == RotationType.CW && startAngle < endAngle)
startAngle += 360.0;
return new ArcTo(end, center, radius, startAngle, endAngle - startAngle);
}
///
/// Geometry entities (display-only curves) as figures, one per entity, skipping lead-in and
/// lead-out layers as the WinForms shape path does.
///
public static List EntityFigures(IEnumerable entities)
{
var figures = new List();
foreach (var entity in entities)
AddEntity(figures, entity);
return figures;
}
private static void AddEntity(List figures, Entity entity)
{
if (entity.Layer != null
&& (string.Equals(entity.Layer.Name, SpecialLayers.Leadin.Name, StringComparison.OrdinalIgnoreCase)
|| string.Equals(entity.Layer.Name, SpecialLayers.Leadout.Name, StringComparison.OrdinalIgnoreCase)))
return;
switch (entity)
{
case Line line:
figures.Add(new PathFigure(line.StartPoint, new[] { new LineTo(line.EndPoint) }));
break;
case Arc arc:
{
var endAngle = Angle.NormalizeDeg(Angle.ToDegrees(arc.EndAngle));
var startAngle = Angle.NormalizeDeg(Angle.ToDegrees(arc.StartAngle));
if (arc.Rotation == RotationType.CCW && endAngle < startAngle)
endAngle += 360.0;
else if (arc.Rotation == RotationType.CW && startAngle < endAngle)
startAngle += 360.0;
var start = PointOn(arc.Center, arc.Radius, startAngle);
var end = PointOn(arc.Center, arc.Radius, endAngle);
figures.Add(new PathFigure(start,
new[] { new ArcTo(end, arc.Center, arc.Radius, startAngle, endAngle - startAngle) }));
break;
}
case Circle circle:
{
var start = new Vector(circle.Center.X + circle.Radius, circle.Center.Y);
figures.Add(new PathFigure(start,
new[] { new ArcTo(start, circle.Center, circle.Radius, 0, 360.0) }));
break;
}
case Polygon polygon when polygon.Vertices.Count > 0:
{
var vertices = polygon.Vertices;
var segments = new List(vertices.Count);
for (var i = 1; i < vertices.Count; i++)
segments.Add(new LineTo(vertices[i]));
// GDI+ AddPolygon closes the figure.
if (vertices[^1].DistanceTo(vertices[0]) > 0)
segments.Add(new LineTo(vertices[0]));
figures.Add(new PathFigure(vertices[0], segments));
break;
}
case Shape shape:
foreach (var child in shape.Entities)
AddEntity(figures, child);
break;
}
}
private static Vector PointOn(Vector center, double radius, double degrees)
{
var radians = Angle.ToRadians(degrees);
return new Vector(center.X + radius * System.Math.Cos(radians), center.Y + radius * System.Math.Sin(radians));
}
/// The bounding box of the figures' points and arc extremes, or null when empty.
public static Box Bounds(IEnumerable figures)
{
double minX = double.MaxValue, minY = double.MaxValue, maxX = double.MinValue, maxY = double.MinValue;
var any = false;
void Add(Vector p)
{
any = true;
minX = System.Math.Min(minX, p.X);
minY = System.Math.Min(minY, p.Y);
maxX = System.Math.Max(maxX, p.X);
maxY = System.Math.Max(maxY, p.Y);
}
foreach (var figure in figures)
{
Add(figure.Start);
foreach (var segment in figure.Segments)
{
if (segment is ArcTo arc)
{
Add(PointOn(arc.Center, arc.Radius, arc.StartAngle));
var from = System.Math.Min(arc.StartAngle, arc.StartAngle + arc.SweepAngle);
var to = System.Math.Max(arc.StartAngle, arc.StartAngle + arc.SweepAngle);
for (var quadrant = System.Math.Ceiling(from / 90.0) * 90.0; quadrant <= to; quadrant += 90.0)
Add(PointOn(arc.Center, arc.Radius, quadrant));
}
Add(segment.End);
}
}
return any ? new Box(minX, minY, maxX - minX, maxY - minY) : null;
}
}