I am writing the code to parse and render g-code for a laser machine. But while rendering the code, it doesn't seem to understand the difference between a line, a circle and an arc although my logic seems to be correct. here is a small sample of the g-code I am trying to parse and render.
G-code
%
O2034
GOTO90000
G100A500.200B500.200 C1500.000 D3000.000
N90000
M98 P9101
GOTO#598
(PART 1)
(CONTOUR 1)
N1 M05
#599=1
E1
E101
G00 X300.000 Y250.000
M101
M102
G41
G03 X300.000 Y250.000 I-50.000 J0.000
M103
(CONTOUR 2)
N2 M05
#599=2
E1
E101
G00 X10.200 Y-0.200
M101
M102
G01 X10.000 Y-0.200
G02 X-0.200 Y10.000 I0.000 J10.200
G01 X-0.200 Y10.200
M103
(CONTOUR 3)
N3 M05
#599=3
E1
E101
M28 G00 X-0.200 Y489.800
M101
M102
G01 X-0.200 Y490.000
G02 X10.000 Y500.200 I10.200 J0.000
G01 X490.000 Y500.200
G02 X500.200 Y490.000 I0.000 J-10.200
G01 X500.200 Y10.000
G02 X490.000 Y-0.200 I-10.200 J0.000
G01 X489.800 Y-0.200
M103
N10G14
G5.1Q0
M103
M41
(G32L0)
G69
M98P9102
M30
this is the code in C# I wrote
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Text;
using System.Linq;
using System.Text.RegularExpressions;
using System.Windows.Forms;
namespace SIL_HMI
{
public enum MeasurementUnit
{
Pixels,
Millimeters
}
class GcodeParser
{
private MeasurementUnit unitOfMeasurement;
private List<GCodePoint> toolpathPoints;
private const float PixelsPerMmforRendering = 1.0f;
private PointF currentPosition;
private Regex gCodeRegex;
private PictureBox nCodeRenderBox;
private TextBox gcodeDisplayBox;
private ProductionPage productionPage;
private float controlX;
private float controlY;
private float currentI;
private float currentJ;
public GcodeParser(PictureBox pictureBox, MeasurementUnit unitOfMeasurement, ProductionPage productionPage)
{
toolpathPoints = new List<GCodePoint>();
InitializeGCodeRegex();
currentPosition = new PointF(0, 0);
nCodeRenderBox = pictureBox;
this.unitOfMeasurement = unitOfMeasurement;
this.productionPage = productionPage;
gcodeDisplayBox = productionPage.GcodeDisplayBox;
}
private void InitializeGCodeRegex()
{
string mCommandPattern = @"^M\d+\s*([^\r\n]*)";
string nCommandPattern = @"^N\d+\s*([^\r\n]*)";
string gCommandPattern = @"^G(?:54|90|00|01|02|03)\s*([^\r\n]*)";
string xCoordinatePattern = @"[Xx]([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)";
string yCoordinatePattern = @"[Yy]([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)";
string iCoordinatePattern = @"[Ii]([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)";
string jCoordinatePattern = @"[Jj]([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)";
string fullPattern = $"{mCommandPattern}|{nCommandPattern}|{gCommandPattern}|{xCoordinatePattern}|{yCoordinatePattern}|{iCoordinatePattern}|{jCoordinatePattern}";
gCodeRegex = new Regex(fullPattern, RegexOptions.Multiline);
}
public string FilterGCodeForRendering(string gCode)
{
StringBuilder filteredGCode = new StringBuilder();
string[] lines = gCode.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
foreach (string line in lines)
{
string upperCaseLine = line.ToUpper();
if (upperCaseLine.Contains("G00") ||
upperCaseLine.Contains("G01") ||
upperCaseLine.Contains("G02") ||
upperCaseLine.Contains("G03"))
{
string processedLine = Regex.Replace(line, @"^N\d+\s*", "");
filteredGCode.AppendLine(processedLine);
}
}
gcodeDisplayBox.Text = filteredGCode.ToString();
return filteredGCode.ToString();
}
class GCodePoint
{
public PointF Point { get; set; }
public string GCodeCommand { get; set; }
}
public void GcodeParse(string gCode, MeasurementUnit unitOfMeasurement)
{
try
{
toolpathPoints.Clear();
string filteredGCode = FilterGCodeForRendering(gCode);
MatchCollection matches = gCodeRegex.Matches(filteredGCode);
currentPosition = new PointF(0, 0);
int index = 0;
foreach (Match match in matches)
{
string line = match.Groups[0].Value;
string command = match.Value.ToUpper();
if (command.StartsWith("N"))
{
HandleNLine(line);
}
else if (command.StartsWith("G00"))
{
HandleRapidMovement(match, unitOfMeasurement, index);
}
else if (command.StartsWith("G01"))
{
HandleLinearMovement(match, unitOfMeasurement, index);
}
else if (command.StartsWith("G02") || command.StartsWith("G03"))
{
HandleCircularMovement(match, unitOfMeasurement, index);
}
index++;
}
Console.WriteLine($"Parsed {toolpathPoints.Count} points.");
}
catch (Exception ex)
{
Console.WriteLine($"Error during parsing: {ex.Message}");
}
}
private void HandleNLine(string line)
{
Console.WriteLine($"{line}");
}
private void HandleRapidMovement(Match match, MeasurementUnit unitOfMeasurement, int index)
{
float x = ExtractCoordinate(match, 'X', currentPosition.X);
float y = ExtractCoordinate(match, 'Y', currentPosition.Y);
if (unitOfMeasurement == MeasurementUnit.Millimeters)
{
x *= PixelsPerMmforRendering;
y *= PixelsPerMmforRendering;
}
string command = match.Value.ToUpper();
toolpathPoints.Add(new GCodePoint { Point = new PointF(x, y), GCodeCommand = command });
Console.WriteLine($"Parsed Rapid movement: X={x}, Y={y}");
currentPosition = new PointF(x, y);
}
private void HandleLinearMovement(Match match, MeasurementUnit unitOfMeasurement, int index)
{
float x = ExtractCoordinate(match, 'X', currentPosition.X);
float y = ExtractCoordinate(match, 'Y', currentPosition.Y);
if (unitOfMeasurement == MeasurementUnit.Millimeters)
{
x *= PixelsPerMmforRendering;
y *= PixelsPerMmforRendering;
}
string command = match.Value.ToUpper();
toolpathPoints.Add(new GCodePoint { Point = new PointF(x, y), GCodeCommand = command });
Console.WriteLine($"Parsed linear movement: X={x}, Y={y}");
currentPosition = new PointF(x, y);
}
private void HandleCircularMovement(Match match, MeasurementUnit unitOfMeasurement, int index)
{
float x = ExtractCoordinate(match, 'X', currentPosition.X);
float y = ExtractCoordinate(match, 'Y', currentPosition.Y);
currentI = ExtractRadius(match, "I", 0);
currentJ = ExtractRadius(match, "J", 0);
if (unitOfMeasurement == MeasurementUnit.Millimeters)
{
x *= PixelsPerMmforRendering;
y *= PixelsPerMmforRendering;
currentI *= PixelsPerMmforRendering;
currentJ *= PixelsPerMmforRendering;
}
float cx = x + currentI;
float cy = y + currentJ;
float radius = (float)Math.Sqrt(currentI * currentI + currentJ * currentJ);
if (toolpathPoints.Count > 0)
{
int previousIndex = (index - 1 + toolpathPoints.Count) % toolpathPoints.Count;
int nextIndex = (index + 1) % toolpathPoints.Count;
bool isRapidBefore = IsRapidMovement(toolpathPoints[previousIndex].GCodeCommand);
bool isRapidAfter = IsRapidMovement(toolpathPoints[nextIndex].GCodeCommand);
if (isRapidBefore && isRapidAfter)
{
float startAngle = 0.0f;
float sweepAngle = 360.0f;
toolpathPoints.Add(new GCodePoint { Point = new PointF(cx, cy), GCodeCommand = match.Value.ToUpper() });
toolpathPoints.Add(new GCodePoint { Point = new PointF(cx + radius, cy), GCodeCommand = match.Value.ToUpper() });
}
else
{
PointF startPoint = toolpathPoints[previousIndex].Point;
PointF endPoint = toolpathPoints[nextIndex].Point;
float startAngle = (float)Math.Atan2(startPoint.Y - cy, startPoint.X - cx) * (180 / (float)Math.PI);
float endAngle = (float)Math.Atan2(endPoint.Y - cy, endPoint.X - cx) * (180 / (float)Math.PI);
toolpathPoints.Add(new GCodePoint { Point = startPoint, GCodeCommand = match.Value.ToUpper() });
toolpathPoints.Add(new GCodePoint { Point = endPoint, GCodeCommand = match.Value.ToUpper() });
}
}
Console.WriteLine($"Parsed circular movement: X={x}, Y={y}, I={currentI}, J={currentJ}, CenterX={cx}, CenterY={cy}, Radius={radius}");
currentPosition = new PointF(x, y);
}
private float ExtractCoordinate(Match match, char axis, float defaultValue)
{
string coordinatePattern = $@"{axis}([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)";
Match coordinateMatch = Regex.Match(match.Groups[0].Value, coordinatePattern);
if (coordinateMatch.Success)
{
return float.Parse(coordinateMatch.Groups[1].Value);
}
return defaultValue;
}
private float ExtractRadius(Match match, string radius, float defaultValue)
{
string coordinatePattern = $@"{radius}([-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?)";
Match coordinateMatch = Regex.Match(match.Groups[0].Value, coordinatePattern);
if (coordinateMatch.Success)
{
return float.Parse(coordinateMatch.Groups[1].Value);
}
return defaultValue;
}
private bool IsRapidMovement(string gCodeCommand)
{
return gCodeCommand.StartsWith("G00", StringComparison.OrdinalIgnoreCase);
}
private bool IsCircularMovement(int currentIndex, List<GCodePoint> points)
{
return points[currentIndex].Point.Equals(points[(currentIndex + 2) % points.Count].Point);
}
public void RenderToolpath()
{
try
{
Bitmap toolpathBitmap = new Bitmap(nCodeRenderBox.Width, nCodeRenderBox.Height);
using (Graphics g = Graphics.FromImage(toolpathBitmap))
{
g.SmoothingMode = SmoothingMode.AntiAlias;
for (int index = 0; index < toolpathPoints.Count; index++)
{
try
{
GCodePoint startPointInfo = toolpathPoints[index];
GCodePoint endPointInfo = toolpathPoints[(index + 1) % toolpathPoints.Count];
PointF startPoint = startPointInfo.Point;
PointF endPoint = endPointInfo.Point;
string startGCodeCommand = startPointInfo.GCodeCommand;
if (startPoint != endPoint)
{
if (IsCircularMovement(index, toolpathPoints))
{
bool isRapidBefore = IsRapidMovement(toolpathPoints[(index - 1 + toolpathPoints.Count) % toolpathPoints.Count].GCodeCommand);
bool isRapidAfter = IsRapidMovement(toolpathPoints[(index + 2) % toolpathPoints.Count].GCodeCommand);
float startAngle = isRapidBefore && isRapidAfter ? 0 : 0.000f;
float sweepAngle = isRapidBefore && isRapidAfter ? 0 : 360.000f;
if (isRapidBefore && isRapidAfter)
{
float cx = startPoint.X + currentI;
float cy = startPoint.Y + currentJ;
float radius = (float)Math.Sqrt(currentI * currentI + currentJ * currentJ);
Color penColor = IsRapidMovement(startGCodeCommand) ? Color.Green : Color.Red;
using (Pen pen = new Pen(penColor, 2))
{
g.DrawArc(pen, cx, cy, 2 * radius, 2 * radius, startAngle, sweepAngle);
}
}
else
{
float cx = startPoint.X + currentI;
float cy = startPoint.Y + currentJ;
float radius = (float)Math.Sqrt(currentI * currentI + currentJ * currentJ);
Color penColor = IsRapidMovement(startGCodeCommand) ? Color.Green : Color.Red;
using (Pen pen = new Pen(penColor, 2))
{
g.DrawArc(pen, cx, cy, 2 * radius, 2 * radius, startAngle, sweepAngle);
}
}
}
else
{
Color penColor = IsRapidMovement(startGCodeCommand) ? Color.Green : Color.Red;
using (Pen pen = new Pen(penColor, 2))
{
g.DrawLine(pen, startPoint, endPoint);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error processing index {index}: {ex.Message}");
}
}
nCodeRenderBox.Image = toolpathBitmap;
nCodeRenderBox.Invalidate();
}
}
catch (Exception ex)
{
Console.WriteLine($"Error during rendering: {ex.Message}");
}
}
}
}
When running the code, it is rendering even the circular movement as lines (in most of the cases).