mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 20:51:06 -04:00
A hole can now be declared a tabbed cutout in the CAD Converter: pick a circle or any edge of a closed hole, set the tab count and width, and drag the tabs around the hole. The definition persists on the drawing and in the nest file, and stays editable from the source entities. The builder replaces the hole with open cut pieces and uncut spans on a new Tab layer. Tabs are material, so area, nesting and overlap checks still see a closed hole, but they are never cut. Plan Cutting and Assign Lead-ins cut each piece from the slug with the internal lead-in and no lead-out. A lead that would reach back over a tab is mirrored, and an arc lead onto an arc pierces straight in. Planning accounts for each span between tabs and runs the open-cut proof, so its own tabbed cutouts certify; tabs followed by a lead-out are still refused. Pre-post review flags programs that still contain tab motions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
643 lines
23 KiB
C#
643 lines
23 KiB
C#
using System.Collections.Generic;
|
|
using System.Drawing;
|
|
using System.Drawing.Drawing2D;
|
|
using OpenNest.CNC;
|
|
using OpenNest.Geometry;
|
|
using OpenNest.Math;
|
|
|
|
namespace OpenNest
|
|
{
|
|
internal static class GraphicsHelper
|
|
{
|
|
public static GraphicsPath GetGraphicsPath(this Program pgm)
|
|
{
|
|
var figures = new List<Figure>();
|
|
var curpos = Vector.Zero;
|
|
|
|
AddProgram(figures, pgm, pgm.Mode, ref curpos);
|
|
|
|
return Combine(figures, bridgeTabs: false);
|
|
}
|
|
|
|
public static GraphicsPath GetGraphicsPath(this Program pgm, Vector origin)
|
|
{
|
|
// Program coordinates are drawing-local in either mode. Translate the
|
|
// finished path so absolute moves cannot discard the part placement.
|
|
var path = pgm.GetGraphicsPath();
|
|
using var translation = new Matrix();
|
|
translation.Translate((float)origin.X, (float)origin.Y);
|
|
path.Transform(translation);
|
|
|
|
return path;
|
|
}
|
|
|
|
public static GraphicsPath GetGraphicsPath(this Shape shape)
|
|
{
|
|
var path = new GraphicsPath();
|
|
|
|
AddShape(path, shape);
|
|
|
|
return path;
|
|
}
|
|
|
|
public static void GetDisplayPaths(
|
|
this Program pgm,
|
|
Vector origin,
|
|
out GraphicsPath cutPath,
|
|
out GraphicsPath leadPath,
|
|
out GraphicsPath etchPath
|
|
) => pgm.GetDisplayPaths(origin, out cutPath, out leadPath, out etchPath, out _);
|
|
|
|
/// <param name="hasOpenCuts">
|
|
/// Whether a cut contour is open, such as a cutout left attached by tabs. Filling
|
|
/// <paramref name="cutPath"/> would close it with a chord; fill <see cref="GetFillPath"/> instead.
|
|
/// </param>
|
|
public static void GetDisplayPaths(
|
|
this Program pgm,
|
|
Vector origin,
|
|
out GraphicsPath cutPath,
|
|
out GraphicsPath leadPath,
|
|
out GraphicsPath etchPath,
|
|
out bool hasOpenCuts
|
|
)
|
|
{
|
|
// A tabbed cutout's tabs are never cut, so they are filled but not drawn as cuts.
|
|
cutPath = GetLayerPath(pgm, origin,
|
|
layer => layer is not (LayerType.Scribe or LayerType.Leadin or LayerType.Leadout or LayerType.Tab),
|
|
bridgeTabs: false, out hasOpenCuts);
|
|
leadPath = GetLayerPath(pgm, origin,
|
|
layer => layer == LayerType.Leadin || layer == LayerType.Leadout, bridgeTabs: false, out _);
|
|
etchPath = GetLayerPath(pgm, origin, layer => layer == LayerType.Scribe, bridgeTabs: false, out _);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The closed material outlines to fill. Break cuts are open lines through material: a
|
|
/// fill would close each one with a chord and leave the sliver between them unfilled.
|
|
/// A contour cut in pieces around tabs is rejoined across its tab gaps for the same reason.
|
|
/// </summary>
|
|
public static GraphicsPath GetFillPath(this Program pgm, Vector origin) =>
|
|
GetLayerPath(pgm, origin, layer => layer is not (LayerType.Scribe or LayerType.Leadin
|
|
or LayerType.Leadout or LayerType.Break), bridgeTabs: true, out _);
|
|
|
|
private static GraphicsPath GetLayerPath(Program pgm, Vector origin,
|
|
System.Func<LayerType, bool> includeLayer, bool bridgeTabs, out bool hasOpenFigures)
|
|
{
|
|
var figures = new List<Figure>();
|
|
var position = Vector.Zero;
|
|
AddProgram(figures, pgm, pgm.Mode, ref position, includeLayer);
|
|
hasOpenFigures = figures.Exists(figure => !figure.IsClosed);
|
|
var path = Combine(figures, bridgeTabs);
|
|
// Place the completed local path; absolute moves must not discard placement.
|
|
using var translation = new Matrix();
|
|
translation.Translate((float)origin.X, (float)origin.Y);
|
|
path.Transform(translation);
|
|
return path;
|
|
}
|
|
|
|
public static Image GetImage(this Program pgm, System.Drawing.Size size)
|
|
{
|
|
return pgm.GetImage(size, Pens.Black, null);
|
|
}
|
|
|
|
public static Image GetImage(this Program pgm, System.Drawing.Size size, Pen pen)
|
|
{
|
|
return pgm.GetImage(size, pen, null);
|
|
}
|
|
|
|
public static Image GetImage(
|
|
this Program pgm,
|
|
System.Drawing.Size size,
|
|
Pen pen,
|
|
Brush brush
|
|
)
|
|
{
|
|
var img = new Bitmap(size.Width, size.Height);
|
|
pgm.GetDisplayPaths(Vector.Zero, out var cuts, out var leads, out var etches);
|
|
using var path = cuts;
|
|
using var leadPath = leads;
|
|
using var etchPath = etches;
|
|
var bounds = path.PointCount > 0 ? path.GetBounds() : etchPath.GetBounds();
|
|
if (path.PointCount > 0 && etchPath.PointCount > 0)
|
|
bounds = RectangleF.Union(bounds, etchPath.GetBounds());
|
|
|
|
var scalex = (size.Height - 10) / System.Math.Max(bounds.Height, 0.001f);
|
|
var scaley = (size.Width - 10) / System.Math.Max(bounds.Width, 0.001f);
|
|
var scale = scalex < scaley ? scalex : scaley;
|
|
|
|
using var matrix = new Matrix();
|
|
matrix.Scale(scale, -scale);
|
|
|
|
path.Transform(matrix);
|
|
etchPath.Transform(matrix);
|
|
|
|
bounds = path.PointCount > 0 ? path.GetBounds() : etchPath.GetBounds();
|
|
if (path.PointCount > 0 && etchPath.PointCount > 0)
|
|
bounds = RectangleF.Union(bounds, etchPath.GetBounds());
|
|
|
|
var offset = new PointF(
|
|
(size.Width - bounds.Width) * 0.5f - bounds.X,
|
|
(size.Height - bounds.Height) * 0.5f - bounds.Y
|
|
);
|
|
|
|
using var graphics = Graphics.FromImage(img);
|
|
graphics.TranslateTransform(offset.X, offset.Y);
|
|
|
|
if (brush != null)
|
|
{
|
|
using var fill = pgm.GetFillPath(Vector.Zero);
|
|
fill.Transform(matrix);
|
|
graphics.FillPath(brush, fill);
|
|
}
|
|
|
|
if (pen == null)
|
|
pen = Pens.Black;
|
|
|
|
using var etchPen = new Pen(ColorScheme.Default.EtchColor, 1.5f);
|
|
graphics.DrawPath(etchPen, etchPath);
|
|
// A coincident cut must remain visible over an etch.
|
|
graphics.DrawPath(pen, path);
|
|
|
|
return img;
|
|
}
|
|
|
|
public static void GetGraphicsPaths(
|
|
this Program pgm,
|
|
Vector origin,
|
|
out GraphicsPath cutPath,
|
|
out GraphicsPath leadPath
|
|
)
|
|
{
|
|
cutPath = new GraphicsPath();
|
|
leadPath = new GraphicsPath();
|
|
var curpos = Vector.Zero;
|
|
|
|
AddProgramSplit(cutPath, leadPath, pgm, pgm.Mode, ref curpos);
|
|
|
|
using var translation = new Matrix();
|
|
translation.Translate((float)origin.X, (float)origin.Y);
|
|
cutPath.Transform(translation);
|
|
leadPath.Transform(translation);
|
|
}
|
|
|
|
private static void AddProgramSplit(
|
|
GraphicsPath cutPath,
|
|
GraphicsPath leadPath,
|
|
Program pgm,
|
|
Mode mode,
|
|
ref Vector curpos
|
|
)
|
|
{
|
|
// Capture the frame origin at entry. Sub-program Offsets are relative
|
|
// to this fixed origin, not to the current tool position.
|
|
var frameOrigin = curpos;
|
|
mode = pgm.Mode;
|
|
|
|
for (var i = 0; i < pgm.Length; ++i)
|
|
{
|
|
var code = pgm[i];
|
|
|
|
switch (code.Type)
|
|
{
|
|
case CodeType.ArcMove:
|
|
var arc = (ArcMove)code;
|
|
if (arc.Suppressed)
|
|
{
|
|
var endpt = arc.EndPoint;
|
|
if (mode == Mode.Incremental)
|
|
endpt += curpos;
|
|
curpos = endpt;
|
|
break;
|
|
}
|
|
var arcPath =
|
|
(arc.Layer == LayerType.Leadin || arc.Layer == LayerType.Leadout)
|
|
? leadPath
|
|
: cutPath;
|
|
AddArc(arcPath, arc, mode, ref curpos);
|
|
break;
|
|
|
|
case CodeType.LinearMove:
|
|
var line = (LinearMove)code;
|
|
if (line.Suppressed)
|
|
{
|
|
var endpt = line.EndPoint;
|
|
if (mode == Mode.Incremental)
|
|
endpt += curpos;
|
|
curpos = endpt;
|
|
break;
|
|
}
|
|
var linePath =
|
|
(line.Layer == LayerType.Leadin || line.Layer == LayerType.Leadout)
|
|
? leadPath
|
|
: cutPath;
|
|
AddLine(linePath, line, mode, ref curpos);
|
|
break;
|
|
|
|
case CodeType.RapidMove:
|
|
{
|
|
var rapid = (RapidMove)code;
|
|
var endpt = rapid.EndPoint;
|
|
if (mode == Mode.Incremental)
|
|
endpt += curpos;
|
|
var dx = endpt.X - curpos.X;
|
|
var dy = endpt.Y - curpos.Y;
|
|
if (dx * dx + dy * dy > 0.001 * 0.001)
|
|
{
|
|
cutPath.StartFigure();
|
|
leadPath.StartFigure();
|
|
}
|
|
curpos = endpt;
|
|
}
|
|
break;
|
|
|
|
case CodeType.SubProgramCall:
|
|
var tmpmode = mode;
|
|
var subpgm = (SubProgramCall)code;
|
|
if (subpgm.Program != null)
|
|
{
|
|
cutPath.StartFigure();
|
|
leadPath.StartFigure();
|
|
curpos = new Vector(
|
|
frameOrigin.X + subpgm.Offset.X,
|
|
frameOrigin.Y + subpgm.Offset.Y
|
|
);
|
|
AddProgramSplit(cutPath, leadPath, subpgm.Program, mode, ref curpos);
|
|
}
|
|
mode = tmpmode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AddArc(GraphicsPath path, ArcMove arc, Mode mode, ref Vector curpos)
|
|
{
|
|
var endpt = arc.EndPoint;
|
|
var center = arc.CenterPoint;
|
|
|
|
if (mode == Mode.Incremental)
|
|
{
|
|
endpt += curpos;
|
|
center += curpos;
|
|
}
|
|
|
|
// start angle in degrees
|
|
var startAngle = Angle.ToDegrees(
|
|
System.Math.Atan2(curpos.Y - center.Y, curpos.X - center.X)
|
|
);
|
|
|
|
// end angle in degrees
|
|
var endAngle = Angle.ToDegrees(
|
|
System.Math.Atan2(endpt.Y - center.Y, endpt.X - center.X)
|
|
);
|
|
|
|
endAngle = Angle.NormalizeDeg(endAngle);
|
|
startAngle = Angle.NormalizeDeg(startAngle);
|
|
|
|
if (arc.Rotation == RotationType.CCW && endAngle < startAngle)
|
|
endAngle += 360.0;
|
|
else if (arc.Rotation == RotationType.CW && startAngle < endAngle)
|
|
startAngle += 360.0;
|
|
|
|
var dx = endpt.X - center.X;
|
|
var dy = endpt.Y - center.Y;
|
|
|
|
var radius = System.Math.Sqrt(dx * dx + dy * dy);
|
|
|
|
var pt = new PointF((float)(center.X - radius), (float)(center.Y - radius));
|
|
var size = (float)(radius * 2.0);
|
|
|
|
if (startAngle.IsEqualTo(endAngle))
|
|
{
|
|
path.AddEllipse(pt.X, pt.Y, size, size);
|
|
}
|
|
else
|
|
{
|
|
var sweepAngle = (endAngle - startAngle);
|
|
|
|
path.AddArc(pt.X, pt.Y, size, size, (float)startAngle, (float)sweepAngle);
|
|
}
|
|
|
|
curpos = endpt;
|
|
}
|
|
|
|
private static void AddLine(
|
|
GraphicsPath path,
|
|
LinearMove line,
|
|
Mode mode,
|
|
ref Vector curpos
|
|
)
|
|
{
|
|
var pt = line.EndPoint;
|
|
|
|
if (mode == Mode.Incremental)
|
|
pt += curpos;
|
|
|
|
var pt1 = new PointF((float)curpos.X, (float)curpos.Y);
|
|
var pt2 = new PointF((float)pt.X, (float)pt.Y);
|
|
|
|
path.AddLine(pt1, pt2);
|
|
|
|
curpos = pt;
|
|
}
|
|
|
|
private static void AddLine(GraphicsPath path, RapidMove line, Mode mode, ref Vector curpos)
|
|
{
|
|
var pt = line.EndPoint;
|
|
|
|
if (mode == Mode.Incremental)
|
|
pt += curpos;
|
|
|
|
curpos = pt;
|
|
}
|
|
|
|
/// <summary>One continuous run of drawn moves, with its program-space ends.</summary>
|
|
private sealed record Figure(GraphicsPath Path, Vector Start, Vector End)
|
|
{
|
|
public bool IsClosed => Start.DistanceTo(End) <= Tolerance.Epsilon;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds every figure to one path. With <paramref name="bridgeTabs"/>, an 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.
|
|
/// </summary>
|
|
private static GraphicsPath Combine(List<Figure> figures, bool bridgeTabs)
|
|
{
|
|
var path = new GraphicsPath();
|
|
var pending = new List<Figure>(figures);
|
|
|
|
while (pending.Count > 0)
|
|
{
|
|
var figure = pending[0];
|
|
pending.RemoveAt(0);
|
|
|
|
if (!bridgeTabs || figure.IsClosed)
|
|
{
|
|
path.AddPath(figure.Path, false);
|
|
figure.Path.Dispose();
|
|
continue;
|
|
}
|
|
|
|
using var chain = new GraphicsPath();
|
|
chain.AddPath(figure.Path, false);
|
|
figure.Path.Dispose();
|
|
var end = figure.End;
|
|
|
|
while (true)
|
|
{
|
|
Figure 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);
|
|
chain.AddPath(next.Path, true);
|
|
next.Path.Dispose();
|
|
end = next.End;
|
|
}
|
|
|
|
path.AddPath(chain, false);
|
|
}
|
|
|
|
return path;
|
|
}
|
|
|
|
private static void AddProgram(List<Figure> figures, Program pgm, Mode mode, ref Vector curpos,
|
|
System.Func<LayerType, bool> includeLayer = null)
|
|
{
|
|
// Capture the frame origin at entry. Sub-program Offsets are relative
|
|
// to this fixed origin, not to the current tool position.
|
|
var frameOrigin = curpos;
|
|
mode = pgm.Mode;
|
|
GraphicsPath currentFigure = null;
|
|
var figureStart = curpos;
|
|
|
|
// Call before moving past a skipped code so the figure records where its cut ended.
|
|
void Flush(Vector end)
|
|
{
|
|
if (currentFigure != null)
|
|
{
|
|
figures.Add(new Figure(currentFigure, figureStart, end));
|
|
currentFigure = null;
|
|
}
|
|
}
|
|
|
|
void BeginFigure(Vector start)
|
|
{
|
|
if (currentFigure == null)
|
|
{
|
|
currentFigure = new GraphicsPath();
|
|
figureStart = start;
|
|
}
|
|
}
|
|
|
|
for (int i = 0; i < pgm.Length; ++i)
|
|
{
|
|
var code = pgm[i];
|
|
|
|
switch (code.Type)
|
|
{
|
|
case CodeType.ArcMove:
|
|
{
|
|
var arc = (ArcMove)code;
|
|
if (includeLayer == null
|
|
? arc.Layer != LayerType.Leadin && arc.Layer != LayerType.Leadout
|
|
: !arc.Suppressed && includeLayer(arc.Layer))
|
|
{
|
|
BeginFigure(curpos);
|
|
AddArc(currentFigure, arc, mode, ref curpos);
|
|
}
|
|
else
|
|
{
|
|
Flush(curpos);
|
|
var endpt = arc.EndPoint;
|
|
if (mode == Mode.Incremental)
|
|
endpt += curpos;
|
|
curpos = endpt;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CodeType.LinearMove:
|
|
{
|
|
var line = (LinearMove)code;
|
|
if (includeLayer == null
|
|
? line.Layer != LayerType.Leadin && line.Layer != LayerType.Leadout
|
|
: !line.Suppressed && includeLayer(line.Layer))
|
|
{
|
|
BeginFigure(curpos);
|
|
AddLine(currentFigure, line, mode, ref curpos);
|
|
}
|
|
else
|
|
{
|
|
Flush(curpos);
|
|
var endpt = line.EndPoint;
|
|
if (mode == Mode.Incremental)
|
|
endpt += curpos;
|
|
curpos = endpt;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case CodeType.RapidMove:
|
|
{
|
|
var rapid = (RapidMove)code;
|
|
var endpt = rapid.EndPoint;
|
|
if (mode == Mode.Incremental)
|
|
endpt += curpos;
|
|
var dx = endpt.X - curpos.X;
|
|
var dy = endpt.Y - curpos.Y;
|
|
if (dx * dx + dy * dy > 0.001 * 0.001)
|
|
Flush(curpos);
|
|
curpos = endpt;
|
|
}
|
|
break;
|
|
|
|
case CodeType.SubProgramCall:
|
|
{
|
|
Flush(curpos);
|
|
var tmpmode = mode;
|
|
var subpgm = (SubProgramCall)code;
|
|
|
|
if (subpgm.Program != null)
|
|
{
|
|
curpos = new Vector(
|
|
frameOrigin.X + subpgm.Offset.X,
|
|
frameOrigin.Y + subpgm.Offset.Y
|
|
);
|
|
AddProgram(figures, subpgm.Program, mode, ref curpos, includeLayer);
|
|
}
|
|
|
|
mode = tmpmode;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
Flush(curpos);
|
|
}
|
|
|
|
private static void AddArc(GraphicsPath path, Arc arc)
|
|
{
|
|
var diameter = arc.Diameter;
|
|
|
|
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 sweepAngle = (endAngle - startAngle);
|
|
|
|
path.AddArc(
|
|
(float)(arc.Center.X - arc.Radius),
|
|
(float)(arc.Center.Y - arc.Radius),
|
|
(float)diameter,
|
|
(float)diameter,
|
|
(float)(startAngle),
|
|
(float)sweepAngle
|
|
);
|
|
}
|
|
|
|
private static void AddCircle(GraphicsPath path, Circle circle)
|
|
{
|
|
var diameter = circle.Diameter;
|
|
|
|
path.AddEllipse(
|
|
(float)(circle.Center.X - circle.Radius),
|
|
(float)(circle.Center.Y - circle.Radius),
|
|
(float)diameter,
|
|
(float)diameter
|
|
);
|
|
}
|
|
|
|
private static void AddLine(GraphicsPath path, Line line)
|
|
{
|
|
path.AddLine(
|
|
(float)line.StartPoint.X,
|
|
(float)line.StartPoint.Y,
|
|
(float)line.EndPoint.X,
|
|
(float)line.EndPoint.Y
|
|
);
|
|
}
|
|
|
|
private static void AddShape(GraphicsPath path, Shape shape)
|
|
{
|
|
foreach (var entity in shape.Entities)
|
|
{
|
|
if (entity.Layer != null)
|
|
{
|
|
if (
|
|
string.Equals(
|
|
entity.Layer.Name,
|
|
SpecialLayers.Leadin.Name,
|
|
System.StringComparison.OrdinalIgnoreCase
|
|
)
|
|
|| string.Equals(
|
|
entity.Layer.Name,
|
|
SpecialLayers.Leadout.Name,
|
|
System.StringComparison.OrdinalIgnoreCase
|
|
)
|
|
)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
switch (entity.Type)
|
|
{
|
|
case EntityType.Arc:
|
|
AddArc(path, (Arc)entity);
|
|
break;
|
|
|
|
case EntityType.Circle:
|
|
AddCircle(path, (Circle)entity);
|
|
break;
|
|
|
|
case EntityType.Line:
|
|
AddLine(path, (Line)entity);
|
|
break;
|
|
|
|
case EntityType.Polygon:
|
|
AddPolygon(path, (Polygon)entity);
|
|
break;
|
|
|
|
case EntityType.Shape:
|
|
var subpath = new GraphicsPath();
|
|
AddShape(subpath, (Shape)entity);
|
|
path.AddPath(subpath, false);
|
|
subpath.Dispose();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void AddPolygon(GraphicsPath path, Polygon polygon)
|
|
{
|
|
var pts = new PointF[polygon.Vertices.Count];
|
|
|
|
for (int i = 0; i < pts.Length; i++)
|
|
{
|
|
var pt = polygon.Vertices[i];
|
|
pts[i] = new PointF((float)pt.X, (float)pt.Y);
|
|
}
|
|
|
|
path.AddPolygon(pts);
|
|
}
|
|
}
|
|
}
|