using OpenNest.CNC; using OpenNest.Geometry; using OpenNest.Math; using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Text; namespace OpenNest.IO { public sealed class ProgramReader { private const int BufferSize = 200; private int codeIndex; private CodeBlock block; private CodeSection section; private Program program; private StreamReader reader; private Dictionary resolvedVariables; public ProgramReader(Stream stream) { reader = new StreamReader(stream); program = new Program(); } public Program Read() { // First pass: read all lines, collect variable definitions var allLines = new List(); var variableDefs = new Dictionary( StringComparer.OrdinalIgnoreCase); var codeLines = new List(); string line; while ((line = reader.ReadLine()) != null) { allLines.Add(line); if (TryParseVariableDefinition(line, out var name, out var expression, out var isInline, out var isGlobal)) variableDefs[name] = (expression, isInline, isGlobal); else codeLines.Add(line); } // Evaluate variables with topological sort for dependency ordering resolvedVariables = ResolveVariables(variableDefs); // Store evaluated variables on the program foreach (var kvp in variableDefs) { var name = kvp.Key; var (expression, isInline, isGlobal) = kvp.Value; var value = resolvedVariables[name]; program.Variables[name] = new VariableDefinition(name, expression, value, isInline, isGlobal); } // Second pass: parse G-code lines with variable substitution foreach (var codeLine in codeLines) { block = ParseBlock(codeLine); ProcessCurrentBlock(); } return program; } private CodeBlock ParseBlock(string line) { var block = new CodeBlock(); Code code = null; for (var i = 0; i < line.Length; ++i) { var c = line[i]; if (c == '$' && code != null && resolvedVariables != null) { // Read the maximal variable name (letters, digits, underscores) var start = i + 1; while (start < line.Length && (char.IsLetterOrDigit(line[start]) || line[start] == '_')) start++; var maxName = line.Substring(i + 1, start - i - 1); // Try longest match first, then progressively shorter to handle // cases like X$widthY0 where "widthY" isn't a variable but "width" is string lookupKey = null; var nameLen = maxName.Length; while (nameLen > 0) { var candidate = maxName.Substring(0, nameLen); lookupKey = resolvedVariables.Keys .FirstOrDefault(k => string.Equals(k, candidate, StringComparison.OrdinalIgnoreCase)); if (lookupKey != null) break; nameLen--; } if (lookupKey != null) { code.Value = resolvedVariables[lookupKey].ToString(CultureInfo.InvariantCulture); code.VariableRef = lookupKey; i += nameLen; // advance past the matched variable name } else { i = start - 1; // no match, skip the whole thing } } else if (char.IsLetter(c)) block.Add((code = new Code(c))); else if (c == ':') { block.Add((new Code(c, line.Remove(0, i + 1).Trim()))); break; } else if (code != null) code.Value += c; } return block; } private void ProcessCurrentBlock() { var code = GetFirstCode(); while (code != null) { switch (code.Id) { case ':': program.Codes.Add(new Comment(code.Value)); code = GetNextCode(); break; case 'G': int value = int.Parse(code.Value); switch (value) { case 0: case 1: section = CodeSection.Line; ReadLine(value == 0); code = GetCurrentCode(); break; case 2: case 3: section = CodeSection.Arc; ReadArc(value == 2 ? RotationType.CW : RotationType.CCW); code = GetCurrentCode(); break; case 65: section = CodeSection.SubProgram; ReadSubProgram(); code = GetCurrentCode(); break; case 40: program.Codes.Add(new Kerf() { Value = KerfType.None }); code = GetNextCode(); break; case 41: program.Codes.Add(new Kerf() { Value = KerfType.Left }); code = GetNextCode(); break; case 42: program.Codes.Add(new Kerf() { Value = KerfType.Right }); code = GetNextCode(); break; case 90: program.Mode = Mode.Absolute; code = GetNextCode(); break; case 91: program.Mode = Mode.Incremental; code = GetNextCode(); break; default: code = GetNextCode(); break; } break; case 'F': var feedrate = new Feedrate() { Value = double.Parse(code.Value) }; if (code.VariableRef != null) feedrate.VariableRef = code.VariableRef; program.Codes.Add(feedrate); code = GetNextCode(); break; default: code = GetNextCode(); break; } } } private void ReadLine(bool isRapid) { var line = new LinearMove(); double x = 0; double y = 0; var layer = LayerType.Cut; var suppressed = false; string xRef = null, yRef = null; while (section == CodeSection.Line) { var code = GetNextCode(); if (code == null) { section = CodeSection.Unknown; break; } switch (code.Id) { case 'X': x = double.Parse(code.Value); xRef = code.VariableRef; break; case 'Y': y = double.Parse(code.Value); yRef = code.VariableRef; break; case ':': { var tags = code.Value.Trim().ToUpper().Split(':'); foreach (var tag in tags) { switch (tag) { case "DISPLAY": layer = LayerType.Display; break; case "LEADIN": layer = LayerType.Leadin; break; case "LEADOUT": layer = LayerType.Leadout; break; case "SCRIBE": layer = LayerType.Scribe; break; case "SUPPRESSED": suppressed = true; break; } } break; } default: section = CodeSection.Unknown; break; } } var refs = BuildVariableRefs(("X", xRef), ("Y", yRef)); if (isRapid) program.Codes.Add(new RapidMove(x, y) { VariableRefs = refs }); else program.Codes.Add(new LinearMove(x, y) { Layer = layer, Suppressed = suppressed, VariableRefs = refs }); } private void ReadArc(RotationType rotation) { double x = 0; double y = 0; double i = 0; double j = 0; var layer = LayerType.Cut; var suppressed = false; string xRef = null, yRef = null, iRef = null, jRef = null; while (section == CodeSection.Arc) { var code = GetNextCode(); if (code == null) { section = CodeSection.Unknown; break; } switch (code.Id) { case 'X': x = double.Parse(code.Value); xRef = code.VariableRef; break; case 'Y': y = double.Parse(code.Value); yRef = code.VariableRef; break; case 'I': i = double.Parse(code.Value); iRef = code.VariableRef; break; case 'J': j = double.Parse(code.Value); jRef = code.VariableRef; break; case ':': { var tags = code.Value.Trim().ToUpper().Split(':'); foreach (var tag in tags) { switch (tag) { case "DISPLAY": layer = LayerType.Display; break; case "LEADIN": layer = LayerType.Leadin; break; case "LEADOUT": layer = LayerType.Leadout; break; case "SCRIBE": layer = LayerType.Scribe; break; case "SUPPRESSED": suppressed = true; break; } } break; } default: section = CodeSection.Unknown; break; } } program.Codes.Add(new ArcMove() { EndPoint = new Vector(x, y), CenterPoint = new Vector(i, j), Rotation = rotation, Layer = layer, Suppressed = suppressed, VariableRefs = BuildVariableRefs(("X", xRef), ("Y", yRef), ("I", iRef), ("J", jRef)) }); } private void ReadSubProgram() { var p = 0; var r = 0.0; while (section == CodeSection.SubProgram) { var code = GetNextCode(); if (code == null) { section = CodeSection.Unknown; break; } switch (code.Id) { case 'P': p = int.Parse(code.Value); break; case 'R': r = double.Parse(code.Value); break; default: section = CodeSection.Unknown; break; } } program.Codes.Add(new SubProgramCall() { Id = p, Rotation = r }); } private Code GetNextCode() { codeIndex++; if (codeIndex >= block.Count) return null; return block[codeIndex]; } private Code GetCurrentCode() { if (codeIndex >= block.Count) return null; return block[codeIndex]; } private Code GetFirstCode() { if (block.Count == 0) return null; codeIndex = 0; return block[codeIndex]; } private static bool TryParseVariableDefinition(string line, out string name, out string expression, out bool isInline, out bool isGlobal) { name = null; expression = null; isInline = false; isGlobal = false; var trimmed = line.Trim(); if (trimmed.Length == 0) return false; // Must start with a letter or underscore (not a G-code letter followed by a digit) var firstChar = trimmed[0]; if (!char.IsLetter(firstChar) && firstChar != '_') return false; // If line starts with a known G-code letter followed by a digit, it's not a variable if (trimmed.Length >= 2 && char.IsDigit(trimmed[1])) { var upper = char.ToUpper(firstChar); if (upper is 'G' or 'M' or 'N' or 'F' or 'X' or 'Y' or 'I' or 'J' or 'T' or 'S' or 'O' or 'P' or 'R') return false; } // Must contain '=' var eqIndex = trimmed.IndexOf('='); if (eqIndex < 1) return false; // Extract name (everything before '=', trimmed) var rawName = trimmed.Substring(0, eqIndex).Trim(); // Validate name: must be identifier (letter/underscore followed by alphanumeric/underscore) if (rawName.Length == 0 || (!char.IsLetter(rawName[0]) && rawName[0] != '_')) return false; for (var i = 1; i < rawName.Length; i++) { if (!char.IsLetterOrDigit(rawName[i]) && rawName[i] != '_') return false; } // Extract expression and flags from the remainder after '=' var remainder = trimmed.Substring(eqIndex + 1).Trim(); // Check for trailing flags: inline and/or global // Parse from the end to separate expression from flags var words = remainder.Split(' ', StringSplitOptions.RemoveEmptyEntries); var flagStart = words.Length; for (var i = words.Length - 1; i >= 0; i--) { var word = words[i].ToLowerInvariant(); if (word == "inline" || word == "global") flagStart = i; else break; } // Build expression from non-flag words var expressionParts = words.Take(flagStart).ToArray(); if (expressionParts.Length == 0) return false; expression = string.Join(" ", expressionParts); // Parse flags for (var i = flagStart; i < words.Length; i++) { var word = words[i].ToLowerInvariant(); if (word == "inline") isInline = true; else if (word == "global") isGlobal = true; } name = rawName; return true; } private static Dictionary ResolveVariables( Dictionary variableDefs) { if (variableDefs.Count == 0) return new Dictionary(StringComparer.OrdinalIgnoreCase); // Build dependency graph var dependencies = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var kvp in variableDefs) { var deps = new List(); var expr = kvp.Value.expression; for (var i = 0; i < expr.Length; i++) { if (expr[i] == '$') { var start = i + 1; while (start < expr.Length && (char.IsLetterOrDigit(expr[start]) || expr[start] == '_')) start++; var refName = expr.Substring(i + 1, start - i - 1); // Find the canonical name (case-insensitive match) var canonical = variableDefs.Keys .FirstOrDefault(k => string.Equals(k, refName, StringComparison.OrdinalIgnoreCase)); if (canonical != null) deps.Add(canonical); i = start - 1; } } dependencies[kvp.Key] = deps; } // Topological sort (Kahn's algorithm) var inDegree = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var name in variableDefs.Keys) inDegree[name] = 0; foreach (var kvp in dependencies) { foreach (var dep in kvp.Value) { if (inDegree.ContainsKey(dep)) inDegree[kvp.Key]++; } } var queue = new Queue(); foreach (var kvp in inDegree) { if (kvp.Value == 0) queue.Enqueue(kvp.Key); } var resolved = new Dictionary(StringComparer.OrdinalIgnoreCase); var order = new List(); while (queue.Count > 0) { var current = queue.Dequeue(); order.Add(current); // Evaluate this variable var expr = variableDefs[current].expression; var value = ExpressionEvaluator.Evaluate(expr, resolved); resolved[current] = value; // Reduce in-degree of dependents foreach (var kvp in dependencies) { if (kvp.Value.Contains(current, StringComparer.OrdinalIgnoreCase)) { inDegree[kvp.Key]--; if (inDegree[kvp.Key] == 0 && !order.Contains(kvp.Key)) queue.Enqueue(kvp.Key); } } } if (order.Count != variableDefs.Count) throw new InvalidOperationException("Circular dependency detected among variables."); return resolved; } private static Dictionary BuildVariableRefs(params (string axis, string varRef)[] refs) { Dictionary result = null; foreach (var (axis, varRef) in refs) { if (varRef != null) { result ??= new Dictionary(); result[axis] = varRef; } } return result; } public void Close() { reader.Close(); } private class Code { public Code(char id) { Id = id; Value = string.Empty; } public Code(char id, string value) { Id = id; Value = value; } public char Id { get; private set; } public string Value { get; set; } public string VariableRef { get; set; } public override string ToString() { return Id + Value; } } private class CodeBlock : List { public void Add(char id, string value) { Add(new Code(id, value)); } public override string ToString() { var builder = new StringBuilder(); foreach (var code in this) builder.Append(code.ToString() + " "); return builder.ToString(); } } private enum CodeSection { Unknown, Arc, Line, SubProgram } } }