Files
OpenNest/OpenNest/GraphicsHelper.cs
T
ajandClaude Opus 5.5 3672697cb2 fix(ui): fill tabbed cutouts as one hole
A cutout cut in pieces around tabs is a set of open cut figures. Filling the
cut outline closed each piece with a chord, drawing a slanted bridge across
the hole. The fill path now rejoins open figures across their tab gaps, and
parts with open cuts use it for drawing, hit-testing and the lead-in overlay.

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

642 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
)
{
cutPath = GetLayerPath(pgm, origin,
layer => layer != LayerType.Scribe && layer != LayerType.Leadin && layer != LayerType.Leadout,
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);
}
}
}