From d80ba3559c5af407da6c33b5968792f94f212023 Mon Sep 17 00:00:00 2001 From: Martin Pittermann Date: Mon, 24 Dec 2018 15:56:37 +0100 Subject: [PATCH] serious gcode parser improvement --- OpenCNCPilot/GCode/GCodeCommands/Line.cs | 7 ++- OpenCNCPilot/GCode/GCodeFile.cs | 68 ++++++++++++++---------- OpenCNCPilot/GCode/GCodeParser.cs | 53 ++++++++++++------ OpenCNCPilot/Util/Vector3.cs | 17 ++++++ 4 files changed, 101 insertions(+), 44 deletions(-) diff --git a/OpenCNCPilot/GCode/GCodeCommands/Line.cs b/OpenCNCPilot/GCode/GCodeCommands/Line.cs index 43f3678..40520f1 100644 --- a/OpenCNCPilot/GCode/GCodeCommands/Line.cs +++ b/OpenCNCPilot/GCode/GCodeCommands/Line.cs @@ -1,12 +1,16 @@ using OpenCNCPilot.Util; using System; using System.Collections.Generic; +using System.Linq; namespace OpenCNCPilot.GCode.GCodeCommands { class Line : Motion { public bool Rapid; + // PositionValid[i] is true if the corresponding coordinate of the end position was defined in the file. + // eg. for a file with "G0 Z15" as the first line, X and Y would still be false + public bool[] PositionValid = new bool[] { false, false, false }; public override double Length { @@ -23,7 +27,7 @@ public override Vector3 Interpolate(double ratio) public override IEnumerable Split(double length) { - if (Rapid) //don't split up rapid motions + if (Rapid || PositionValid.Any(isValid => !isValid)) //don't split up rapid or not fully defined motions { yield return this; yield break; @@ -44,6 +48,7 @@ public override IEnumerable Split(double length) immediate.Start = lastEnd; immediate.End = end; immediate.Feed = Feed; + immediate.PositionValid = new bool[] { true, true, true }; yield return immediate; diff --git a/OpenCNCPilot/GCode/GCodeFile.cs b/OpenCNCPilot/GCode/GCodeFile.cs index 2827265..3b1d9ba 100644 --- a/OpenCNCPilot/GCode/GCodeFile.cs +++ b/OpenCNCPilot/GCode/GCodeFile.cs @@ -55,34 +55,34 @@ private GCodeFile(List toolpath) Vector3 min = Vector3.MaxValue, max = Vector3.MinValue; Vector3 minfeed = Vector3.MaxValue, maxfeed = Vector3.MinValue; - foreach (Motion m in Enumerable.Concat(Toolpath.OfType(), Toolpath.OfType().SelectMany(a => a.Split(0.1)))) + foreach (Command c in Toolpath) { - TravelDistance += m.Length; + if (c is Line) + { + Line l = (Line)c; + if (l.PositionValid.Any(isValid => !isValid)) + continue; + } - if (m is Line && !((Line)m).Rapid && ((Line)m).Feed > 0.0) - TotalTime += TimeSpan.FromMinutes(m.Length / m.Feed); + if (c is Motion) + { + ContainsMotion = true; - ContainsMotion = true; + Motion m = (Motion)c; - for (int i = 0; i < 3; i++) - { - if (m.End[i] > max[i]) - max[i] = m.End[i]; + TravelDistance += m.Length; - if (m.End[i] < min[i]) - min[i] = m.End[i]; - } + if (m is Line && !((Line)m).Rapid && ((Line)m).Feed > 0.0) + TotalTime += TimeSpan.FromMinutes(m.Length / m.Feed); - if (m is Line && (m as Line).Rapid) - continue; + min = Vector3.ElementwiseMin(min, m.End); + max = Vector3.ElementwiseMax(max, m.End); - for (int i = 0; i < 3; i++) - { - if (m.End[i] > maxfeed[i]) - maxfeed[i] = m.End[i]; + if (m is Line && (m as Line).Rapid) + continue; - if (m.End[i] < minfeed[i]) - minfeed[i] = m.End[i]; + minfeed = Vector3.ElementwiseMin(minfeed, m.End); + maxfeed = Vector3.ElementwiseMax(maxfeed, m.End); } } @@ -98,7 +98,6 @@ private GCodeFile(List toolpath) Size = size; - MaxFeed = maxfeed; MinFeed = minfeed; Vector3 sizefeed = MaxFeed - MinFeed; @@ -169,6 +168,7 @@ public void GetModel(LinesVisual3D line, LinesVisual3D rapid, LinesVisual3D arc) linePoints.Add(l.Start.ToPoint3D()); linePoints.Add(l.End.ToPoint3D()); } + continue; } @@ -189,7 +189,7 @@ public void GetModel(LinesVisual3D line, LinesVisual3D rapid, LinesVisual3D arc) arc.Points = arcPoints; sw.Stop(); - Console.WriteLine("Generating the Toolpath Model took {0} ms", sw.ElapsedMilliseconds); + Console.WriteLine("Generating the toolpath model took {0} ms", sw.ElapsedMilliseconds); } public List GetGCode() @@ -221,11 +221,11 @@ public List GetGCode() string code = l.Rapid ? "G0" : "G1"; - if (State.Position.X != l.End.X) + if (State.Position.X != l.End.X && l.PositionValid[0]) code += string.Format(nfi, " X{0:0.###}", l.End.X); - if (State.Position.Y != l.End.Y) + if (State.Position.Y != l.End.Y && l.PositionValid[1]) code += string.Format(nfi, " Y{0:0.###}", l.End.Y); - if (State.Position.Z != l.End.Z) + if (State.Position.Z != l.End.Z && l.PositionValid[2]) code += string.Format(nfi, " Z{0:0.###}", l.End.Z); GCode.Add(code); @@ -351,6 +351,7 @@ public GCodeFile ArcsToLines(double length) l.End = segment.End; l.Feed = segment.Feed; l.Rapid = false; + l.PositionValid = new bool[] { true, true, true }; newFile.Add(l); } } @@ -379,7 +380,19 @@ public GCodeFile ApplyHeightMap(HeightMap map) { Arc a = m as Arc; if (a.Plane != ArcPlane.XY) - throw new Exception("GCode contains arcs in YZ or XZ plane (G18/19), can't apply HeightMap. Use Arcs to Lines if you really need this."); + throw new Exception("GCode contains arcs in YZ or XZ plane (G18/19), can't apply height map. Use 'Arcs to Lines' if you really need this."); + } + + if (m is Line) + { + Line l = (Line)m; + + // do not split up or modify any lines that are rapid or not fully defined + if (l.PositionValid.Any(isValid => !isValid) || l.Rapid) + { + newToolPath.Add(l); + continue; + } } foreach (Motion subMotion in m.Split(segmentLength)) @@ -418,7 +431,7 @@ public GCodeFile RotateCW() // would be possible, but I'm too lazy to implement this properly if (oldArc.Plane != ArcPlane.XY) - throw new Exception("GCode contains arcs in YZ or XZ plane (G18/19), can't rotate gcode. Use Arcs to Lines if you really need this."); + throw new Exception("GCode contains arcs in YZ or XZ plane (G18/19), can't rotate gcode. Use 'Arcs to Lines' if you really need this."); newArc.Direction = oldArc.Direction; newArc.Plane = oldArc.Plane; @@ -431,6 +444,7 @@ public GCodeFile RotateCW() Line oldLine = (Line)oldMotion; Line newLine = new Line(); newLine.Rapid = oldLine.Rapid; + newLine.PositionValid = oldLine.PositionValid; newMotion = newLine; } else diff --git a/OpenCNCPilot/GCode/GCodeParser.cs b/OpenCNCPilot/GCode/GCodeParser.cs index 75dc172..41ab2dd 100644 --- a/OpenCNCPilot/GCode/GCodeParser.cs +++ b/OpenCNCPilot/GCode/GCodeParser.cs @@ -23,6 +23,7 @@ public enum ParseUnit class ParserState { public Vector3 Position; + public bool[] PositionValid; // true if the position for this coordinate was previously specified in absolute terms, to prevent the start point of (0, 0, 0) to influence the output file public ArcPlane Plane; public double Feed; public ParseDistanceMode DistanceMode; @@ -32,7 +33,8 @@ class ParserState public ParserState() { - Position = new Vector3(); + Position = Vector3.MinValue; + PositionValid = new bool[] { false, false, false }; Plane = ArcPlane.XY; Feed = 0; DistanceMode = ParseDistanceMode.Absolute; @@ -100,7 +102,7 @@ public static void Parse(IEnumerable file) sw.Stop(); - Console.WriteLine("Parsing the GCode File took {0} ms", sw.ElapsedMilliseconds); + Console.WriteLine("parsing the G code file took {0} ms", sw.ElapsedMilliseconds); } static string CleanupLine(string line, int lineNumber) @@ -152,7 +154,7 @@ static void Parse(string line, int lineNumber) if (!ValidWords.Contains(Words[i].Command)) { - Warnings.Add($"ignoring unknown word (letter): \"{Words[i]}\" in line {lineNumber}"); + Warnings.Add($"ignoring unknown word (letter): \"{Words[i]}\". (line {lineNumber})"); Words.RemoveAt(i--); continue; } @@ -174,7 +176,7 @@ static void Parse(string line, int lineNumber) int param = (int)Words[i].Parameter; if (param != Words[i].Parameter || param < 0) - throw new ParseException("MCode can only have integer parameters", lineNumber); + throw new ParseException("M code can only have positive integer parameters", lineNumber); Commands.Add(new MCode() { Code = param }); @@ -188,7 +190,7 @@ static void Parse(string line, int lineNumber) double param = Words[i].Parameter; if (param < 0) - Warnings.Add($"Spindle Speed must be positive in line {lineNumber}"); + Warnings.Add($"spindle speed must be positive. (line {lineNumber})"); Commands.Add(new Spindle() { Speed = Math.Abs(param) }); @@ -270,7 +272,7 @@ static void Parse(string line, int lineNumber) if (Words.Count >= 2 && Words[i + 1].Command == 'P') { if (Words[i + 1].Parameter < 0) - Warnings.Add($"Dwell time must be positive in line {lineNumber}"); + Warnings.Add($"dwell time must be positive. (line {lineNumber})"); Commands.Add(new Dwell() { Seconds = Math.Abs(Words[i + 1].Parameter) }); Words.RemoveAt(i + 1); @@ -280,7 +282,7 @@ static void Parse(string line, int lineNumber) } } - Warnings.Add($"ignoring unknown command G{param} in line {lineNumber}"); + Warnings.Add($"ignoring unknown command G{param}. (line {lineNumber})"); Words.RemoveAt(i--); #endregion } @@ -299,12 +301,22 @@ static void Parse(string line, int lineNumber) } if (MotionMode < 0) - throw new ParseException("No Motion Mode active", lineNumber); + throw new ParseException("no motion mode active", lineNumber); double UnitMultiplier = (State.Unit == ParseUnit.Metric) ? 1 : 25.4; Vector3 EndPos = State.Position; + if (State.DistanceMode == ParseDistanceMode.Incremental && State.PositionValid.Any(isValid => !isValid)) + { + throw new ParseException("incremental motion is only allowed after an absolute position has been established (eg. with \"G90 G0 X0 Y0 Z5\")", lineNumber); + } + + if ((MotionMode == 2 || MotionMode == 3) && State.PositionValid.Any(isValid => !isValid)) + { + throw new ParseException("arcs (G2/G3) are only allowed after an absolute position has been established (eg. with \"G90 G0 X0 Y0 Z5\")", lineNumber); + } + #region FindEndPos { int Incremental = (State.DistanceMode == ParseDistanceMode.Incremental) ? 1 : 0; @@ -315,6 +327,7 @@ static void Parse(string line, int lineNumber) continue; EndPos.X = Words[i].Parameter * UnitMultiplier + Incremental * EndPos.X; Words.RemoveAt(i); + State.PositionValid[0] = true; break; } @@ -324,6 +337,7 @@ static void Parse(string line, int lineNumber) continue; EndPos.Y = Words[i].Parameter * UnitMultiplier + Incremental * EndPos.Y; Words.RemoveAt(i); + State.PositionValid[1] = true; break; } @@ -333,6 +347,7 @@ static void Parse(string line, int lineNumber) continue; EndPos.Z = Words[i].Parameter * UnitMultiplier + Incremental * EndPos.Z; Words.RemoveAt(i); + State.PositionValid[2] = true; break; } } @@ -340,19 +355,25 @@ static void Parse(string line, int lineNumber) if (MotionMode != 0 && State.Feed <= 0) { - throw new ParseException("Feed Rate Undefined", lineNumber); + throw new ParseException("feed rate undefined", lineNumber); + } + + if (MotionMode == 1 && State.PositionValid.Any(isValid => !isValid)) + { + Warnings.Add($"a feed move is used before an absolute position is established, height maps will not be applied to this motion. (line {lineNumber})"); } if (MotionMode <= 1) { if (Words.Count > 0) - Warnings.Add($"Motion Command must be last in line (ignoring unused Words {string.Join(" ", Words)} in Block) in line {lineNumber}"); + Warnings.Add($"motion command must be last in line (ignoring unused words {string.Join(" ", Words)} in block). (line {lineNumber})"); Line motion = new Line(); motion.Start = State.Position; motion.End = EndPos; motion.Feed = State.Feed; motion.Rapid = MotionMode == 0; + State.PositionValid.CopyTo(motion.PositionValid, 0); Commands.Add(motion); State.Position = EndPos; @@ -394,7 +415,7 @@ static void Parse(string line, int lineNumber) U = Words[i].Parameter * UnitMultiplier + ArcIncremental * State.Position.X; break; case ArcPlane.YZ: - throw new ParseException("Current Plane is YZ, I word is invalid", lineNumber); + throw new ParseException("current plane is YZ, I word is invalid", lineNumber); case ArcPlane.ZX: V = Words[i].Parameter * UnitMultiplier + ArcIncremental * State.Position.X; break; @@ -419,7 +440,7 @@ static void Parse(string line, int lineNumber) U = Words[i].Parameter * UnitMultiplier + ArcIncremental * State.Position.Y; break; case ArcPlane.ZX: - throw new ParseException("Current Plane is ZX, J word is invalid", lineNumber); + throw new ParseException("current plane is ZX, J word is invalid", lineNumber); } IJKused = true; @@ -435,7 +456,7 @@ static void Parse(string line, int lineNumber) switch (State.Plane) { case ArcPlane.XY: - throw new ParseException("Current Plane is XY, K word is invalid", lineNumber); + throw new ParseException("current plane is XY, K word is invalid", lineNumber); case ArcPlane.YZ: V = Words[i].Parameter * UnitMultiplier + ArcIncremental * State.Position.Z; break; @@ -458,7 +479,7 @@ static void Parse(string line, int lineNumber) continue; if (IJKused) - throw new ParseException("Both IJK and R notation used", lineNumber); + throw new ParseException("both IJK and R notation used", lineNumber); if (State.Position == EndPos) throw new ParseException("arcs in R-notation must have non-coincident start and end points", lineNumber); @@ -466,7 +487,7 @@ static void Parse(string line, int lineNumber) double Radius = Words[i].Parameter * UnitMultiplier; if (Radius == 0) - throw new ParseException("Radius can't be zero", lineNumber); + throw new ParseException("radius can't be zero", lineNumber); double A, B; @@ -512,7 +533,7 @@ static void Parse(string line, int lineNumber) #endregion if (Words.Count > 0) - Warnings.Add($"Motion Command must be last in line (ignoring unused Words {string.Join(" ", Words)} in Block) in line {lineNumber}"); + Warnings.Add($"motion command must be last in line (ignoring unused words {string.Join(" ", Words)} in block). (line {lineNumber})"); Arc arc = new Arc(); arc.Start = State.Position; diff --git a/OpenCNCPilot/Util/Vector3.cs b/OpenCNCPilot/Util/Vector3.cs index 9c261f0..56f148a 100644 --- a/OpenCNCPilot/Util/Vector3.cs +++ b/OpenCNCPilot/Util/Vector3.cs @@ -1595,5 +1595,22 @@ public static Vector3 Parse(string input) return new Vector3(values); } + + public static Vector3 ElementwiseMax(Vector3 v1, Vector3 v2) + { + return new Vector3( + Math.Max(v1.X, v2.X), + Math.Max(v1.Y, v2.Y), + Math.Max(v1.Z, v2.Z) + ); + } + public static Vector3 ElementwiseMin(Vector3 v1, Vector3 v2) + { + return new Vector3( + Math.Min(v1.X, v2.X), + Math.Min(v1.Y, v2.Y), + Math.Min(v1.Z, v2.Z) + ); + } } } \ No newline at end of file