Files
OpenNest/OpenNest/GraphicsHelper.cs
T
ajandClaude Opus 5.5 6f555345e3 feat(cad): cut breakoff joining lines as tabbed open break cuts
Breakoffs are now picked by their joining line, default to the smaller side as scrap
(flippable), and cut that line between tabs on a new Break layer instead of slots.
Plan Cutting and Assign Lead-ins enter each cut from the scrap with the nest's internal
lead-in, or pierce on the line without one; checks treat break cuts as intentional open
cuts that are not material.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-17 17:46:35 -04:00

550 lines
19 KiB
C#

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 path = new GraphicsPath();
var curpos = Vector.Zero;
AddProgram(path, pgm, pgm.Mode, ref curpos);
return path;
}
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
)
{
cutPath = GetLayerPath(pgm, origin,
layer => layer != LayerType.Scribe && layer != LayerType.Leadin && layer != LayerType.Leadout);
leadPath = GetLayerPath(pgm, origin,
layer => layer == LayerType.Leadin || layer == LayerType.Leadout);
etchPath = GetLayerPath(pgm, origin, layer => layer == LayerType.Scribe);
}
/// <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.
/// </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));
private static GraphicsPath GetLayerPath(Program pgm, Vector origin, System.Func<LayerType, bool> includeLayer)
{
var path = new GraphicsPath();
var position = Vector.Zero;
AddProgram(path, pgm, pgm.Mode, ref position, includeLayer);
// 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;
}
private static void AddProgram(GraphicsPath path, 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;
void Flush()
{
if (currentFigure != null)
{
path.AddPath(currentFigure, false);
currentFigure.Dispose();
currentFigure = null;
}
}
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))
{
if (currentFigure == null)
currentFigure = new GraphicsPath();
AddArc(currentFigure, arc, mode, ref curpos);
}
else
{
Flush();
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))
{
if (currentFigure == null)
currentFigure = new GraphicsPath();
AddLine(currentFigure, line, mode, ref curpos);
}
else
{
Flush();
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 = endpt;
}
break;
case CodeType.SubProgramCall:
{
Flush();
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(path, subpgm.Program, mode, ref curpos, includeLayer);
}
mode = tmpmode;
break;
}
}
}
Flush();
}
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);
}
}
}