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
(); 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 _); /// /// Whether a cut contour is open, such as a cutout left attached by tabs. Filling /// would close it with a chord; fill instead. /// 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 _); } /// /// 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. /// 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 includeLayer, bool bridgeTabs, out bool hasOpenFigures) { var figures = new List
(); 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, Color? etchColor = null ) { 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(etchColor ?? 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; } /// One continuous run of drawn moves, with its program-space ends. private sealed record Figure(GraphicsPath Path, Vector Start, Vector End) { public bool IsClosed => Start.DistanceTo(End) <= Tolerance.Epsilon; } /// /// Adds every figure to one path. With , 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. /// private static GraphicsPath Combine(List
figures, bool bridgeTabs) { var path = new GraphicsPath(); var pending = new List
(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
figures, Program pgm, Mode mode, ref Vector curpos, System.Func 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); } } }