Files
OpenNest/OpenNest/GraphicsHelper.cs
T
aj 0b322817d7 fix(core): use chain tolerance for entity gap check to prevent spurious rapids
Ellipse-to-arc conversion creates tiny floating-point gaps (~0.00002")
between consecutive arc segments. ShapeBuilder chains these with
ChainTolerance (0.0001"), but ConvertGeometry checked gaps with Epsilon
(0.00001"). Gaps between these thresholds generated spurious rapid moves
that broke GraphicsPath figures, causing diagonal fill artifacts from
GDI+'s implicit figure closing.

Root cause fix: align ConvertGeometry's gap check with ShapeBuilder's
ChainTolerance so precision gaps are absorbed instead of generating rapids.

Defense-in-depth: GraphicsHelper no longer breaks figures at near-zero
rapids, protecting against any programs with residual tiny rapids.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-23 21:32:08 -04:00

446 lines
15 KiB
C#

using OpenNest.CNC;
using OpenNest.Geometry;
using OpenNest.Math;
using System.Drawing;
using System.Drawing.Drawing2D;
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)
{
var path = new GraphicsPath();
var curpos = origin;
AddProgram(path, pgm, pgm.Mode, ref curpos);
return path;
}
public static GraphicsPath GetGraphicsPath(this Shape shape)
{
var path = new GraphicsPath();
AddShape(path, shape);
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);
var path = pgm.GetGraphicsPath();
var bounds = path.GetBounds();
var scalex = (size.Height - 10) / bounds.Height;
var scaley = (size.Width - 10) / bounds.Width;
var scale = scalex < scaley ? scalex : scaley;
var matrix = new Matrix();
matrix.Scale(scale, -scale);
path.Transform(matrix);
bounds = path.GetBounds();
var offset = new PointF(
(size.Width - bounds.Width) * 0.5f - bounds.X,
(size.Height - bounds.Height) * 0.5f - bounds.Y);
var graphics = Graphics.FromImage(img);
graphics.TranslateTransform(offset.X, offset.Y);
if (brush != null)
graphics.FillPath(brush, path);
if (pen == null)
pen = Pens.Black;
graphics.DrawPath(pen, path);
matrix.Dispose();
graphics.Dispose();
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 = origin;
AddProgramSplit(cutPath, leadPath, pgm, pgm.Mode, ref curpos);
}
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)
{
// 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 (arc.Layer != LayerType.Leadin && arc.Layer != LayerType.Leadout)
{
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 (line.Layer != LayerType.Leadin && line.Layer != LayerType.Leadout)
{
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);
}
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);
}
}
}